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},
+}