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  }