1#!/usr/bin/env python 2# Copyright (c) 2012 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Archives or replays webpages and creates SKPs in a Google Storage location. 7 8To archive webpages and store SKP files (archives should be rarely updated): 9 10cd skia 11python tools/skp/webpages_playback.py --data_store=gs://your-bucket-name \ 12--record --page_sets=all --skia_tools=/home/default/trunk/out/Debug/ \ 13--browser_executable=/tmp/chromium/out/Release/chrome 14 15The above command uses Google Storage bucket 'your-bucket-name' to download 16needed files. 17 18To replay archived webpages and re-generate SKP files (should be run whenever 19SkPicture.PICTURE_VERSION changes): 20 21cd skia 22python tools/skp/webpages_playback.py --data_store=gs://your-bucket-name \ 23--page_sets=all --skia_tools=/home/default/trunk/out/Debug/ \ 24--browser_executable=/tmp/chromium/out/Release/chrome 25 26 27Specify the --page_sets flag (default value is 'all') to pick a list of which 28webpages should be archived and/or replayed. Eg: 29 30--page_sets=tools/skp/page_sets/skia_yahooanswers_desktop.py,\ 31tools/skp/page_sets/skia_googlecalendar_nexus10.py 32 33The --browser_executable flag should point to the browser binary you want to use 34to capture archives and/or capture SKP files. Majority of the time it should be 35a newly built chrome binary. 36 37The --data_store flag controls where the needed artifacts are downloaded from. 38It also controls where the generated artifacts, such as recorded webpages and 39resulting skp renderings, are uploaded to. URLs with scheme 'gs://' use Google 40Storage. Otherwise use local filesystem. 41 42The --upload=True flag means generated artifacts will be 43uploaded or copied to the location specified by --data_store. (default value is 44False if not specified). 45 46The --non-interactive flag controls whether the script will prompt the user 47(default value is False if not specified). 48 49The --skia_tools flag if specified will allow this script to run 50debugger, render_pictures, and render_pdfs on the captured 51SKP(s). The tools are run after all SKPs are succesfully captured to make sure 52they can be added to the buildbots with no breakages. 53""" 54 55 56from __future__ import print_function 57import datetime 58import glob 59import optparse 60import os 61import posixpath 62import shutil 63import subprocess 64import sys 65import tempfile 66import time 67import traceback 68 69 70ROOT_PLAYBACK_DIR_NAME = 'playback' 71SKPICTURES_DIR_NAME = 'skps' 72 73GS_PREFIX = 'gs://' 74 75PARTNERS_GS_BUCKET = 'gs://chrome-partner-telemetry' 76 77# Local archive and SKP directories. 78LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR = os.path.join( 79 os.path.abspath(os.path.dirname(__file__)), 'page_sets', 'data') 80TMP_SKP_DIR = tempfile.mkdtemp() 81 82# Location of the credentials.json file and the string that represents missing 83# passwords. 84CREDENTIALS_FILE_PATH = os.path.join( 85 os.path.abspath(os.path.dirname(__file__)), 'page_sets', 'data', 86 'credentials.json' 87) 88 89# Name of the SKP benchmark 90SKP_BENCHMARK = 'skpicture_printer' 91 92# The max base name length of Skp files. 93MAX_SKP_BASE_NAME_LEN = 31 94 95# Dictionary of device to platform prefixes for SKP files. 96DEVICE_TO_PLATFORM_PREFIX = { 97 'desktop': 'desk', 98 'mobile': 'mobi', 99 'tablet': 'tabl' 100} 101 102# How many times the record_wpr binary should be retried. 103RETRY_RECORD_WPR_COUNT = 5 104# How many times the run_benchmark binary should be retried. 105RETRY_RUN_MEASUREMENT_COUNT = 3 106 107# Location of the credentials.json file in Google Storage. 108CREDENTIALS_GS_PATH = 'playback/credentials/credentials.json' 109 110X11_DISPLAY = os.getenv('DISPLAY', ':0') 111 112# Path to Chromium's page sets. 113CHROMIUM_PAGE_SETS_PATH = os.path.join('tools', 'perf', 'page_sets') 114 115# Dictionary of supported Chromium page sets to their file prefixes. 116CHROMIUM_PAGE_SETS_TO_PREFIX = { 117} 118 119PAGE_SETS_TO_EXCLUSIONS = { 120 # See skbug.com/7348 121 'key_mobile_sites_smooth.py': '"(digg|worldjournal|twitter|espn)"', 122 # See skbug.com/7421 123 'top_25_smooth.py': '"(mail\.google\.com)"', 124} 125 126 127class InvalidSKPException(Exception): 128 """Raised when the created SKP is invalid.""" 129 pass 130 131 132def remove_prefix(s, prefix): 133 if s.startswith(prefix): 134 return s[len(prefix):] 135 return s 136 137 138class SkPicturePlayback(object): 139 """Class that archives or replays webpages and creates SKPs.""" 140 141 def __init__(self, parse_options): 142 """Constructs a SkPicturePlayback BuildStep instance.""" 143 assert parse_options.browser_executable, 'Must specify --browser_executable' 144 self._browser_executable = parse_options.browser_executable 145 self._browser_args = '--disable-setuid-sandbox --disable-field-trial-config' 146 if parse_options.browser_extra_args: 147 self._browser_args = '%s %s' % ( 148 self._browser_args, parse_options.browser_extra_args) 149 150 self._chrome_page_sets_path = os.path.join(parse_options.chrome_src_path, 151 CHROMIUM_PAGE_SETS_PATH) 152 self._all_page_sets_specified = parse_options.page_sets == 'all' 153 self._page_sets = self._ParsePageSets(parse_options.page_sets) 154 155 self._record = parse_options.record 156 self._skia_tools = parse_options.skia_tools 157 self._non_interactive = parse_options.non_interactive 158 self._upload = parse_options.upload 159 self._skp_prefix = parse_options.skp_prefix 160 data_store_location = parse_options.data_store 161 if data_store_location.startswith(GS_PREFIX): 162 self.gs = GoogleStorageDataStore(data_store_location) 163 else: 164 self.gs = LocalFileSystemDataStore(data_store_location) 165 self._upload_to_partner_bucket = parse_options.upload_to_partner_bucket 166 self._alternate_upload_dir = parse_options.alternate_upload_dir 167 self._telemetry_binaries_dir = os.path.join(parse_options.chrome_src_path, 168 'tools', 'perf') 169 self._catapult_dir = os.path.join(parse_options.chrome_src_path, 170 'third_party', 'catapult') 171 172 self._local_skp_dir = os.path.join( 173 parse_options.output_dir, ROOT_PLAYBACK_DIR_NAME, SKPICTURES_DIR_NAME) 174 self._local_record_webpages_archive_dir = os.path.join( 175 parse_options.output_dir, ROOT_PLAYBACK_DIR_NAME, 'webpages_archive') 176 177 # List of SKP files generated by this script. 178 self._skp_files = [] 179 180 def _ParsePageSets(self, page_sets): 181 if not page_sets: 182 raise ValueError('Must specify at least one page_set!') 183 elif self._all_page_sets_specified: 184 # Get everything from the page_sets directory. 185 page_sets_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 186 'page_sets') 187 ps = [os.path.join(page_sets_dir, page_set) 188 for page_set in os.listdir(page_sets_dir) 189 if not os.path.isdir(os.path.join(page_sets_dir, page_set)) and 190 page_set.endswith('.py')] 191 chromium_ps = [ 192 os.path.join(self._chrome_page_sets_path, cr_page_set) 193 for cr_page_set in CHROMIUM_PAGE_SETS_TO_PREFIX] 194 ps.extend(chromium_ps) 195 elif '*' in page_sets: 196 # Explode and return the glob. 197 ps = glob.glob(page_sets) 198 else: 199 ps = page_sets.split(',') 200 ps.sort() 201 return ps 202 203 def _IsChromiumPageSet(self, page_set): 204 """Returns true if the specified page set is a Chromium page set.""" 205 return page_set.startswith(self._chrome_page_sets_path) 206 207 def Run(self): 208 """Run the SkPicturePlayback BuildStep.""" 209 210 # Download the credentials file if it was not previously downloaded. 211 if not os.path.isfile(CREDENTIALS_FILE_PATH): 212 # Download the credentials.json file from Google Storage. 213 self.gs.download_file(CREDENTIALS_GS_PATH, CREDENTIALS_FILE_PATH) 214 215 if not os.path.isfile(CREDENTIALS_FILE_PATH): 216 raise Exception("""Could not locate credentials file in the storage. 217 Please create a credentials file in gs://%s that contains: 218 { 219 "google": { 220 "username": "google_testing_account_username", 221 "password": "google_testing_account_password" 222 } 223 }\n\n""" % CREDENTIALS_GS_PATH) 224 225 # Delete any left over data files in the data directory. 226 for archive_file in glob.glob( 227 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR, 'skia_*')): 228 os.remove(archive_file) 229 230 # Create the required local storage directories. 231 self._CreateLocalStorageDirs() 232 233 # Start the timer. 234 start_time = time.time() 235 236 # Loop through all page_sets. 237 for page_set in self._page_sets: 238 if os.path.basename(page_set) == '__init__.py': 239 continue 240 page_set_basename = os.path.basename(page_set).split('.')[0] 241 page_set_json_name = page_set_basename + '.json' 242 wpr_data_file_glob = ( 243 page_set.split(os.path.sep)[-1].split('.')[0] + '_*.wprgo') 244 page_set_dir = os.path.dirname(page_set) 245 246 if self._IsChromiumPageSet(page_set): 247 print('Using Chromium\'s captured archives for Chromium\'s page sets.') 248 elif self._record: 249 # Create an archive of the specified webpages if '--record=True' is 250 # specified. 251 record_wpr_cmd = ( 252 'PYTHONPATH=%s:%s:$PYTHONPATH' % (page_set_dir, self._catapult_dir), 253 'DISPLAY=%s' % X11_DISPLAY, 254 os.path.join(self._telemetry_binaries_dir, 'record_wpr'), 255 '--extra-browser-args="%s"' % self._browser_args, 256 '--browser=exact', 257 '--browser-executable=%s' % self._browser_executable, 258 '%s_page_set' % page_set_basename, 259 '--page-set-base-dir=%s' % page_set_dir 260 ) 261 for _ in range(RETRY_RECORD_WPR_COUNT): 262 try: 263 subprocess.check_call(' '.join(record_wpr_cmd), shell=True) 264 265 # Copy over the created archive into the local webpages archive 266 # directory. 267 for wpr_data_file in glob.glob(os.path.join( 268 LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR, wpr_data_file_glob)): 269 shutil.copy( 270 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR, wpr_data_file), 271 self._local_record_webpages_archive_dir) 272 shutil.copy( 273 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR, 274 page_set_json_name), 275 self._local_record_webpages_archive_dir) 276 277 # Break out of the retry loop since there were no errors. 278 break 279 except Exception: 280 # There was a failure continue with the loop. 281 traceback.print_exc() 282 else: 283 # If we get here then record_wpr did not succeed and thus did not 284 # break out of the loop. 285 raise Exception('record_wpr failed for page_set: %s' % page_set) 286 287 else: 288 # Get the webpages archive so that it can be replayed. 289 self._DownloadWebpagesArchive(wpr_data_file_glob, page_set_json_name) 290 291 run_benchmark_cmd = [ 292 'PYTHONPATH=%s:%s:$PYTHONPATH' % (page_set_dir, self._catapult_dir), 293 'DISPLAY=%s' % X11_DISPLAY, 294 'timeout', '1800', 295 os.path.join(self._telemetry_binaries_dir, 'run_benchmark'), 296 '--extra-browser-args="%s"' % self._browser_args, 297 '--browser=exact', 298 '--browser-executable=%s' % self._browser_executable, 299 SKP_BENCHMARK, 300 '--page-set-name=%s' % page_set_basename, 301 '--page-set-base-dir=%s' % page_set_dir, 302 '--skp-outdir=%s' % TMP_SKP_DIR, 303 '--also-run-disabled-tests', 304 ] 305 306 exclusions = PAGE_SETS_TO_EXCLUSIONS.get(os.path.basename(page_set)) 307 if exclusions: 308 run_benchmark_cmd.append('--story-filter-exclude=' + exclusions) 309 310 for _ in range(RETRY_RUN_MEASUREMENT_COUNT): 311 try: 312 print('\n\n=======Capturing SKP of %s=======\n\n' % page_set) 313 subprocess.check_call(' '.join(run_benchmark_cmd), shell=True) 314 except subprocess.CalledProcessError: 315 # There was a failure continue with the loop. 316 traceback.print_exc() 317 print('\n\n=======Retrying %s=======\n\n' % page_set) 318 time.sleep(10) 319 continue 320 321 try: 322 # Rename generated SKP files into more descriptive names. 323 self._RenameSkpFiles(page_set) 324 except InvalidSKPException: 325 # There was a failure continue with the loop. 326 traceback.print_exc() 327 print('\n\n=======Retrying %s=======\n\n' % page_set) 328 time.sleep(10) 329 continue 330 331 # Break out of the retry loop since there were no errors. 332 break 333 else: 334 # If we get here then run_benchmark did not succeed and thus did not 335 # break out of the loop. 336 raise Exception('run_benchmark failed for page_set: %s' % page_set) 337 338 print('\n\n=======Capturing SKP files took %s seconds=======\n\n' % ( 339 time.time() - start_time)) 340 341 if self._skia_tools: 342 render_pictures_cmd = [ 343 os.path.join(self._skia_tools, 'render_pictures'), 344 '-r', self._local_skp_dir 345 ] 346 render_pdfs_cmd = [ 347 os.path.join(self._skia_tools, 'render_pdfs'), 348 '-r', self._local_skp_dir 349 ] 350 351 for tools_cmd in (render_pictures_cmd, render_pdfs_cmd): 352 print('\n\n=======Running %s=======' % ' '.join(tools_cmd)) 353 subprocess.check_call(tools_cmd) 354 355 if not self._non_interactive: 356 print('\n\n=======Running debugger=======') 357 os.system('%s %s' % (os.path.join(self._skia_tools, 'debugger'), 358 self._local_skp_dir)) 359 360 print('\n\n') 361 362 if self._upload: 363 print('\n\n=======Uploading to %s=======\n\n' % self.gs.target_type()) 364 # Copy the directory structure in the root directory into Google Storage. 365 dest_dir_name = ROOT_PLAYBACK_DIR_NAME 366 if self._alternate_upload_dir: 367 dest_dir_name = self._alternate_upload_dir 368 369 self.gs.upload_dir_contents( 370 self._local_skp_dir, dest_dir=dest_dir_name) 371 372 print('\n\n=======New SKPs have been uploaded to %s =======\n\n' % 373 posixpath.join(self.gs.target_name(), dest_dir_name, 374 SKPICTURES_DIR_NAME)) 375 376 else: 377 print('\n\n=======Not Uploading to %s=======\n\n' % self.gs.target_type()) 378 print('Generated resources are available in %s\n\n' % self._local_skp_dir) 379 380 if self._upload_to_partner_bucket: 381 print('\n\n=======Uploading to Partner bucket %s =======\n\n' % 382 PARTNERS_GS_BUCKET) 383 partner_gs = GoogleStorageDataStore(PARTNERS_GS_BUCKET) 384 timestamp = datetime.datetime.utcnow().strftime('%Y-%m-%d') 385 upload_dir = posixpath.join(SKPICTURES_DIR_NAME, timestamp) 386 try: 387 partner_gs.delete_path(upload_dir) 388 except subprocess.CalledProcessError: 389 print('Cannot delete %s because it does not exist yet.' % upload_dir) 390 print('Uploading %s to %s' % (self._local_skp_dir, upload_dir)) 391 partner_gs.upload_dir_contents(self._local_skp_dir, upload_dir) 392 print('\n\n=======New SKPs have been uploaded to %s =======\n\n' % 393 posixpath.join(partner_gs.target_name(), upload_dir)) 394 395 return 0 396 397 def _GetSkiaSkpFileName(self, page_set): 398 """Returns the SKP file name for Skia page sets.""" 399 # /path/to/skia_yahooanswers_desktop.py -> skia_yahooanswers_desktop.py 400 ps_filename = os.path.basename(page_set) 401 # skia_yahooanswers_desktop.py -> skia_yahooanswers_desktop 402 ps_basename, _ = os.path.splitext(ps_filename) 403 # skia_yahooanswers_desktop -> skia, yahooanswers, desktop 404 _, page_name, device = ps_basename.split('_') 405 basename = '%s_%s' % (DEVICE_TO_PLATFORM_PREFIX[device], page_name) 406 return basename[:MAX_SKP_BASE_NAME_LEN] + '.skp' 407 408 def _GetChromiumSkpFileName(self, page_set, site): 409 """Returns the SKP file name for Chromium page sets.""" 410 # /path/to/http___mobile_news_sandbox_pt0 -> http___mobile_news_sandbox_pt0 411 _, webpage = os.path.split(site) 412 # http___mobile_news_sandbox_pt0 -> mobile_news_sandbox_pt0 413 for prefix in ('http___', 'https___', 'www_'): 414 if webpage.startswith(prefix): 415 webpage = webpage[len(prefix):] 416 # /path/to/skia_yahooanswers_desktop.py -> skia_yahooanswers_desktop.py 417 ps_filename = os.path.basename(page_set) 418 # http___mobile_news_sandbox -> pagesetprefix_http___mobile_news_sandbox 419 basename = '%s_%s' % (CHROMIUM_PAGE_SETS_TO_PREFIX[ps_filename], webpage) 420 return basename[:MAX_SKP_BASE_NAME_LEN] + '.skp' 421 422 def _RenameSkpFiles(self, page_set): 423 """Rename generated SKP files into more descriptive names. 424 425 Look into the subdirectory of TMP_SKP_DIR and find the most interesting 426 .skp in there to be this page_set's representative .skp. 427 428 Throws InvalidSKPException if the chosen .skp is less than 1KB. This 429 typically happens when there is a 404 or a redirect loop. Anything greater 430 than 1KB seems to have captured at least some useful information. 431 """ 432 subdirs = glob.glob(os.path.join(TMP_SKP_DIR, '*')) 433 for site in subdirs: 434 if self._IsChromiumPageSet(page_set): 435 filename = self._GetChromiumSkpFileName(page_set, site) 436 else: 437 filename = self._GetSkiaSkpFileName(page_set) 438 filename = filename.lower() 439 440 if self._skp_prefix: 441 filename = '%s%s' % (self._skp_prefix, filename) 442 443 # We choose the largest .skp as the most likely to be interesting. 444 largest_skp = max(glob.glob(os.path.join(site, '*.skp')), 445 key=lambda path: os.stat(path).st_size) 446 dest = os.path.join(self._local_skp_dir, filename) 447 print('Moving', largest_skp, 'to', dest) 448 shutil.move(largest_skp, dest) 449 self._skp_files.append(filename) 450 shutil.rmtree(site) 451 skp_size = os.path.getsize(dest) 452 if skp_size < 1024: 453 raise InvalidSKPException( 454 'Size of %s is only %d. Something is wrong.' % (dest, skp_size)) 455 456 457 def _CreateLocalStorageDirs(self): 458 """Creates required local storage directories for this script.""" 459 for d in (self._local_record_webpages_archive_dir, 460 self._local_skp_dir): 461 if os.path.exists(d): 462 shutil.rmtree(d) 463 os.makedirs(d) 464 465 def _DownloadWebpagesArchive(self, wpr_data_file, page_set_json_name): 466 """Downloads the webpages archive and its required page set from GS.""" 467 wpr_source = posixpath.join(ROOT_PLAYBACK_DIR_NAME, 'webpages_archive', 468 wpr_data_file) 469 page_set_source = posixpath.join(ROOT_PLAYBACK_DIR_NAME, 470 'webpages_archive', 471 page_set_json_name) 472 gs = self.gs 473 if (gs.does_storage_object_exist(wpr_source) and 474 gs.does_storage_object_exist(page_set_source)): 475 gs.download_file(wpr_source, LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR) 476 gs.download_file(page_set_source, 477 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR, 478 page_set_json_name)) 479 else: 480 raise Exception('%s and %s do not exist in %s!' % (gs.target_type(), 481 wpr_source, page_set_source)) 482 483class DataStore: 484 """An abstract base class for uploading recordings to a data storage. 485 The interface emulates the google storage api.""" 486 def target_name(self): 487 raise NotImplementedError() 488 def target_type(self): 489 raise NotImplementedError() 490 def does_storage_object_exist(self, name): 491 raise NotImplementedError() 492 def download_file(self, name, local_path): 493 raise NotImplementedError() 494 def upload_dir_contents(self, source_dir, dest_dir): 495 raise NotImplementedError() 496 497 498class GoogleStorageDataStore(DataStore): 499 def __init__(self, data_store_url): 500 self._url = data_store_url.rstrip('/') 501 502 def target_name(self): 503 return self._url 504 505 def target_type(self): 506 return 'Google Storage' 507 508 def does_storage_object_exist(self, name): 509 try: 510 output = subprocess.check_output([ 511 'gsutil', 'ls', '/'.join((self._url, name))]) 512 except subprocess.CalledProcessError: 513 return False 514 if len(output.splitlines()) != 1: 515 return False 516 return True 517 518 def delete_path(self, path): 519 subprocess.check_call(['gsutil', 'rm', '-r', '/'.join((self._url, path))]) 520 521 def download_file(self, name, local_path): 522 subprocess.check_call([ 523 'gsutil', 'cp', '/'.join((self._url, name)), local_path]) 524 525 def upload_dir_contents(self, source_dir, dest_dir): 526 subprocess.check_call([ 527 'gsutil', 'cp', '-r', source_dir, '/'.join((self._url, dest_dir))]) 528 529 530class LocalFileSystemDataStore(DataStore): 531 def __init__(self, data_store_location): 532 self._base_dir = data_store_location 533 def target_name(self): 534 return self._base_dir 535 def target_type(self): 536 return self._base_dir 537 def does_storage_object_exist(self, name): 538 return os.path.isfile(os.path.join(self._base_dir, name)) 539 def delete_path(self, path): 540 shutil.rmtree(path) 541 def download_file(self, name, local_path): 542 shutil.copyfile(os.path.join(self._base_dir, name), local_path) 543 def upload_dir_contents(self, source_dir, dest_dir): 544 def copytree(source_dir, dest_dir): 545 if not os.path.exists(dest_dir): 546 os.makedirs(dest_dir) 547 for item in os.listdir(source_dir): 548 source = os.path.join(source_dir, item) 549 dest = os.path.join(dest_dir, item) 550 if os.path.isdir(source): 551 copytree(source, dest) 552 else: 553 shutil.copy2(source, dest) 554 copytree(source_dir, os.path.join(self._base_dir, dest_dir)) 555 556if '__main__' == __name__: 557 option_parser = optparse.OptionParser() 558 option_parser.add_option( 559 '', '--page_sets', 560 help='Specifies the page sets to use to archive. Supports globs.', 561 default='all') 562 option_parser.add_option( 563 '', '--record', action='store_true', 564 help='Specifies whether a new website archive should be created.', 565 default=False) 566 option_parser.add_option( 567 '', '--skia_tools', 568 help=('Path to compiled Skia executable tools. ' 569 'render_pictures/render_pdfs is run on the set ' 570 'after all SKPs are captured. If the script is run without ' 571 '--non-interactive then the debugger is also run at the end. Debug ' 572 'builds are recommended because they seem to catch more failures ' 573 'than Release builds.'), 574 default=None) 575 option_parser.add_option( 576 '', '--upload', action='store_true', 577 help=('Uploads to Google Storage or copies to local filesystem storage ' 578 ' if this is True.'), 579 default=False) 580 option_parser.add_option( 581 '', '--upload_to_partner_bucket', action='store_true', 582 help=('Uploads SKPs to the chrome-partner-telemetry Google Storage ' 583 'bucket if true.'), 584 default=False) 585 option_parser.add_option( 586 '', '--data_store', 587 help=('The location of the file storage to use to download and upload ' 588 'files. Can be \'gs://<bucket>\' for Google Storage, or ' 589 'a directory for local filesystem storage'), 590 default='gs://skia-skps') 591 option_parser.add_option( 592 '', '--alternate_upload_dir', 593 help= ('Uploads to a different directory in Google Storage or local ' 594 'storage if this flag is specified'), 595 default=None) 596 option_parser.add_option( 597 '', '--output_dir', 598 help=('Temporary directory where SKPs and webpage archives will be ' 599 'outputted to.'), 600 default=tempfile.gettempdir()) 601 option_parser.add_option( 602 '', '--browser_executable', 603 help='The exact browser executable to run.', 604 default=None) 605 option_parser.add_option( 606 '', '--browser_extra_args', 607 help='Additional arguments to pass to the browser.', 608 default=None) 609 option_parser.add_option( 610 '', '--chrome_src_path', 611 help='Path to the chromium src directory.', 612 default=None) 613 option_parser.add_option( 614 '', '--non-interactive', action='store_true', 615 help='Runs the script without any prompts. If this flag is specified and ' 616 '--skia_tools is specified then the debugger is not run.', 617 default=False) 618 option_parser.add_option( 619 '', '--skp_prefix', 620 help='Prefix to add to the names of generated SKPs.', 621 default=None) 622 options, unused_args = option_parser.parse_args() 623 624 playback = SkPicturePlayback(options) 625 sys.exit(playback.Run()) 626