compilebench: add a linker benchmark

This links cmd/compile, which is the largest binary in GOROOT.

Unfortunately, this can't measure the linker's allocation metrics
prior to CL 176057 because the fix for golang/go#18641 was applied to
the compiler but not the linker.

Change-Id: Ia44d80f1d727c1608cedaa4b8ad2a05903774875
Reviewed-on: https://go-review.googlesource.com/c/tools/+/175801
Run-TryBot: Austin Clements <austin@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Russ Cox <rsc@golang.org>
Reviewed-by: Cherry Zhang <cherryyz@google.com>
diff --git a/cmd/compilebench/main.go b/cmd/compilebench/main.go
index 0233821..029308a 100644
--- a/cmd/compilebench/main.go
+++ b/cmd/compilebench/main.go
@@ -22,6 +22,12 @@
 //	-compileflags 'list'
 //		Pass the space-separated list of flags to the compilation.
 //
+//	-link exe
+//		Use exe as the path to the cmd/link binary.
+//
+//	-linkflags 'list'
+//		Pass the space-separated list of flags to the linker.
+//
 //	-count n
 //		Run each benchmark n times (default 1).
 //
@@ -90,6 +96,7 @@
 var (
 	goroot   string
 	compiler string
+	linker   string
 	runRE    *regexp.Regexp
 	is6g     bool
 )
@@ -100,6 +107,8 @@
 	flagObj            = flag.Bool("obj", false, "report object file stats")
 	flagCompiler       = flag.String("compile", "", "use `exe` as the cmd/compile binary")
 	flagCompilerFlags  = flag.String("compileflags", "", "additional `flags` to pass to compile")
+	flagLinker         = flag.String("link", "", "use `exe` as the cmd/link binary")
+	flagLinkerFlags    = flag.String("linkflags", "", "additional `flags` to pass to link")
 	flagRun            = flag.String("run", "", "run benchmarks matching `regexp`")
 	flagCount          = flag.Int("count", 1, "run benchmarks `n` times")
 	flagCpuprofile     = flag.String("cpuprofile", "", "write CPU profile to `file`")
@@ -130,6 +139,7 @@
 	{"BenchmarkReflect", compile{"reflect"}},
 	{"BenchmarkTar", compile{"archive/tar"}},
 	{"BenchmarkXML", compile{"encoding/xml"}},
+	{"BenchmarkLinkCompiler", link{"cmd/compile"}},
 	{"BenchmarkStdCmd", goBuild{[]string{"std", "cmd"}}},
 	{"BenchmarkHelloSize", size{"$GOROOT/test/helloworld.go", false}},
 	{"BenchmarkCmdGoSize", size{"cmd/go", true}},
@@ -160,16 +170,16 @@
 
 	compiler = *flagCompiler
 	if compiler == "" {
-		out, err := exec.Command(*flagGoCmd, "tool", "-n", "compile").CombinedOutput()
-		if err != nil {
-			out, err = exec.Command(*flagGoCmd, "tool", "-n", "6g").CombinedOutput()
+		var foundTool string
+		foundTool, compiler = toolPath("compile", "6g")
+		if foundTool == "6g" {
 			is6g = true
-			if err != nil {
-				out, err = exec.Command(*flagGoCmd, "tool", "-n", "compile").CombinedOutput()
-				log.Fatalf("go tool -n compiler: %v\n%s", err, out)
-			}
 		}
-		compiler = strings.TrimSpace(string(out))
+	}
+
+	linker = *flagLinker
+	if linker == "" && !is6g { // TODO: Support 6l
+		_, linker = toolPath("link")
 	}
 
 	if is6g {
@@ -190,6 +200,7 @@
 	if *flagPackage != "" {
 		tests = []test{
 			{"BenchmarkPkg", compile{*flagPackage}},
+			{"BenchmarkPkgLink", link{*flagPackage}},
 		}
 		runRE = nil
 	}
@@ -208,6 +219,39 @@
 	}
 }
 
+func toolPath(names ...string) (found, path string) {
+	var out1 []byte
+	var err1 error
+	for i, name := range names {
+		out, err := exec.Command(*flagGoCmd, "tool", "-n", name).CombinedOutput()
+		if err == nil {
+			return name, strings.TrimSpace(string(out))
+		}
+		if i == 0 {
+			out1, err1 = out, err
+		}
+	}
+	log.Fatalf("go tool -n %s: %v\n%s", names[0], err1, out1)
+	return "", ""
+}
+
+type Pkg struct {
+	Dir     string
+	GoFiles []string
+}
+
+func goList(dir string) (*Pkg, error) {
+	var pkg Pkg
+	out, err := exec.Command(*flagGoCmd, "list", "-json", dir).Output()
+	if err != nil {
+		return nil, fmt.Errorf("go list -json %s: %v\n", dir, err)
+	}
+	if err := json.Unmarshal(out, &pkg); err != nil {
+		return nil, fmt.Errorf("go list -json %s: unmarshal: %v", dir, err)
+	}
+	return &pkg, nil
+}
+
 func runCmd(name string, cmd *exec.Cmd) error {
 	start := time.Now()
 	out, err := cmd.CombinedOutput()
@@ -286,16 +330,9 @@
 	}
 
 	// Find dir and source file list.
-	var pkg struct {
-		Dir     string
-		GoFiles []string
-	}
-	out, err = exec.Command(*flagGoCmd, "list", "-json", c.dir).Output()
+	pkg, err := goList(c.dir)
 	if err != nil {
-		return fmt.Errorf("go list -json %s: %v\n", c.dir, err)
-	}
-	if err := json.Unmarshal(out, &pkg); err != nil {
-		return fmt.Errorf("go list -json %s: unmarshal: %v", c.dir, err)
+		return err
 	}
 
 	args := []string{"-o", "_compilebench_.o"}
@@ -324,6 +361,52 @@
 	return nil
 }
 
+type link struct{ dir string }
+
+func (link) long() bool { return false }
+
+func (r link) run(name string, count int) error {
+	if linker == "" {
+		// No linker. Skip the test.
+		return nil
+	}
+
+	// Build dependencies.
+	out, err := exec.Command(*flagGoCmd, "build", "-i", "-o", "/dev/null", r.dir).CombinedOutput()
+	if err != nil {
+		return fmt.Errorf("go build -i %s: %v\n%s", r.dir, err, out)
+	}
+
+	// Build the main package.
+	pkg, err := goList(r.dir)
+	if err != nil {
+		return err
+	}
+	args := []string{"-o", "_compilebench_.o"}
+	args = append(args, pkg.GoFiles...)
+	cmd := exec.Command(compiler, args...)
+	cmd.Dir = pkg.Dir
+	cmd.Stdout = os.Stderr
+	cmd.Stderr = os.Stderr
+	err = cmd.Run()
+	if err != nil {
+		return fmt.Errorf("compiling: %v", err)
+	}
+	defer os.Remove(pkg.Dir + "/_compilebench_.o")
+
+	// Link the main package.
+	args = []string{"-o", "_compilebench_.exe"}
+	args = append(args, strings.Fields(*flagLinkerFlags)...)
+	args = append(args, "_compilebench_.o")
+	if err := runBuildCmd(name, count, pkg.Dir, linker, args); err != nil {
+		return err
+	}
+	fmt.Println()
+	defer os.Remove(pkg.Dir + "/_compilebench_.exe")
+
+	return err
+}
+
 // runBuildCmd runs "tool args..." in dir, measures standard build
 // tool metrics, and prints a benchmark line. The caller may print
 // additional metrics and then must print a newline.