From 25f779623d43f6f99e5d5f44200a41804a12e40d Mon Sep 17 00:00:00 2001 From: Alexander Schulze Date: Thu, 12 Jan 2023 12:29:30 +0100 Subject: [PATCH] [builtins][pgo] Add helper script to interact with PGO profile bucket We start to host PGO profiles for builtins on a GCP bucket. This script supports various workflows to download profiles for tagged git versions. In a first step, we provide profiles for tagged git versions only. The script identifies this version from the current checkout and downloads (or validates the existence of) the profiles to a directory where they'll be used during build time. We introduce `checkout_v8_builtins_pgo_profiles` to the DEPS file (defaults to False). If set, we call the new helper script to download the profiles within the gclient sync step. The profile download is added to the Chromium project in crrev.com/c/4131525. Bug: chromium:1382471 Change-Id: I74ba4f3c102a85e230be7ef17b9c87621a1eab14 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/4111528 Commit-Queue: Alexander Schulze Reviewed-by: Liviu Rau Cr-Commit-Position: refs/heads/main@{#85253} --- DEPS | 13 ++ tools/PRESUBMIT.py | 17 ++- tools/builtins-pgo/download_profiles.py | 151 +++++++++++++++++++ tools/builtins-pgo/download_profiles_test.py | 62 ++++++++ 4 files changed, 240 insertions(+), 3 deletions(-) create mode 100755 tools/builtins-pgo/download_profiles.py create mode 100644 tools/builtins-pgo/download_profiles_test.py diff --git a/DEPS b/DEPS index 71098552a0..eda70cd242 100644 --- a/DEPS +++ b/DEPS @@ -39,6 +39,9 @@ vars = { # Fetch clang-tidy into the same bin/ directory as our clang binary. 'checkout_clang_tidy': False, + # Fetch and build V8 builtins with PGO profiles + 'checkout_v8_builtins_pgo_profiles': False, + 'chromium_url': 'https://chromium.googlesource.com', 'android_url': 'https://android.googlesource.com', 'download_gcmole': False, @@ -621,6 +624,16 @@ hooks = [ 'tools/generate-header-include-checks.py', ], }, + { + 'name': 'checkout_v8_builtins_pgo_profiles', + 'pattern': '.', + 'condition': 'checkout_v8_builtins_pgo_profiles', + 'action': [ + 'python3', + 'tools/builtins-pgo/download_profiles.py', + 'download', + ], + }, { # Clean up build dirs for crbug.com/1337238. # After a libc++ roll and revert, .ninja_deps would get into a state diff --git a/tools/PRESUBMIT.py b/tools/PRESUBMIT.py index ded0016793..6212decfe0 100644 --- a/tools/PRESUBMIT.py +++ b/tools/PRESUBMIT.py @@ -6,9 +6,20 @@ # use Python3 instead of Python2 when running the code in this file. USE_PYTHON3 = True +TEST_DIRECTORIES = [ + 'unittests', + 'builtins-pgo', +] + def CheckChangeOnCommit(input_api, output_api): - tests = input_api.canned_checks.GetUnitTestsInDirectory( - input_api, output_api, 'unittests', files_to_check=[r'.+_test\.py$'], - run_on_python2=False) + tests = [] + for directory in TEST_DIRECTORIES: + tests += input_api.canned_checks.GetUnitTestsInDirectory( + input_api, + output_api, + directory, + files_to_check=[r'.+_test\.py$'], + run_on_python2=False) + return input_api.RunTests(tests) diff --git a/tools/builtins-pgo/download_profiles.py b/tools/builtins-pgo/download_profiles.py new file mode 100755 index 0000000000..cad3cd0cc3 --- /dev/null +++ b/tools/builtins-pgo/download_profiles.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 + +# Copyright 2023 the V8 project authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can +# be found in the LICENSE file. +""" +Download PGO profiles for V8 builtins. The version is pulled from V8's version +file (include/v8-version.h). + +See argparse documentation for usage details. +""" + +import argparse +import os +import pathlib +import re +import sys + +FILENAME = os.path.basename(__file__) +PGO_PROFILE_BUCKET = 'chromium-v8-builtins-pgo' +PGO_PROFILE_DIR = pathlib.Path(os.path.dirname(__file__)) + +BASE_DIR = PGO_PROFILE_DIR.parents[1] +DEPOT_TOOLS_DEFAULT_PATH = os.path.join(BASE_DIR, 'third_party', 'depot_tools') +VERSION_FILE = BASE_DIR / 'include' / 'v8-version.h' +VERSION_RE = r"""#define V8_MAJOR_VERSION (\d+) +#define V8_MINOR_VERSION (\d+) +#define V8_BUILD_NUMBER (\d+) +#define V8_PATCH_LEVEL (\d+)""" + + +def main(cmd_args=None): + args = parse_args(cmd_args) + import_gsutil(args) + version = retrieve_version(args) + perform_action(version, args) + sys.exit(0) + + +def parse_args(cmd_args): + parser = argparse.ArgumentParser( + description=( + f'Download PGO profiles for V8 builtins generated for the version ' + f'defined in {VERSION_FILE}.'), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog='\n'.join([ + f'examples:', f' {FILENAME} download', + f' {FILENAME} validate --bucket=chromium-v8-builtins-pgo-staging', + f'', f'return codes:', + f' 0 - profiles successfully downloaded or validated', + f' 1 - unexpected error, see stdout', + f' 2 - invalid arguments specified, see {FILENAME} --help', + f' 3 - invalid path to depot_tools provided' + f' 4 - gsutil was unable to retrieve data from the bucket' + ]), + ) + + parser.add_argument( + 'action', + choices=['download', 'validate'], + help=( + 'download or validate profiles for the currently checked out version' + ), + ) + + parser.add_argument( + '--version', + help=('download (or validate) profiles for this version (e.g. 11.0.226.0 ' + 'or 11.0.226.2), defaults to the version in v8\'s version file'), + ) + + parser.add_argument( + '--depot-tools', + help=('path to depot tools, defaults to V8\'s version in ' + f'{DEPOT_TOOLS_DEFAULT_PATH}.'), + type=pathlib.Path, + default=DEPOT_TOOLS_DEFAULT_PATH, + ) + + return parser.parse_args(cmd_args) + + +def import_gsutil(args): + abs_depot_tools_path = os.path.abspath(args.depot_tools) + file = os.path.join(abs_depot_tools_path, 'download_from_google_storage.py') + if not pathlib.Path(file).is_file(): + print(f'{file} does not exist; check --depot-tools path.', file=sys.stderr) + sys.exit(3) + + sys.path.append(abs_depot_tools_path) + globals()['gcs_download'] = __import__('download_from_google_storage') + + +def retrieve_version(args): + if args.version: + return args.version + + with open(VERSION_FILE) as f: + version_tuple = re.search(VERSION_RE, f.read()).groups(0) + return '.'.join(version_tuple) + + +def perform_action(version, args): + path = f'{PGO_PROFILE_BUCKET}/by-version/{version}' + + if args.action == 'download': + cmd = ['cp', '-R', f'gs://{path}/*.profile', str(PGO_PROFILE_DIR)] + failure_hint = f'https://storage.googleapis.com/{path} does not exist.' + call_gsutil(cmd, failure_hint) + return + + if args.action == 'validate': + meta_json = f'{path}/meta.json' + cmd = ['stat', f'gs://{meta_json}'] + failure_hint = f'https://storage.googleapis.com/{meta_json} does not exist.' + call_gsutil(cmd, failure_hint) + return + + raise AssertionError(f'Invalid action: {args.action}') + + +def call_gsutil(cmd, failure_hint): + # Load gsutil from depot tools, and execute command + gsutil = gcs_download.Gsutil(gcs_download.GSUTIL_DEFAULT_PATH) + returncode, stdout, stderr = gsutil.check_call(*cmd) + if returncode != 0: + print_error(['gsutil', *cmd], returncode, stdout, stderr, failure_hint) + sys.exit(4) + + +def print_error(cmd, returncode, stdout, stderr, failure_hint): + message = [ + 'The following command did not succeed:', + f' $ {" ".join(cmd)}', + ] + sections = [ + ('return code', str(returncode)), + ('stdout', stdout.strip()), + ('stderr', stderr.strip()), + ('hint', failure_hint), + ] + for label, output in sections: + if not output: + continue + message += [f'{label}:', " " + "\n ".join(output.split("\n"))] + + print('\n'.join(message), file=sys.stderr) + + +if __name__ == '__main__': + main() diff --git a/tools/builtins-pgo/download_profiles_test.py b/tools/builtins-pgo/download_profiles_test.py new file mode 100644 index 0000000000..8ae844f7ea --- /dev/null +++ b/tools/builtins-pgo/download_profiles_test.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +# Copyright 2023 the V8 project authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can +# be found in the LICENSE file. + +import contextlib +import io +import os +import unittest + +from tempfile import TemporaryDirectory +from unittest.mock import patch + +from download_profiles import main + + +class TestDownloadProfiles(unittest.TestCase): + + def _test_cmd(self, cmd, exitcode): + out = io.StringIO() + err = io.StringIO() + with self.assertRaises(SystemExit) as se, \ + contextlib.redirect_stdout(out), \ + contextlib.redirect_stderr(err): + main(cmd) + self.assertEqual(se.exception.code, exitcode) + return out.getvalue(), err.getvalue() + + def test_validate_profiles(self): + out, err = self._test_cmd(['validate', '--version', '11.1.0.0'], 0) + self.assertEqual(len(out), 0) + self.assertEqual(len(err), 0) + + def test_download_profiles(self): + with TemporaryDirectory() as td, \ + patch('download_profiles.PGO_PROFILE_DIR', td): + out, err = self._test_cmd(['download', '--version', '11.1.0.0'], 0) + self.assertEqual(len(out), 0) + self.assertEqual(len(err), 0) + self.assertGreater( + len([f for f in os.listdir(td) if f.endswith('.profile')]), 0) + + def test_invalid_args(self): + out, err = self._test_cmd(['invalid-action'], 2) + self.assertEqual(len(out), 0) + self.assertGreater(len(err), 0) + + def test_invalid_depot_tools_path(self): + out, err = self._test_cmd( + ['validate', '--depot-tools', '/no-depot-tools-path'], 3) + self.assertEqual(len(out), 0) + self.assertGreater(len(err), 0) + + def test_missing_profiles(self): + out, err = self._test_cmd(['download', '--version', '0.0.0.42'], 4) + self.assertEqual(len(out), 0) + self.assertGreater(len(err), 0) + + +if __name__ == '__main__': + unittest.main()