1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963
|
# Copyright 2018 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# Script to automate updating existing WPR benchmarks from live versions of the
# sites. Only supported on Mac/Linux.
from __future__ import print_function
import argparse
import datetime
import json
import os
import pathlib
import random
import re
import shutil
import subprocess
import tempfile
import time
import shlex
import webbrowser
from chrome_telemetry_build import chromium_config
from core import cli_helpers
from core import path_util
from core.services import luci_auth
from core.services import pinpoint_service
from core.services import request
path_util.AddTelemetryToPath()
from telemetry import record_wpr
from telemetry.wpr import archive_info
from telemetry.internal.browser import browser_finder
from telemetry.internal.browser import browser_options
from telemetry.internal.util import binary_manager as telemetry_binary_manager
import py_utils
from py_utils import binary_manager, cloud_storage
SRC_ROOT = os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', '..', '..', '..'))
RESULTS2JSON = os.path.join(
SRC_ROOT, 'third_party', 'catapult', 'tracing', 'bin', 'results2json')
HISTOGRAM2CSV = os.path.join(
SRC_ROOT, 'third_party', 'catapult', 'tracing', 'bin', 'histograms2csv')
RUN_BENCHMARK = os.path.join(SRC_ROOT, 'tools', 'perf', 'run_benchmark')
DATA_DIR = os.path.join(SRC_ROOT, 'tools', 'perf', 'page_sets', 'data')
RECORD_WPR = os.path.join(SRC_ROOT, 'tools', 'perf', 'record_wpr')
DEFAULT_REVIEWERS = ['johnchen@chromium.org']
MISSING_RESOURCE_RE = re.compile(
r'\[network\]: Failed to load resource: the server responded with a status '
r'of 404 \(\) ([^\s]+)')
TELEMETRY_BIN_DEPS_CONFIG = os.path.join(
path_util.GetTelemetryDir(), 'telemetry', 'binary_dependencies.json')
def _GetBranchName():
branch_name = subprocess.check_output(
['git', 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
if isinstance(branch_name, bytes):
return branch_name.decode("utf-8")
return branch_name
def _OpenBrowser(url):
# Redirect I/O before invoking browser to avoid it spamming our output.
# Based on https://stackoverflow.com/a/2323563.
savout = os.dup(1)
saverr = os.dup(2)
os.close(1)
os.close(2)
os.open(os.devnull, os.O_RDWR)
try:
webbrowser.open(url)
finally:
os.dup2(savout, 1)
os.dup2(saverr, 2)
def _SendCLForReview(comment):
subprocess.check_call(
['git', 'cl', 'comments', '--publish', '--add-comment', comment])
def _EnsureEditor():
if 'EDITOR' not in os.environ:
os.environ['EDITOR'] = cli_helpers.Prompt(
'Looks like EDITOR environment varible is not defined. Please enter '
'the command to view logs: ')
def _OpenEditor(filepath):
subprocess.check_call(shlex.split(os.environ['EDITOR']) + [filepath])
def _PrepareEnv():
# Enforce the same local settings for recording and replays on the bots.
env = os.environ.copy()
env['LC_ALL'] = 'en_US.UTF-8'
return env
def _ExtractLogFile(out_file):
# This method extracts the name of the chrome log file from the
# run_benchmark output log and copies it to the temporary directory next to
# the log file, which ensures that it is not overridden by the next run.
try:
line = subprocess.check_output(
['grep', 'Chrome log file will be saved in', out_file])
os.rename(line.split()[-1], out_file + '.chrome.log')
except subprocess.CalledProcessError as e:
cli_helpers.Error('Could not find log file: {error}', error=e)
def _PrintResultsHTMLInfo(out_file):
results_file = out_file + '.results.html'
histogram_json = out_file + '.hist.json'
histogram_csv = out_file + '.hist.csv'
cli_helpers.Run(
[RESULTS2JSON, results_file, histogram_json], env=_PrepareEnv())
cli_helpers.Run(
[HISTOGRAM2CSV, histogram_json, histogram_csv], env=_PrepareEnv())
cli_helpers.Info('Metrics results: file://{path}', path=results_file)
names = set([
'console:error:network',
'console:error:js',
'console:error:all',
'console:error:security'])
with open(histogram_csv) as f:
for line in f.readlines():
line = line.split(',')
if line[0] in names:
cli_helpers.Info(' %-26s%s' % ('[%s]:' % line[0], line[2]))
def _ExtractMissingURLsFromLog(replay_log):
missing_urls = []
with open(replay_log) as replay_file:
for line in replay_file:
match = MISSING_RESOURCE_RE.search(line)
if match:
missing_urls.append(match.group(1))
return missing_urls
def _CountLogLines(log_file, line_matcher_re=r'.*'):
num_lines = 0
line_matcher = re.compile(line_matcher_re)
with open(log_file) as log_file_handle:
for line in log_file_handle:
if line_matcher.search(line):
num_lines += 1
return num_lines
def _UploadArchiveToGoogleStorage(archive):
"""Uploads specified WPR archive to the GS."""
cli_helpers.Run([
'upload_to_google_storage.py', '--bucket=chrome-partner-telemetry',
archive])
def _GitAddArtifactHash(archive):
"""Stages changes into SHA1 file for commit."""
archive_sha1 = archive + '.sha1'
if not os.path.exists(archive_sha1):
cli_helpers.Error(
'Could not find upload artifact: {sha}', sha=archive_sha1)
return False
cli_helpers.Run(['git', 'add', archive_sha1])
return True
def _PrintRunInfo(out_file, chrome_log_file=False, results_details=True):
try:
if results_details:
_PrintResultsHTMLInfo(out_file)
except Exception as e:
cli_helpers.Error('Could not print results.html tests: %s' % e)
cli_helpers.Info('Stdout/Stderr Log: %s' % out_file)
if chrome_log_file:
cli_helpers.Info('Chrome Log: %s.chrome.log' % out_file)
cli_helpers.Info(' Total output: %d' % _CountLogLines(out_file))
cli_helpers.Info(' Total Console: %d' %
_CountLogLines(out_file, r'DevTools console'))
cli_helpers.Info(' [security]: %d' %
_CountLogLines(out_file, r'DevTools console .security.'))
cli_helpers.Info(' [network]: %d' %
_CountLogLines(out_file, r'DevTools console .network.'))
chrome_log = '%s.chrome.log' % out_file
if os.path.isfile(chrome_log):
cli_helpers.Info(' [javascript]: %d' %
_CountLogLines(chrome_log, r'Uncaught .*Error'))
if results_details:
missing_urls = _ExtractMissingURLsFromLog(out_file)
if missing_urls:
cli_helpers.Info( 'Missing URLs in the archive:')
for missing_url in missing_urls:
cli_helpers.Info(' - %s' % missing_url)
class WprUpdater(object):
def __init__(self, args):
self._ValidateArguments(args)
self.story = args.story
self.bss = args.bss
# TODO(sergiyb): Implement a method that auto-detects a single connected
# device when device_id is set to 'auto'. This should take advantage of the
# method adb_wrapper.Devices in catapult repo.
self.device_id = args.device_id
self.repeat = args.repeat
self.binary = args.binary
self.output_dir = tempfile.mkdtemp()
self.bug_id = args.bug_id
self.reviewers = args.reviewers or DEFAULT_REVIEWERS
self.wpr_go_bin = None
self._LoadArchiveInfo()
def _ValidateArguments(self, args):
if not args.story:
raise ValueError('--story is required!')
def _LoadArchiveInfo(self):
config = chromium_config.GetDefaultChromiumConfig()
tmp_recorder = record_wpr.WprRecorder(config, self.bss, parse_flags=False)
self.wpr_archive_info = tmp_recorder.story_set.wpr_archive_info
def _CheckLog(self, command, log_name):
# This is a wrapper around cli_helpers.CheckLog that adds timestamp to the
# log filename and substitutes placeholders such as {src}, {story},
# {device_id} in the command.
story_regex = '^%s$' % re.escape(self.story)
command = [
c.format(src=SRC_ROOT, story=story_regex, device_id=self.device_id)
for c in command]
timestamp = datetime.datetime.now().strftime('%Y_%m_%d_%H_%M_%S')
log_path = os.path.join(self.output_dir, '%s_%s' % (log_name, timestamp))
cli_helpers.CheckLog(command, log_path=log_path, env=_PrepareEnv())
return log_path
def _IsDesktop(self):
return self.device_id is None
def _GetAllWprArchives(self):
return self.wpr_archive_info.data['archives']
def _GetWprArchivesForStory(self):
archives = self._GetAllWprArchives()
return archives.get(self.story)
def _GetWprArchivePathsAndUsageForStory(self):
"""Parses JSON story config to extract info about WPR archive
Returns:
A list of 2-tuple with path to the current WPR archive for specified
story and whether it is used by other benchmarks too.
"""
archives = self._GetAllWprArchives()
archive = self._GetWprArchivesForStory()
if archive is None:
return []
existing = []
for a in archive.values():
used_in_other_stories = any(
a in config.values() for story, config in archives.items()
if story != self.story)
existing.append((os.path.join(DATA_DIR, a), used_in_other_stories))
return existing
def _DeleteExistingWpr(self):
"""Deletes current WPR archive."""
archive = self._GetWprArchivesForStory()
if archive is None:
return
ans = cli_helpers.Ask(
'For this story, should I erase all existing recordings that '
'aren\'t used by other stories? Select yes if this is your '
'first time re-recording this story.',
['yes', 'no'], default='no')
if ans == 'no':
return
for archive, used_in_other_stories in self._GetWprArchivePathsAndUsageForStory():
if used_in_other_stories:
continue
cli_helpers.Info('Deleting WPR: {archive}', archive=archive)
if os.path.exists(archive):
os.remove(archive)
archive_sha1 = archive + '.sha1'
if os.path.exists(archive_sha1):
os.remove(archive_sha1)
self.wpr_archive_info.RemoveStory(self.story)
def _ExtractResultsFile(self, out_file):
results_file = out_file + '.results.html'
os.rename(os.path.join(self.output_dir, 'results.html'), results_file)
def _BrowserArgs(self):
"""Generates args to be passed to RUN_BENCHMARK and UPDATE_WPR scripts."""
if self.binary:
return [
'--browser-executable=%s' % self.binary,
'--browser=exact',
]
if self._IsDesktop():
return ['--browser=system']
return ['--browser=android-system-chrome']
def _RunBenchmark(self, log_name, live=False):
args = [RUN_BENCHMARK, 'run'] + self._BrowserArgs()
benchmark = self.bss
if self.bss == 'desktop_system_health_story_set':
benchmark = 'system_health.memory_desktop'
elif self.bss == 'mobile_system_health_story_set':
benchmark = 'system_health.memory_mobile'
args.append(benchmark)
if not self._IsDesktop():
args.append('--device={device_id}')
args.extend([
'--output-format=html', '--show-stdout', '--reset-results',
'--story-filter={story}', '--browser-logging-verbosity=verbose',
'--pageset-repeat=%s' % self.repeat, '--output-dir', self.output_dir,
'--also-run-disabled-tests', '--legacy-json-trace-format'
])
if live:
args.append('--use-live-sites')
out_file = self._CheckLog(args, log_name=log_name)
self._ExtractResultsFile(out_file)
if self._IsDesktop(): # Mobile test runner does not product the log file.
_ExtractLogFile(out_file)
return out_file
def _GetBranchIssueUrl(self):
output_file = os.path.join(self.output_dir, 'git_cl_issue.json')
subprocess.check_output(['git', 'cl', 'issue', '--json', output_file])
with open(output_file, 'r') as output_fd:
return json.load(output_fd)['issue_url']
def _SanitizedBranchPrefix(self):
return 'update-wpr-%s' % re.sub(r'[^A-Za-z0-9-_.]', r'-', self.story)
def _CreateBranch(self):
new_branch_name = '%s-%d' % (
self._SanitizedBranchPrefix(), random.randint(0, 10000))
cli_helpers.Run(['git', 'new-branch', new_branch_name])
def _FilterLogForDiff(self, log_filename):
"""Removes unimportant details from console logs for cleaner diffs.
For example, log line from file `log_filename`
2018-02-01 22:23:22,123 operation abcdef01-abcd-abcd-0123-abcdef012345
from /tmp/tmpX34v/results.html took 22145ms when accessed via
https://127.0.0.1:1233/endpoint
would become
<timestamp> operation <guid> from /tmp/tmp<random>/results.html took
<duration> when accessed via https://127.0.0.1:<port>/endpoint
Returns:
Path to the filtered log.
"""
with open(log_filename,
encoding='utf-8') as src, tempfile.NamedTemporaryFile(
encoding='utf-8',
mode='w+',
suffix='diff',
dir=self.output_dir,
delete=False) as dest:
for line in src:
# Remove timestamps.
line = re.sub(
r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}', r'<timestamp>', line)
# Remove GUIDs.
line = re.sub(
r'[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}', r'<guid>', line)
# Remove random letters in paths to temp dirs and files.
line = re.sub(r'(/tmp/tmp)[^/\s]+', r'\1<random>', line)
# Remove random port in localhost URLs.
line = re.sub(r'(://127.0.0.1:)\d+', r'\1<port>', line)
# Remove random durations in ms.
line = re.sub(r'\d+ ms', r'<duration>', line)
dest.write(line)
return dest.name
def _GetTargetFromConfiguration(self, configuration):
"""Returns the target that should be used for a Pinpoint job."""
if configuration == 'android-pixel6-perf':
return 'performance_test_suite_android_trichrome_chrome_google_64_32_bundle'
if configuration in ('linux-perf', 'win-10-perf',
'mac-10_12_laptop_low_end-perf'):
return 'performance_test_suite'
raise RuntimeError('Unknown configuration %s' % configuration)
def _StartPinpointJob(self, configuration):
"""Creates, starts a Pinpoint job and returns its URL."""
try:
resp = pinpoint_service.NewJob(
base_git_hash='HEAD',
target=self._GetTargetFromConfiguration(configuration),
patch=self._GetBranchIssueUrl(),
bug_id=self.bug_id or '',
story=self.story,
extra_test_args='--pageset-repeat=%d' % self.repeat,
configuration=configuration,
benchmark='system_health.common_%s' %
('desktop' if self._IsDesktop() else 'mobile'))
except request.RequestError as e:
cli_helpers.Comment(
'Failed to start a Pinpoint job for {config} automatically:\n {err}',
config=configuration, err=e.content)
return None
cli_helpers.Info(
'Started a Pinpoint job for {configuration} at {url}',
configuration=configuration, url=resp['jobUrl'])
return resp['jobUrl']
def _AddMissingURLsToArchive(self, replay_out_file):
existing_wprs = self._GetWprArchivePathsAndUsageForStory()
if len(existing_wprs) == 0:
return
if len(existing_wprs) == 1:
archive = existing_wprs[0][0]
else:
cli_helpers.Comment("WPR Archives for this story:")
print(str(self._GetWprArchivesForStory()))
archive = cli_helpers.Ask(
'Which archive should I add URLs to?',
[e[0] for e in existing_wprs])
missing_urls = _ExtractMissingURLsFromLog(replay_out_file)
if not missing_urls:
return
if not self.wpr_go_bin:
self.wpr_go_bin = (
binary_manager.BinaryManager([TELEMETRY_BIN_DEPS_CONFIG]).FetchPath(
'wpr_go', py_utils.GetHostArchName(), py_utils.GetHostOsName()))
subprocess.check_call([self.wpr_go_bin, 'add', archive] + missing_urls)
def LiveRun(self):
cli_helpers.Step('LIVE RUN: %s' % self.story)
out_file = self._RunBenchmark(
log_name='live', live=True)
_PrintRunInfo(out_file, chrome_log_file=self._IsDesktop())
return out_file
def Cleanup(self):
if cli_helpers.Ask('Should I clean up the temp dir with logs?'):
shutil.rmtree(self.output_dir, ignore_errors=True)
else:
cli_helpers.Comment(
'No problem. All logs will remain in %s - feel free to remove that '
'directory when done.' % self.output_dir)
def RecordWpr(self):
cli_helpers.Step('RECORD WPR: %s' % self.story)
self._DeleteExistingWpr()
args = ([RECORD_WPR, self.bss, '--story-filter={story}'] +
self._BrowserArgs())
if not self._IsDesktop():
args.append('--device={device_id}')
out_file = self._CheckLog(args, log_name='record')
_PrintRunInfo(
out_file, chrome_log_file=self._IsDesktop(), results_details=False)
self._LoadArchiveInfo() # record_wpr overwrote this file
def ReplayWpr(self):
cli_helpers.Step('REPLAY WPR: %s' % self.story)
out_file = self._RunBenchmark(
log_name='replay', live=False)
_PrintRunInfo(out_file, chrome_log_file=self._IsDesktop())
return out_file
def UploadWpr(self):
# Attempts to upload all archives used by a story.
# Note, if GoogleStorage already has the latest version
# of a story, upload will be skipped.
cli_helpers.Step('UPLOAD WPR: %s' % self.story)
archives = self._GetWprArchivePathsAndUsageForStory()
for archive in {a[0] for a in archives}:
if not os.path.exists(archive):
continue
_UploadArchiveToGoogleStorage(archive)
if not _GitAddArtifactHash(archive):
return False
return True
def UploadCL(self, short_description=False):
cli_helpers.Step('UPLOAD CL: %s' % self.story)
if short_description:
commit_message = 'Automated upload'
else:
commit_message = (
'Add %s system health story\n\nThis CL was created automatically '
'with tools/perf/update_wpr script' % self.story)
if self.bug_id:
commit_message += '\n\nBug: %s' % self.bug_id
if subprocess.call(['git', 'diff', '--quiet']):
cli_helpers.Run(['git', 'commit', '-a', '-m', commit_message])
commit_msg_file = os.path.join(self.output_dir, 'commit_message.tmp')
with open(commit_msg_file, 'w') as fd:
fd.write(commit_message)
return cli_helpers.Run([
'git', 'cl', 'upload',
'--reviewers', ','.join(self.reviewers),
'--force', # to prevent message editor from appearing
'--message-file', commit_msg_file], ok_fail=True)
def StartPinpointJobs(self, configs=None):
job_urls = []
failed_configs = []
if not configs:
if self._IsDesktop():
configs = ['linux-perf', 'win-10-perf', 'mac-10_12_laptop_low_end-perf']
else:
configs = ['android-pixel6-perf']
for config in configs:
job_url = self._StartPinpointJob(config)
if not job_url:
failed_configs.append(config)
else:
job_urls.append(job_url)
return job_urls, failed_configs
def AutoRun(self):
# Let the quest begin...
cli_helpers.Comment(
'This script will help you update the recording of a story. It will go '
'through the following stages, which you can also invoke manually via '
'subcommand specified in parentheses:')
cli_helpers.Comment(' - help create a new branch if needed')
cli_helpers.Comment(' - run story with live network connection (live)')
cli_helpers.Comment(' - record story (record)')
cli_helpers.Comment(' - replay the recording (replay)')
cli_helpers.Comment(' - upload the recording to Google Storage (upload)')
cli_helpers.Comment(
' - upload CL with updated recording reference (review)')
cli_helpers.Comment(' - trigger pinpoint tryjobs (pinpoint)')
cli_helpers.Comment(' - post links to these jobs on the CL')
cli_helpers.Comment(
'Note that you can always enter prefix of the answer to any of the '
'questions asked below, e.g. "y" for "yes" or "j" for "just-replay".')
# TODO(sergiyb): Detect if benchmark is not implemented and try to add it
# automatically by copying the same benchmark without :<current-year> suffix
# and changing name of the test, name of the benchmark and the year tag.
# Create branch if needed.
reuse_cl = False
branch = _GetBranchName()
if branch == 'HEAD':
cli_helpers.Comment('You are not on a branch.')
if not cli_helpers.Ask(
'Should script create a new branch automatically?'):
cli_helpers.Comment(
'Please create a new branch and start this script again')
return
self._CreateBranch()
else:
issue = self._GetBranchIssueUrl()
if issue is not None:
issue_message = 'with an associated issue: %s' % issue
else:
issue_message = 'without an associated issue'
cli_helpers.Comment(
'You are on a branch {branch} {issue_message}. Please commit or '
'stash any changes unrelated to the updated story before '
'proceeding.', branch=branch, issue_message=issue_message)
is_update_wpr_branch = re.match(
r'%s-\d+' % self._SanitizedBranchPrefix(), branch)
ans = cli_helpers.Ask(
'Should the script create a new branch automatically, reuse '
'existing one or exit?', answers=['create', 'reuse', 'exit'],
default='reuse' if is_update_wpr_branch else 'create')
if ans == 'create':
self._CreateBranch()
elif ans == 'reuse':
reuse_cl = issue is not None
elif ans == 'exit':
return
# Live run.
live_out_file = self.LiveRun()
cli_helpers.Comment(
'Please inspect the live run results above for any errors.')
ans = None
while ans != 'continue':
ans = cli_helpers.Ask(
'Should I continue with recording, view metric results in a browser, '
'view stdout/stderr output or stop?',
['continue', 'metrics', 'output', 'stop'], default='continue')
if ans == 'stop':
cli_helpers.Comment(
'Please update the story class to resolve the observed issues and '
'then run this script again.')
return
if ans == 'metrics':
_OpenBrowser('file://%s.results.html' % live_out_file)
elif ans == 'output':
_OpenEditor(live_out_file)
# Record & replay.
action = 'record'
replay_out_file = None
while action != 'continue':
if action == 'record':
self.RecordWpr()
if action == 'add-missing':
self._AddMissingURLsToArchive(replay_out_file)
if action in ['record', 'add-missing', 'just-replay']:
replay_out_file = self.ReplayWpr()
cli_helpers.Comment(
'Check that the console:error:all metrics above have low values '
'and are similar to the live run above.')
if action == 'diff':
diff_path = os.path.join(self.output_dir, 'live_replay.diff')
with open(diff_path, 'w') as diff_file:
subprocess.call([
'diff', '--color', self._FilterLogForDiff(live_out_file),
self._FilterLogForDiff(replay_out_file)], stdout=diff_file)
_OpenEditor(diff_path)
if action == 'stop':
return
action = cli_helpers.Ask(
'Should I record and replay again, just replay, add all missing URLs '
'into archive and try replay again, continue with uploading CL, stop '
'and exit, or would you prefer to see diff between live/replay '
'console logs?',
['record', 'just-replay', 'add-missing', 'continue', 'stop', 'diff'],
default='continue')
# Upload WPR and create a WIP CL for the new story.
if not self.UploadWpr():
return
while self.UploadCL(short_description=reuse_cl) != 0:
if not cli_helpers.Ask('Upload failed. Should I try again?'):
return
# Gerrit needs some time to sync its backends, hence we sleep here for 5
# seconds. Otherwise, pinpoint app may get an answer that the CL that we've
# just uploaded does not exist yet.
cli_helpers.Comment(
'Waiting 20 seconds for the Gerrit backends to sync, so that Pinpoint '
'app can detect the newly-created CL.')
time.sleep(20)
# Trigger pinpoint jobs.
configs_to_trigger = None
job_urls = []
while True:
new_job_urls, configs_to_trigger = self.StartPinpointJobs(
configs_to_trigger)
job_urls.extend(new_job_urls)
if not configs_to_trigger or not cli_helpers.Ask(
'Do you want to try triggering the failed configs again?'):
break
if configs_to_trigger:
if not cli_helpers.Ask(
'Some jobs failed to trigger. Do you still want to send created '
'CL for review?', default='no'):
return
# Post a link to the triggered jobs, publish CL for review and open it.
_SendCLForReview(
'Started the following Pinpoint jobs:\n%s' %
'\n'.join(' - %s' % url for url in job_urls))
cli_helpers.Comment(
'Posted a message with Pinpoint job URLs on the CL and sent it for '
'review. Opening the CL in a browser...')
_OpenBrowser(self._GetBranchIssueUrl())
# Hooray, you won! :-)
cli_helpers.Comment(
'Thank you, you have successfully updated the recording for %s. Please '
'wait for LGTM and land the created CL.' % self.story)
class CrossbenchWprUpdater(object):
"""This class helps to update WPR archive files for the Crossbench tool.
Currently it supports `android-trichrome-chrome-google-64-32-bundle` browser
type only. The assumption is a single Android device is attached to the
machine, and the device is connected to the network.
"""
_CB_TOOL = os.path.join(SRC_ROOT, 'third_party', 'crossbench', 'cb.py')
_BUCKET = cloud_storage.PARTNER_BUCKET
_CHROME_BROWSER = '--browser=%s'
_DEFAULT_BROWSER = 'android-trichrome-chrome-google-64-32-bundle'
def __init__(self, args):
self.story = args.story
self.bss = args.bss
self.device_id = args.device_id or 'adb'
self.repeat = args.repeat
self.binary = args.binary
self.bug_id = args.bug_id
self.reviewers = args.reviewers or DEFAULT_REVIEWERS
self.wpr_go_bin = None
self.cb_wprgo_file = args.cb_wprgo_file
self._SetupOutput(args)
self._LoadArchiveInfo()
self._find_browser(self._DEFAULT_BROWSER)
def _SetupOutput(self, args):
timestamp = datetime.datetime.now().strftime('%Y_%m_%d_%H_%M_%S')
self.output_dir = os.path.join(args.output_dir or tempfile.mkdtemp(),
timestamp)
if os.path.exists(self.output_dir):
raise FileExistsError(f'{self.output_dir} already exists!')
pathlib.Path(self.output_dir).mkdir(parents=True)
def _LoadArchiveInfo(self):
self.wpr_archive_info = archive_info.WprArchiveInfo.FromFile(
str(self.ArchiveFilePath(self.bss)), self._BUCKET)
def _find_browser(self, browser_arg):
options = browser_options.BrowserFinderOptions()
options.chrome_root = pathlib.Path(SRC_ROOT)
parser = options.CreateParser()
telemetry_binary_manager.InitDependencyManager(None)
parser.parse_args([self._CHROME_BROWSER % browser_arg])
# Finding the browser package and installing the required dependencies.
possible_browser = browser_finder.FindBrowser(options)
if not possible_browser:
raise ValueError(f'Unable to find Chrome browser of type: {browser_arg}')
self.browser = possible_browser.settings.package
def AutoRun(self):
story_msg = f'the {self.story} in ' if self.story else ''
if not cli_helpers.Ask(
f'This script generates archive file for {story_msg} the '
f'{self.bss} benchmark. It will generate a commit in your current '
'branch. If you need to create a new branch or have uncommitted '
'changes, please stop the script and create a fresh branch. Do '
'you want to continue?',
answers={'yes': True, 'no': False},
default='no'):
return
cb_wprgo = self.RecordWpr()
self.ReplayWpr(cb_wprgo)
if not cli_helpers.Ask(
f'The {cb_wprgo} file has been generated and replayed. Please '
f'see the Crossbench log file in {self.output_dir}. Are you sure '
'to upload the new archive file to the cloud?',
answers={'yes': True, 'no': False},
default='no'):
return
if not self.UploadWpr(cb_wprgo):
cli_helpers.Error(f'Unabled to upload {cb_wprgo} to the cloud!')
return
cli_helpers.Comment(
'Thank you, you have successfully updated the archive file. Please '
'run `git status` to review the chagnes, upload the CL and send it to '
f'{self.reviewers} for reviewing.')
def LiveRun(self):
cb_output_dir = os.path.join(self.output_dir, 'cb_live')
command = self._GenerateCommandList([], cb_output_dir)
self._CheckLog(command, log_name='live')
def RecordWpr(self):
cli_helpers.Step(f'RECORD WPR: {self.bss}')
cb_output_dir = os.path.join(self.output_dir, 'cb_record')
command = self._GenerateCommandList(['--probe=wpr'], cb_output_dir)
self._CheckLog(command, log_name='record')
cb_wprgo = os.path.join(cb_output_dir, 'archive.wprgo')
cli_helpers.Info(f'WPRGO acrhive file: {cb_wprgo}')
return cb_wprgo
def ReplayWpr(self, cb_wprgo=None):
cb_wprgo = cb_wprgo or self.cb_wprgo_file
if not os.path.exists(cb_wprgo):
raise FileNotFoundError(f'{cb_wprgo} not found!')
cb_output_dir = os.path.join(self.output_dir, 'cb_replay')
cli_helpers.Step(f'REPLAY WPR: {cb_wprgo}')
network = [f'--network={{type:"wpr", path:"{cb_wprgo}"}}']
command = self._GenerateCommandList(network, cb_output_dir)
self._CheckLog(command, log_name='replay')
def UploadWpr(self, cb_wprgo=None):
cb_wprgo = cb_wprgo or self.cb_wprgo_file
if not os.path.exists(cb_wprgo):
raise FileNotFoundError(f'{cb_wprgo} not found!')
cli_helpers.Step(f'UPLOAD WPR: {cb_wprgo}')
archive = self._GetDataWprArchivePath()
self._CopyTempWprgoToData(cb_wprgo, archive)
_UploadArchiveToGoogleStorage(archive)
return _GitAddArtifactHash(archive)
def UploadCL(self):
raise NotImplementedError()
def StartPinpointJobs(self):
raise NotImplementedError()
def Cleanup(self):
pass
@staticmethod
def ArchiveFilePath(benchmark_name):
return os.path.join(DATA_DIR, f'crossbench_android_{benchmark_name}.json')
def _CheckLog(self, command, log_name):
log_path = os.path.join(self.output_dir, f'{log_name}.log')
cli_helpers.CheckLog(command, log_path=log_path, env=_PrepareEnv())
cli_helpers.Info(f'Stdout/Stderr Log: {log_path}')
return log_path
def _GenerateCommandList(self, args=None, cb_output_dir=None):
args = args or []
cb_output_dir = cb_output_dir or self.output_dir
command = [
f'{self._CB_TOOL}',
self.bss,
'--repeat=1',
f'--browser={self.device_id}:{self.browser}',
'--verbose',
'--debug',
'--no-symlinks',
f'--out-dir={cb_output_dir}',
] + args
if self.story:
command += [f'--story={self.story}']
return command
def _GetDataWprArchivePath(self):
"""Gets the data archive file name by parsing the JSON story config."""
archives = self.wpr_archive_info.data['archives']
archive = None
if self.story:
archive = archives.get(self.story)
elif archives and len(archives.keys()) == 1:
archive = next(iter(archives.values()))
if not archive:
raise ValueError('Either --story is required or it was not found!')
archive_name = next(iter(archive.values()))
return os.path.join(DATA_DIR, archive_name)
def _CopyTempWprgoToData(self, src_path, des_path):
"""Copies the archive file to the `DATA_DIR` in the repository."""
if os.path.exists(des_path):
os.remove(des_path)
shutil.copy2(src_path, des_path)
def Main(argv):
parser = argparse.ArgumentParser()
parser.add_argument('-s',
'--story',
dest='story',
required=False,
help='Story to be recorded, replayed or uploaded. '
'If you are recording a system_health benchmark, '
'use desktop_system_health_story_set or '
'mobile_system_health_story_set')
parser.add_argument(
'-bss',
'--benchmark-or-story-set',
dest='bss',
required=True,
help='Benchmark or story set to be recorded, replayed or uploaded. '
'If you are recording a system health story, use '
'desktop_system_health_story_set or mobile_system_health_story_set.')
parser.add_argument(
'-d', '--device-id', dest='device_id',
help='Specify the device serial number listed by `adb devices`. When not '
'specified, the script runs in desktop mode.')
parser.add_argument(
'-b', '--bug', dest='bug_id',
help='Bug ID to be referenced on created CL')
parser.add_argument(
'-r', '--reviewer', action='append', dest='reviewers',
help='Email of the reviewer(s) for the created CL.')
parser.add_argument(
'--pageset-repeat', type=int, default=1, dest='repeat',
help='Number of times to repeat the entire pageset.')
parser.add_argument(
'--binary', default=None,
help='Path to the Chromium/Chrome binary relative to output directory. '
'Defaults to default Chrome browser installed if not specified.')
parser.add_argument('-cb',
'--crossbench',
action='store_true',
dest='is_cb',
default=False,
help='Whether to use the Crossbench tool.')
parser.add_argument(
'--out',
'--out-dir',
'--output-dir',
dest='output_dir',
default=None,
help='Path to generate log and archive files for Crossbench tool. '
'Defaults to generate a random folder in the system temp folder.')
parser.add_argument(
'--cb-wprgo',
'--cb-wprgo-file',
dest='cb_wprgo_file',
default=None,
help='Path to the target Crossbench WPRGO file.'
'Defaults to generate `archive.wprgo` file in the output folder.')
subparsers = parser.add_subparsers(
title='Mode in which to run this script', dest='command')
subparsers.add_parser(
'auto', help='interactive mode automating updating a recording')
subparsers.add_parser('live', help='run story on a live website')
subparsers.add_parser('record', help='record story from a live website')
subparsers.add_parser('replay', help='replay story from the recording')
subparsers.add_parser('upload', help='upload recording to the Google Storage')
subparsers.add_parser('review', help='create a CL with updated recording')
subparsers.add_parser(
'pinpoint', help='trigger Pinpoint jobs to test the recording')
args = parser.parse_args(argv)
if args.is_cb:
updater = CrossbenchWprUpdater(args)
else:
updater = WprUpdater(args)
try:
if args.command == 'auto':
_EnsureEditor()
luci_auth.CheckLoggedIn()
updater.AutoRun()
elif args.command =='live':
updater.LiveRun()
elif args.command == 'record':
updater.RecordWpr()
elif args.command == 'replay':
updater.ReplayWpr()
elif args.command == 'upload':
updater.UploadWpr()
elif args.command == 'review':
updater.UploadCL()
elif args.command == 'pinpoint':
luci_auth.CheckLoggedIn()
updater.StartPinpointJobs()
finally:
updater.Cleanup()
|