xref: /aosp_15_r20/external/skia/tools/skp/webpages_playback.py (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
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