blob: 99808eda32419d4b0783f150e2f90c62935cfe48 [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
10cd ../buildbot/slave/skia_slave_scripts
11python 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
14
15
16To replay archived webpages and re-generate SKP files (should be run whenever
17SkPicture.PICTURE_VERSION changes):
18
19cd ../buildbot/slave/skia_slave_scripts
20python 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
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
28--page_sets=page_sets/skia_yahooanswers_desktop.json,\
29page_sets/skia_wikipedia_galaxynexus.json
30
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.
45To preview the captured SKP before proceeding to the next page_set specify both
46--skia_tools and --view_debugger_output.
47"""
48
49import glob
50import optparse
51import os
52import posixpath
53import shutil
54import subprocess
55import sys
56import tempfile
57import time
58import traceback
59
60sys.path.insert(0, os.getcwd())
61
62from common.py.utils import gs_utils
63from common.py.utils import shell_utils
64
65ROOT_PLAYBACK_DIR_NAME = 'playback'
66SKPICTURES_DIR_NAME = 'skps'
67
68
69# Local archive and SKP directories.
70LOCAL_PLAYBACK_ROOT_DIR = os.path.join(
71 tempfile.gettempdir(), ROOT_PLAYBACK_DIR_NAME)
72LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR = os.path.join(
73 os.path.abspath(os.path.dirname(__file__)), 'page_sets', 'data')
74TMP_SKP_DIR = tempfile.mkdtemp()
75
rmistryf802f322014-10-22 05:04:43 -070076# Location of the credentials.json file and the string that represents missing
77# passwords.
78CREDENTIALS_FILE_PATH = os.path.join(
79 os.path.abspath(os.path.dirname(__file__)), 'page_sets', 'data',
80 'credentials.json'
81)
82
borenetdc89ca52014-10-17 07:37:05 -070083# Name of the SKP benchmark
84SKP_BENCHMARK = 'skpicture_printer'
85
86# The max base name length of Skp files.
87MAX_SKP_BASE_NAME_LEN = 31
88
89# Dictionary of device to platform prefixes for SKP files.
90DEVICE_TO_PLATFORM_PREFIX = {
91 'desktop': 'desk',
92 'galaxynexus': 'mobi',
93 'nexus10': 'tabl'
94}
95
96# How many times the record_wpr binary should be retried.
97RETRY_RECORD_WPR_COUNT = 5
borenet78399152014-10-17 12:15:46 -070098# How many times the run_benchmark binary should be retried.
borenetdc89ca52014-10-17 07:37:05 -070099RETRY_RUN_MEASUREMENT_COUNT = 5
100
rmistryf802f322014-10-22 05:04:43 -0700101# Location of the credentials.json file in Google Storage.
102CREDENTIALS_GS_PATH = '/playback/credentials/credentials.json'
103
borenetdc89ca52014-10-17 07:37:05 -0700104X11_DISPLAY = os.getenv('DISPLAY', ':0')
105
106GS_PREDEFINED_ACL = gs_utils.GSUtils.PredefinedACL.PRIVATE
107GS_FINE_GRAINED_ACL_LIST = [
108 (gs_utils.GSUtils.IdType.GROUP_BY_DOMAIN, 'google.com',
109 gs_utils.GSUtils.Permission.READ),
110]
111
112
113class SkPicturePlayback(object):
114 """Class that archives or replays webpages and creates SKPs."""
115
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
120
121 self._all_page_sets_specified = parse_options.page_sets == 'all'
122 self._page_sets = self._ParsePageSets(parse_options.page_sets)
123
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,
132 'tools', 'perf')
133
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')
138
139 # List of SKP files generated by this script.
140 self._skp_files = []
141
142 def _ParsePageSets(self, page_sets):
143 if not 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__)),
148 'page_sets')
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)
156 else:
157 ps = page_sets.split(',')
158 ps.sort()
159 return ps
160
161 def Run(self):
162 """Run the SkPicturePlayback BuildStep."""
163
rmistryf802f322014-10-22 05:04:43 -0700164 # 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:
167 {
168 "google": {
169 "username": "google_testing_account_username",
170 "password": "google_testing_account_password"
171 },
172 "facebook": {
173 "username": "facebook_testing_account_username",
174 "password": "facebook_testing_account_password"
175 }
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)
183
borenetdc89ca52014-10-17 07:37:05 -0700184 # 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)
188
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)
192
193 # Create the required local storage directories.
194 self._CreateLocalStorageDirs()
195
196 # Start the timer.
197 start_time = time.time()
198
199 # Loop through all page_sets.
200 for page_set in self._page_sets:
201
rmistry7620bf02014-10-27 06:42:11 -0700202 page_set_basename = os.path.basename(page_set).split('.')[0]
203 page_set_json_name = page_set_basename + '.json'
borenetdc89ca52014-10-17 07:37:05 -0700204 wpr_data_file = page_set.split(os.path.sep)[-1].split('.')[0] + '_000.wpr'
rmistry7620bf02014-10-27 06:42:11 -0700205 page_set_dir = os.path.dirname(page_set)
borenetdc89ca52014-10-17 07:37:05 -0700206
207 if self._record:
208 # Create an archive of the specified webpages if '--record=True' is
209 # specified.
210 record_wpr_cmd = (
rmistry7620bf02014-10-27 06:42:11 -0700211 'PYTHONPATH=%s:$PYTHONPATH' % page_set_dir,
borenetdc89ca52014-10-17 07:37:05 -0700212 'DISPLAY=%s' % X11_DISPLAY,
213 os.path.join(self._telemetry_binaries_dir, 'record_wpr'),
214 '--extra-browser-args=--disable-setuid-sandbox',
215 '--browser=exact',
216 '--browser-executable=%s' % self._browser_executable,
rmistry7620bf02014-10-27 06:42:11 -0700217 '%s_page_set' % page_set_basename,
218 '--page-set-base-dir=%s' % page_set_dir
borenetdc89ca52014-10-17 07:37:05 -0700219 )
220 for _ in range(RETRY_RECORD_WPR_COUNT):
rmistry0ec28af2014-10-28 14:25:17 -0700221 try:
222 shell_utils.run(' '.join(record_wpr_cmd), shell=True)
borenetdc89ca52014-10-17 07:37:05 -0700223 # Break out of the retry loop since there were no errors.
224 break
rmistry0ec28af2014-10-28 14:25:17 -0700225 except Exception:
226 # There was a failure continue with the loop.
227 traceback.print_exc()
borenetdc89ca52014-10-17 07:37:05 -0700228 else:
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)
232
233 else:
234 if not self._skip_all_gs_access:
235 # Get the webpages archive so that it can be replayed.
rmistry7620bf02014-10-27 06:42:11 -0700236 self._DownloadWebpagesArchive(wpr_data_file, page_set_json_name)
borenetdc89ca52014-10-17 07:37:05 -0700237
borenet78399152014-10-17 12:15:46 -0700238 run_benchmark_cmd = (
rmistryf802f322014-10-22 05:04:43 -0700239 'PYTHONPATH=%s:$PYTHONPATH' % page_set_dir,
borenetdc89ca52014-10-17 07:37:05 -0700240 'DISPLAY=%s' % X11_DISPLAY,
241 'timeout', '300',
borenet78399152014-10-17 12:15:46 -0700242 os.path.join(self._telemetry_binaries_dir, 'run_benchmark'),
borenetdc89ca52014-10-17 07:37:05 -0700243 '--extra-browser-args=--disable-setuid-sandbox',
244 '--browser=exact',
245 '--browser-executable=%s' % self._browser_executable,
246 SKP_BENCHMARK,
rmistry7620bf02014-10-27 06:42:11 -0700247 '--page-set-name=%s' % page_set_basename,
rmistryf802f322014-10-22 05:04:43 -0700248 '--page-set-base-dir=%s' % page_set_dir,
249 '--skp-outdir=%s' % TMP_SKP_DIR,
250 '--also-run-disabled-tests'
borenetdc89ca52014-10-17 07:37:05 -0700251 )
borenetdc89ca52014-10-17 07:37:05 -0700252
253 for _ in range(RETRY_RUN_MEASUREMENT_COUNT):
254 try:
255 print '\n\n=======Capturing SKP of %s=======\n\n' % page_set
borenet78399152014-10-17 12:15:46 -0700256 shell_utils.run(' '.join(run_benchmark_cmd), shell=True)
borenetdc89ca52014-10-17 07:37:05 -0700257 except shell_utils.CommandFailedException:
258 # skpicture_printer sometimes fails with AssertionError but the
259 # captured SKP is still valid. This is a known issue.
260 pass
261
262 if self._record:
263 # Move over the created archive into the local webpages archive
264 # directory.
265 shutil.move(
266 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR, wpr_data_file),
267 self._local_record_webpages_archive_dir)
268 shutil.move(
269 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR,
rmistry7620bf02014-10-27 06:42:11 -0700270 page_set_json_name),
borenetdc89ca52014-10-17 07:37:05 -0700271 self._local_record_webpages_archive_dir)
272
273 # Rename generated SKP files into more descriptive names.
274 try:
275 self._RenameSkpFiles(page_set)
276 # Break out of the retry loop since there were no errors.
277 break
278 except Exception:
279 # There was a failure continue with the loop.
280 traceback.print_exc()
281 print '\n\n=======Retrying %s=======\n\n' % page_set
282 time.sleep(10)
283 else:
borenet78399152014-10-17 12:15:46 -0700284 # If we get here then run_benchmark did not succeed and thus did not
borenetdc89ca52014-10-17 07:37:05 -0700285 # break out of the loop.
borenet78399152014-10-17 12:15:46 -0700286 raise Exception('run_benchmark failed for page_set: %s' % page_set)
borenetdc89ca52014-10-17 07:37:05 -0700287
borenetdc89ca52014-10-17 07:37:05 -0700288 print '\n\n=======Capturing SKP files took %s seconds=======\n\n' % (
289 time.time() - start_time)
290
291 if self._skia_tools:
292 render_pictures_cmd = [
293 os.path.join(self._skia_tools, 'render_pictures'),
294 '-r', self._local_skp_dir
295 ]
296 render_pdfs_cmd = [
297 os.path.join(self._skia_tools, 'render_pdfs'),
kkinnunen3a6aa862014-12-03 04:22:06 -0800298 '-r', self._local_skp_dir
borenetdc89ca52014-10-17 07:37:05 -0700299 ]
300
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)
rmistry0ec28af2014-10-28 14:25:17 -0700304 (code, _) = shell_utils.log_process_after_completion(proc, echo=False)
borenetdc89ca52014-10-17 07:37:05 -0700305 if code != 0:
306 raise Exception('%s failed!' % ' '.join(tools_cmd))
307
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, '*')))
312
313 print '\n\n'
314
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
321
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)
328
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))
331 else:
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)
335
336 return 0
337
338 def _RenameSkpFiles(self, page_set):
339 """Rename generated SKP files into more descriptive names.
340
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.
343 """
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.
346
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('_')
353
354 basename = '%s_%s' % (DEVICE_TO_PLATFORM_PREFIX[device], page_name)
355 filename = basename[:MAX_SKP_BASE_NAME_LEN] + '.skp'
356
357 subdirs = glob.glob(os.path.join(TMP_SKP_DIR, '*'))
358 assert len(subdirs) == 1
359 for site in subdirs:
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)
367 shutil.rmtree(site)
368
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):
374 shutil.rmtree(d)
375 os.makedirs(d)
376
rmistry7620bf02014-10-27 06:42:11 -0700377 def _DownloadWebpagesArchive(self, wpr_data_file, page_set_json_name):
borenetdc89ca52014-10-17 07:37:05 -0700378 """Downloads the webpages archive and its required page set from GS."""
379 wpr_source = posixpath.join(ROOT_PLAYBACK_DIR_NAME, 'webpages_archive',
380 wpr_data_file)
381 page_set_source = posixpath.join(ROOT_PLAYBACK_DIR_NAME,
382 'webpages_archive',
rmistry7620bf02014-10-27 06:42:11 -0700383 page_set_json_name)
borenetdc89ca52014-10-17 07:37:05 -0700384 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,
390 wpr_data_file))
391 gs.download_file(gs_bucket, page_set_source,
392 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR,
rmistry7620bf02014-10-27 06:42:11 -0700393 page_set_json_name))
borenetdc89ca52014-10-17 07:37:05 -0700394 else:
395 raise Exception('%s and %s do not exist in Google Storage!' % (
396 wpr_source, page_set_source))
397
398
399if '__main__' == __name__:
400 option_parser = optparse.OptionParser()
401 option_parser.add_option(
402 '', '--page_sets',
403 help='Specifies the page sets to use to archive. Supports globs.',
404 default='all')
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.',
411 default=False)
412 option_parser.add_option(
413 '', '--record', action='store_true',
414 help='Specifies whether a new website archive should be created.',
415 default=False)
416 option_parser.add_option(
417 '', '--dest_gsbase',
418 help='gs:// bucket_name, the bucket to upload the file to.',
419 default='gs://chromium-skia-gm')
420 option_parser.add_option(
421 '', '--skia_tools',
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.'),
428 default=None)
429 option_parser.add_option(
430 '', '--upload_to_gs', action='store_true',
431 help='Does not upload to Google Storage if this is False.',
432 default=False)
433 option_parser.add_option(
434 '', '--alternate_upload_dir',
435 help='Uploads to a different directory in Google Storage if this flag is '
436 'specified',
437 default=None)
438 option_parser.add_option(
439 '', '--output_dir',
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.',
445 default=None)
446 option_parser.add_option(
447 '', '--chrome_src_path',
448 help='Path to the chromium src directory.',
449 default=None)
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.',
454 default=False)
455 options, unused_args = option_parser.parse_args()
456
457 playback = SkPicturePlayback(options)
458 sys.exit(playback.Run())