github.com/mutagen-io/mutagen@v0.18.0-rc1/tools/scan_bench/main.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"os/signal"
    10  	"time"
    11  
    12  	"github.com/spf13/pflag"
    13  
    14  	"google.golang.org/protobuf/proto"
    15  
    16  	"github.com/dustin/go-humanize"
    17  
    18  	"github.com/mutagen-io/mutagen/cmd"
    19  	"github.com/mutagen-io/mutagen/cmd/profile"
    20  
    21  	"github.com/mutagen-io/mutagen/pkg/filesystem/behavior"
    22  	"github.com/mutagen-io/mutagen/pkg/synchronization/core"
    23  	"github.com/mutagen-io/mutagen/pkg/synchronization/core/ignore"
    24  	dockerignore "github.com/mutagen-io/mutagen/pkg/synchronization/core/ignore/docker"
    25  	mutagenignore "github.com/mutagen-io/mutagen/pkg/synchronization/core/ignore/mutagen"
    26  	"github.com/mutagen-io/mutagen/pkg/synchronization/hashing"
    27  )
    28  
    29  const (
    30  	snapshotFile = "snapshot_test"
    31  	cacheFile    = "cache_test"
    32  )
    33  
    34  const usage = `scan_bench [-h|--help] [-p|--profile] [-d|--digest=(` + digestFlagOptions + `)]
    35             [--ignore-syntax=(mutagen|docker)] [-i|--ignore=<pattern>] <path>
    36  `
    37  
    38  // ignoreCachesIntersectionEqual compares two ignore caches, ensuring that keys
    39  // which are present in both caches have the same value. It's the closest we can
    40  // get to the core package's testAcceleratedCacheIsSubset without having access
    41  // to the members of IgnoreCacheKey.
    42  func ignoreCachesIntersectionEqual(first, second ignore.IgnoreCache) bool {
    43  	// Check matches from first in second.
    44  	for key, firstValue := range first {
    45  		if secondValue, ok := second[key]; ok && secondValue != firstValue {
    46  			return false
    47  		}
    48  	}
    49  
    50  	// Check matches from second in first.
    51  	for key, secondValue := range second {
    52  		if firstValue, ok := first[key]; ok && firstValue != secondValue {
    53  			return false
    54  		}
    55  	}
    56  
    57  	// Success.
    58  	return true
    59  }
    60  
    61  func main() {
    62  	// Parse command line arguments.
    63  	flagSet := pflag.NewFlagSet("scan_bench", pflag.ContinueOnError)
    64  	flagSet.SetOutput(io.Discard)
    65  	var enableProfile bool
    66  	var digest string
    67  	var ignoreSyntaxName string
    68  	var ignores []string
    69  	flagSet.BoolVarP(&enableProfile, "profile", "p", false, "enable profiling")
    70  	flagSet.StringVarP(&digest, "digest", "d", "sha1", "specify digest algorithm")
    71  	flagSet.StringVar(&ignoreSyntaxName, "ignore-syntax", "mutagen", "specify ignore syntax")
    72  	flagSet.StringSliceVarP(&ignores, "ignore", "i", nil, "specify ignore paths")
    73  	if err := flagSet.Parse(os.Args[1:]); err != nil {
    74  		if err == pflag.ErrHelp {
    75  			fmt.Fprint(os.Stdout, usage)
    76  			return
    77  		} else {
    78  			cmd.Fatal(fmt.Errorf("unable to parse command line: %w", err))
    79  		}
    80  	}
    81  	arguments := flagSet.Args()
    82  	if len(arguments) != 1 {
    83  		cmd.Fatal(errors.New("invalid number of paths specified"))
    84  	}
    85  	path := arguments[0]
    86  
    87  	// Create a context for the scan. The main reason for using a custom context
    88  	// instead of using context.Background() is that the latter provides a
    89  	// context that returns a nil result from Done(). To ensure that we fully
    90  	// understand the impact of preemption checks, we want a context that will
    91  	// return a non-nil completion channel. This also allows us to wire up
    92  	// interrupt handling to this context.
    93  	ctx, cancel := context.WithCancel(context.Background())
    94  	defer cancel()
    95  
    96  	// Wire up termination signals to context cancellation.
    97  	signalTermination := make(chan os.Signal, 1)
    98  	signal.Notify(signalTermination, cmd.TerminationSignals...)
    99  	go func() {
   100  		<-signalTermination
   101  		cancel()
   102  	}()
   103  
   104  	// Parse the hashing algorithm and create a hasher.
   105  	var hashingAlgorithm hashing.Algorithm
   106  	if err := hashingAlgorithm.UnmarshalText([]byte(digest)); err != nil {
   107  		cmd.Fatal(fmt.Errorf("unable to parse hashing algorithm: %w", err))
   108  	} else if hashingAlgorithm.SupportStatus() != hashing.AlgorithmSupportStatusSupported {
   109  		cmd.Fatal(fmt.Errorf("%s hashing not supported", hashingAlgorithm.Description()))
   110  	}
   111  	hasher := hashingAlgorithm.Factory()()
   112  
   113  	// Parse the ignore syntax.
   114  	var ignoreSyntax ignore.Syntax
   115  	if err := ignoreSyntax.UnmarshalText([]byte(ignoreSyntaxName)); err != nil {
   116  		cmd.Fatal(fmt.Errorf("unable to parse ignore syntax: %w", err))
   117  	}
   118  
   119  	// Create an ignorer.
   120  	var ignorer ignore.Ignorer
   121  	if ignoreSyntax == ignore.Syntax_SyntaxMutagen {
   122  		if i, err := mutagenignore.NewIgnorer(ignores); err != nil {
   123  			cmd.Fatal(fmt.Errorf("unable to create Mutagen-style ignorer: %w", err))
   124  		} else {
   125  			ignorer = i
   126  		}
   127  	} else if ignoreSyntax == ignore.Syntax_SyntaxDocker {
   128  		if i, err := dockerignore.NewIgnorer(ignores); err != nil {
   129  			cmd.Fatal(fmt.Errorf("unable to create Docker-style ignorer: %w", err))
   130  		} else {
   131  			ignorer = i
   132  		}
   133  	} else {
   134  		panic("unhandled ignore syntax")
   135  	}
   136  
   137  	// Print information.
   138  	fmt.Println("Analyzing", path)
   139  
   140  	// Perform a full (cold) scan. If requested, enable CPU and memory
   141  	// profiling.
   142  	var profiler *profile.Profile
   143  	if enableProfile {
   144  		if p, err := profile.New("scan_full_cold"); err != nil {
   145  			cmd.Fatal(fmt.Errorf("unable to create profiler: %w", err))
   146  		} else {
   147  			profiler = p
   148  		}
   149  	}
   150  	start := time.Now()
   151  	snapshot, cache, ignoreCache, err := core.Scan(
   152  		ctx,
   153  		path,
   154  		nil, nil,
   155  		hasher, nil,
   156  		ignorer, nil,
   157  		behavior.ProbeMode_ProbeModeProbe,
   158  		core.SymbolicLinkMode_SymbolicLinkModePortable,
   159  		core.PermissionsMode_PermissionsModePortable,
   160  	)
   161  	if err != nil {
   162  		cmd.Fatal(fmt.Errorf("unable to perform cold scan: %w", err))
   163  	}
   164  	if snapshot.Content == nil {
   165  		fmt.Println("No content at the specified path!")
   166  	}
   167  	stop := time.Now()
   168  	if enableProfile {
   169  		if err = profiler.Finalize(); err != nil {
   170  			cmd.Fatal(fmt.Errorf("unable to finalize profiler: %w", err))
   171  		}
   172  		profiler = nil
   173  	}
   174  	fmt.Println("Cold scan took", stop.Sub(start))
   175  	fmt.Println("Root preserves executability:", snapshot.PreservesExecutability)
   176  	fmt.Println("Root requires Unicode recomposition:", snapshot.DecomposesUnicode)
   177  
   178  	// Perform a full (warm) scan. If requested, enable CPU and memory
   179  	// profiling.
   180  	if enableProfile {
   181  		if profiler, err = profile.New("scan_full_warm"); err != nil {
   182  			cmd.Fatal(fmt.Errorf("unable to create profiler: %w", err))
   183  		}
   184  	}
   185  	start = time.Now()
   186  	newSnapshot, newCache, newIgnoreCache, err := core.Scan(
   187  		ctx,
   188  		path,
   189  		nil, nil,
   190  		hasher, cache,
   191  		ignorer, ignoreCache,
   192  		behavior.ProbeMode_ProbeModeProbe,
   193  		core.SymbolicLinkMode_SymbolicLinkModePortable,
   194  		core.PermissionsMode_PermissionsModePortable,
   195  	)
   196  	if err != nil {
   197  		cmd.Fatal(fmt.Errorf("unable to perform warm scan: %w", err))
   198  	}
   199  	stop = time.Now()
   200  	if enableProfile {
   201  		if err = profiler.Finalize(); err != nil {
   202  			cmd.Fatal(fmt.Errorf("unable to finalize profiler: %w", err))
   203  		}
   204  		profiler = nil
   205  	}
   206  	fmt.Println("Warm scan took", stop.Sub(start))
   207  
   208  	// Compare the warm scan results with the baseline results.
   209  	if !newSnapshot.Equal(snapshot) {
   210  		cmd.Fatal(errors.New("snapshot mismatch"))
   211  	} else if !newCache.Equal(cache) {
   212  		cmd.Fatal(errors.New("cache mismatch"))
   213  	} else if len(newIgnoreCache) != len(ignoreCache) {
   214  		cmd.Fatal(errors.New("ignore cache length mismatch"))
   215  	} else if !ignoreCachesIntersectionEqual(newIgnoreCache, ignoreCache) {
   216  		cmd.Fatal(errors.New("ignore cache mismatch"))
   217  	}
   218  
   219  	// Perform a second full (warm) scan. If requested, enable CPU and memory
   220  	// profiling. A second warm scan will provide a more accurate real-world
   221  	// assessment of performance because the cold scan won't have wiped out the
   222  	// majority of the filesystem caches.
   223  	if enableProfile {
   224  		if profiler, err = profile.New("scan_second_full_warm"); err != nil {
   225  			cmd.Fatal(fmt.Errorf("unable to create profiler: %w", err))
   226  		}
   227  	}
   228  	start = time.Now()
   229  	newSnapshot, newCache, newIgnoreCache, err = core.Scan(
   230  		ctx,
   231  		path,
   232  		nil, nil,
   233  		hasher, cache,
   234  		ignorer, ignoreCache,
   235  		behavior.ProbeMode_ProbeModeProbe,
   236  		core.SymbolicLinkMode_SymbolicLinkModePortable,
   237  		core.PermissionsMode_PermissionsModePortable,
   238  	)
   239  	if err != nil {
   240  		cmd.Fatal(fmt.Errorf("unable to perform second warm scan: %w", err))
   241  	}
   242  	stop = time.Now()
   243  	if enableProfile {
   244  		if err = profiler.Finalize(); err != nil {
   245  			cmd.Fatal(fmt.Errorf("unable to finalize profiler: %w", err))
   246  		}
   247  		profiler = nil
   248  	}
   249  	fmt.Println("Second warm scan took", stop.Sub(start))
   250  
   251  	// Compare the warm scan results with the baseline results.
   252  	if !newSnapshot.Equal(snapshot) {
   253  		cmd.Fatal(errors.New("snapshot mismatch"))
   254  	} else if !newCache.Equal(cache) {
   255  		cmd.Fatal(errors.New("cache mismatch"))
   256  	} else if len(newIgnoreCache) != len(ignoreCache) {
   257  		cmd.Fatal(errors.New("ignore cache length mismatch"))
   258  	} else if !ignoreCachesIntersectionEqual(newIgnoreCache, ignoreCache) {
   259  		cmd.Fatal(errors.New("ignore cache mismatch"))
   260  	}
   261  
   262  	// Perform an accelerated scan (with a re-check path). If requested, enable
   263  	// CPU and memory profiling.
   264  	if enableProfile {
   265  		if profiler, err = profile.New("scan_accelerated_recheck"); err != nil {
   266  			cmd.Fatal(fmt.Errorf("unable to create profiler: %w", err))
   267  		}
   268  	}
   269  	start = time.Now()
   270  	newSnapshot, newCache, newIgnoreCache, err = core.Scan(
   271  		ctx,
   272  		path,
   273  		snapshot, map[string]bool{"fake path": true},
   274  		hasher, cache,
   275  		ignorer, ignoreCache,
   276  		behavior.ProbeMode_ProbeModeProbe,
   277  		core.SymbolicLinkMode_SymbolicLinkModePortable,
   278  		core.PermissionsMode_PermissionsModePortable,
   279  	)
   280  	if err != nil {
   281  		cmd.Fatal(fmt.Errorf("unable to perform accelerated scan (with re-check paths): %w", err))
   282  	}
   283  	stop = time.Now()
   284  	if enableProfile {
   285  		if err = profiler.Finalize(); err != nil {
   286  			cmd.Fatal(fmt.Errorf("unable to finalize profiler: %w", err))
   287  		}
   288  		profiler = nil
   289  	}
   290  	fmt.Println("Accelerated scan (with re-check paths) took", stop.Sub(start))
   291  
   292  	// Compare the accelerated scan results with the baseline results.
   293  	if !newSnapshot.Equal(snapshot) {
   294  		cmd.Fatal(errors.New("snapshot mismatch"))
   295  	} else if !newCache.Equal(cache) {
   296  		cmd.Fatal(errors.New("cache mismatch"))
   297  	} else if !ignoreCachesIntersectionEqual(newIgnoreCache, ignoreCache) {
   298  		cmd.Fatal(errors.New("ignore cache mismatch"))
   299  	}
   300  
   301  	// Perform an accelerated scan (without any re-check paths). If requested,
   302  	// enable CPU and memory profiling.
   303  	if enableProfile {
   304  		if profiler, err = profile.New("scan_accelerated_no_recheck"); err != nil {
   305  			cmd.Fatal(fmt.Errorf("unable to create profiler: %w", err))
   306  		}
   307  	}
   308  	start = time.Now()
   309  	newSnapshot, newCache, newIgnoreCache, err = core.Scan(
   310  		ctx,
   311  		path,
   312  		snapshot, nil,
   313  		hasher, cache,
   314  		ignorer, ignoreCache,
   315  		behavior.ProbeMode_ProbeModeProbe,
   316  		core.SymbolicLinkMode_SymbolicLinkModePortable,
   317  		core.PermissionsMode_PermissionsModePortable,
   318  	)
   319  	if err != nil {
   320  		cmd.Fatal(fmt.Errorf("unable to perform accelerated scan (without re-check paths): %w", err))
   321  	}
   322  	stop = time.Now()
   323  	if enableProfile {
   324  		if err = profiler.Finalize(); err != nil {
   325  			cmd.Fatal(fmt.Errorf("unable to finalize profiler: %w", err))
   326  		}
   327  		profiler = nil
   328  	}
   329  	fmt.Println("Accelerated scan (without re-check paths) took", stop.Sub(start))
   330  
   331  	// Compare the accelerated scan results with the baseline results.
   332  	if !newSnapshot.Equal(snapshot) {
   333  		cmd.Fatal(errors.New("snapshot mismatch"))
   334  	} else if !newCache.Equal(cache) {
   335  		cmd.Fatal(errors.New("cache mismatch"))
   336  	} else if !ignoreCachesIntersectionEqual(newIgnoreCache, ignoreCache) {
   337  		cmd.Fatal(errors.New("ignore cache mismatch"))
   338  	}
   339  
   340  	// Validate the snapshot.
   341  	start = time.Now()
   342  	if err := snapshot.EnsureValid(); err != nil {
   343  		cmd.Fatal(fmt.Errorf("invalid snapshot: %w", err))
   344  	}
   345  	stop = time.Now()
   346  	fmt.Println("Snapshot validation took", stop.Sub(start))
   347  
   348  	// Count snapshot content entries.
   349  	start = time.Now()
   350  	entryCount := snapshot.Content.Count()
   351  	stop = time.Now()
   352  	fmt.Println("Snapshot entry counting took", stop.Sub(start))
   353  	fmt.Println("Snapshot contained", entryCount, "entries")
   354  
   355  	// Print snapshot statistics.
   356  	fmt.Println("Snapshot contained", snapshot.Directories, "directories")
   357  	fmt.Println("Snapshot contained", snapshot.Files, "files")
   358  	fmt.Println("Snapshot contained", snapshot.SymbolicLinks, "symbolic links")
   359  	fmt.Println("Snapshot files totaled", humanize.Bytes(snapshot.TotalFileSize))
   360  
   361  	// Measure how long phantom reification would take on snapshots of this size
   362  	// and print reified directory counts for comparison.
   363  	start = time.Now()
   364  	_, _, αDirectoryCount, βDirectoryCount := core.ReifyPhantomDirectories(
   365  		snapshot.Content, snapshot.Content, snapshot.Content,
   366  	)
   367  	stop = time.Now()
   368  	fmt.Println("Phantom directory reification took", stop.Sub(start))
   369  	fmt.Println("Reified alpha snapshot contained", αDirectoryCount, "directories")
   370  	fmt.Println("Reified beta snapshot contained", βDirectoryCount, "directories")
   371  
   372  	// Perform a deep copy of the snapshot contents.
   373  	start = time.Now()
   374  	snapshot.Content.Copy(core.EntryCopyBehaviorDeep)
   375  	stop = time.Now()
   376  	fmt.Println("Snapshot entry copying took", stop.Sub(start))
   377  
   378  	// Serialize the snapshot.
   379  	if enableProfile {
   380  		if profiler, err = profile.New("serialize_snapshot"); err != nil {
   381  			cmd.Fatal(fmt.Errorf("unable to create profiler: %w", err))
   382  		}
   383  	}
   384  	start = time.Now()
   385  	marshaling := proto.MarshalOptions{Deterministic: true}
   386  	serializedSnapshot, err := marshaling.Marshal(snapshot)
   387  	if err != nil {
   388  		cmd.Fatal(fmt.Errorf("unable to serialize snapshot: %w", err))
   389  	}
   390  	stop = time.Now()
   391  	if enableProfile {
   392  		if err = profiler.Finalize(); err != nil {
   393  			cmd.Fatal(fmt.Errorf("unable to finalize profiler: %w", err))
   394  		}
   395  		profiler = nil
   396  	}
   397  	fmt.Println("Snapshot serialization took", stop.Sub(start))
   398  
   399  	// Deserialize the snapshot.
   400  	if enableProfile {
   401  		if profiler, err = profile.New("deserialize_snapshot"); err != nil {
   402  			cmd.Fatal(fmt.Errorf("unable to create profiler: %w", err))
   403  		}
   404  	}
   405  	start = time.Now()
   406  	deserializedSnapshot := &core.Snapshot{}
   407  	if err = proto.Unmarshal(serializedSnapshot, deserializedSnapshot); err != nil {
   408  		cmd.Fatal(fmt.Errorf("unable to deserialize snapshot: %w", err))
   409  	}
   410  	stop = time.Now()
   411  	if enableProfile {
   412  		if err = profiler.Finalize(); err != nil {
   413  			cmd.Fatal(fmt.Errorf("unable to finalize profiler: %w", err))
   414  		}
   415  		profiler = nil
   416  	}
   417  	fmt.Println("Snapshot deserialization took", stop.Sub(start))
   418  
   419  	// Validate the deserialized snapshot.
   420  	start = time.Now()
   421  	if err = deserializedSnapshot.EnsureValid(); err != nil {
   422  		cmd.Fatal(fmt.Errorf("deserialized snapshot invalid: %w", err))
   423  	}
   424  	stop = time.Now()
   425  	fmt.Println("Snapshot validation took", stop.Sub(start))
   426  
   427  	// Write the serialized snapshot to disk.
   428  	start = time.Now()
   429  	if err = os.WriteFile(snapshotFile, serializedSnapshot, 0600); err != nil {
   430  		cmd.Fatal(fmt.Errorf("unable to write snapshot to disk: %w", err))
   431  	}
   432  	stop = time.Now()
   433  	fmt.Println("Snapshot write took", stop.Sub(start))
   434  
   435  	// Read the serialized snapshot from disk.
   436  	start = time.Now()
   437  	if _, err = os.ReadFile(snapshotFile); err != nil {
   438  		cmd.Fatal(fmt.Errorf("unable to read snapshot from disk: %w", err))
   439  	}
   440  	stop = time.Now()
   441  	fmt.Println("Snapshot read took", stop.Sub(start))
   442  
   443  	// Remove the temporary file.
   444  	if err = os.Remove(snapshotFile); err != nil {
   445  		cmd.Fatal(fmt.Errorf("unable to remove snapshot from disk: %w", err))
   446  	}
   447  
   448  	// Print serialized snapshot size.
   449  	fmt.Println("Serialized snapshot size is", len(serializedSnapshot), "bytes")
   450  
   451  	// Print whether or not snapshots are equivalent.
   452  	fmt.Println("Original/deserialized snapshots equivalent?", deserializedSnapshot.Equal(snapshot))
   453  
   454  	// Checksum it.
   455  	start = time.Now()
   456  	hasher.Reset()
   457  	hasher.Write(serializedSnapshot)
   458  	hasher.Sum(nil)
   459  	stop = time.Now()
   460  	fmt.Println("Snapshot digest took", stop.Sub(start))
   461  
   462  	// Serialize the cache.
   463  	if enableProfile {
   464  		if profiler, err = profile.New("serialize_cache"); err != nil {
   465  			cmd.Fatal(fmt.Errorf("unable to create profiler: %w", err))
   466  		}
   467  	}
   468  	start = time.Now()
   469  	serializedCache, err := proto.Marshal(cache)
   470  	if err != nil {
   471  		cmd.Fatal(fmt.Errorf("unable to serialize cache: %w", err))
   472  	}
   473  	stop = time.Now()
   474  	if enableProfile {
   475  		if err = profiler.Finalize(); err != nil {
   476  			cmd.Fatal(fmt.Errorf("unable to finalize profiler: %w", err))
   477  		}
   478  		profiler = nil
   479  	}
   480  	fmt.Println("Cache serialization took", stop.Sub(start))
   481  
   482  	// Deserialize the cache.
   483  	if enableProfile {
   484  		if profiler, err = profile.New("deserialize_cache"); err != nil {
   485  			cmd.Fatal(fmt.Errorf("unable to create profiler: %w", err))
   486  		}
   487  	}
   488  	start = time.Now()
   489  	deserializedCache := &core.Cache{}
   490  	if err = proto.Unmarshal(serializedCache, deserializedCache); err != nil {
   491  		cmd.Fatal(fmt.Errorf("unable to deserialize cache: %w", err))
   492  	}
   493  	stop = time.Now()
   494  	if enableProfile {
   495  		if err = profiler.Finalize(); err != nil {
   496  			cmd.Fatal(fmt.Errorf("unable to finalize profiler: %w", err))
   497  		}
   498  		profiler = nil
   499  	}
   500  	fmt.Println("Cache deserialization took", stop.Sub(start))
   501  
   502  	// Write the serialized cache to disk.
   503  	start = time.Now()
   504  	if err = os.WriteFile(cacheFile, serializedCache, 0600); err != nil {
   505  		cmd.Fatal(fmt.Errorf("unable to write cache to disk: %w", err))
   506  	}
   507  	stop = time.Now()
   508  	fmt.Println("Cache write took", stop.Sub(start))
   509  
   510  	// Read the serialized cache from disk.
   511  	start = time.Now()
   512  	if _, err = os.ReadFile(cacheFile); err != nil {
   513  		cmd.Fatal(fmt.Errorf("unable to read cache from disk: %w", err))
   514  	}
   515  	stop = time.Now()
   516  	fmt.Println("Cache read took", stop.Sub(start))
   517  
   518  	// Remove the temporary file.
   519  	if err = os.Remove(cacheFile); err != nil {
   520  		cmd.Fatal(fmt.Errorf("unable to remove cache from disk: %w", err))
   521  	}
   522  
   523  	// Print serialized cache size.
   524  	fmt.Println("Serialized cache size is", len(serializedCache), "bytes")
   525  
   526  	// Generate a reverse lookup map for the cache.
   527  	start = time.Now()
   528  	if _, err = cache.GenerateReverseLookupMap(); err != nil {
   529  		cmd.Fatal(fmt.Errorf("unable to generate reverse lookup map: %w", err))
   530  	}
   531  	stop = time.Now()
   532  	fmt.Println("Reverse lookup map generation took", stop.Sub(start))
   533  }