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