bench_test.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. // Copyright (c) 2020, The Garble Authors.
  2. // See LICENSE for licensing information.
  3. package main
  4. import (
  5. _ "embed"
  6. "flag"
  7. "fmt"
  8. "os"
  9. "os/exec"
  10. "path/filepath"
  11. "regexp"
  12. "strconv"
  13. "strings"
  14. "testing"
  15. "time"
  16. qt "github.com/frankban/quicktest"
  17. )
  18. //go:embed testdata/bench/main.go
  19. var benchSourceMain []byte
  20. var (
  21. rxBuiltRuntime = regexp.MustCompile(`(?m)^runtime$`)
  22. rxBuiltMain = regexp.MustCompile(`(?m)^test/main$`)
  23. )
  24. // BenchmarkBuild is a benchmark for 'garble build' on a fairly simple
  25. // main package with a handful of standard library depedencies.
  26. //
  27. // We use a real garble binary and exec it, to simulate what the real user would
  28. // run. The real obfuscation and compilation will happen in sub-processes
  29. // anyway, so skipping one exec layer doesn't help us in any way.
  30. //
  31. // The benchmark isn't parallel, because in practice users build once at a time,
  32. // and each build already spawns concurrent processes and goroutines to do work.
  33. //
  34. // At the moment, each iteration takes 1-2s on a laptop, so we can't make the
  35. // benchmark include any more features unless we make it significantly faster.
  36. func BenchmarkBuild(b *testing.B) {
  37. // As of Go 1.17, using -benchtime=Nx with N larger than 1 results in two
  38. // calls to BenchmarkBuild, with the first having b.N==1 to discover
  39. // sub-benchmarks. Unfortunately, we do a significant amount of work both
  40. // during setup and during that first iteration, which is pointless.
  41. // To avoid that, detect the scenario in a hacky way, and return early.
  42. // See https://github.com/golang/go/issues/32051.
  43. benchtime := flag.Lookup("test.benchtime").Value.String()
  44. if b.N == 1 && strings.HasSuffix(benchtime, "x") && benchtime != "1x" {
  45. return
  46. }
  47. tdir := b.TempDir()
  48. // We collect extra metrics.
  49. var memoryAllocs, cachedTime, systemTime int64
  50. outputBin := filepath.Join(tdir, "output")
  51. sourceDir := filepath.Join(tdir, "src")
  52. qt.Assert(b, os.Mkdir(sourceDir, 0o777), qt.IsNil)
  53. writeSourceFile := func(name string, content []byte) {
  54. err := os.WriteFile(filepath.Join(sourceDir, name), content, 0o666)
  55. qt.Assert(b, err, qt.IsNil)
  56. }
  57. writeSourceFile("go.mod", []byte("module test/main"))
  58. writeSourceFile("main.go", benchSourceMain)
  59. rxGarbleAllocs := regexp.MustCompile(`(?m)^garble allocs: ([0-9]+)`)
  60. b.ResetTimer()
  61. b.StopTimer()
  62. for i := 0; i < b.N; i++ {
  63. // First we do a fresh build, using empty cache directories,
  64. // and the second does an incremental rebuild reusing the same cache directories.
  65. goCache := filepath.Join(tdir, "go-cache")
  66. qt.Assert(b, os.RemoveAll(goCache), qt.IsNil)
  67. qt.Assert(b, os.Mkdir(goCache, 0o777), qt.IsNil)
  68. garbleCache := filepath.Join(tdir, "garble-cache")
  69. qt.Assert(b, os.RemoveAll(garbleCache), qt.IsNil)
  70. qt.Assert(b, os.Mkdir(garbleCache, 0o777), qt.IsNil)
  71. env := append(os.Environ(),
  72. "RUN_GARBLE_MAIN=true",
  73. "GOCACHE="+goCache,
  74. "GARBLE_CACHE="+garbleCache,
  75. "GARBLE_WRITE_ALLOCS=true",
  76. )
  77. args := []string{"build", "-v", "-o=" + outputBin, sourceDir}
  78. for _, cached := range []bool{false, true} {
  79. // The cached rebuild will reuse all dependencies,
  80. // but rebuild the main package itself.
  81. if cached {
  82. writeSourceFile("rebuild.go", []byte(fmt.Sprintf("package main\nvar v%d int", i)))
  83. }
  84. cmd := exec.Command(os.Args[0], args...)
  85. cmd.Env = env
  86. cmd.Dir = sourceDir
  87. cachedStart := time.Now()
  88. b.StartTimer()
  89. out, err := cmd.CombinedOutput()
  90. b.StopTimer()
  91. if cached {
  92. cachedTime += time.Since(cachedStart).Nanoseconds()
  93. }
  94. qt.Assert(b, err, qt.IsNil, qt.Commentf("output: %s", out))
  95. if !cached {
  96. // Ensure that we built all packages, as expected.
  97. qt.Assert(b, rxBuiltRuntime.Match(out), qt.IsTrue)
  98. } else {
  99. // Ensure that we only rebuilt the main package, as expected.
  100. qt.Assert(b, rxBuiltRuntime.Match(out), qt.IsFalse)
  101. }
  102. qt.Assert(b, rxBuiltMain.Match(out), qt.IsTrue)
  103. matches := rxGarbleAllocs.FindAllSubmatch(out, -1)
  104. if !cached {
  105. // The non-cached version should have at least a handful of
  106. // sub-processes; catch if our logic breaks.
  107. qt.Assert(b, len(matches) > 5, qt.IsTrue)
  108. }
  109. for _, match := range matches {
  110. allocs, err := strconv.ParseInt(string(match[1]), 10, 64)
  111. qt.Assert(b, err, qt.IsNil)
  112. memoryAllocs += allocs
  113. }
  114. systemTime += int64(cmd.ProcessState.SystemTime())
  115. }
  116. }
  117. // We can't use "allocs/op" as it's reserved for ReportAllocs.
  118. b.ReportMetric(float64(memoryAllocs)/float64(b.N), "mallocs/op")
  119. b.ReportMetric(float64(cachedTime)/float64(b.N), "cached-ns/op")
  120. b.ReportMetric(float64(systemTime)/float64(b.N), "sys-ns/op")
  121. info, err := os.Stat(outputBin)
  122. if err != nil {
  123. b.Fatal(err)
  124. }
  125. b.ReportMetric(float64(info.Size()), "bin-B")
  126. }