xref: /aosp_15_r20/external/skia/infra/bots/task_drivers/canvaskit_gold/canvaskit_gold.go (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1// Copyright 2022 Google LLC
2//
3// Use of this source code is governed by a BSD-style license that can be
4// found in the LICENSE file.
5//
6// This executable builds and tests CanvasKit. The tests produce images (aka gms) and these
7// are uploaded to Gold.
8// It requires unzip to be installed (which Bazel already requires).
9package main
10
11import (
12	"context"
13	"flag"
14	"fmt"
15	"os"
16	"path/filepath"
17	"strconv"
18	"strings"
19
20	sk_exec "go.skia.org/infra/go/exec"
21	"go.skia.org/infra/task_driver/go/lib/bazel"
22	"go.skia.org/infra/task_driver/go/lib/os_steps"
23	"go.skia.org/infra/task_driver/go/td"
24	"go.skia.org/skia/infra/bots/task_drivers/common"
25)
26
27// This value is arbitrarily selected. It is smaller than our maximum RBE pool size.
28const rbeJobs = 100
29
30var (
31	// Required properties for this task.
32	projectId = flag.String("project_id", "", "ID of the Google Cloud project.")
33	taskId    = flag.String("task_id", "", "ID of this task.")
34	taskName  = flag.String("task_name", "", "Name of the task.")
35	workdir   = flag.String("workdir", ".", "Working directory, the root directory of a full Skia checkout")
36	// goldctl data
37	goldctlPath      = flag.String("goldctl_path", "", "The path to the golctl binary on disk.")
38	gitCommit        = flag.String("git_commit", "", "The git hash to which the data should be associated. This will be used when changelist_id and patchset_order are not set to report data to Gold that belongs on the primary branch.")
39	changelistID     = flag.String("changelist_id", "", "Should be non-empty only when run on the CQ.")
40	patchsetOrderStr = flag.String("patchset_order", "", "Should be non-zero only when run on the CQ.")
41	tryjobID         = flag.String("tryjob_id", "", "Should be non-zero only when run on the CQ.")
42	// goldctl keys
43	browser         = flag.String("browser", "Chrome", "The browser running the tests")
44	compilationMode = flag.String("compilation_mode", "Release", "How the binary was compiled")
45	cpuOrGPU        = flag.String("cpu_or_gpu", "GPU", "The render backend")
46	cpuOrGPUValue   = flag.String("cpu_or_gpu_value", "WebGL2", "What variant of the render backend")
47
48	// Optional flags.
49	local  = flag.Bool("local", false, "True if running locally (as opposed to on the CI/CQ)")
50	output = flag.String("o", "", "If provided, dump a JSON blob of step data to the given file. Prints to stdout if '-' is given.")
51)
52
53func main() {
54	bazelFlags := common.MakeBazelFlags(common.MakeBazelFlagsOpts{
55		Label:  true,
56		Config: true,
57	})
58
59	ctx := td.StartRun(projectId, taskId, taskName, output, local)
60	defer td.EndRun(ctx)
61
62	bazelFlags.Validate(ctx)
63
64	goldctlAbsPath := td.MustGetAbsolutePathOfFlag(ctx, *goldctlPath, "gold_ctl_path")
65	wd := td.MustGetAbsolutePathOfFlag(ctx, *workdir, "workdir")
66	skiaDir := filepath.Join(wd, "skia")
67	patchsetOrder := 0
68	if *patchsetOrderStr != "" {
69		var err error
70		patchsetOrder, err = strconv.Atoi(*patchsetOrderStr)
71		if err != nil {
72			fmt.Println("Non-integer value passed in to --patchset_order")
73			td.Fatal(ctx, err)
74		}
75	}
76
77	opts := bazel.BazelOptions{
78		// We want the cache to be on a bigger disk than default. The root disk, where the home
79		// directory (and default Bazel cache) lives, is only 15 GB on our GCE VMs.
80		CachePath: *bazelFlags.CacheDir,
81	}
82	if err := bazel.EnsureBazelRCFile(ctx, opts); err != nil {
83		td.Fatal(ctx, err)
84	}
85
86	if err := bazelTest(ctx, skiaDir, *bazelFlags.Label, *bazelFlags.Config,
87		"--config=linux_rbe", "--test_output=streamed", "--jobs="+strconv.Itoa(rbeJobs)); err != nil {
88		td.Fatal(ctx, err)
89	}
90
91	conf := goldctlConfig{
92		goldctlPath:   goldctlAbsPath,
93		gitCommit:     *gitCommit,
94		changelistID:  *changelistID,
95		patchsetOrder: patchsetOrder,
96		tryjobID:      *tryjobID,
97		corpus:        "canvaskit",
98		keys: map[string]string{
99			"arch":             "wasm32", // https://github.com/bazelbuild/platforms/blob/da5541f26b7de1dc8e04c075c99df5351742a4a2/cpu/BUILD#L109
100			"configuration":    *bazelFlags.Config,
101			"browser":          *browser,
102			"compilation_mode": *compilationMode,
103			"cpu_or_gpu":       *cpuOrGPU,
104			"cpu_or_gpu_value": *cpuOrGPUValue,
105		},
106	}
107	if err := uploadDataToGold(ctx, *bazelFlags.Label, skiaDir, conf); err != nil {
108		td.Fatal(ctx, err)
109	}
110
111	if !*local {
112		if err := common.BazelCleanIfLowDiskSpace(ctx, *bazelFlags.CacheDir, skiaDir, "bazelisk"); err != nil {
113			td.Fatal(ctx, err)
114		}
115	}
116}
117
118func bazelTest(ctx context.Context, checkoutDir, label, config string, args ...string) error {
119	step := fmt.Sprintf("Running Test %s with config %s and %d extra flags", label, config, len(args))
120	return td.Do(ctx, td.Props(step), func(ctx context.Context) error {
121		runCmd := &sk_exec.Command{
122			Name: "bazelisk",
123			Args: append([]string{"test",
124				label,
125				"--config=" + config, // Should be defined in //bazel/buildrc
126			}, args...),
127			InheritEnv: true, // Makes sure bazelisk is on PATH
128			Dir:        checkoutDir,
129			LogStdout:  true,
130			LogStderr:  true,
131		}
132		_, err := sk_exec.RunCommand(ctx, runCmd)
133		if err != nil {
134			return err
135		}
136		return nil
137	})
138}
139
140type goldctlConfig struct {
141	goldctlPath   string
142	gitCommit     string
143	changelistID  string
144	patchsetOrder int
145	tryjobID      string
146	corpus        string
147	keys          map[string]string
148}
149
150func uploadDataToGold(ctx context.Context, label, checkoutDir string, cfg goldctlConfig) error {
151	return td.Do(ctx, td.Props("Upload to Gold"), func(ctx context.Context) error {
152		zipExtractDir, err := os_steps.TempDir(ctx, "", "gold_outputs")
153		if err != nil {
154			return err
155		}
156
157		// Turn "//path/to:target" into "path/to/target".
158		labelBazelTestlogsPath := strings.ReplaceAll(label, "//", "")
159		labelBazelTestlogsPath = strings.ReplaceAll(label, ":", "/")
160
161		if err := extractZip(ctx, filepath.Join(checkoutDir, "bazel-testlogs", labelBazelTestlogsPath, "test.outputs", "outputs.zip"), zipExtractDir); err != nil {
162			return err
163		}
164
165		goldWorkDir, err := os_steps.TempDir(ctx, "", "gold_workdir")
166		if err != nil {
167			return err
168		}
169
170		if err := setupGoldctl(ctx, cfg, goldWorkDir); err != nil {
171			return err
172		}
173
174		if err := addAllGoldImages(ctx, cfg.goldctlPath, zipExtractDir, goldWorkDir); err != nil {
175			return err
176		}
177
178		if err := finalizeGoldctl(ctx, cfg.goldctlPath, goldWorkDir); err != nil {
179			return err
180		}
181		return nil
182	})
183}
184
185func extractZip(ctx context.Context, zipPath, targetDir string) error {
186	runCmd := &sk_exec.Command{
187		Name:      "unzip",
188		Args:      []string{zipPath, "-d", targetDir},
189		LogStdout: true,
190		LogStderr: true,
191	}
192	_, err := sk_exec.RunCommand(ctx, runCmd)
193	if err != nil {
194		return err
195	}
196	return nil
197}
198
199func setupGoldctl(ctx context.Context, cfg goldctlConfig, workDir string) error {
200	authCmd := &sk_exec.Command{
201		Name:      cfg.goldctlPath,
202		Args:      []string{"auth", "--work-dir=" + workDir, "--luci"},
203		LogStdout: true,
204		LogStderr: true,
205	}
206	if _, err := sk_exec.RunCommand(ctx, authCmd); err != nil {
207		return err
208	}
209
210	initArgs := []string{"imgtest", "init", "--work-dir", workDir,
211		"--instance", "skia", "--corpus", cfg.corpus,
212		"--commit", cfg.gitCommit, "--url", "https://gold.skia.org", "--bucket", "skia-infra-gm"}
213
214	if cfg.changelistID != "" {
215		ps := strconv.Itoa(cfg.patchsetOrder)
216		initArgs = append(initArgs, "--crs", "gerrit", "--changelist", cfg.changelistID,
217			"--patchset", ps, "--cis", "buildbucket", "--jobid", cfg.tryjobID)
218	}
219
220	for key, value := range cfg.keys {
221		initArgs = append(initArgs, "--key="+key+":"+value)
222	}
223
224	initCmd := &sk_exec.Command{
225		Name:      cfg.goldctlPath,
226		Args:      initArgs,
227		LogStdout: true,
228		LogStderr: true,
229	}
230	if _, err := sk_exec.RunCommand(ctx, initCmd); err != nil {
231		return err
232	}
233	return nil
234}
235
236func addAllGoldImages(ctx context.Context, goldctlPath, pngsDir, workDir string) error {
237	pngFiles, err := os.ReadDir(pngsDir)
238	if err != nil {
239		return err
240	}
241	return td.Do(ctx, td.Props(fmt.Sprintf("Upload %d images to Gold", len(pngFiles))), func(ctx context.Context) error {
242		for _, entry := range pngFiles {
243			// We expect the filename to be testname.optional_config.png
244			baseName := filepath.Base(entry.Name())
245			parts := strings.Split(baseName, ".")
246			testName := parts[0]
247			addArgs := []string{
248				"imgtest", "add",
249				"--work-dir", workDir,
250				"--png-file", filepath.Join(pngsDir, filepath.Base(entry.Name())),
251				"--test-name", testName,
252			}
253			if len(parts) == 3 {
254				// There was a config specified.
255				addArgs = append(addArgs, "--add-test-key=config:"+parts[1])
256			}
257
258			addCmd := &sk_exec.Command{
259				Name:      goldctlPath,
260				Args:      addArgs,
261				LogStdout: true,
262				LogStderr: true,
263			}
264			if _, err := sk_exec.RunCommand(ctx, addCmd); err != nil {
265				return err
266			}
267		}
268		return nil
269	})
270}
271
272// finalizeGoldctl uploads the JSON file created from adding all the test PNGs. Then, Gold begins
273// ingesting the data.
274func finalizeGoldctl(ctx context.Context, goldctlPath, workDir string) error {
275	finalizeCmd := &sk_exec.Command{
276		Name:      goldctlPath,
277		Args:      []string{"imgtest", "finalize", "--work-dir=" + workDir},
278		LogStdout: true,
279		LogStderr: true,
280	}
281	if _, err := sk_exec.RunCommand(ctx, finalizeCmd); err != nil {
282		return err
283	}
284	return nil
285}
286