You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
garble/bench_test.go

144 lines
4.6 KiB
Go

// Copyright (c) 2020, The Garble Authors.
// See LICENSE for licensing information.
package main
import (
_ "embed"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"testing"
"time"
"github.com/go-quicktest/qt"
)
//go:embed testdata/bench/main.go
var benchSourceMain []byte
var (
rxBuiltRuntime = regexp.MustCompile(`(?m)^runtime$`)
rxBuiltMain = regexp.MustCompile(`(?m)^test/main$`)
)
// BenchmarkBuild is a benchmark for 'garble build' on a fairly simple
// main package with a handful of standard library depedencies.
//
// We use a real garble binary and exec it, to simulate what the real user would
// run. The real obfuscation and compilation will happen in sub-processes
// anyway, so skipping one exec layer doesn't help us in any way.
//
// The benchmark isn't parallel, because in practice users build once at a time,
// and each build already spawns concurrent processes and goroutines to do work.
//
// At the moment, each iteration takes 1-2s on a laptop, so we can't make the
// benchmark include any more features unless we make it significantly faster.
func BenchmarkBuild(b *testing.B) {
// As of Go 1.17, using -benchtime=Nx with N larger than 1 results in two
// calls to BenchmarkBuild, with the first having b.N==1 to discover
// sub-benchmarks. Unfortunately, we do a significant amount of work both
// during setup and during that first iteration, which is pointless.
// To avoid that, detect the scenario in a hacky way, and return early.
// See https://github.com/golang/go/issues/32051.
benchtime := flag.Lookup("test.benchtime").Value.String()
if b.N == 1 && strings.HasSuffix(benchtime, "x") && benchtime != "1x" {
return
}
tdir := b.TempDir()
// We collect extra metrics.
var memoryAllocs, cachedTime, systemTime int64
outputBin := filepath.Join(tdir, "output")
sourceDir := filepath.Join(tdir, "src")
qt.Assert(b, qt.IsNil(os.Mkdir(sourceDir, 0o777)))
writeSourceFile := func(name string, content []byte) {
err := os.WriteFile(filepath.Join(sourceDir, name), content, 0o666)
qt.Assert(b, qt.IsNil(err))
}
writeSourceFile("go.mod", []byte("module test/main"))
writeSourceFile("main.go", benchSourceMain)
rxGarbleAllocs := regexp.MustCompile(`(?m)^garble allocs: ([0-9]+)`)
b.ResetTimer()
b.StopTimer()
for i := range b.N {
// First we do a fresh build, using empty cache directories,
// and the second does an incremental rebuild reusing the same cache directories.
goCache := filepath.Join(tdir, "go-cache")
qt.Assert(b, qt.IsNil(os.RemoveAll(goCache)))
qt.Assert(b, qt.IsNil(os.Mkdir(goCache, 0o777)))
garbleCache := filepath.Join(tdir, "garble-cache")
qt.Assert(b, qt.IsNil(os.RemoveAll(garbleCache)))
qt.Assert(b, qt.IsNil(os.Mkdir(garbleCache, 0o777)))
env := []string{
"RUN_GARBLE_MAIN=true",
"GOCACHE=" + goCache,
"GARBLE_CACHE=" + garbleCache,
"GARBLE_WRITE_ALLOCS=true",
}
args := []string{"build", "-v", "-o=" + outputBin, sourceDir}
for _, cached := range []bool{false, true} {
// The cached rebuild will reuse all dependencies,
// but rebuild the main package itself.
if cached {
writeSourceFile("rebuild.go", []byte(fmt.Sprintf("package main\nvar v%d int", i)))
}
cmd := exec.Command(os.Args[0], args...)
cmd.Env = append(cmd.Environ(), env...)
cmd.Dir = sourceDir
cachedStart := time.Now()
b.StartTimer()
out, err := cmd.CombinedOutput()
b.StopTimer()
if cached {
cachedTime += time.Since(cachedStart).Nanoseconds()
}
qt.Assert(b, qt.IsNil(err), qt.Commentf("output: %s", out))
if !cached {
// Ensure that we built all packages, as expected.
qt.Assert(b, qt.IsTrue(rxBuiltRuntime.Match(out)))
} else {
// Ensure that we only rebuilt the main package, as expected.
qt.Assert(b, qt.IsFalse(rxBuiltRuntime.Match(out)))
}
qt.Assert(b, qt.IsTrue(rxBuiltMain.Match(out)))
matches := rxGarbleAllocs.FindAllSubmatch(out, -1)
if !cached {
// The non-cached version should have at least a handful of
// sub-processes; catch if our logic breaks.
qt.Assert(b, qt.IsTrue(len(matches) > 5))
}
for _, match := range matches {
allocs, err := strconv.ParseInt(string(match[1]), 10, 64)
qt.Assert(b, qt.IsNil(err))
memoryAllocs += allocs
}
systemTime += int64(cmd.ProcessState.SystemTime())
}
}
// We can't use "allocs/op" as it's reserved for ReportAllocs.
b.ReportMetric(float64(memoryAllocs)/float64(b.N), "mallocs/op")
b.ReportMetric(float64(cachedTime)/float64(b.N), "cached-ns/op")
b.ReportMetric(float64(systemTime)/float64(b.N), "sys-ns/op")
info, err := os.Stat(outputBin)
if err != nil {
b.Fatal(err)
}
b.ReportMetric(float64(info.Size()), "bin-B")
}