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 }