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.
6 """Archives or replays webpages and creates SKPs in a Google Storage location.
8 To archive webpages and store SKP files (archives should be rarely updated):
10 cd ../buildbot/slave/skia_slave_scripts
11 python webpages_playback.py --dest_gsbase=gs://rmistry --record \
12 --page_sets=all --skia_tools=/home/default/trunk/out/Debug/ \
13 --browser_executable=/tmp/chromium/out/Release/chrome
16 To replay archived webpages and re-generate SKP files (should be run whenever
17 SkPicture.PICTURE_VERSION changes):
19 cd ../buildbot/slave/skia_slave_scripts
20 python webpages_playback.py --dest_gsbase=gs://rmistry \
21 --page_sets=all --skia_tools=/home/default/trunk/out/Debug/ \
22 --browser_executable=/tmp/chromium/out/Release/chrome
25 Specify the --page_sets flag (default value is 'all') to pick a list of which
26 webpages should be archived and/or replayed. Eg:
28 --page_sets=page_sets/skia_yahooanswers_desktop.json,\
29 page_sets/skia_wikipedia_galaxynexus.json
31 The --browser_executable flag should point to the browser binary you want to use
32 to capture archives and/or capture SKP files. Majority of the time it should be
33 a newly built chrome binary.
35 The --upload_to_gs flag controls whether generated artifacts will be uploaded
36 to Google Storage (default value is False if not specified).
38 The --non-interactive flag controls whether the script will prompt the user
39 (default value is False if not specified).
41 The --skia_tools flag if specified will allow this script to run
42 debugger, render_pictures, and render_pdfs on the captured
43 SKP(s). The tools are run after all SKPs are succesfully captured to make sure
44 they can be added to the buildbots with no breakages.
45 To preview the captured SKP before proceeding to the next page_set specify both
46 --skia_tools and --view_debugger_output.
60 sys.path.insert(0, os.getcwd())
62 from common.py.utils import gs_utils
63 from common.py.utils import shell_utils
65 ROOT_PLAYBACK_DIR_NAME = 'playback'
66 SKPICTURES_DIR_NAME = 'skps'
69 # Local archive and SKP directories.
70 LOCAL_PLAYBACK_ROOT_DIR = os.path.join(
71 tempfile.gettempdir(), ROOT_PLAYBACK_DIR_NAME)
72 LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR = os.path.join(
73 os.path.abspath(os.path.dirname(__file__)), 'page_sets', 'data')
74 TMP_SKP_DIR = tempfile.mkdtemp()
76 # Location of the credentials.json file and the string that represents missing
78 CREDENTIALS_FILE_PATH = os.path.join(
79 os.path.abspath(os.path.dirname(__file__)), 'page_sets', 'data',
83 # Name of the SKP benchmark
84 SKP_BENCHMARK = 'skpicture_printer'
86 # The max base name length of Skp files.
87 MAX_SKP_BASE_NAME_LEN = 31
89 # Dictionary of device to platform prefixes for SKP files.
90 DEVICE_TO_PLATFORM_PREFIX = {
92 'galaxynexus': 'mobi',
96 # How many times the record_wpr binary should be retried.
97 RETRY_RECORD_WPR_COUNT = 5
98 # How many times the run_benchmark binary should be retried.
99 RETRY_RUN_MEASUREMENT_COUNT = 5
101 # Location of the credentials.json file in Google Storage.
102 CREDENTIALS_GS_PATH = '/playback/credentials/credentials.json'
104 X11_DISPLAY = os.getenv('DISPLAY', ':0')
106 GS_PREDEFINED_ACL = gs_utils.GSUtils.PredefinedACL.PRIVATE
107 GS_FINE_GRAINED_ACL_LIST = [
108 (gs_utils.GSUtils.IdType.GROUP_BY_DOMAIN, 'google.com',
109 gs_utils.GSUtils.Permission.READ),
113 class SkPicturePlayback(object):
114 """Class that archives or replays webpages and creates SKPs."""
116 def __init__(self, parse_options):
117 """Constructs a SkPicturePlayback BuildStep instance."""
118 assert parse_options.browser_executable, 'Must specify --browser_executable'
119 self._browser_executable = parse_options.browser_executable
121 self._all_page_sets_specified = parse_options.page_sets == 'all'
122 self._page_sets = self._ParsePageSets(parse_options.page_sets)
124 self._dest_gsbase = parse_options.dest_gsbase
125 self._record = parse_options.record
126 self._skia_tools = parse_options.skia_tools
127 self._non_interactive = parse_options.non_interactive
128 self._upload_to_gs = parse_options.upload_to_gs
129 self._alternate_upload_dir = parse_options.alternate_upload_dir
130 self._skip_all_gs_access = parse_options.skip_all_gs_access
131 self._telemetry_binaries_dir = os.path.join(parse_options.chrome_src_path,
134 self._local_skp_dir = os.path.join(
135 parse_options.output_dir, ROOT_PLAYBACK_DIR_NAME, SKPICTURES_DIR_NAME)
136 self._local_record_webpages_archive_dir = os.path.join(
137 parse_options.output_dir, ROOT_PLAYBACK_DIR_NAME, 'webpages_archive')
139 # List of SKP files generated by this script.
142 def _ParsePageSets(self, page_sets):
144 raise ValueError('Must specify at least one page_set!')
145 elif self._all_page_sets_specified:
146 # Get everything from the page_sets directory.
147 page_sets_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)),
149 ps = [os.path.join(page_sets_dir, page_set)
150 for page_set in os.listdir(page_sets_dir)
151 if not os.path.isdir(os.path.join(page_sets_dir, page_set)) and
152 page_set.endswith('.py')]
153 elif '*' in page_sets:
154 # Explode and return the glob.
155 ps = glob.glob(page_sets)
157 ps = page_sets.split(',')
162 """Run the SkPicturePlayback BuildStep."""
164 # Download the credentials file if it was not previously downloaded.
165 if self._skip_all_gs_access:
166 print """\n\nPlease create a %s file that contains:
169 "username": "google_testing_account_username",
170 "password": "google_testing_account_password"
173 "username": "facebook_testing_account_username",
174 "password": "facebook_testing_account_password"
176 }\n\n""" % CREDENTIALS_FILE_PATH
177 raw_input("Please press a key when you are ready to proceed...")
178 elif not os.path.isfile(CREDENTIALS_FILE_PATH):
179 # Download the credentials.json file from Google Storage.
180 gs_bucket = self._dest_gsbase.lstrip(gs_utils.GS_PREFIX)
181 gs_utils.GSUtils().download_file(gs_bucket, CREDENTIALS_GS_PATH,
182 CREDENTIALS_FILE_PATH)
184 # Delete any left over data files in the data directory.
185 for archive_file in glob.glob(
186 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR, 'skia_*')):
187 os.remove(archive_file)
189 # Delete the local root directory if it already exists.
190 if os.path.exists(LOCAL_PLAYBACK_ROOT_DIR):
191 shutil.rmtree(LOCAL_PLAYBACK_ROOT_DIR)
193 # Create the required local storage directories.
194 self._CreateLocalStorageDirs()
197 start_time = time.time()
199 # Loop through all page_sets.
200 for page_set in self._page_sets:
202 page_set_basename = os.path.basename(page_set).split('.')[0]
203 page_set_json_name = page_set_basename + '.json'
204 wpr_data_file = page_set.split(os.path.sep)[-1].split('.')[0] + '_000.wpr'
205 page_set_dir = os.path.dirname(page_set)
208 # Create an archive of the specified webpages if '--record=True' is
211 'PYTHONPATH=%s:$PYTHONPATH' % page_set_dir,
212 'DISPLAY=%s' % X11_DISPLAY,
213 os.path.join(self._telemetry_binaries_dir, 'record_wpr'),
214 '--extra-browser-args=--disable-setuid-sandbox',
216 '--browser-executable=%s' % self._browser_executable,
217 '%s_page_set' % page_set_basename,
218 '--page-set-base-dir=%s' % page_set_dir
220 for _ in range(RETRY_RECORD_WPR_COUNT):
222 shell_utils.run(' '.join(record_wpr_cmd), shell=True)
223 # Break out of the retry loop since there were no errors.
226 # There was a failure continue with the loop.
227 traceback.print_exc()
229 # If we get here then record_wpr did not succeed and thus did not
230 # break out of the loop.
231 raise Exception('record_wpr failed for page_set: %s' % page_set)
234 if not self._skip_all_gs_access:
235 # Get the webpages archive so that it can be replayed.
236 self._DownloadWebpagesArchive(wpr_data_file, page_set_json_name)
238 run_benchmark_cmd = (
239 'PYTHONPATH=%s:$PYTHONPATH' % page_set_dir,
240 'DISPLAY=%s' % X11_DISPLAY,
242 os.path.join(self._telemetry_binaries_dir, 'run_benchmark'),
243 '--extra-browser-args=--disable-setuid-sandbox',
245 '--browser-executable=%s' % self._browser_executable,
247 '--page-set-name=%s' % page_set_basename,
248 '--page-set-base-dir=%s' % page_set_dir,
249 '--skp-outdir=%s' % TMP_SKP_DIR,
250 '--also-run-disabled-tests'
253 for _ in range(RETRY_RUN_MEASUREMENT_COUNT):
255 print '\n\n=======Capturing SKP of %s=======\n\n' % page_set
256 shell_utils.run(' '.join(run_benchmark_cmd), shell=True)
257 except shell_utils.CommandFailedException:
258 # skpicture_printer sometimes fails with AssertionError but the
259 # captured SKP is still valid. This is a known issue.
263 # Move over the created archive into the local webpages archive
266 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR, wpr_data_file),
267 self._local_record_webpages_archive_dir)
269 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR,
271 self._local_record_webpages_archive_dir)
273 # Rename generated SKP files into more descriptive names.
275 self._RenameSkpFiles(page_set)
276 # Break out of the retry loop since there were no errors.
279 # There was a failure continue with the loop.
280 traceback.print_exc()
281 print '\n\n=======Retrying %s=======\n\n' % page_set
284 # If we get here then run_benchmark did not succeed and thus did not
285 # break out of the loop.
286 raise Exception('run_benchmark failed for page_set: %s' % page_set)
288 print '\n\n=======Capturing SKP files took %s seconds=======\n\n' % (
289 time.time() - start_time)
292 render_pictures_cmd = [
293 os.path.join(self._skia_tools, 'render_pictures'),
294 '-r', self._local_skp_dir
297 os.path.join(self._skia_tools, 'render_pdfs'),
301 for tools_cmd in (render_pictures_cmd, render_pdfs_cmd):
302 print '\n\n=======Running %s=======' % ' '.join(tools_cmd)
303 proc = subprocess.Popen(tools_cmd)
304 (code, _) = shell_utils.log_process_after_completion(proc, echo=False)
306 raise Exception('%s failed!' % ' '.join(tools_cmd))
308 if not self._non_interactive:
309 print '\n\n=======Running debugger======='
310 os.system('%s %s' % (os.path.join(self._skia_tools, 'debugger'),
311 os.path.join(self._local_skp_dir, '*')))
315 if not self._skip_all_gs_access and self._upload_to_gs:
316 print '\n\n=======Uploading to Google Storage=======\n\n'
317 # Copy the directory structure in the root directory into Google Storage.
318 dest_dir_name = ROOT_PLAYBACK_DIR_NAME
319 if self._alternate_upload_dir:
320 dest_dir_name = self._alternate_upload_dir
322 gs_bucket = self._dest_gsbase.lstrip(gs_utils.GS_PREFIX)
323 gs_utils.GSUtils().upload_dir_contents(
324 LOCAL_PLAYBACK_ROOT_DIR, gs_bucket, dest_dir_name,
325 upload_if=gs_utils.GSUtils.UploadIf.IF_MODIFIED,
326 predefined_acl=GS_PREDEFINED_ACL,
327 fine_grained_acl_list=GS_FINE_GRAINED_ACL_LIST)
329 print '\n\n=======New SKPs have been uploaded to %s =======\n\n' % (
330 posixpath.join(self._dest_gsbase, dest_dir_name, SKPICTURES_DIR_NAME))
332 print '\n\n=======Not Uploading to Google Storage=======\n\n'
333 print 'Generated resources are available in %s\n\n' % (
334 LOCAL_PLAYBACK_ROOT_DIR)
338 def _RenameSkpFiles(self, page_set):
339 """Rename generated SKP files into more descriptive names.
341 Look into the subdirectory of TMP_SKP_DIR and find the most interesting
342 .skp in there to be this page_set's representative .skp.
344 # Here's where we're assuming there's one page per pageset.
345 # If there were more than one, we'd overwrite filename below.
347 # /path/to/skia_yahooanswers_desktop.json -> skia_yahooanswers_desktop.json
348 _, ps_filename = os.path.split(page_set)
349 # skia_yahooanswers_desktop.json -> skia_yahooanswers_desktop
350 ps_basename, _ = os.path.splitext(ps_filename)
351 # skia_yahooanswers_desktop -> skia, yahooanswers, desktop
352 _, page_name, device = ps_basename.split('_')
354 basename = '%s_%s' % (DEVICE_TO_PLATFORM_PREFIX[device], page_name)
355 filename = basename[:MAX_SKP_BASE_NAME_LEN] + '.skp'
357 subdirs = glob.glob(os.path.join(TMP_SKP_DIR, '*'))
358 assert len(subdirs) == 1
360 # We choose the largest .skp as the most likely to be interesting.
361 largest_skp = max(glob.glob(os.path.join(site, '*.skp')),
362 key=lambda path: os.stat(path).st_size)
363 dest = os.path.join(self._local_skp_dir, filename)
364 print 'Moving', largest_skp, 'to', dest
365 shutil.move(largest_skp, dest)
366 self._skp_files.append(filename)
369 def _CreateLocalStorageDirs(self):
370 """Creates required local storage directories for this script."""
371 for d in (self._local_record_webpages_archive_dir,
372 self._local_skp_dir):
373 if os.path.exists(d):
377 def _DownloadWebpagesArchive(self, wpr_data_file, page_set_json_name):
378 """Downloads the webpages archive and its required page set from GS."""
379 wpr_source = posixpath.join(ROOT_PLAYBACK_DIR_NAME, 'webpages_archive',
381 page_set_source = posixpath.join(ROOT_PLAYBACK_DIR_NAME,
384 gs = gs_utils.GSUtils()
385 gs_bucket = self._dest_gsbase.lstrip(gs_utils.GS_PREFIX)
386 if (gs.does_storage_object_exist(gs_bucket, wpr_source) and
387 gs.does_storage_object_exist(gs_bucket, page_set_source)):
388 gs.download_file(gs_bucket, wpr_source,
389 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR,
391 gs.download_file(gs_bucket, page_set_source,
392 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR,
395 raise Exception('%s and %s do not exist in Google Storage!' % (
396 wpr_source, page_set_source))
399 if '__main__' == __name__:
400 option_parser = optparse.OptionParser()
401 option_parser.add_option(
403 help='Specifies the page sets to use to archive. Supports globs.',
405 option_parser.add_option(
406 '', '--skip_all_gs_access', action='store_true',
407 help='All Google Storage interactions will be skipped if this flag is '
408 'specified. This is useful for cases where the user does not have '
409 'the required .boto file but would like to generate webpage '
410 'archives and SKPs from the Skia page sets.',
412 option_parser.add_option(
413 '', '--record', action='store_true',
414 help='Specifies whether a new website archive should be created.',
416 option_parser.add_option(
418 help='gs:// bucket_name, the bucket to upload the file to.',
419 default='gs://chromium-skia-gm')
420 option_parser.add_option(
422 help=('Path to compiled Skia executable tools. '
423 'render_pictures/render_pdfs is run on the set '
424 'after all SKPs are captured. If the script is run without '
425 '--non-interactive then the debugger is also run at the end. Debug '
426 'builds are recommended because they seem to catch more failures '
427 'than Release builds.'),
429 option_parser.add_option(
430 '', '--upload_to_gs', action='store_true',
431 help='Does not upload to Google Storage if this is False.',
433 option_parser.add_option(
434 '', '--alternate_upload_dir',
435 help='Uploads to a different directory in Google Storage if this flag is '
438 option_parser.add_option(
440 help='Directory where SKPs and webpage archives will be outputted to.',
441 default=tempfile.gettempdir())
442 option_parser.add_option(
443 '', '--browser_executable',
444 help='The exact browser executable to run.',
446 option_parser.add_option(
447 '', '--chrome_src_path',
448 help='Path to the chromium src directory.',
450 option_parser.add_option(
451 '', '--non-interactive', action='store_true',
452 help='Runs the script without any prompts. If this flag is specified and '
453 '--skia_tools is specified then the debugger is not run.',
455 options, unused_args = option_parser.parse_args()
457 playback = SkPicturePlayback(options)
458 sys.exit(playback.Run())