[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 <alexschulze@chromium.org>
Reviewed-by: Liviu Rau <liviurau@chromium.org>
Cr-Commit-Position: refs/heads/main@{#85253}
This commit is contained in:
Alexander Schulze 2023-01-12 12:29:30 +01:00 committed by V8 LUCI CQ
parent d8cd42360d
commit 25f779623d
4 changed files with 240 additions and 3 deletions

13
DEPS
View File

@ -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

View File

@ -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$'],
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)

View File

@ -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()

View File

@ -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()