blob: 97fec5172d2881c08127823da04f95304bf64119 [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# Stdout that signifies that a recording has failed.
84RECORD_FAILURE_MSG = 'The recording has not been updated for these pages.'
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 'galaxynexus': 'mobi',
96 'nexus10': 'tabl'
97}
98
99# How many times the record_wpr binary should be retried.
100RETRY_RECORD_WPR_COUNT = 5
borenet78399152014-10-17 12:15:46 -0700101# How many times the run_benchmark binary should be retried.
borenetdc89ca52014-10-17 07:37:05 -0700102RETRY_RUN_MEASUREMENT_COUNT = 5
103
rmistryf802f322014-10-22 05:04:43 -0700104# Location of the credentials.json file in Google Storage.
105CREDENTIALS_GS_PATH = '/playback/credentials/credentials.json'
106
borenetdc89ca52014-10-17 07:37:05 -0700107X11_DISPLAY = os.getenv('DISPLAY', ':0')
108
109GS_PREDEFINED_ACL = gs_utils.GSUtils.PredefinedACL.PRIVATE
110GS_FINE_GRAINED_ACL_LIST = [
111 (gs_utils.GSUtils.IdType.GROUP_BY_DOMAIN, 'google.com',
112 gs_utils.GSUtils.Permission.READ),
113]
114
115
116class SkPicturePlayback(object):
117 """Class that archives or replays webpages and creates SKPs."""
118
119 def __init__(self, parse_options):
120 """Constructs a SkPicturePlayback BuildStep instance."""
121 assert parse_options.browser_executable, 'Must specify --browser_executable'
122 self._browser_executable = parse_options.browser_executable
123
124 self._all_page_sets_specified = parse_options.page_sets == 'all'
125 self._page_sets = self._ParsePageSets(parse_options.page_sets)
126
127 self._dest_gsbase = parse_options.dest_gsbase
128 self._record = parse_options.record
129 self._skia_tools = parse_options.skia_tools
130 self._non_interactive = parse_options.non_interactive
131 self._upload_to_gs = parse_options.upload_to_gs
132 self._alternate_upload_dir = parse_options.alternate_upload_dir
133 self._skip_all_gs_access = parse_options.skip_all_gs_access
134 self._telemetry_binaries_dir = os.path.join(parse_options.chrome_src_path,
135 'tools', 'perf')
136
137 self._local_skp_dir = os.path.join(
138 parse_options.output_dir, ROOT_PLAYBACK_DIR_NAME, SKPICTURES_DIR_NAME)
139 self._local_record_webpages_archive_dir = os.path.join(
140 parse_options.output_dir, ROOT_PLAYBACK_DIR_NAME, 'webpages_archive')
141
142 # List of SKP files generated by this script.
143 self._skp_files = []
144
145 def _ParsePageSets(self, page_sets):
146 if not page_sets:
147 raise ValueError('Must specify at least one page_set!')
148 elif self._all_page_sets_specified:
149 # Get everything from the page_sets directory.
150 page_sets_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)),
151 'page_sets')
152 ps = [os.path.join(page_sets_dir, page_set)
153 for page_set in os.listdir(page_sets_dir)
154 if not os.path.isdir(os.path.join(page_sets_dir, page_set)) and
155 page_set.endswith('.py')]
156 elif '*' in page_sets:
157 # Explode and return the glob.
158 ps = glob.glob(page_sets)
159 else:
160 ps = page_sets.split(',')
161 ps.sort()
162 return ps
163
164 def Run(self):
165 """Run the SkPicturePlayback BuildStep."""
166
rmistryf802f322014-10-22 05:04:43 -0700167 # Download the credentials file if it was not previously downloaded.
168 if self._skip_all_gs_access:
169 print """\n\nPlease create a %s file that contains:
170 {
171 "google": {
172 "username": "google_testing_account_username",
173 "password": "google_testing_account_password"
174 },
175 "facebook": {
176 "username": "facebook_testing_account_username",
177 "password": "facebook_testing_account_password"
178 }
179 }\n\n""" % CREDENTIALS_FILE_PATH
180 raw_input("Please press a key when you are ready to proceed...")
181 elif not os.path.isfile(CREDENTIALS_FILE_PATH):
182 # Download the credentials.json file from Google Storage.
183 gs_bucket = self._dest_gsbase.lstrip(gs_utils.GS_PREFIX)
184 gs_utils.GSUtils().download_file(gs_bucket, CREDENTIALS_GS_PATH,
185 CREDENTIALS_FILE_PATH)
186
borenetdc89ca52014-10-17 07:37:05 -0700187 # Delete any left over data files in the data directory.
188 for archive_file in glob.glob(
189 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR, 'skia_*')):
190 os.remove(archive_file)
191
192 # Delete the local root directory if it already exists.
193 if os.path.exists(LOCAL_PLAYBACK_ROOT_DIR):
194 shutil.rmtree(LOCAL_PLAYBACK_ROOT_DIR)
195
196 # Create the required local storage directories.
197 self._CreateLocalStorageDirs()
198
199 # Start the timer.
200 start_time = time.time()
201
202 # Loop through all page_sets.
203 for page_set in self._page_sets:
204
rmistry7620bf02014-10-27 06:42:11 -0700205 page_set_basename = os.path.basename(page_set).split('.')[0]
206 page_set_json_name = page_set_basename + '.json'
borenetdc89ca52014-10-17 07:37:05 -0700207 wpr_data_file = page_set.split(os.path.sep)[-1].split('.')[0] + '_000.wpr'
rmistry7620bf02014-10-27 06:42:11 -0700208 page_set_dir = os.path.dirname(page_set)
borenetdc89ca52014-10-17 07:37:05 -0700209
210 if self._record:
211 # Create an archive of the specified webpages if '--record=True' is
212 # specified.
213 record_wpr_cmd = (
rmistry7620bf02014-10-27 06:42:11 -0700214 'PYTHONPATH=%s:$PYTHONPATH' % page_set_dir,
borenetdc89ca52014-10-17 07:37:05 -0700215 'DISPLAY=%s' % X11_DISPLAY,
216 os.path.join(self._telemetry_binaries_dir, 'record_wpr'),
217 '--extra-browser-args=--disable-setuid-sandbox',
218 '--browser=exact',
219 '--browser-executable=%s' % self._browser_executable,
rmistry7620bf02014-10-27 06:42:11 -0700220 '%s_page_set' % page_set_basename,
221 '--page-set-base-dir=%s' % page_set_dir
borenetdc89ca52014-10-17 07:37:05 -0700222 )
223 for _ in range(RETRY_RECORD_WPR_COUNT):
224 output = shell_utils.run(' '.join(record_wpr_cmd), shell=True)
225 if RECORD_FAILURE_MSG in output:
226 print output
227 else:
228 # Break out of the retry loop since there were no errors.
229 break
230 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'),
300 self._local_skp_dir
301 ]
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)
306 (code, output) = shell_utils.log_process_after_completion(proc,
307 echo=False)
308 if code != 0:
309 raise Exception('%s failed!' % ' '.join(tools_cmd))
310
311 if not self._non_interactive:
312 print '\n\n=======Running debugger======='
313 os.system('%s %s' % (os.path.join(self._skia_tools, 'debugger'),
314 os.path.join(self._local_skp_dir, '*')))
315
316 print '\n\n'
317
318 if not self._skip_all_gs_access and self._upload_to_gs:
319 print '\n\n=======Uploading to Google Storage=======\n\n'
320 # Copy the directory structure in the root directory into Google Storage.
321 dest_dir_name = ROOT_PLAYBACK_DIR_NAME
322 if self._alternate_upload_dir:
323 dest_dir_name = self._alternate_upload_dir
324
325 gs_bucket = self._dest_gsbase.lstrip(gs_utils.GS_PREFIX)
326 gs_utils.GSUtils().upload_dir_contents(
327 LOCAL_PLAYBACK_ROOT_DIR, gs_bucket, dest_dir_name,
328 upload_if=gs_utils.GSUtils.UploadIf.IF_MODIFIED,
329 predefined_acl=GS_PREDEFINED_ACL,
330 fine_grained_acl_list=GS_FINE_GRAINED_ACL_LIST)
331
332 print '\n\n=======New SKPs have been uploaded to %s =======\n\n' % (
333 posixpath.join(self._dest_gsbase, dest_dir_name, SKPICTURES_DIR_NAME))
334 else:
335 print '\n\n=======Not Uploading to Google Storage=======\n\n'
336 print 'Generated resources are available in %s\n\n' % (
337 LOCAL_PLAYBACK_ROOT_DIR)
338
339 return 0
340
341 def _RenameSkpFiles(self, page_set):
342 """Rename generated SKP files into more descriptive names.
343
344 Look into the subdirectory of TMP_SKP_DIR and find the most interesting
345 .skp in there to be this page_set's representative .skp.
346 """
347 # Here's where we're assuming there's one page per pageset.
348 # If there were more than one, we'd overwrite filename below.
349
350 # /path/to/skia_yahooanswers_desktop.json -> skia_yahooanswers_desktop.json
351 _, ps_filename = os.path.split(page_set)
352 # skia_yahooanswers_desktop.json -> skia_yahooanswers_desktop
353 ps_basename, _ = os.path.splitext(ps_filename)
354 # skia_yahooanswers_desktop -> skia, yahooanswers, desktop
355 _, page_name, device = ps_basename.split('_')
356
357 basename = '%s_%s' % (DEVICE_TO_PLATFORM_PREFIX[device], page_name)
358 filename = basename[:MAX_SKP_BASE_NAME_LEN] + '.skp'
359
360 subdirs = glob.glob(os.path.join(TMP_SKP_DIR, '*'))
361 assert len(subdirs) == 1
362 for site in subdirs:
363 # We choose the largest .skp as the most likely to be interesting.
364 largest_skp = max(glob.glob(os.path.join(site, '*.skp')),
365 key=lambda path: os.stat(path).st_size)
366 dest = os.path.join(self._local_skp_dir, filename)
367 print 'Moving', largest_skp, 'to', dest
368 shutil.move(largest_skp, dest)
369 self._skp_files.append(filename)
370 shutil.rmtree(site)
371
372 def _CreateLocalStorageDirs(self):
373 """Creates required local storage directories for this script."""
374 for d in (self._local_record_webpages_archive_dir,
375 self._local_skp_dir):
376 if os.path.exists(d):
377 shutil.rmtree(d)
378 os.makedirs(d)
379
rmistry7620bf02014-10-27 06:42:11 -0700380 def _DownloadWebpagesArchive(self, wpr_data_file, page_set_json_name):
borenetdc89ca52014-10-17 07:37:05 -0700381 """Downloads the webpages archive and its required page set from GS."""
382 wpr_source = posixpath.join(ROOT_PLAYBACK_DIR_NAME, 'webpages_archive',
383 wpr_data_file)
384 page_set_source = posixpath.join(ROOT_PLAYBACK_DIR_NAME,
385 'webpages_archive',
rmistry7620bf02014-10-27 06:42:11 -0700386 page_set_json_name)
borenetdc89ca52014-10-17 07:37:05 -0700387 gs = gs_utils.GSUtils()
388 gs_bucket = self._dest_gsbase.lstrip(gs_utils.GS_PREFIX)
389 if (gs.does_storage_object_exist(gs_bucket, wpr_source) and
390 gs.does_storage_object_exist(gs_bucket, page_set_source)):
391 gs.download_file(gs_bucket, wpr_source,
392 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR,
393 wpr_data_file))
394 gs.download_file(gs_bucket, page_set_source,
395 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR,
rmistry7620bf02014-10-27 06:42:11 -0700396 page_set_json_name))
borenetdc89ca52014-10-17 07:37:05 -0700397 else:
398 raise Exception('%s and %s do not exist in Google Storage!' % (
399 wpr_source, page_set_source))
400
401
402if '__main__' == __name__:
403 option_parser = optparse.OptionParser()
404 option_parser.add_option(
405 '', '--page_sets',
406 help='Specifies the page sets to use to archive. Supports globs.',
407 default='all')
408 option_parser.add_option(
409 '', '--skip_all_gs_access', action='store_true',
410 help='All Google Storage interactions will be skipped if this flag is '
411 'specified. This is useful for cases where the user does not have '
412 'the required .boto file but would like to generate webpage '
413 'archives and SKPs from the Skia page sets.',
414 default=False)
415 option_parser.add_option(
416 '', '--record', action='store_true',
417 help='Specifies whether a new website archive should be created.',
418 default=False)
419 option_parser.add_option(
420 '', '--dest_gsbase',
421 help='gs:// bucket_name, the bucket to upload the file to.',
422 default='gs://chromium-skia-gm')
423 option_parser.add_option(
424 '', '--skia_tools',
425 help=('Path to compiled Skia executable tools. '
426 'render_pictures/render_pdfs is run on the set '
427 'after all SKPs are captured. If the script is run without '
428 '--non-interactive then the debugger is also run at the end. Debug '
429 'builds are recommended because they seem to catch more failures '
430 'than Release builds.'),
431 default=None)
432 option_parser.add_option(
433 '', '--upload_to_gs', action='store_true',
434 help='Does not upload to Google Storage if this is False.',
435 default=False)
436 option_parser.add_option(
437 '', '--alternate_upload_dir',
438 help='Uploads to a different directory in Google Storage if this flag is '
439 'specified',
440 default=None)
441 option_parser.add_option(
442 '', '--output_dir',
443 help='Directory where SKPs and webpage archives will be outputted to.',
444 default=tempfile.gettempdir())
445 option_parser.add_option(
446 '', '--browser_executable',
447 help='The exact browser executable to run.',
448 default=None)
449 option_parser.add_option(
450 '', '--chrome_src_path',
451 help='Path to the chromium src directory.',
452 default=None)
453 option_parser.add_option(
454 '', '--non-interactive', action='store_true',
455 help='Runs the script without any prompts. If this flag is specified and '
456 '--skia_tools is specified then the debugger is not run.',
457 default=False)
458 options, unused_args = option_parser.parse_args()
459
460 playback = SkPicturePlayback(options)
461 sys.exit(playback.Run())