1 // Copyright 2022 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 // This task driver takes a binary (e.g. "dm") built by a Build-* task (e.g.
6 // "Build-Debian10-Clang-x86_64-Release"), runs Bloaty against the binary, and uploads the resulting
7 // code size statistics to the GCS bucket belonging to the https://codesize.skia.org service.
19 "cloud.google.com/go/storage"
20 "google.golang.org/api/option"
22 "go.skia.org/infra/go/auth"
23 "go.skia.org/infra/go/exec"
24 "go.skia.org/infra/go/gcs"
25 "go.skia.org/infra/go/gcs/gcsclient"
26 "go.skia.org/infra/go/gerrit"
27 "go.skia.org/infra/go/gitiles"
28 "go.skia.org/infra/go/now"
29 "go.skia.org/infra/go/skerr"
30 "go.skia.org/infra/task_driver/go/lib/auth_steps"
31 "go.skia.org/infra/task_driver/go/lib/checkout"
32 "go.skia.org/infra/task_driver/go/td"
33 "go.skia.org/infra/task_scheduler/go/types"
36 const gcsBucketName = "skia-codesize"
38 // BloatyOutputMetadata contains the Bloaty version and command-line arguments used, and metadata
39 // about the task where Bloaty was invoked. This struct is serialized into a JSON file that is
40 // uploaded to GCS alongside the Bloaty output file.
42 // TODO(lovisolo): Move this struct to the buildbot repository.
43 type BloatyOutputMetadata struct {
44 Version int `json:"version"` // Schema version of this file, starting at 1.
45 Timestamp string `json:"timestamp"`
47 SwarmingTaskID string `json:"swarming_task_id"`
48 SwarmingServer string `json:"swarming_server"`
50 TaskID string `json:"task_id"`
51 TaskName string `json:"task_name"`
52 CompileTaskName string `json:"compile_task_name"`
53 BinaryName string `json:"binary_name"`
55 BloatyCipdVersion string `json:"bloaty_cipd_version"`
56 BloatyArgs []string `json:"bloaty_args"`
58 PatchIssue string `json:"patch_issue"`
59 PatchServer string `json:"patch_server"`
60 PatchSet string `json:"patch_set"`
61 Repo string `json:"repo"`
62 Revision string `json:"revision"`
64 CommitTimestamp string `json:"commit_timestamp"`
65 Author string `json:"author"`
66 Subject string `json:"subject"`
71 projectID = flag.String("project_id", "", "ID of the Google Cloud project.")
72 taskID = flag.String("task_id", "", "ID of this task.")
73 taskName = flag.String("task_name", "", "Name of the task.")
74 compileTaskName = flag.String("compile_task_name", "", "Name of the compile task that produced the binary to analyze.")
75 binaryName = flag.String("binary_name", "", "Name of the binary to analyze (e.g. \"dm\").")
76 bloatyCIPDVersion = flag.String("bloaty_cipd_version", "", "Version of the \"bloaty\" CIPD package used.")
77 output = flag.String("o", "", "If provided, dump a JSON blob of step data to the given file. Prints to stdout if '-' is given.")
78 local = flag.Bool("local", true, "True if running locally (as opposed to on the bots).")
80 checkoutFlags = checkout.SetupFlags(nil)
82 ctx := td.StartRun(projectID, taskID, taskName, output, local)
85 // The repository state contains the commit hash and patch/patchset if available.
86 repoState, err := checkout.GetRepoState(checkoutFlags)
88 td.Fatal(ctx, skerr.Wrap(err))
91 // Make an HTTP client with the required permissions to hit GCS, Gerrit and Gitiles.
92 httpClient, err := auth_steps.InitHttpClient(ctx, *local, auth.ScopeReadWrite, gerrit.AuthScope, auth.ScopeUserinfoEmail)
94 td.Fatal(ctx, skerr.Wrap(err))
97 // Make a GCS client with the required permissions to upload to the codesize.skia.org GCS bucket.
98 store, err := storage.NewClient(ctx, option.WithHTTPClient(httpClient))
100 td.Fatal(ctx, skerr.Wrap(err))
102 gcsClient := gcsclient.New(store, gcsBucketName)
104 // Make a Gerrit client.
105 gerrit, err := gerrit.NewGerrit(repoState.Server, httpClient)
107 td.Fatal(ctx, skerr.Wrap(err))
110 // Make a Gitiles client.
111 gitilesRepo := gitiles.NewRepo(repoState.Repo, httpClient)
113 args := runStepsArgs{
114 repoState: repoState,
116 gitilesRepo: gitilesRepo,
117 gcsClient: gcsClient,
118 swarmingTaskID: os.Getenv("SWARMING_TASK_ID"),
119 swarmingServer: os.Getenv("SWARMING_SERVER"),
122 compileTaskName: *compileTaskName,
123 binaryName: *binaryName,
124 bloatyCIPDVersion: *bloatyCIPDVersion,
127 if err := runSteps(ctx, args); err != nil {
128 td.Fatal(ctx, skerr.Wrap(err))
132 // runStepsArgs contains the input arguments to the runSteps function.
133 type runStepsArgs struct {
134 repoState types.RepoState
135 gerrit *gerrit.Gerrit
136 gitilesRepo gitiles.GitilesRepo
137 gcsClient gcs.GCSClient
138 swarmingTaskID string
139 swarmingServer string
142 compileTaskName string
144 bloatyCIPDVersion string
147 // runSteps runs the main steps of this task driver.
148 func runSteps(ctx context.Context, args runStepsArgs) error {
152 commitTimestamp string
155 // Read the CL subject, author and timestamp. We talk to Gerrit when running as a tryjob, or to
156 // Gitiles when running as a post-submit task.
157 if args.repoState.IsTryJob() {
158 issue, err := strconv.ParseInt(args.repoState.Issue, 10, 64)
160 return skerr.Wrap(err)
162 patchset, err := strconv.ParseInt(args.repoState.Patchset, 10, 64)
164 return skerr.Wrap(err)
166 changeInfo, err := args.gerrit.GetIssueProperties(ctx, issue)
168 return skerr.Wrap(err)
170 // This matches the format of the author field returned by Gitiles.
171 author = fmt.Sprintf("%s (%s)", changeInfo.Owner.Name, changeInfo.Owner.Email)
172 subject = changeInfo.Subject
173 for _, revision := range changeInfo.Revisions {
174 if revision.Number == patchset {
175 commitTimestamp = revision.CreatedString
180 longCommit, err := args.gitilesRepo.Details(ctx, args.repoState.Revision)
182 return skerr.Wrap(err)
184 author = longCommit.Author
185 subject = longCommit.Subject
186 commitTimestamp = longCommit.Timestamp.Format(time.RFC3339)
189 // Run Bloaty and capture its output.
190 bloatyOutput, bloatyArgs, err := runBloaty(ctx, args.binaryName)
192 return skerr.Wrap(err)
195 // Build metadata structure.
196 metadata := &BloatyOutputMetadata{
198 Timestamp: now.Now(ctx).Format(time.RFC3339),
199 SwarmingTaskID: args.swarmingTaskID,
200 SwarmingServer: args.swarmingServer,
202 TaskName: args.taskName,
203 CompileTaskName: args.compileTaskName,
204 BinaryName: args.binaryName,
205 BloatyCipdVersion: args.bloatyCIPDVersion,
206 BloatyArgs: bloatyArgs,
207 PatchIssue: args.repoState.Issue,
208 PatchServer: args.repoState.Server,
209 PatchSet: args.repoState.Patchset,
210 Repo: args.repoState.Repo,
211 Revision: args.repoState.Revision,
212 CommitTimestamp: commitTimestamp,
217 gcsDir := computeTargetGCSDirectory(ctx, args.repoState, args.taskID, args.compileTaskName)
219 // Upload Bloaty output TSV file to GCS.
220 if err = uploadFileToGCS(ctx, args.gcsClient, fmt.Sprintf("%s/%s.tsv", gcsDir, args.binaryName), []byte(bloatyOutput)); err != nil {
221 return skerr.Wrap(err)
224 // Upload pretty-printed JSON metadata file to GCS.
225 jsonMetadata, err := json.MarshalIndent(metadata, "", " ")
227 return skerr.Wrap(err)
229 if err = uploadFileToGCS(ctx, args.gcsClient, fmt.Sprintf("%s/%s.json", gcsDir, args.binaryName), jsonMetadata); err != nil {
230 return skerr.Wrap(err)
236 // runBloaty runs Bloaty against the given binary and returns the Bloaty output in TSV format and
237 // the Bloaty command-line arguments used.
238 func runBloaty(ctx context.Context, binaryName string) (string, []string, error) {
239 err := td.Do(ctx, td.Props("List files under $PWD/build (for debugging purposes)"), func(ctx context.Context) error {
240 runCmd := &exec.Command{
242 Args: []string{"build"},
247 _, err := exec.RunCommand(ctx, runCmd)
251 return "", []string{}, skerr.Wrap(err)
254 runCmd := &exec.Command{
255 Name: "bloaty/bloaty",
257 "build/" + binaryName,
259 "compileunits,symbols",
269 var bloatyOutput string
271 if err := td.Do(ctx, td.Props(fmt.Sprintf("Run Bloaty against binary %q", binaryName)), func(ctx context.Context) error {
272 bloatyOutput, err = exec.RunCommand(ctx, runCmd)
275 return "", []string{}, skerr.Wrap(err)
278 return bloatyOutput, runCmd.Args, nil
281 // computeTargetGCSDirectory computs the target GCS directory where to upload the Bloaty output file
282 // and JSON metadata file.
283 func computeTargetGCSDirectory(ctx context.Context, repoState types.RepoState, taskID, compileTaskName string) string {
284 yearMonthDate := now.Now(ctx).Format("2006/01/02") // YYYY/MM/DD.
285 if repoState.IsTryJob() {
286 // Example: 2022/01/31/tryjob/12345/3/CkPp9ElAaEXyYWNHpXHU/Build-Debian10-Clang-x86_64-Release
287 return fmt.Sprintf("%s/tryjob/%s/%s/%s/%s", yearMonthDate, repoState.Patch.Issue, repoState.Patch.Patchset, taskID, compileTaskName)
289 // Example: 2022/01/31/033ccea12c0949d0f712471bfcb4ed6daf69aaff/Build-Debian10-Clang-x86_64-Release
290 return fmt.Sprintf("%s/%s/%s", yearMonthDate, repoState.Revision, compileTaskName)
294 // uploadFileToGCS uploads a file to the codesize.skia.org GCS bucket.
295 func uploadFileToGCS(ctx context.Context, gcsClient gcs.GCSClient, path string, contents []byte) error {
296 gcsURL := fmt.Sprintf("gs://%s/%s", gcsBucketName, path)
297 return td.Do(ctx, td.Props(fmt.Sprintf("Upload %s", gcsURL)), func(ctx context.Context) error {
298 if err := gcsClient.SetFileContents(ctx, path, gcs.FILE_WRITE_OPTS_TEXT, contents); err != nil {
299 return fmt.Errorf("Could not write task to %s: %s", gcsURL, err)