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