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