gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/test/benchmarks/tools/fio.go (about) 1 // Copyright 2020 The gVisor Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package tools 16 17 import ( 18 "encoding/json" 19 "fmt" 20 "strconv" 21 "strings" 22 "testing" 23 ) 24 25 // IOEngine is a I/O engine name passed to `fio`. 26 type IOEngine string 27 28 // Names of FIO I/O engines. 29 const ( 30 EngineSync = IOEngine("sync") 31 EngineLibAIO = IOEngine("libaio") 32 ) 33 34 // Fio makes 'fio' commands and parses their output. 35 type Fio struct { 36 Test string // test to run: read, write, randread, randwrite. 37 IOEngine IOEngine // ioengine to use: sync or libaio. 38 SizeMB int // total size to be read/written in megabytes. 39 BlockSizeKB int // block size to be read/written in kilobytes. 40 IODepth int // I/O depth for reads/writes when using libaio. 41 Jobs int // Number of jobs running in concurrent threads. 42 Direct bool // Whether to use direct I/O (O_DIRECT) or not. 43 } 44 45 // MakeCmd makes a 'fio' command. 46 func (f *Fio) MakeCmd(filename string) []string { 47 cmd := []string{"fio", "--output-format=json"} 48 cmd = append(cmd, fmt.Sprintf("--name=%s", f.Test)) 49 cmd = append(cmd, fmt.Sprintf("--ioengine=%s", string(f.IOEngine))) 50 if f.Jobs == 0 { 51 f.Jobs = 1 52 } 53 cmd = append(cmd, fmt.Sprintf("--numjobs=%d", f.Jobs)) 54 cmd = append(cmd, fmt.Sprintf("--max-jobs=%d", f.Jobs)) 55 cmd = append(cmd, fmt.Sprintf("--size=%dM", f.SizeMB)) 56 cmd = append(cmd, fmt.Sprintf("--blocksize=%dK", f.BlockSizeKB)) 57 cmd = append(cmd, fmt.Sprintf("--filename=%s", filename)) 58 if f.IODepth != 1 && f.IOEngine != EngineLibAIO { 59 panic(fmt.Sprintf("iodepth=%d does not make sense with ioengine=%q", f.IODepth, f.IOEngine)) 60 } 61 cmd = append(cmd, fmt.Sprintf("--iodepth=%d", f.IODepth)) 62 if f.Direct { 63 cmd = append(cmd, "--direct=1") 64 } else { 65 cmd = append(cmd, "--direct=0") 66 } 67 cmd = append(cmd, fmt.Sprintf("--rw=%s", f.Test)) 68 if f.Test == "read" || f.Test == "randread" { 69 // Don't call `fallocate` during read-only tests. 70 // Calling `fallocate` is not a typical operation for an application to do 71 // when it is only trying to read a file. 72 // This has performance implications for gVisor, so we override fio's 73 // default behavior for read-only benchmarks to be more representative of 74 // real-world read-only performance. 75 cmd = append(cmd, "--fallocate=none") 76 } 77 return cmd 78 } 79 80 // Parameters returns the test parameters and the overall test name derived 81 // from them. 82 func (f *Fio) Parameters(b *testing.B, additional ...Parameter) ([]Parameter, string) { 83 b.Helper() 84 if f.Jobs == 0 { 85 f.Jobs = 1 86 } 87 operation := Parameter{ 88 Name: "operation", 89 Value: f.Test, 90 } 91 ioEngine := Parameter{ 92 Name: "ioEngine", 93 Value: string(f.IOEngine), 94 } 95 jobs := Parameter{ 96 Name: "jobs", 97 Value: strconv.Itoa(f.Jobs), 98 } 99 blockSize := Parameter{ 100 Name: "blockSize", 101 Value: fmt.Sprintf("%dK", f.BlockSizeKB), 102 } 103 directIO := Parameter{ 104 Name: "directIO", 105 Value: strconv.FormatBool(f.Direct), 106 } 107 parameters := []Parameter{operation, ioEngine, jobs, blockSize, directIO} 108 parameters = append(parameters, additional...) 109 name, err := ParametersToName(parameters...) 110 if err != nil { 111 b.Fatalf("Failed to create parameter list: %v", err) 112 } 113 return parameters, name 114 } 115 116 // Report reports metrics based on output from an 'fio' command. 117 func (f *Fio) Report(b *testing.B, output string) { 118 b.Helper() 119 // Parse the output and report the metrics. 120 isRead := strings.Contains(f.Test, "read") 121 bw, err := f.parseBandwidth(output, isRead) 122 if err != nil { 123 b.Fatalf("failed to parse bandwidth from %s with: %v", output, err) 124 } 125 ReportCustomMetric(b, bw, "bandwidth" /*metric name*/, "bytes_per_second" /*unit*/) 126 127 iops, err := f.parseIOps(output, isRead) 128 if err != nil { 129 b.Fatalf("failed to parse iops from %s with: %v", output, err) 130 } 131 ReportCustomMetric(b, iops, "io_ops" /*metric name*/, "ops_per_second" /*unit*/) 132 } 133 134 // parseBandwidth reports the bandwidth in b/s. 135 func (f *Fio) parseBandwidth(data string, isRead bool) (float64, error) { 136 op := "write" 137 if isRead { 138 op = "read" 139 } 140 result, err := f.parseFioJSON(data, op, "bw") 141 if err != nil { 142 return 0, err 143 } 144 return result * 1024, nil 145 } 146 147 // parseIOps reports the write IO per second metric. 148 func (f *Fio) parseIOps(data string, isRead bool) (float64, error) { 149 if isRead { 150 return f.parseFioJSON(data, "read", "iops") 151 } 152 return f.parseFioJSON(data, "write", "iops") 153 } 154 155 // fioResult is for parsing FioJSON. 156 type fioResult struct { 157 Jobs []fioJob 158 } 159 160 // fioJob is for parsing FioJSON. 161 type fioJob map[string]json.RawMessage 162 163 // fioMetrics is for parsing FioJSON. 164 type fioMetrics map[string]json.RawMessage 165 166 // parseFioJSON parses data and grabs "op" (read or write) and "metric" 167 // (bw or iops) from the JSON. 168 func (f *Fio) parseFioJSON(data, op, metric string) (float64, error) { 169 var result fioResult 170 if err := json.Unmarshal([]byte(data), &result); err != nil { 171 return 0, fmt.Errorf("could not unmarshal data: %v", err) 172 } 173 174 if len(result.Jobs) < 1 { 175 return 0, fmt.Errorf("no jobs present to parse") 176 } 177 178 var metrics fioMetrics 179 if err := json.Unmarshal(result.Jobs[0][op], &metrics); err != nil { 180 return 0, fmt.Errorf("could not unmarshal jobs: %v", err) 181 } 182 183 if _, ok := metrics[metric]; !ok { 184 return 0, fmt.Errorf("no metric found for op: %s", op) 185 } 186 return strconv.ParseFloat(string(metrics[metric]), 64) 187 }