gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/test/benchmarks/fs/fsbench/fsbench.go (about)

     1  // Copyright 2022 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 fsbench provides utility functions for filesystem benchmarks.
    16  package fsbench
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"strings"
    22  	"testing"
    23  
    24  	"gvisor.dev/gvisor/pkg/cleanup"
    25  	"gvisor.dev/gvisor/pkg/test/dockerutil"
    26  	"gvisor.dev/gvisor/test/benchmarks/harness"
    27  	"gvisor.dev/gvisor/test/benchmarks/tools"
    28  	"gvisor.dev/gvisor/test/metricsviz"
    29  )
    30  
    31  // FSBenchmark represents a set of work to perform within a container that is instrumented with
    32  // different filesystem configurations.
    33  type FSBenchmark struct {
    34  	// Image is the Docker image to load.
    35  	Image string
    36  	// WorkDir is where the action takes place.
    37  	// The commands below are run from a directory that has the same file as what the container image
    38  	// has at this directory.
    39  	WorkDir string
    40  	// RunCmd is the command to run to execute the benchmark.
    41  	RunCmd []string
    42  	// WantOutput, if set, is verified to be a substring of the output of RunCmd.
    43  	WantOutput string
    44  	// CleanCmd, if set, is run to clean up between benchmarks.
    45  	CleanCmd []string
    46  	// Variants is a list of benchmark variants to run.
    47  	// If unset, the typical set is used.
    48  	Variants []Variant
    49  	// Callback is an optional function that is called after each execution of
    50  	// the workload being benchmarked. It can be used to perform workload
    51  	// specific metric reporting.
    52  	Callback func(b *testing.B, output string)
    53  }
    54  
    55  // Variant is a specific configuration for a benchmark.
    56  // Dimensions here are clean/dirty cache (do or don't drop caches)
    57  // and if the mount on which we are compiling is a tmpfs/bind mount.
    58  type Variant struct {
    59  	// clearCache drops caches before running.
    60  	clearCache bool
    61  	// fsType is the type of filesystem to use.
    62  	fsType harness.FileSystemType
    63  }
    64  
    65  // TypicalVariants returns the typical full set of benchmark variants.
    66  func TypicalVariants() []Variant {
    67  	variants := make([]Variant, 0, 8)
    68  	for _, filesys := range []harness.FileSystemType{harness.BindFS, harness.TmpFS, harness.RootFS, harness.FuseFS} {
    69  		variants = append(variants, Variant{
    70  			clearCache: true,
    71  			fsType:     filesys,
    72  		})
    73  		variants = append(variants, Variant{
    74  			clearCache: false,
    75  			fsType:     filesys,
    76  		})
    77  	}
    78  	return variants
    79  }
    80  
    81  // RunWithDifferentFilesystems runs a
    82  func RunWithDifferentFilesystems(ctx context.Context, b *testing.B, machine harness.Machine, bm FSBenchmark) {
    83  	b.Helper()
    84  
    85  	benchmarkVariants := bm.Variants
    86  	if len(benchmarkVariants) == 0 {
    87  		benchmarkVariants = TypicalVariants()
    88  	}
    89  	for _, variant := range benchmarkVariants {
    90  		pageCache := tools.Parameter{
    91  			Name:  "page_cache",
    92  			Value: "dirty",
    93  		}
    94  		if variant.clearCache {
    95  			pageCache.Value = "clean"
    96  		}
    97  
    98  		filesystem := tools.Parameter{
    99  			Name:  "filesystem",
   100  			Value: string(variant.fsType),
   101  		}
   102  		name, err := tools.ParametersToName(pageCache, filesystem)
   103  		if err != nil {
   104  			b.Fatalf("Failed to parse parameters: %v", err)
   105  		}
   106  
   107  		b.Run(name, func(b *testing.B) {
   108  			// Grab a container.
   109  			container := machine.GetContainer(ctx, b)
   110  			cu := cleanup.Make(func() {
   111  				container.CleanUp(ctx)
   112  			})
   113  			defer cu.Clean()
   114  			mts, prefix, err := harness.MakeMount(machine, variant.fsType, &cu)
   115  			if err != nil {
   116  				b.Fatalf("Failed to make mount: %v", err)
   117  			}
   118  
   119  			runOpts := dockerutil.RunOpts{
   120  				Image:  bm.Image,
   121  				Mounts: mts,
   122  			}
   123  
   124  			// Start a container and sleep.
   125  			if err := container.Spawn(ctx, runOpts, "sleep", "24h"); err != nil {
   126  				b.Fatalf("run failed with: %v", err)
   127  			}
   128  			defer metricsviz.FromContainerLogs(ctx, b, container)
   129  
   130  			// Ignore safetext/shsprintf linter suggestion.
   131  			mkdirCmd := fmt.Sprintf("mkdir -p %s", prefix)
   132  			out, err := container.Exec(ctx, dockerutil.ExecOpts{}, "/bin/sh", "-c", mkdirCmd)
   133  			if err != nil {
   134  				b.Fatalf("failed to make directory: %v (%s)", err, out)
   135  			}
   136  
   137  			if variant.fsType == harness.FuseFS {
   138  				container.CopyFiles(&runOpts, "/fusebin", "test/runner/fuse/fuse")
   139  				_, err := container.ExecProcess(ctx, dockerutil.ExecOpts{
   140  					Privileged: true,
   141  				}, "/fusebin/fuse", "--dir="+prefix, "--debug=false")
   142  				if err != nil {
   143  					b.Fatalf("starting fuse server failed with: %v", err)
   144  				}
   145  			}
   146  
   147  			cpCmd := fmt.Sprintf("cp -r %s %s/.", bm.WorkDir, prefix)
   148  			if out, err := container.Exec(ctx, dockerutil.ExecOpts{},
   149  				"/bin/sh", "-c", cpCmd); err != nil {
   150  				b.Fatalf("failed to copy directory: %v (%s)", err, out)
   151  			}
   152  
   153  			b.ResetTimer()
   154  			b.StopTimer()
   155  
   156  			// Drop Caches and bazel clean should happen inside the loop as we may use
   157  			// time options with b.N. (e.g. Run for an hour.)
   158  			for i := 0; i < b.N; i++ {
   159  				// Drop Caches for clear cache runs.
   160  				if variant.clearCache {
   161  					if err := harness.DropCaches(machine); err != nil {
   162  						b.Skipf("failed to drop caches: %v. You probably need root.", err)
   163  					}
   164  				}
   165  
   166  				b.StartTimer()
   167  				got, err := container.Exec(ctx, dockerutil.ExecOpts{
   168  					WorkDir: prefix + bm.WorkDir,
   169  				}, bm.RunCmd...)
   170  				if err != nil {
   171  					b.Fatalf("Command %v failed with: %v logs: %s", bm.RunCmd, err, got)
   172  				}
   173  				b.StopTimer()
   174  
   175  				if bm.WantOutput != "" && !strings.Contains(got, bm.WantOutput) {
   176  					b.Fatalf("string %s not in: %s", bm.WantOutput, got)
   177  				}
   178  
   179  				if bm.Callback != nil {
   180  					bm.Callback(b, got)
   181  				}
   182  
   183  				// Clean the container in case we are doing another run.
   184  				if i < b.N-1 && len(bm.CleanCmd) != 0 {
   185  					if _, err = container.Exec(ctx, dockerutil.ExecOpts{
   186  						WorkDir: prefix + bm.WorkDir,
   187  					}, bm.CleanCmd...); err != nil {
   188  						b.Fatalf("Cleanup command %v failed with: %v", bm.CleanCmd, err)
   189  					}
   190  				}
   191  			}
   192  		})
   193  	}
   194  }