blob: d5054bff8dcaf401f10973d5aa590c052ee24eb9 [file] [log] [blame]
borenetdc89ca52014-10-17 07:37:05 -07001#!/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
kkinnunenfcf35c52014-12-03 05:51:24 -080010cd skia
11python tools/skp/webpages_playback.py --dest_gsbase=gs://rmistry --record \
borenetdc89ca52014-10-17 07:37:05 -070012--page_sets=all --skia_tools=/home/default/trunk/out/Debug/ \
13--browser_executable=/tmp/chromium/out/Release/chrome
14
15
16To replay archived webpages and re-generate SKP files (should be run whenever
17SkPicture.PICTURE_VERSION changes):
18
kkinnunenfcf35c52014-12-03 05:51:24 -080019cd skia
20python tools/skp/webpages_playback.py --dest_gsbase=gs://rmistry \
borenetdc89ca52014-10-17 07:37:05 -070021--page_sets=all --skia_tools=/home/default/trunk/out/Debug/ \
22--browser_executable=/tmp/chromium/out/Release/chrome
23
24
25Specify the --page_sets flag (default value is 'all') to pick a list of which
26webpages should be archived and/or replayed. Eg:
27
kkinnunenfcf35c52014-12-03 05:51:24 -080028--page_sets=tools/skp/page_sets/skia_yahooanswers_desktop.py,\
29tools/skp/page_sets/skia_googlecalendar_nexus10.py
borenetdc89ca52014-10-17 07:37:05 -070030
31The --browser_executable flag should point to the browser binary you want to use
32to capture archives and/or capture SKP files. Majority of the time it should be
33a newly built chrome binary.
34
35The --upload_to_gs flag controls whether generated artifacts will be uploaded
36to Google Storage (default value is False if not specified).
37
38The --non-interactive flag controls whether the script will prompt the user
39(default value is False if not specified).
40
41The --skia_tools flag if specified will allow this script to run
42debugger, render_pictures, and render_pdfs on the captured
43SKP(s). The tools are run after all SKPs are succesfully captured to make sure
44they can be added to the buildbots with no breakages.
borenetdc89ca52014-10-17 07:37:05 -070045"""
46
47import glob
48import optparse
49import os
50import posixpath
51import shutil
52import subprocess
53import sys
54import tempfile
55import time
56import traceback
57
58sys.path.insert(0, os.getcwd())
59
60from common.py.utils import gs_utils
61from common.py.utils import shell_utils
62
63ROOT_PLAYBACK_DIR_NAME = 'playback'
64SKPICTURES_DIR_NAME = 'skps'
65
66
67# Local archive and SKP directories.
68LOCAL_PLAYBACK_ROOT_DIR = os.path.join(
69 tempfile.gettempdir(), ROOT_PLAYBACK_DIR_NAME)
70LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR = os.path.join(
71 os.path.abspath(os.path.dirname(__file__)), 'page_sets', 'data')
72TMP_SKP_DIR = tempfile.mkdtemp()
73
rmistryf802f322014-10-22 05:04:43 -070074# Location of the credentials.json file and the string that represents missing
75# passwords.
76CREDENTIALS_FILE_PATH = os.path.join(
77 os.path.abspath(os.path.dirname(__file__)), 'page_sets', 'data',
78 'credentials.json'
79)
80
borenetdc89ca52014-10-17 07:37:05 -070081# Name of the SKP benchmark
82SKP_BENCHMARK = 'skpicture_printer'
83
84# The max base name length of Skp files.
85MAX_SKP_BASE_NAME_LEN = 31
86
87# Dictionary of device to platform prefixes for SKP files.
88DEVICE_TO_PLATFORM_PREFIX = {
89 'desktop': 'desk',
90 'galaxynexus': 'mobi',
91 'nexus10': 'tabl'
92}
93
94# How many times the record_wpr binary should be retried.
95RETRY_RECORD_WPR_COUNT = 5
borenet78399152014-10-17 12:15:46 -070096# How many times the run_benchmark binary should be retried.
borenetdc89ca52014-10-17 07:37:05 -070097RETRY_RUN_MEASUREMENT_COUNT = 5
98
rmistryf802f322014-10-22 05:04:43 -070099# Location of the credentials.json file in Google Storage.
100CREDENTIALS_GS_PATH = '/playback/credentials/credentials.json'
101
borenetdc89ca52014-10-17 07:37:05 -0700102X11_DISPLAY = os.getenv('DISPLAY', ':0')
103
104GS_PREDEFINED_ACL = gs_utils.GSUtils.PredefinedACL.PRIVATE
105GS_FINE_GRAINED_ACL_LIST = [
106 (gs_utils.GSUtils.IdType.GROUP_BY_DOMAIN, 'google.com',
107 gs_utils.GSUtils.Permission.READ),
108]
109
kkinnunene75d2d22014-12-03 04:38:46 -0800110def remove_prefix(s, prefix):
111 if s.startswith(prefix):
112 return s[len(prefix):]
113 return s
borenetdc89ca52014-10-17 07:37:05 -0700114
115class SkPicturePlayback(object):
116 """Class that archives or replays webpages and creates SKPs."""
117
118 def __init__(self, parse_options):
119 """Constructs a SkPicturePlayback BuildStep instance."""
120 assert parse_options.browser_executable, 'Must specify --browser_executable'
121 self._browser_executable = parse_options.browser_executable
122
123 self._all_page_sets_specified = parse_options.page_sets == 'all'
124 self._page_sets = self._ParsePageSets(parse_options.page_sets)
125
126 self._dest_gsbase = parse_options.dest_gsbase
127 self._record = parse_options.record
128 self._skia_tools = parse_options.skia_tools
129 self._non_interactive = parse_options.non_interactive
130 self._upload_to_gs = parse_options.upload_to_gs
131 self._alternate_upload_dir = parse_options.alternate_upload_dir
132 self._skip_all_gs_access = parse_options.skip_all_gs_access
133 self._telemetry_binaries_dir = os.path.join(parse_options.chrome_src_path,
134 'tools', 'perf')
135
136 self._local_skp_dir = os.path.join(
137 parse_options.output_dir, ROOT_PLAYBACK_DIR_NAME, SKPICTURES_DIR_NAME)
138 self._local_record_webpages_archive_dir = os.path.join(
139 parse_options.output_dir, ROOT_PLAYBACK_DIR_NAME, 'webpages_archive')
140
141 # List of SKP files generated by this script.
142 self._skp_files = []
143
144 def _ParsePageSets(self, page_sets):
145 if not page_sets:
146 raise ValueError('Must specify at least one page_set!')
147 elif self._all_page_sets_specified:
148 # Get everything from the page_sets directory.
149 page_sets_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)),
150 'page_sets')
151 ps = [os.path.join(page_sets_dir, page_set)
152 for page_set in os.listdir(page_sets_dir)
153 if not os.path.isdir(os.path.join(page_sets_dir, page_set)) and
154 page_set.endswith('.py')]
155 elif '*' in page_sets:
156 # Explode and return the glob.
157 ps = glob.glob(page_sets)
158 else:
159 ps = page_sets.split(',')
160 ps.sort()
161 return ps
162
163 def Run(self):
164 """Run the SkPicturePlayback BuildStep."""
165
rmistryf802f322014-10-22 05:04:43 -0700166 # Download the credentials file if it was not previously downloaded.
167 if self._skip_all_gs_access:
168 print """\n\nPlease create a %s file that contains:
169 {
170 "google": {
171 "username": "google_testing_account_username",
172 "password": "google_testing_account_password"
173 },
174 "facebook": {
175 "username": "facebook_testing_account_username",
176 "password": "facebook_testing_account_password"
177 }
178 }\n\n""" % CREDENTIALS_FILE_PATH
179 raw_input("Please press a key when you are ready to proceed...")
180 elif not os.path.isfile(CREDENTIALS_FILE_PATH):
181 # Download the credentials.json file from Google Storage.
kkinnunene75d2d22014-12-03 04:38:46 -0800182 gs_bucket = remove_prefix(self._dest_gsbase.lstrip(), gs_utils.GS_PREFIX)
rmistryf802f322014-10-22 05:04:43 -0700183 gs_utils.GSUtils().download_file(gs_bucket, CREDENTIALS_GS_PATH,
184 CREDENTIALS_FILE_PATH)
185
borenetdc89ca52014-10-17 07:37:05 -0700186 # Delete any left over data files in the data directory.
187 for archive_file in glob.glob(
188 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR, 'skia_*')):
189 os.remove(archive_file)
190
191 # Delete the local root directory if it already exists.
192 if os.path.exists(LOCAL_PLAYBACK_ROOT_DIR):
193 shutil.rmtree(LOCAL_PLAYBACK_ROOT_DIR)
194
195 # Create the required local storage directories.
196 self._CreateLocalStorageDirs()
197
198 # Start the timer.
199 start_time = time.time()
200
201 # Loop through all page_sets.
202 for page_set in self._page_sets:
203
rmistry7620bf02014-10-27 06:42:11 -0700204 page_set_basename = os.path.basename(page_set).split('.')[0]
205 page_set_json_name = page_set_basename + '.json'
borenetdc89ca52014-10-17 07:37:05 -0700206 wpr_data_file = page_set.split(os.path.sep)[-1].split('.')[0] + '_000.wpr'
rmistry7620bf02014-10-27 06:42:11 -0700207 page_set_dir = os.path.dirname(page_set)
borenetdc89ca52014-10-17 07:37:05 -0700208
209 if self._record:
210 # Create an archive of the specified webpages if '--record=True' is
211 # specified.
212 record_wpr_cmd = (
rmistry7620bf02014-10-27 06:42:11 -0700213 'PYTHONPATH=%s:$PYTHONPATH' % page_set_dir,
borenetdc89ca52014-10-17 07:37:05 -0700214 'DISPLAY=%s' % X11_DISPLAY,
215 os.path.join(self._telemetry_binaries_dir, 'record_wpr'),
216 '--extra-browser-args=--disable-setuid-sandbox',
217 '--browser=exact',
218 '--browser-executable=%s' % self._browser_executable,
rmistry7620bf02014-10-27 06:42:11 -0700219 '%s_page_set' % page_set_basename,
220 '--page-set-base-dir=%s' % page_set_dir
borenetdc89ca52014-10-17 07:37:05 -0700221 )
222 for _ in range(RETRY_RECORD_WPR_COUNT):
rmistry0ec28af2014-10-28 14:25:17 -0700223 try:
224 shell_utils.run(' '.join(record_wpr_cmd), shell=True)
borenetdc89ca52014-10-17 07:37:05 -0700225 # Break out of the retry loop since there were no errors.
226 break
rmistry0ec28af2014-10-28 14:25:17 -0700227 except Exception:
228 # There was a failure continue with the loop.
229 traceback.print_exc()
borenetdc89ca52014-10-17 07:37:05 -0700230 else:
231 # If we get here then record_wpr did not succeed and thus did not
232 # break out of the loop.
233 raise Exception('record_wpr failed for page_set: %s' % page_set)
234
235 else:
236 if not self._skip_all_gs_access:
237 # Get the webpages archive so that it can be replayed.
rmistry7620bf02014-10-27 06:42:11 -0700238 self._DownloadWebpagesArchive(wpr_data_file, page_set_json_name)
borenetdc89ca52014-10-17 07:37:05 -0700239
borenet78399152014-10-17 12:15:46 -0700240 run_benchmark_cmd = (
rmistryf802f322014-10-22 05:04:43 -0700241 'PYTHONPATH=%s:$PYTHONPATH' % page_set_dir,
borenetdc89ca52014-10-17 07:37:05 -0700242 'DISPLAY=%s' % X11_DISPLAY,
243 'timeout', '300',
borenet78399152014-10-17 12:15:46 -0700244 os.path.join(self._telemetry_binaries_dir, 'run_benchmark'),
borenetdc89ca52014-10-17 07:37:05 -0700245 '--extra-browser-args=--disable-setuid-sandbox',
246 '--browser=exact',
247 '--browser-executable=%s' % self._browser_executable,
248 SKP_BENCHMARK,
rmistry7620bf02014-10-27 06:42:11 -0700249 '--page-set-name=%s' % page_set_basename,
rmistryf802f322014-10-22 05:04:43 -0700250 '--page-set-base-dir=%s' % page_set_dir,
251 '--skp-outdir=%s' % TMP_SKP_DIR,
252 '--also-run-disabled-tests'
borenetdc89ca52014-10-17 07:37:05 -0700253 )
borenetdc89ca52014-10-17 07:37:05 -0700254
255 for _ in range(RETRY_RUN_MEASUREMENT_COUNT):
256 try:
257 print '\n\n=======Capturing SKP of %s=======\n\n' % page_set
borenet78399152014-10-17 12:15:46 -0700258 shell_utils.run(' '.join(run_benchmark_cmd), shell=True)
borenetdc89ca52014-10-17 07:37:05 -0700259 except shell_utils.CommandFailedException:
260 # skpicture_printer sometimes fails with AssertionError but the
261 # captured SKP is still valid. This is a known issue.
262 pass
263
264 if self._record:
265 # Move over the created archive into the local webpages archive
266 # directory.
267 shutil.move(
268 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR, wpr_data_file),
269 self._local_record_webpages_archive_dir)
270 shutil.move(
271 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR,
rmistry7620bf02014-10-27 06:42:11 -0700272 page_set_json_name),
borenetdc89ca52014-10-17 07:37:05 -0700273 self._local_record_webpages_archive_dir)
274
275 # Rename generated SKP files into more descriptive names.
276 try:
277 self._RenameSkpFiles(page_set)
278 # Break out of the retry loop since there were no errors.
279 break
280 except Exception:
281 # There was a failure continue with the loop.
282 traceback.print_exc()
283 print '\n\n=======Retrying %s=======\n\n' % page_set
284 time.sleep(10)
285 else:
borenet78399152014-10-17 12:15:46 -0700286 # If we get here then run_benchmark did not succeed and thus did not
borenetdc89ca52014-10-17 07:37:05 -0700287 # break out of the loop.
borenet78399152014-10-17 12:15:46 -0700288 raise Exception('run_benchmark failed for page_set: %s' % page_set)
borenetdc89ca52014-10-17 07:37:05 -0700289
borenetdc89ca52014-10-17 07:37:05 -0700290 print '\n\n=======Capturing SKP files took %s seconds=======\n\n' % (
291 time.time() - start_time)
292
293 if self._skia_tools:
294 render_pictures_cmd = [
295 os.path.join(self._skia_tools, 'render_pictures'),
296 '-r', self._local_skp_dir
297 ]
298 render_pdfs_cmd = [
299 os.path.join(self._skia_tools, 'render_pdfs'),
kkinnunen3a6aa862014-12-03 04:22:06 -0800300 '-r', self._local_skp_dir
borenetdc89ca52014-10-17 07:37:05 -0700301 ]
302
303 for tools_cmd in (render_pictures_cmd, render_pdfs_cmd):
304 print '\n\n=======Running %s=======' % ' '.join(tools_cmd)
305 proc = subprocess.Popen(tools_cmd)
rmistry0ec28af2014-10-28 14:25:17 -0700306 (code, _) = shell_utils.log_process_after_completion(proc, echo=False)
borenetdc89ca52014-10-17 07:37:05 -0700307 if code != 0:
308 raise Exception('%s failed!' % ' '.join(tools_cmd))
309
310 if not self._non_interactive:
311 print '\n\n=======Running debugger======='
312 os.system('%s %s' % (os.path.join(self._skia_tools, 'debugger'),
313 os.path.join(self._local_skp_dir, '*')))
314
315 print '\n\n'
316
317 if not self._skip_all_gs_access and self._upload_to_gs:
318 print '\n\n=======Uploading to Google Storage=======\n\n'
319 # Copy the directory structure in the root directory into Google Storage.
320 dest_dir_name = ROOT_PLAYBACK_DIR_NAME
321 if self._alternate_upload_dir:
322 dest_dir_name = self._alternate_upload_dir
323
kkinnunene75d2d22014-12-03 04:38:46 -0800324 gs_bucket = remove_prefix(self._dest_gsbase.lstrip(), gs_utils.GS_PREFIX)
borenetdc89ca52014-10-17 07:37:05 -0700325 gs_utils.GSUtils().upload_dir_contents(
326 LOCAL_PLAYBACK_ROOT_DIR, gs_bucket, dest_dir_name,
327 upload_if=gs_utils.GSUtils.UploadIf.IF_MODIFIED,
328 predefined_acl=GS_PREDEFINED_ACL,
329 fine_grained_acl_list=GS_FINE_GRAINED_ACL_LIST)
330
331 print '\n\n=======New SKPs have been uploaded to %s =======\n\n' % (
332 posixpath.join(self._dest_gsbase, dest_dir_name, SKPICTURES_DIR_NAME))
333 else:
334 print '\n\n=======Not Uploading to Google Storage=======\n\n'
335 print 'Generated resources are available in %s\n\n' % (
336 LOCAL_PLAYBACK_ROOT_DIR)
337
338 return 0
339
340 def _RenameSkpFiles(self, page_set):
341 """Rename generated SKP files into more descriptive names.
342
343 Look into the subdirectory of TMP_SKP_DIR and find the most interesting
344 .skp in there to be this page_set's representative .skp.
345 """
346 # Here's where we're assuming there's one page per pageset.
347 # If there were more than one, we'd overwrite filename below.
348
349 # /path/to/skia_yahooanswers_desktop.json -> skia_yahooanswers_desktop.json
350 _, ps_filename = os.path.split(page_set)
351 # skia_yahooanswers_desktop.json -> skia_yahooanswers_desktop
352 ps_basename, _ = os.path.splitext(ps_filename)
353 # skia_yahooanswers_desktop -> skia, yahooanswers, desktop
354 _, page_name, device = ps_basename.split('_')
355
356 basename = '%s_%s' % (DEVICE_TO_PLATFORM_PREFIX[device], page_name)
357 filename = basename[:MAX_SKP_BASE_NAME_LEN] + '.skp'
358
359 subdirs = glob.glob(os.path.join(TMP_SKP_DIR, '*'))
360 assert len(subdirs) == 1
361 for site in subdirs:
362 # We choose the largest .skp as the most likely to be interesting.
363 largest_skp = max(glob.glob(os.path.join(site, '*.skp')),
364 key=lambda path: os.stat(path).st_size)
365 dest = os.path.join(self._local_skp_dir, filename)
366 print 'Moving', largest_skp, 'to', dest
367 shutil.move(largest_skp, dest)
368 self._skp_files.append(filename)
369 shutil.rmtree(site)
370
371 def _CreateLocalStorageDirs(self):
372 """Creates required local storage directories for this script."""
373 for d in (self._local_record_webpages_archive_dir,
374 self._local_skp_dir):
375 if os.path.exists(d):
376 shutil.rmtree(d)
377 os.makedirs(d)
378
rmistry7620bf02014-10-27 06:42:11 -0700379 def _DownloadWebpagesArchive(self, wpr_data_file, page_set_json_name):
borenetdc89ca52014-10-17 07:37:05 -0700380 """Downloads the webpages archive and its required page set from GS."""
381 wpr_source = posixpath.join(ROOT_PLAYBACK_DIR_NAME, 'webpages_archive',
382 wpr_data_file)
383 page_set_source = posixpath.join(ROOT_PLAYBACK_DIR_NAME,
384 'webpages_archive',
rmistry7620bf02014-10-27 06:42:11 -0700385 page_set_json_name)
borenetdc89ca52014-10-17 07:37:05 -0700386 gs = gs_utils.GSUtils()
kkinnunene75d2d22014-12-03 04:38:46 -0800387 gs_bucket = remove_prefix(self._dest_gsbase.lstrip(), gs_utils.GS_PREFIX)
borenetdc89ca52014-10-17 07:37:05 -0700388 if (gs.does_storage_object_exist(gs_bucket, wpr_source) and
389 gs.does_storage_object_exist(gs_bucket, page_set_source)):
390 gs.download_file(gs_bucket, wpr_source,
391 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR,
392 wpr_data_file))
393 gs.download_file(gs_bucket, page_set_source,
394 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR,
rmistry7620bf02014-10-27 06:42:11 -0700395 page_set_json_name))
borenetdc89ca52014-10-17 07:37:05 -0700396 else:
397 raise Exception('%s and %s do not exist in Google Storage!' % (
398 wpr_source, page_set_source))
399
400
401if '__main__' == __name__:
402 option_parser = optparse.OptionParser()
403 option_parser.add_option(
404 '', '--page_sets',
405 help='Specifies the page sets to use to archive. Supports globs.',
406 default='all')
407 option_parser.add_option(
408 '', '--skip_all_gs_access', action='store_true',
409 help='All Google Storage interactions will be skipped if this flag is '
410 'specified. This is useful for cases where the user does not have '
411 'the required .boto file but would like to generate webpage '
412 'archives and SKPs from the Skia page sets.',
413 default=False)
414 option_parser.add_option(
415 '', '--record', action='store_true',
416 help='Specifies whether a new website archive should be created.',
417 default=False)
418 option_parser.add_option(
419 '', '--dest_gsbase',
420 help='gs:// bucket_name, the bucket to upload the file to.',
421 default='gs://chromium-skia-gm')
422 option_parser.add_option(
423 '', '--skia_tools',
424 help=('Path to compiled Skia executable tools. '
425 'render_pictures/render_pdfs is run on the set '
426 'after all SKPs are captured. If the script is run without '
427 '--non-interactive then the debugger is also run at the end. Debug '
428 'builds are recommended because they seem to catch more failures '
429 'than Release builds.'),
430 default=None)
431 option_parser.add_option(
432 '', '--upload_to_gs', action='store_true',
433 help='Does not upload to Google Storage if this is False.',
434 default=False)
435 option_parser.add_option(
436 '', '--alternate_upload_dir',
437 help='Uploads to a different directory in Google Storage if this flag is '
438 'specified',
439 default=None)
440 option_parser.add_option(
441 '', '--output_dir',
442 help='Directory where SKPs and webpage archives will be outputted to.',
443 default=tempfile.gettempdir())
444 option_parser.add_option(
445 '', '--browser_executable',
446 help='The exact browser executable to run.',
447 default=None)
448 option_parser.add_option(
449 '', '--chrome_src_path',
450 help='Path to the chromium src directory.',
451 default=None)
452 option_parser.add_option(
453 '', '--non-interactive', action='store_true',
454 help='Runs the script without any prompts. If this flag is specified and '
455 '--skia_tools is specified then the debugger is not run.',
456 default=False)
457 options, unused_args = option_parser.parse_args()
458
459 playback = SkPicturePlayback(options)
460 sys.exit(playback.Run())