sweet: add esbuild benchmark This is a fairly mature CLI application that advertises its speed. The benchmark added here is a bit short, but we can add more benchmarks or build our own. We just want esbuild to do something interesting. Change-Id: Ic0ed62aa17cc61315bdd96df90d3d059a560d2e2 Reviewed-on: https://go-review.googlesource.com/c/benchmarks/+/614538 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Michael Pratt <mpratt@google.com>
diff --git a/sweet/benchmarks/esbuild/main.go b/sweet/benchmarks/esbuild/main.go new file mode 100644 index 0000000..e4aeab8 --- /dev/null +++ b/sweet/benchmarks/esbuild/main.go
@@ -0,0 +1,168 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + "path/filepath" + + "golang.org/x/benchmarks/sweet/benchmarks/internal/cgroups" + "golang.org/x/benchmarks/sweet/benchmarks/internal/driver" + "golang.org/x/benchmarks/sweet/common/diagnostics" +) + +var ( + esbuildBin string + esbuildSrc string + tmpDir string + benchName string +) + +func init() { + driver.SetFlags(flag.CommandLine) + flag.StringVar(&esbuildBin, "bin", "", "path to esbuild binary") + flag.StringVar(&esbuildSrc, "src", "", "path to JS/TS to pack") + flag.StringVar(&tmpDir, "tmp", "", "work directory (cleared before use)") + flag.StringVar(&benchName, "bench", "", "benchmark name") +} + +func main() { + flag.Parse() + if esbuildBin == "" { + fmt.Fprintln(os.Stderr, "expected non-empty bin flag") + os.Exit(1) + } + if esbuildSrc == "" { + fmt.Fprintln(os.Stderr, "expected non-empty src flag") + os.Exit(1) + } + if tmpDir == "" { + fmt.Fprintln(os.Stderr, "expected non-empty tmp flag") + os.Exit(1) + } + if err := run(benchName, esbuildBin, esbuildSrc, tmpDir); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +var benchArgsFuncs = map[string]func(src, tmp string) []string{ + // Args taken from https://github.com/evanw/esbuild/blob/main/Makefile. + "ThreeJS": func(src, tmp string) []string { + return []string{"--bundle", + "--global-name=THREE", + "--sourcemap", + "--minify", + "--timing", + "--outfile=" + filepath.Join(tmp, "out-three.js"), + filepath.Join(src, "src", "entry.js"), + } + }, + "RomeTS": func(src, tmp string) []string { + return []string{"--bundle", + "--platform=node", + "--sourcemap", + "--minify", + "--timing", + "--outfile=" + filepath.Join(tmp, "out-rome.js"), + filepath.Join(src, "src", "entry.ts"), + } + }, + "ReactAdminJS": func(src, tmp string) []string { + return []string{ + "--alias:data-generator-retail=" + filepath.Join(src, "repo/examples/data-generator/src"), + "--alias:ra-core=" + filepath.Join(src, "repo/packages/ra-core/src"), + "--alias:ra-data-fakerest=" + filepath.Join(src, "repo/packages/ra-data-fakerest/src"), + "--alias:ra-data-graphql-simple=" + filepath.Join(src, "repo/packages/ra-data-graphql-simple/src"), + "--alias:ra-data-graphql=" + filepath.Join(src, "repo/packages/ra-data-graphql/src"), + "--alias:ra-data-simple-rest=" + filepath.Join(src, "repo/packages/ra-data-simple-rest/src"), + "--alias:ra-i18n-polyglot=" + filepath.Join(src, "repo/packages/ra-i18n-polyglot/src"), + "--alias:ra-input-rich-text=" + filepath.Join(src, "repo/packages/ra-input-rich-text/src"), + "--alias:ra-language-english=" + filepath.Join(src, "repo/packages/ra-language-english/src"), + "--alias:ra-language-french=" + filepath.Join(src, "repo/packages/ra-language-french/src"), + "--alias:ra-ui-materialui=" + filepath.Join(src, "repo/packages/ra-ui-materialui/src"), + "--alias:react-admin=" + filepath.Join(src, "repo/packages/react-admin/src"), + "--bundle", + "--define:process.env.REACT_APP_DATA_PROVIDER=null", + "--format=esm", + "--loader:.png=file", + "--loader:.svg=file", + "--minify", + "--sourcemap", + "--splitting", + "--target=esnext", + "--timing", + "--outdir=" + filepath.Join(tmp, "out-readmin"), + filepath.Join(src, "repo/examples/demo/src/index.tsx"), + } + }, +} + +func run(name, bin, src, tmp string) error { + // Get the args for this benchmark. + argsFunc, ok := benchArgsFuncs[name] + if !ok { + return fmt.Errorf("unknown benchmark %s", name) + } + cmdArgs := append([]string{bin}, argsFunc(src, tmp)...) + + // Add prefix to benchmark name. + name = "ESBuild" + name + + // Set up diagnostics. + var diagFiles []*driver.DiagnosticFile + diag := driver.NewDiagnostics(name) + if df, err := diag.Create(diagnostics.Perf); err != nil { + fmt.Fprintf(os.Stderr, "failed to create %s diagnostics: %s\n", diagnostics.Perf, err) + } else if df != nil { + df.Close() + diagFiles = append(diagFiles, df) + + perfArgs := []string{"perf", "record", "-o", df.Name()} + perfArgs = append(perfArgs, driver.PerfFlags()...) + perfArgs = append(perfArgs, cmdArgs...) + cmdArgs = perfArgs + } + for _, typ := range []diagnostics.Type{diagnostics.CPUProfile, diagnostics.MemProfile, diagnostics.Trace} { + df, err := diag.Create(typ) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to create %s diagnostics: %s\n", typ, err) + continue + } else if df != nil { + df.Close() + diagFiles = append(diagFiles, df) + + flag := "--" + string(typ) + if typ == diagnostics.MemProfile { + flag = "--heap" + } + // N.B. Flags in esbuild are fairly idiosyncratic. Flags that accept a parameter + // need to appear after an "=" character without spaces between the flag or the + // parameter. + cmdArgs = append(cmdArgs, flag+"="+df.Name()) + } + } + + baseCmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) + baseCmd.Stdout = os.Stderr // Redirect all tool output to stderr. + baseCmd.Stderr = os.Stderr + cmd, err := cgroups.WrapCommand(baseCmd, "test.scope") + if err != nil { + return err + } + return driver.RunBenchmark(name, func(d *driver.B) error { + defer diag.Commit(d) + defer func() { + for _, df := range diagFiles { + df.Commit() + } + }() + defer d.StopTimer() + return cmd.Run() + }, []driver.RunOption{driver.DoTime(true), driver.DoAvgRSS(cmd.RSSFunc())}...) +}
diff --git a/sweet/cmd/sweet/benchmark.go b/sweet/cmd/sweet/benchmark.go index 21136d6..2b2fa7c 100644 --- a/sweet/cmd/sweet/benchmark.go +++ b/sweet/cmd/sweet/benchmark.go
@@ -55,6 +55,12 @@ generator: generators.None{}, }, { + name: "esbuild", + description: "JavaScript/Typescript bundler", + harness: &harnesses.ESBuild{}, + generator: generators.None{}, + }, + { name: "go-build", description: "Go build command", harness: harnesses.GoBuild{}, @@ -101,6 +107,7 @@ allBenchmarksMap["bleve-index"], allBenchmarksMap["cockroachdb"], allBenchmarksMap["etcd"], + allBenchmarksMap["esbuild"], allBenchmarksMap["go-build"], allBenchmarksMap["gopher-lua"], }
diff --git a/sweet/cmd/sweet/integration_test.go b/sweet/cmd/sweet/integration_test.go index 38676fb..0722f79 100644 --- a/sweet/cmd/sweet/integration_test.go +++ b/sweet/cmd/sweet/integration_test.go
@@ -209,6 +209,7 @@ {"go-build", 4}, {"cockroachdb", 1}, {"etcd", 1}, + {"esbuild", 1}, {"bleve-index", 1}, {"gopher-lua", 1}, {"markdown", 1},
diff --git a/sweet/harnesses/esbuild.go b/sweet/harnesses/esbuild.go new file mode 100644 index 0000000..b333f51 --- /dev/null +++ b/sweet/harnesses/esbuild.go
@@ -0,0 +1,115 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package harnesses + +import ( + "errors" + "fmt" + "os/exec" + "path/filepath" + + "golang.org/x/benchmarks/sweet/common" + "golang.org/x/benchmarks/sweet/common/log" +) + +type ESBuild struct { + haveYarn bool +} + +func (h *ESBuild) CheckPrerequisites() error { + // Check if we have the yarn command. + if _, err := exec.LookPath("yarn"); err == nil { + h.haveYarn = true + } else if !errors.Is(err, exec.ErrNotFound) { + return err + } + return nil +} + +func (h *ESBuild) Get(gcfg *common.GetConfig) error { + err := gitShallowClone( + gcfg.SrcDir, + "https://github.com/evanw/esbuild", + "v0.23.1", + ) + if err != nil { + return err + } + runMake := func(rules ...string) error { + cmd := exec.Command("make", append([]string{"-C", gcfg.SrcDir}, rules...)...) + log.TraceCommand(cmd, false) + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to run make: %v: output:\n%s", err, out) + } + return nil + } + // Fetch downstream benchmark dependencies. + if err := runMake("bench/three"); err != nil { + return err + } + if h.haveYarn { + if err := runMake("bench/readmin"); err != nil { + return err + } + } + // Run the command but ignore errors. There's something weird going on with + // a sed command in this make rule, but we don't actually need it to succeed + // to run the benchmarks. + _ = runMake("bench/rome") + return nil +} + +func (h *ESBuild) Build(cfg *common.Config, bcfg *common.BuildConfig) error { + // Generate a symlink to the repository and put it in bin. + // It's not a binary, but it's the only place we can put it + // and still access it in Run. + link := filepath.Join(bcfg.BinDir, "esbuild-src") + err := symlink(link, bcfg.SrcDir) + if err != nil { + return err + } + // Build driver. + if err := cfg.GoTool().BuildPath(bcfg.BenchDir, filepath.Join(bcfg.BinDir, "esbuild-bench")); err != nil { + return err + } + // Build esbuild. + return cfg.GoTool().BuildPath(filepath.Join(bcfg.SrcDir, "cmd", "esbuild"), filepath.Join(bcfg.BinDir, "esbuild")) +} + +func (h *ESBuild) Run(cfg *common.Config, rcfg *common.RunConfig) error { + for _, b := range esbuildBenchmarks { + if b.needYarn && !h.haveYarn { + continue + } + cmd := exec.Command( + filepath.Join(rcfg.BinDir, "esbuild-bench"), + "-bin", filepath.Join(rcfg.BinDir, "esbuild"), + "-src", filepath.Join(rcfg.BinDir, "esbuild-src", b.src), + "-tmp", rcfg.TmpDir, + "-bench", b.name, + ) + cmd.Args = append(cmd.Args, rcfg.Args...) + cmd.Env = cfg.ExecEnv.Collapse() + cmd.Stdout = rcfg.Results + cmd.Stderr = rcfg.Log + log.TraceCommand(cmd, false) + if err := cmd.Run(); err != nil { + return err + } + } + return nil +} + +type esbuildBenchmark struct { + name string + src string + needYarn bool +} + +var esbuildBenchmarks = []esbuildBenchmark{ + {"ThreeJS", filepath.Join("bench", "three"), false}, + {"RomeTS", filepath.Join("bench", "rome"), false}, + {"ReactAdminJS", filepath.Join("bench", "readmin"), true}, +}