blob: d2df01f32daf29591bacece873e87e5fce5711a8 [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
kkinnunene75d2d22014-12-03 04:38:46 -0800112def remove_prefix(s, prefix):
113 if s.startswith(prefix):
114 return s[len(prefix):]
115 return s
borenetdc89ca52014-10-17 07:37:05 -0700116
117class SkPicturePlayback(object):
118 """Class that archives or replays webpages and creates SKPs."""
119
120 def __init__(self, parse_options):
121 """Constructs a SkPicturePlayback BuildStep instance."""
122 assert parse_options.browser_executable, 'Must specify --browser_executable'
123 self._browser_executable = parse_options.browser_executable
124
125 self._all_page_sets_specified = parse_options.page_sets == 'all'
126 self._page_sets = self._ParsePageSets(parse_options.page_sets)
127
128 self._dest_gsbase = parse_options.dest_gsbase
129 self._record = parse_options.record
130 self._skia_tools = parse_options.skia_tools
131 self._non_interactive = parse_options.non_interactive
132 self._upload_to_gs = parse_options.upload_to_gs
133 self._alternate_upload_dir = parse_options.alternate_upload_dir
134 self._skip_all_gs_access = parse_options.skip_all_gs_access
135 self._telemetry_binaries_dir = os.path.join(parse_options.chrome_src_path,
136 'tools', 'perf')
137
138 self._local_skp_dir = os.path.join(
139 parse_options.output_dir, ROOT_PLAYBACK_DIR_NAME, SKPICTURES_DIR_NAME)
140 self._local_record_webpages_archive_dir = os.path.join(
141 parse_options.output_dir, ROOT_PLAYBACK_DIR_NAME, 'webpages_archive')
142
143 # List of SKP files generated by this script.
144 self._skp_files = []
145
146 def _ParsePageSets(self, page_sets):
147 if not page_sets:
148 raise ValueError('Must specify at least one page_set!')
149 elif self._all_page_sets_specified:
150 # Get everything from the page_sets directory.
151 page_sets_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)),
152 'page_sets')
153 ps = [os.path.join(page_sets_dir, page_set)
154 for page_set in os.listdir(page_sets_dir)
155 if not os.path.isdir(os.path.join(page_sets_dir, page_set)) and
156 page_set.endswith('.py')]
157 elif '*' in page_sets:
158 # Explode and return the glob.
159 ps = glob.glob(page_sets)
160 else:
161 ps = page_sets.split(',')
162 ps.sort()
163 return ps
164
165 def Run(self):
166 """Run the SkPicturePlayback BuildStep."""
167
rmistryf802f322014-10-22 05:04:43 -0700168 # Download the credentials file if it was not previously downloaded.
169 if self._skip_all_gs_access:
170 print """\n\nPlease create a %s file that contains:
171 {
172 "google": {
173 "username": "google_testing_account_username",
174 "password": "google_testing_account_password"
175 },
176 "facebook": {
177 "username": "facebook_testing_account_username",
178 "password": "facebook_testing_account_password"
179 }
180 }\n\n""" % CREDENTIALS_FILE_PATH
181 raw_input("Please press a key when you are ready to proceed...")
182 elif not os.path.isfile(CREDENTIALS_FILE_PATH):
183 # Download the credentials.json file from Google Storage.
kkinnunene75d2d22014-12-03 04:38:46 -0800184 gs_bucket = remove_prefix(self._dest_gsbase.lstrip(), gs_utils.GS_PREFIX)
rmistryf802f322014-10-22 05:04:43 -0700185 gs_utils.GSUtils().download_file(gs_bucket, CREDENTIALS_GS_PATH,
186 CREDENTIALS_FILE_PATH)
187
borenetdc89ca52014-10-17 07:37:05 -0700188 # Delete any left over data files in the data directory.
189 for archive_file in glob.glob(
190 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR, 'skia_*')):
191 os.remove(archive_file)
192
193 # Delete the local root directory if it already exists.
194 if os.path.exists(LOCAL_PLAYBACK_ROOT_DIR):
195 shutil.rmtree(LOCAL_PLAYBACK_ROOT_DIR)
196
197 # Create the required local storage directories.
198 self._CreateLocalStorageDirs()
199
200 # Start the timer.
201 start_time = time.time()
202
203 # Loop through all page_sets.
204 for page_set in self._page_sets:
205
rmistry7620bf02014-10-27 06:42:11 -0700206 page_set_basename = os.path.basename(page_set).split('.')[0]
207 page_set_json_name = page_set_basename + '.json'
borenetdc89ca52014-10-17 07:37:05 -0700208 wpr_data_file = page_set.split(os.path.sep)[-1].split('.')[0] + '_000.wpr'
rmistry7620bf02014-10-27 06:42:11 -0700209 page_set_dir = os.path.dirname(page_set)
borenetdc89ca52014-10-17 07:37:05 -0700210
211 if self._record:
212 # Create an archive of the specified webpages if '--record=True' is
213 # specified.
214 record_wpr_cmd = (
rmistry7620bf02014-10-27 06:42:11 -0700215 'PYTHONPATH=%s:$PYTHONPATH' % page_set_dir,
borenetdc89ca52014-10-17 07:37:05 -0700216 'DISPLAY=%s' % X11_DISPLAY,
217 os.path.join(self._telemetry_binaries_dir, 'record_wpr'),
218 '--extra-browser-args=--disable-setuid-sandbox',
219 '--browser=exact',
220 '--browser-executable=%s' % self._browser_executable,
rmistry7620bf02014-10-27 06:42:11 -0700221 '%s_page_set' % page_set_basename,
222 '--page-set-base-dir=%s' % page_set_dir
borenetdc89ca52014-10-17 07:37:05 -0700223 )
224 for _ in range(RETRY_RECORD_WPR_COUNT):
rmistry0ec28af2014-10-28 14:25:17 -0700225 try:
226 shell_utils.run(' '.join(record_wpr_cmd), shell=True)
borenetdc89ca52014-10-17 07:37:05 -0700227 # Break out of the retry loop since there were no errors.
228 break
rmistry0ec28af2014-10-28 14:25:17 -0700229 except Exception:
230 # There was a failure continue with the loop.
231 traceback.print_exc()
borenetdc89ca52014-10-17 07:37:05 -0700232 else:
233 # If we get here then record_wpr did not succeed and thus did not
234 # break out of the loop.
235 raise Exception('record_wpr failed for page_set: %s' % page_set)
236
237 else:
238 if not self._skip_all_gs_access:
239 # Get the webpages archive so that it can be replayed.
rmistry7620bf02014-10-27 06:42:11 -0700240 self._DownloadWebpagesArchive(wpr_data_file, page_set_json_name)
borenetdc89ca52014-10-17 07:37:05 -0700241
borenet78399152014-10-17 12:15:46 -0700242 run_benchmark_cmd = (
rmistryf802f322014-10-22 05:04:43 -0700243 'PYTHONPATH=%s:$PYTHONPATH' % page_set_dir,
borenetdc89ca52014-10-17 07:37:05 -0700244 'DISPLAY=%s' % X11_DISPLAY,
245 'timeout', '300',
borenet78399152014-10-17 12:15:46 -0700246 os.path.join(self._telemetry_binaries_dir, 'run_benchmark'),
borenetdc89ca52014-10-17 07:37:05 -0700247 '--extra-browser-args=--disable-setuid-sandbox',
248 '--browser=exact',
249 '--browser-executable=%s' % self._browser_executable,
250 SKP_BENCHMARK,
rmistry7620bf02014-10-27 06:42:11 -0700251 '--page-set-name=%s' % page_set_basename,
rmistryf802f322014-10-22 05:04:43 -0700252 '--page-set-base-dir=%s' % page_set_dir,
253 '--skp-outdir=%s' % TMP_SKP_DIR,
254 '--also-run-disabled-tests'
borenetdc89ca52014-10-17 07:37:05 -0700255 )
borenetdc89ca52014-10-17 07:37:05 -0700256
257 for _ in range(RETRY_RUN_MEASUREMENT_COUNT):
258 try:
259 print '\n\n=======Capturing SKP of %s=======\n\n' % page_set
borenet78399152014-10-17 12:15:46 -0700260 shell_utils.run(' '.join(run_benchmark_cmd), shell=True)
borenetdc89ca52014-10-17 07:37:05 -0700261 except shell_utils.CommandFailedException:
262 # skpicture_printer sometimes fails with AssertionError but the
263 # captured SKP is still valid. This is a known issue.
264 pass
265
266 if self._record:
267 # Move over the created archive into the local webpages archive
268 # directory.
269 shutil.move(
270 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR, wpr_data_file),
271 self._local_record_webpages_archive_dir)
272 shutil.move(
273 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR,
rmistry7620bf02014-10-27 06:42:11 -0700274 page_set_json_name),
borenetdc89ca52014-10-17 07:37:05 -0700275 self._local_record_webpages_archive_dir)
276
277 # Rename generated SKP files into more descriptive names.
278 try:
279 self._RenameSkpFiles(page_set)
280 # Break out of the retry loop since there were no errors.
281 break
282 except Exception:
283 # There was a failure continue with the loop.
284 traceback.print_exc()
285 print '\n\n=======Retrying %s=======\n\n' % page_set
286 time.sleep(10)
287 else:
borenet78399152014-10-17 12:15:46 -0700288 # If we get here then run_benchmark did not succeed and thus did not
borenetdc89ca52014-10-17 07:37:05 -0700289 # break out of the loop.
borenet78399152014-10-17 12:15:46 -0700290 raise Exception('run_benchmark failed for page_set: %s' % page_set)
borenetdc89ca52014-10-17 07:37:05 -0700291
borenetdc89ca52014-10-17 07:37:05 -0700292 print '\n\n=======Capturing SKP files took %s seconds=======\n\n' % (
293 time.time() - start_time)
294
295 if self._skia_tools:
296 render_pictures_cmd = [
297 os.path.join(self._skia_tools, 'render_pictures'),
298 '-r', self._local_skp_dir
299 ]
300 render_pdfs_cmd = [
301 os.path.join(self._skia_tools, 'render_pdfs'),
kkinnunen3a6aa862014-12-03 04:22:06 -0800302 '-r', self._local_skp_dir
borenetdc89ca52014-10-17 07:37:05 -0700303 ]
304
305 for tools_cmd in (render_pictures_cmd, render_pdfs_cmd):
306 print '\n\n=======Running %s=======' % ' '.join(tools_cmd)
307 proc = subprocess.Popen(tools_cmd)
rmistry0ec28af2014-10-28 14:25:17 -0700308 (code, _) = shell_utils.log_process_after_completion(proc, echo=False)
borenetdc89ca52014-10-17 07:37:05 -0700309 if code != 0:
310 raise Exception('%s failed!' % ' '.join(tools_cmd))
311
312 if not self._non_interactive:
313 print '\n\n=======Running debugger======='
314 os.system('%s %s' % (os.path.join(self._skia_tools, 'debugger'),
315 os.path.join(self._local_skp_dir, '*')))
316
317 print '\n\n'
318
319 if not self._skip_all_gs_access and self._upload_to_gs:
320 print '\n\n=======Uploading to Google Storage=======\n\n'
321 # Copy the directory structure in the root directory into Google Storage.
322 dest_dir_name = ROOT_PLAYBACK_DIR_NAME
323 if self._alternate_upload_dir:
324 dest_dir_name = self._alternate_upload_dir
325
kkinnunene75d2d22014-12-03 04:38:46 -0800326 gs_bucket = remove_prefix(self._dest_gsbase.lstrip(), gs_utils.GS_PREFIX)
borenetdc89ca52014-10-17 07:37:05 -0700327 gs_utils.GSUtils().upload_dir_contents(
328 LOCAL_PLAYBACK_ROOT_DIR, gs_bucket, dest_dir_name,
329 upload_if=gs_utils.GSUtils.UploadIf.IF_MODIFIED,
330 predefined_acl=GS_PREDEFINED_ACL,
331 fine_grained_acl_list=GS_FINE_GRAINED_ACL_LIST)
332
333 print '\n\n=======New SKPs have been uploaded to %s =======\n\n' % (
334 posixpath.join(self._dest_gsbase, dest_dir_name, SKPICTURES_DIR_NAME))
335 else:
336 print '\n\n=======Not Uploading to Google Storage=======\n\n'
337 print 'Generated resources are available in %s\n\n' % (
338 LOCAL_PLAYBACK_ROOT_DIR)
339
340 return 0
341
342 def _RenameSkpFiles(self, page_set):
343 """Rename generated SKP files into more descriptive names.
344
345 Look into the subdirectory of TMP_SKP_DIR and find the most interesting
346 .skp in there to be this page_set's representative .skp.
347 """
348 # Here's where we're assuming there's one page per pageset.
349 # If there were more than one, we'd overwrite filename below.
350
351 # /path/to/skia_yahooanswers_desktop.json -> skia_yahooanswers_desktop.json
352 _, ps_filename = os.path.split(page_set)
353 # skia_yahooanswers_desktop.json -> skia_yahooanswers_desktop
354 ps_basename, _ = os.path.splitext(ps_filename)
355 # skia_yahooanswers_desktop -> skia, yahooanswers, desktop
356 _, page_name, device = ps_basename.split('_')
357
358 basename = '%s_%s' % (DEVICE_TO_PLATFORM_PREFIX[device], page_name)
359 filename = basename[:MAX_SKP_BASE_NAME_LEN] + '.skp'
360
361 subdirs = glob.glob(os.path.join(TMP_SKP_DIR, '*'))
362 assert len(subdirs) == 1
363 for site in subdirs:
364 # We choose the largest .skp as the most likely to be interesting.
365 largest_skp = max(glob.glob(os.path.join(site, '*.skp')),
366 key=lambda path: os.stat(path).st_size)
367 dest = os.path.join(self._local_skp_dir, filename)
368 print 'Moving', largest_skp, 'to', dest
369 shutil.move(largest_skp, dest)
370 self._skp_files.append(filename)
371 shutil.rmtree(site)
372
373 def _CreateLocalStorageDirs(self):
374 """Creates required local storage directories for this script."""
375 for d in (self._local_record_webpages_archive_dir,
376 self._local_skp_dir):
377 if os.path.exists(d):
378 shutil.rmtree(d)
379 os.makedirs(d)
380
rmistry7620bf02014-10-27 06:42:11 -0700381 def _DownloadWebpagesArchive(self, wpr_data_file, page_set_json_name):
borenetdc89ca52014-10-17 07:37:05 -0700382 """Downloads the webpages archive and its required page set from GS."""
383 wpr_source = posixpath.join(ROOT_PLAYBACK_DIR_NAME, 'webpages_archive',
384 wpr_data_file)
385 page_set_source = posixpath.join(ROOT_PLAYBACK_DIR_NAME,
386 'webpages_archive',
rmistry7620bf02014-10-27 06:42:11 -0700387 page_set_json_name)
borenetdc89ca52014-10-17 07:37:05 -0700388 gs = gs_utils.GSUtils()
kkinnunene75d2d22014-12-03 04:38:46 -0800389 gs_bucket = remove_prefix(self._dest_gsbase.lstrip(), gs_utils.GS_PREFIX)
borenetdc89ca52014-10-17 07:37:05 -0700390 if (gs.does_storage_object_exist(gs_bucket, wpr_source) and
391 gs.does_storage_object_exist(gs_bucket, page_set_source)):
392 gs.download_file(gs_bucket, wpr_source,
393 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR,
394 wpr_data_file))
395 gs.download_file(gs_bucket, page_set_source,
396 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR,
rmistry7620bf02014-10-27 06:42:11 -0700397 page_set_json_name))
borenetdc89ca52014-10-17 07:37:05 -0700398 else:
399 raise Exception('%s and %s do not exist in Google Storage!' % (
400 wpr_source, page_set_source))
401
402
403if '__main__' == __name__:
404 option_parser = optparse.OptionParser()
405 option_parser.add_option(
406 '', '--page_sets',
407 help='Specifies the page sets to use to archive. Supports globs.',
408 default='all')
409 option_parser.add_option(
410 '', '--skip_all_gs_access', action='store_true',
411 help='All Google Storage interactions will be skipped if this flag is '
412 'specified. This is useful for cases where the user does not have '
413 'the required .boto file but would like to generate webpage '
414 'archives and SKPs from the Skia page sets.',
415 default=False)
416 option_parser.add_option(
417 '', '--record', action='store_true',
418 help='Specifies whether a new website archive should be created.',
419 default=False)
420 option_parser.add_option(
421 '', '--dest_gsbase',
422 help='gs:// bucket_name, the bucket to upload the file to.',
423 default='gs://chromium-skia-gm')
424 option_parser.add_option(
425 '', '--skia_tools',
426 help=('Path to compiled Skia executable tools. '
427 'render_pictures/render_pdfs is run on the set '
428 'after all SKPs are captured. If the script is run without '
429 '--non-interactive then the debugger is also run at the end. Debug '
430 'builds are recommended because they seem to catch more failures '
431 'than Release builds.'),
432 default=None)
433 option_parser.add_option(
434 '', '--upload_to_gs', action='store_true',
435 help='Does not upload to Google Storage if this is False.',
436 default=False)
437 option_parser.add_option(
438 '', '--alternate_upload_dir',
439 help='Uploads to a different directory in Google Storage if this flag is '
440 'specified',
441 default=None)
442 option_parser.add_option(
443 '', '--output_dir',
444 help='Directory where SKPs and webpage archives will be outputted to.',
445 default=tempfile.gettempdir())
446 option_parser.add_option(
447 '', '--browser_executable',
448 help='The exact browser executable to run.',
449 default=None)
450 option_parser.add_option(
451 '', '--chrome_src_path',
452 help='Path to the chromium src directory.',
453 default=None)
454 option_parser.add_option(
455 '', '--non-interactive', action='store_true',
456 help='Runs the script without any prompts. If this flag is specified and '
457 '--skia_tools is specified then the debugger is not run.',
458 default=False)
459 options, unused_args = option_parser.parse_args()
460
461 playback = SkPicturePlayback(options)
462 sys.exit(playback.Run())