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