xref: /aosp_15_r20/external/bazelbuild-rules_go/go/tools/bazel_benchmark/bazel_benchmark.go (revision 9bb1b549b6a84214c53be0924760be030e66b93a)
1// Copyright 2018 The Bazel Authors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//    http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package main
16
17import (
18	"bytes"
19	"encoding/csv"
20	"errors"
21	"flag"
22	"fmt"
23	"io/ioutil"
24	"log"
25	"os"
26	"os/exec"
27	"path/filepath"
28	"strings"
29	"text/template"
30	"time"
31)
32
33var programName = filepath.Base(os.Args[0])
34
35type substitutions struct {
36	RulesGoDir string
37}
38
39type serverState int
40
41const (
42	asleep serverState = iota
43	awake
44)
45
46type cleanState int
47
48const (
49	clean cleanState = iota
50	incr
51)
52
53type benchmark struct {
54	desc        string
55	serverState serverState
56	cleanState  cleanState
57	incrFile    string
58	targets     []string
59	result      time.Duration
60}
61
62var benchmarks = []benchmark{
63	{
64		desc:        "hello_asleep_clean",
65		serverState: asleep,
66		cleanState:  clean,
67		targets:     []string{"//:hello"},
68	}, {
69		desc:        "hello_awake_clean",
70		serverState: awake,
71		cleanState:  clean,
72		targets:     []string{"//:hello"},
73	}, {
74		desc:        "hello_asleep_incr",
75		serverState: asleep,
76		cleanState:  incr,
77		incrFile:    "hello.go",
78		targets:     []string{"//:hello"},
79	}, {
80		desc:        "hello_awake_incr",
81		serverState: awake,
82		cleanState:  incr,
83		incrFile:    "hello.go",
84		targets:     []string{"//:hello"},
85	}, {
86		desc:        "popular_repos_awake_clean",
87		serverState: awake,
88		cleanState:  clean,
89		targets:     []string{"@io_bazel_rules_go//tests/integration/popular_repos:all"},
90	},
91	// TODO: more substantial Kubernetes targets
92}
93
94func main() {
95	log.SetFlags(0)
96	log.SetPrefix(programName + ": ")
97	if err := run(os.Args[1:]); err != nil {
98		log.Fatal(err)
99	}
100}
101
102func run(args []string) error {
103	fs := flag.NewFlagSet(programName, flag.ExitOnError)
104	var rulesGoDir, outPath string
105	fs.StringVar(&rulesGoDir, "rules_go_dir", "", "directory where rules_go is checked out")
106	fs.StringVar(&outPath, "out", "", "csv file to append results to")
107	var keep bool
108	fs.BoolVar(&keep, "keep", false, "if true, the workspace directory won't be deleted at the end")
109	if err := fs.Parse(args); err != nil {
110		return err
111	}
112	if rulesGoDir == "" {
113		return errors.New("-rules_go_dir not set")
114	}
115	if abs, err := filepath.Abs(rulesGoDir); err != nil {
116		return err
117	} else {
118		rulesGoDir = abs
119	}
120	if outPath == "" {
121		return errors.New("-out not set")
122	}
123	if abs, err := filepath.Abs(outPath); err != nil {
124		return err
125	} else {
126		outPath = abs
127	}
128
129	commit, err := getCommit(rulesGoDir)
130	if err != nil {
131		return err
132	}
133
134	dir, err := setupWorkspace(rulesGoDir)
135	if err != nil {
136		return err
137	}
138	if !keep {
139		defer cleanupWorkspace(dir)
140	}
141
142	bazelVersion, err := getBazelVersion()
143	if err != nil {
144		return err
145	}
146
147	log.Printf("running benchmarks in %s", dir)
148	targetSet := make(map[string]bool)
149	for _, b := range benchmarks {
150		for _, t := range b.targets {
151			targetSet[t] = true
152		}
153	}
154	allTargets := make([]string, 0, len(targetSet))
155	for t := range targetSet {
156		allTargets = append(allTargets, t)
157	}
158	fetch(allTargets)
159
160	for i := range benchmarks {
161		b := &benchmarks[i]
162		log.Printf("running benchmark %d/%d: %s", i+1, len(benchmarks), b.desc)
163		if err := runBenchmark(b); err != nil {
164			return fmt.Errorf("error running benchmark %s: %v", b.desc, err)
165		}
166	}
167
168	log.Printf("writing results to %s", outPath)
169	return recordResults(outPath, time.Now().UTC(), bazelVersion, commit, benchmarks)
170}
171
172func getCommit(rulesGoDir string) (commit string, err error) {
173	wd, err := os.Getwd()
174	if err != nil {
175		return "", err
176	}
177	if err := os.Chdir(rulesGoDir); err != nil {
178		return "", err
179	}
180	defer func() {
181		if cderr := os.Chdir(wd); cderr != nil {
182			if err != nil {
183				err = cderr
184			}
185		}
186	}()
187	out, err := exec.Command("git", "rev-parse", "HEAD").Output()
188	if err != nil {
189		return "", err
190	}
191	outStr := strings.TrimSpace(string(out))
192	if len(outStr) < 7 {
193		return "", errors.New("git output too short")
194	}
195	return outStr[:7], nil
196}
197
198func setupWorkspace(rulesGoDir string) (workspaceDir string, err error) {
199	workspaceDir, err = ioutil.TempDir("", "bazel_benchmark")
200	if err != nil {
201		return "", err
202	}
203	defer func() {
204		if err != nil {
205			os.RemoveAll(workspaceDir)
206		}
207	}()
208	benchmarkDir := filepath.Join(rulesGoDir, "go", "tools", "bazel_benchmark")
209	files, err := ioutil.ReadDir(benchmarkDir)
210	if err != nil {
211		return "", err
212	}
213	substitutions := substitutions{
214		RulesGoDir: filepath.Join(benchmarkDir, "..", "..", ".."),
215	}
216	for _, f := range files {
217		name := f.Name()
218		if filepath.Ext(name) != ".in" {
219			continue
220		}
221		srcPath := filepath.Join(benchmarkDir, name)
222		tpl, err := template.ParseFiles(srcPath)
223		if err != nil {
224			return "", err
225		}
226		dstPath := filepath.Join(workspaceDir, name[:len(name)-len(".in")])
227		out, err := os.Create(dstPath)
228		if err != nil {
229			return "", err
230		}
231		if err := tpl.Execute(out, substitutions); err != nil {
232			out.Close()
233			return "", err
234		}
235		if err := out.Close(); err != nil {
236			return "", err
237		}
238	}
239	if err := os.Chdir(workspaceDir); err != nil {
240		return "", err
241	}
242	return workspaceDir, nil
243}
244
245func cleanupWorkspace(dir string) error {
246	if err := logBazelCommand("clean", "--expunge"); err != nil {
247		return err
248	}
249	return os.RemoveAll(dir)
250}
251
252func getBazelVersion() (string, error) {
253	out, err := exec.Command("bazel", "version").Output()
254	if err != nil {
255		return "", err
256	}
257	prefix := []byte("Build label: ")
258	i := bytes.Index(out, prefix)
259	if i < 0 {
260		return "", errors.New("could not find bazel version in output")
261	}
262	out = out[i+len(prefix):]
263	i = bytes.IndexByte(out, '\n')
264	if i >= 0 {
265		out = out[:i]
266	}
267	return string(out), nil
268}
269
270func fetch(targets []string) error {
271	return logBazelCommand("fetch", targets...)
272}
273
274func runBenchmark(b *benchmark) error {
275	switch b.cleanState {
276	case clean:
277		if err := logBazelCommand("clean"); err != nil {
278			return err
279		}
280	case incr:
281		if err := logBazelCommand("build", b.targets...); err != nil {
282			return err
283		}
284		if b.incrFile == "" {
285			return errors.New("incrFile not set")
286		}
287		data, err := ioutil.ReadFile(b.incrFile)
288		if err != nil {
289			return err
290		}
291		data = bytes.Replace(data, []byte("INCR"), []byte("INCR."), -1)
292		if err := ioutil.WriteFile(b.incrFile, data, 0666); err != nil {
293			return err
294		}
295	}
296	if b.serverState == asleep {
297		if err := logBazelCommand("shutdown"); err != nil {
298			return err
299		}
300	}
301	start := time.Now()
302	if err := logBazelCommand("build", b.targets...); err != nil {
303		return err
304	}
305	b.result = time.Since(start)
306	return nil
307}
308
309func recordResults(outPath string, t time.Time, bazelVersion, commit string, benchmarks []benchmark) (err error) {
310	// TODO(jayconrod): update the header if new columns are added.
311	columnMap, outExists, err := buildColumnMap(outPath, benchmarks)
312	header := buildHeader(columnMap)
313	record := buildRecord(t, bazelVersion, commit, benchmarks, columnMap)
314	defer func() {
315		if err != nil {
316			log.Printf("error writing results: %s: %v", outPath, err)
317			log.Print("data are printed below")
318			log.Print(strings.Join(header, ","))
319			log.Print(strings.Join(record, ","))
320		}
321	}()
322	outFile, err := os.OpenFile(outPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
323	if err != nil {
324		return err
325	}
326	defer func() {
327		if cerr := outFile.Close(); err != nil {
328			err = cerr
329		}
330	}()
331	outCsv := csv.NewWriter(outFile)
332	if !outExists {
333		outCsv.Write(header)
334	}
335	outCsv.Write(record)
336	outCsv.Flush()
337	return outCsv.Error()
338}
339
340func logBazelCommand(command string, args ...string) error {
341	args = append([]string{command}, args...)
342	cmd := exec.Command("bazel", args...)
343	log.Printf("bazel %s\n", strings.Join(args, " "))
344	cmd.Stdout = os.Stderr
345	cmd.Stderr = os.Stderr
346	return cmd.Run()
347}
348
349func buildColumnMap(outPath string, benchmarks []benchmark) (columnMap map[string]int, outExists bool, err error) {
350	columnMap = make(map[string]int)
351	{
352		inFile, oerr := os.Open(outPath)
353		if oerr != nil {
354			goto doneReading
355		}
356		outExists = true
357		defer inFile.Close()
358		inCsv := csv.NewReader(inFile)
359		var header []string
360		header, err = inCsv.Read()
361		if err != nil {
362			goto doneReading
363		}
364		for i, column := range header {
365			columnMap[column] = i
366		}
367	}
368
369doneReading:
370	for _, s := range []string{"time", "bazel_version", "commit"} {
371		if _, ok := columnMap[s]; !ok {
372			columnMap[s] = len(columnMap)
373		}
374	}
375	for _, b := range benchmarks {
376		if _, ok := columnMap[b.desc]; !ok {
377			columnMap[b.desc] = len(columnMap)
378		}
379	}
380	return columnMap, outExists, err
381}
382
383func buildHeader(columnMap map[string]int) []string {
384	header := make([]string, len(columnMap))
385	for name, i := range columnMap {
386		header[i] = name
387	}
388	return header
389}
390
391func buildRecord(t time.Time, bazelVersion, commit string, benchmarks []benchmark, columnMap map[string]int) []string {
392	record := make([]string, len(columnMap))
393	record[columnMap["time"]] = t.Format("2006-01-02 15:04:05")
394	record[columnMap["bazel_version"]] = bazelVersion
395	record[columnMap["commit"]] = commit
396	for _, b := range benchmarks {
397		record[columnMap[b.desc]] = fmt.Sprintf("%.3f", b.result.Seconds())
398	}
399	return record
400}
401