| # Copyright 2021 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from recipe_engine import recipe_api |
| |
| SCOPES = ["https://www.googleapis.com/auth/cloud-platform"] |
| |
| |
| class BinarySizeApi(recipe_api.RecipeApi): |
| """APIs for checking and diffing binary sizes.""" |
| |
| # This exit code indicates that the CI build was not successful and thus the |
| # the size diff could not be computed. |
| # Keep in sync with |
| # https://fuchsia.googlesource.com/infra/infra/+/main/cmd/size_diff/ci.go. |
| CI_BUILD_NOT_SUCCESSFUL_EXIT_CODE = 20 |
| |
| def check_budgets(self, step_name, binary_sizes_json_input): |
| """Check if the input binary sizes object exceeds one or more budgets. |
| |
| Args: |
| step_name (str): Name of the step. |
| binary_sizes_json_input (Path): Path for input binary sizes object |
| as JSON. |
| |
| Raises: |
| StepFailure: one or more budgets were exceeded. |
| """ |
| args = [ |
| "budgets", |
| "-binary-sizes-json-input", |
| binary_sizes_json_input, |
| ] |
| step = self._run_size_check( |
| step_name, args, stderr=self.m.raw_io.output_text(), ok_ret="any" |
| ) |
| |
| # test_ids should use underscore instead of spaces |
| prepared_step = self.m.reported_step.prepare_step( |
| test_id=step_name.replace(" ", "_"), step=step |
| ) |
| try: |
| if step.retcode: |
| step.presentation.status = self.m.step.FAILURE |
| step.presentation.logs["stderr"] = step.stderr |
| prepared_step.add_artifact("stderr", step.stderr) |
| raise self.m.step.StepFailure( |
| f"Binary size checks failed: {step.stderr}\n\nFor next " |
| f"steps, please refer to " |
| f"http://go/tq-resilience-home/size-stats/size-check-failure-steps\n" |
| f"Contact http://go/fuchsia-size-rotation if help is " |
| f"needed to adjust the size budgets." |
| ) |
| finally: |
| prepared_step.upload() |
| |
| def diff_ci( |
| self, |
| step_name, |
| gitiles_remote, |
| base_commit, |
| ci_builder, |
| binary_sizes_json_input, |
| ): |
| """Compute diff of the input binary sizes object against a binary sizes |
| object from CI. |
| |
| Args: |
| step_name (str): Name of the step. |
| gitiles_remote (str): Gitiles remote for base commit. |
| base_commit (str): Base commit as sha1. |
| ci_builder (builder_pb2.BuilderID): CI builder to inspect. |
| binary_sizes_json_input (Path): Path for input binary sizes object |
| as JSON. |
| |
| Returns: |
| StepData: Step data of the size diff tool. |
| dict: Binary size diff with keys: |
| component_diffs (seq(dict)): Per-component diffs, each with keys: |
| name (str): Name of the component. |
| baseline_size (int): Baseline size of the component in bytes. |
| size (int): Size of the component in bytes. |
| size_diff (int): Size diff in bytes. |
| budget (int): Budget of the component in bytes. |
| creep_budget (int): Creep budget of the component in bytes. |
| budget_exceeded (bool): Whether the budget is exceeded. |
| creep_budget_exceeded (bool): Whether the creep budget is |
| exceeded. |
| budget_exceeded (bool): Whether one or more budgets are exceeded. |
| creep_budget_exceeded (bool): Whether one or more creep budgets |
| are exceeded. |
| baseline_build_id (int): The baseline build ID that was used. |
| """ |
| args = [ |
| "ci", |
| "-gitiles-remote", |
| gitiles_remote, |
| "-base-commit", |
| base_commit, |
| "-builder", |
| self.m.buildbucket_util.full_builder_name(ci_builder), |
| "-binary-sizes-json-input", |
| binary_sizes_json_input, |
| "-json-output", |
| self.m.json.output(), |
| ] |
| step = self._run_size_diff( |
| step_name, args, ok_ret=(0, self.CI_BUILD_NOT_SUCCESSFUL_EXIT_CODE) |
| ) |
| # If the size diff could not be computed, keep track of this as a |
| # property, but do not raise an error. Computing the size diff is |
| # best-effort as of now. |
| step.presentation.properties["size_ci_build_not_successful"] = ( |
| step.retcode == self.CI_BUILD_NOT_SUCCESSFUL_EXIT_CODE |
| ) |
| if step.retcode: |
| return (step, None) |
| diff = step.json.output |
| step.presentation.properties["size_creep_budget_exceeded"] = diff[ |
| "creep_budget_exceeded" |
| ] |
| step.presentation.links["ci_build"] = self.m.buildbucket.build_url( |
| build_id=diff["baseline_build_id"] |
| ) |
| return (step, diff) |
| |
| def diff_product_size( |
| self, |
| assembly_manifest_path, |
| gitiles_remote, |
| base_commit, |
| ci_builder, |
| tool, |
| build_dir, |
| ): |
| """Compute product size diff based on current build's assembly manifest |
| against base commit build's assembly manifest. |
| |
| Args: |
| assembly_manifest_path (str): Build path of the assembly manifest. |
| gitiles_remote (str): Gitiles remote for base commit. |
| base_commit (str): Base commit as sha1. |
| ci_builder (builder_pb2.BuilderID): CI builder to inspect. |
| tool (str): Path to ffx tool. |
| build_dir (str): Path to build directory of this build. |
| |
| Output: |
| prints the diff to stdout log, and does not return anything. |
| """ |
| args = [ |
| "manifest", |
| "-gitiles-remote", |
| gitiles_remote, |
| "-base-commit", |
| base_commit, |
| "-builder", |
| self.m.buildbucket_util.full_builder_name(ci_builder), |
| ] |
| assembly_manifest_url = self._run_size_diff( |
| "read base assembly_manifest_url property", |
| args, |
| ok_ret=(0, self.CI_BUILD_NOT_SUCCESSFUL_EXIT_CODE), |
| stdout=self.m.raw_io.output_text(), |
| step_test_data=lambda: self.m.raw_io.test_api.stream_output_text( |
| "test_assembly_manifest_url" |
| ), |
| ).stdout |
| if not assembly_manifest_url: # pragma: no cover |
| return |
| assembly_manifest_url = assembly_manifest_url.strip('"') |
| |
| gcs_access_token = self.m.service_account.default().get_access_token( |
| scopes=["https://www.googleapis.com/auth/cloud-platform"] |
| ) |
| get_access_token_script = self.m.path.mkstemp("get_access_token.sh") |
| self.m.file.write_text( |
| "write get_access_token.sh", |
| get_access_token_script, |
| f"#!/usr/bin/bash\necho {gcs_access_token}\n", |
| include_log=False, |
| ) |
| self.m.step( |
| "make bash script executable", |
| ["chmod", "+x", get_access_token_script], |
| ) |
| |
| self.m.step( |
| "run product size diff", |
| [ |
| tool, |
| "--config", |
| "assembly_enabled=true", |
| "assembly", |
| "size-check", |
| "product", |
| "--auth", |
| get_access_token_script, |
| "--assembly-manifest", |
| "%s/%s" % (build_dir, assembly_manifest_path), |
| "--base-assembly-manifest", |
| assembly_manifest_url, |
| ], |
| step_test_data=lambda: self.m.raw_io.test_api.stream_output_text( |
| "test product size diff" |
| ), |
| ) |
| |
| @property |
| def _size_check_tool(self): |
| return self.m.cipd_ensure( |
| self.resource("size_check/cipd.ensure"), |
| "fuchsia/infra/size_check/${platform}", |
| ) |
| |
| def _run_size_check(self, step_name, args, **kwargs): |
| return self.m.step(step_name, [self._size_check_tool] + args, **kwargs) |
| |
| @property |
| def _size_diff_tool(self): |
| return self.m.cipd_ensure( |
| self.resource("size_diff/cipd.ensure"), |
| "fuchsia/infra/size_diff/${platform}", |
| ) |
| |
| def _run_size_diff(self, step_name, args, **kwargs): |
| return self.m.step(step_name, [self._size_diff_tool] + args, **kwargs) |