blob: 394e14ea4d5ecb8449fc170e6d69a506f9857447 [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
205 page_set_basename = os.path.basename(page_set).split('.')[0] + '.json'
206 wpr_data_file = page_set.split(os.path.sep)[-1].split('.')[0] + '_000.wpr'
207
208 if self._record:
209 # Create an archive of the specified webpages if '--record=True' is
210 # specified.
211 record_wpr_cmd = (
212 '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,
217 page_set
218 )
219 for _ in range(RETRY_RECORD_WPR_COUNT):
220 output = shell_utils.run(' '.join(record_wpr_cmd), shell=True)
221 if RECORD_FAILURE_MSG in output:
222 print output
223 else:
224 # Break out of the retry loop since there were no errors.
225 break
226 else:
227 # If we get here then record_wpr did not succeed and thus did not
228 # break out of the loop.
229 raise Exception('record_wpr failed for page_set: %s' % page_set)
230
231 else:
232 if not self._skip_all_gs_access:
233 # Get the webpages archive so that it can be replayed.
234 self._DownloadWebpagesArchive(wpr_data_file, page_set_basename)
235
236 page_set_name = os.path.basename(page_set).split('.')[0]
rmistryf802f322014-10-22 05:04:43 -0700237 page_set_dir = os.path.dirname(page_set)
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,
rmistryf802f322014-10-22 05:04:43 -0700247 '--page-set-name=%s' % page_set_name,
248 '--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,
270 page_set_basename),
271 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'),
298 self._local_skp_dir
299 ]
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)
304 (code, output) = shell_utils.log_process_after_completion(proc,
305 echo=False)
306 if code != 0:
307 raise Exception('%s failed!' % ' '.join(tools_cmd))
308
309 if not self._non_interactive:
310 print '\n\n=======Running debugger======='
311 os.system('%s %s' % (os.path.join(self._skia_tools, 'debugger'),
312 os.path.join(self._local_skp_dir, '*')))
313
314 print '\n\n'
315
316 if not self._skip_all_gs_access and self._upload_to_gs:
317 print '\n\n=======Uploading to Google Storage=======\n\n'
318 # Copy the directory structure in the root directory into Google Storage.
319 dest_dir_name = ROOT_PLAYBACK_DIR_NAME
320 if self._alternate_upload_dir:
321 dest_dir_name = self._alternate_upload_dir
322
323 gs_bucket = self._dest_gsbase.lstrip(gs_utils.GS_PREFIX)
324 gs_utils.GSUtils().upload_dir_contents(
325 LOCAL_PLAYBACK_ROOT_DIR, gs_bucket, dest_dir_name,
326 upload_if=gs_utils.GSUtils.UploadIf.IF_MODIFIED,
327 predefined_acl=GS_PREDEFINED_ACL,
328 fine_grained_acl_list=GS_FINE_GRAINED_ACL_LIST)
329
330 print '\n\n=======New SKPs have been uploaded to %s =======\n\n' % (
331 posixpath.join(self._dest_gsbase, dest_dir_name, SKPICTURES_DIR_NAME))
332 else:
333 print '\n\n=======Not Uploading to Google Storage=======\n\n'
334 print 'Generated resources are available in %s\n\n' % (
335 LOCAL_PLAYBACK_ROOT_DIR)
336
337 return 0
338
339 def _RenameSkpFiles(self, page_set):
340 """Rename generated SKP files into more descriptive names.
341
342 Look into the subdirectory of TMP_SKP_DIR and find the most interesting
343 .skp in there to be this page_set's representative .skp.
344 """
345 # Here's where we're assuming there's one page per pageset.
346 # If there were more than one, we'd overwrite filename below.
347
348 # /path/to/skia_yahooanswers_desktop.json -> skia_yahooanswers_desktop.json
349 _, ps_filename = os.path.split(page_set)
350 # skia_yahooanswers_desktop.json -> skia_yahooanswers_desktop
351 ps_basename, _ = os.path.splitext(ps_filename)
352 # skia_yahooanswers_desktop -> skia, yahooanswers, desktop
353 _, page_name, device = ps_basename.split('_')
354
355 basename = '%s_%s' % (DEVICE_TO_PLATFORM_PREFIX[device], page_name)
356 filename = basename[:MAX_SKP_BASE_NAME_LEN] + '.skp'
357
358 subdirs = glob.glob(os.path.join(TMP_SKP_DIR, '*'))
359 assert len(subdirs) == 1
360 for site in subdirs:
361 # We choose the largest .skp as the most likely to be interesting.
362 largest_skp = max(glob.glob(os.path.join(site, '*.skp')),
363 key=lambda path: os.stat(path).st_size)
364 dest = os.path.join(self._local_skp_dir, filename)
365 print 'Moving', largest_skp, 'to', dest
366 shutil.move(largest_skp, dest)
367 self._skp_files.append(filename)
368 shutil.rmtree(site)
369
370 def _CreateLocalStorageDirs(self):
371 """Creates required local storage directories for this script."""
372 for d in (self._local_record_webpages_archive_dir,
373 self._local_skp_dir):
374 if os.path.exists(d):
375 shutil.rmtree(d)
376 os.makedirs(d)
377
378 def _DownloadWebpagesArchive(self, wpr_data_file, page_set_basename):
379 """Downloads the webpages archive and its required page set from GS."""
380 wpr_source = posixpath.join(ROOT_PLAYBACK_DIR_NAME, 'webpages_archive',
381 wpr_data_file)
382 page_set_source = posixpath.join(ROOT_PLAYBACK_DIR_NAME,
383 'webpages_archive',
384 page_set_basename)
385 gs = gs_utils.GSUtils()
386 gs_bucket = self._dest_gsbase.lstrip(gs_utils.GS_PREFIX)
387 if (gs.does_storage_object_exist(gs_bucket, wpr_source) and
388 gs.does_storage_object_exist(gs_bucket, page_set_source)):
389 gs.download_file(gs_bucket, wpr_source,
390 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR,
391 wpr_data_file))
392 gs.download_file(gs_bucket, page_set_source,
393 os.path.join(LOCAL_REPLAY_WEBPAGES_ARCHIVE_DIR,
394 page_set_basename))
395 else:
396 raise Exception('%s and %s do not exist in Google Storage!' % (
397 wpr_source, page_set_source))
398
399
400if '__main__' == __name__:
401 option_parser = optparse.OptionParser()
402 option_parser.add_option(
403 '', '--page_sets',
404 help='Specifies the page sets to use to archive. Supports globs.',
405 default='all')
406 option_parser.add_option(
407 '', '--skip_all_gs_access', action='store_true',
408 help='All Google Storage interactions will be skipped if this flag is '
409 'specified. This is useful for cases where the user does not have '
410 'the required .boto file but would like to generate webpage '
411 'archives and SKPs from the Skia page sets.',
412 default=False)
413 option_parser.add_option(
414 '', '--record', action='store_true',
415 help='Specifies whether a new website archive should be created.',
416 default=False)
417 option_parser.add_option(
418 '', '--dest_gsbase',
419 help='gs:// bucket_name, the bucket to upload the file to.',
420 default='gs://chromium-skia-gm')
421 option_parser.add_option(
422 '', '--skia_tools',
423 help=('Path to compiled Skia executable tools. '
424 'render_pictures/render_pdfs is run on the set '
425 'after all SKPs are captured. If the script is run without '
426 '--non-interactive then the debugger is also run at the end. Debug '
427 'builds are recommended because they seem to catch more failures '
428 'than Release builds.'),
429 default=None)
430 option_parser.add_option(
431 '', '--upload_to_gs', action='store_true',
432 help='Does not upload to Google Storage if this is False.',
433 default=False)
434 option_parser.add_option(
435 '', '--alternate_upload_dir',
436 help='Uploads to a different directory in Google Storage if this flag is '
437 'specified',
438 default=None)
439 option_parser.add_option(
440 '', '--output_dir',
441 help='Directory where SKPs and webpage archives will be outputted to.',
442 default=tempfile.gettempdir())
443 option_parser.add_option(
444 '', '--browser_executable',
445 help='The exact browser executable to run.',
446 default=None)
447 option_parser.add_option(
448 '', '--chrome_src_path',
449 help='Path to the chromium src directory.',
450 default=None)
451 option_parser.add_option(
452 '', '--non-interactive', action='store_true',
453 help='Runs the script without any prompts. If this flag is specified and '
454 '--skia_tools is specified then the debugger is not run.',
455 default=False)
456 options, unused_args = option_parser.parse_args()
457
458 playback = SkPicturePlayback(options)
459 sys.exit(playback.Run())