| OLD | NEW |
| 1 #! /usr/bin/env python | 1 #! /usr/bin/env python |
| 2 # Copyright 2016 The Chromium Authors. All rights reserved. | 2 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Instructs Chrome to load series of web pages and reports results. | 6 """Instructs Chrome to load series of web pages and reports results. |
| 7 | 7 |
| 8 When running Chrome is sandwiched between preprocessed disk caches and | 8 When running Chrome is sandwiched between preprocessed disk caches and |
| 9 WepPageReplay serving all connections. | 9 WepPageReplay serving all connections. |
| 10 | 10 |
| 11 TODO(pasko): implement cache preparation and WPR. | 11 TODO(pasko): implement cache preparation and WPR. |
| 12 """ | 12 """ |
| 13 | 13 |
| 14 import argparse | 14 import argparse |
| 15 from datetime import datetime |
| 15 import json | 16 import json |
| 16 import logging | 17 import logging |
| 17 import os | 18 import os |
| 18 import shutil | 19 import shutil |
| 20 import subprocess |
| 19 import sys | 21 import sys |
| 20 import tempfile | 22 import tempfile |
| 21 import time | 23 import time |
| 22 import zipfile | 24 import zipfile |
| 23 | 25 |
| 24 _SRC_DIR = os.path.abspath(os.path.join( | 26 _SRC_DIR = os.path.abspath(os.path.join( |
| 25 os.path.dirname(__file__), '..', '..', '..')) | 27 os.path.dirname(__file__), '..', '..', '..')) |
| 26 | 28 |
| 27 sys.path.append(os.path.join(_SRC_DIR, 'third_party', 'catapult', 'devil')) | 29 sys.path.append(os.path.join(_SRC_DIR, 'third_party', 'catapult', 'devil')) |
| 28 from devil.android import device_utils | 30 from devil.android import device_utils |
| (...skipping 23 matching lines...) Expand all Loading... |
| 52 _REAL_INDEX_FILE_NAME = 'the-real-index' | 54 _REAL_INDEX_FILE_NAME = 'the-real-index' |
| 53 | 55 |
| 54 # Name of the chrome package. | 56 # Name of the chrome package. |
| 55 _CHROME_PACKAGE = ( | 57 _CHROME_PACKAGE = ( |
| 56 constants.PACKAGE_INFO[device_setup.DEFAULT_CHROME_PACKAGE].package) | 58 constants.PACKAGE_INFO[device_setup.DEFAULT_CHROME_PACKAGE].package) |
| 57 | 59 |
| 58 # An estimate of time to wait for the device to become idle after expensive | 60 # An estimate of time to wait for the device to become idle after expensive |
| 59 # operations, such as opening the launcher activity. | 61 # operations, such as opening the launcher activity. |
| 60 _TIME_TO_DEVICE_IDLE_SECONDS = 2 | 62 _TIME_TO_DEVICE_IDLE_SECONDS = 2 |
| 61 | 63 |
| 64 # Cache directory's path on the device. |
| 65 _REMOTE_CACHE_DIRECTORY = '/data/data/' + _CHROME_PACKAGE + '/cache/Cache' |
| 66 |
| 62 | 67 |
| 63 def _ReadUrlsFromJobDescription(job_name): | 68 def _ReadUrlsFromJobDescription(job_name): |
| 64 """Retrieves the list of URLs associated with the job name.""" | 69 """Retrieves the list of URLs associated with the job name.""" |
| 65 try: | 70 try: |
| 66 # Extra sugar: attempt to load from a relative path. | 71 # Extra sugar: attempt to load from a relative path. |
| 67 json_file_name = os.path.join(os.path.dirname(__file__), _JOB_SEARCH_PATH, | 72 json_file_name = os.path.join(os.path.dirname(__file__), _JOB_SEARCH_PATH, |
| 68 job_name) | 73 job_name) |
| 69 with open(json_file_name) as f: | 74 with open(json_file_name) as f: |
| 70 json_data = json.load(f) | 75 json_data = json.load(f) |
| 71 except IOError: | 76 except IOError: |
| (...skipping 25 matching lines...) Expand all Loading... |
| 97 json.dump({'traceEvents': events['events'], 'metadata': {}}, f, indent=2) | 102 json.dump({'traceEvents': events['events'], 'metadata': {}}, f, indent=2) |
| 98 except IOError: | 103 except IOError: |
| 99 logging.warning('Could not save a trace: %s' % filename) | 104 logging.warning('Could not save a trace: %s' % filename) |
| 100 # Swallow the exception. | 105 # Swallow the exception. |
| 101 | 106 |
| 102 | 107 |
| 103 def _UpdateTimestampFromAdbStat(filename, stat): | 108 def _UpdateTimestampFromAdbStat(filename, stat): |
| 104 os.utime(filename, (stat.st_time, stat.st_time)) | 109 os.utime(filename, (stat.st_time, stat.st_time)) |
| 105 | 110 |
| 106 | 111 |
| 112 def _AdbShell(adb, cmd): |
| 113 adb.Shell(subprocess.list2cmdline(cmd)) |
| 114 |
| 115 |
| 116 def _AdbUtime(adb, filename, timestamp): |
| 117 """Adb equivalent of os.utime(filename, (timestamp, timestamp)) |
| 118 """ |
| 119 touch_stamp = datetime.fromtimestamp(timestamp).strftime('%Y%m%d.%H%M%S') |
| 120 _AdbShell(adb, ['touch', '-t', touch_stamp, filename]) |
| 121 |
| 122 |
| 107 def _PullBrowserCache(device): | 123 def _PullBrowserCache(device): |
| 108 """Pulls the browser cache from the device and saves it locally. | 124 """Pulls the browser cache from the device and saves it locally. |
| 109 | 125 |
| 110 Cache is saved with the same file structure as on the device. Timestamps are | 126 Cache is saved with the same file structure as on the device. Timestamps are |
| 111 important to preserve because indexing and eviction depends on them. | 127 important to preserve because indexing and eviction depends on them. |
| 112 | 128 |
| 113 Returns: | 129 Returns: |
| 114 Temporary directory containing all the browser cache. | 130 Temporary directory containing all the browser cache. |
| 115 """ | 131 """ |
| 116 save_target = tempfile.mkdtemp(suffix='.cache') | 132 save_target = tempfile.mkdtemp(suffix='.cache') |
| 117 cache_directory = '/data/data/' + _CHROME_PACKAGE + '/cache/Cache' | 133 for filename, stat in device.adb.Ls(_REMOTE_CACHE_DIRECTORY): |
| 118 for filename, stat in device.adb.Ls(cache_directory): | |
| 119 if filename == '..': | 134 if filename == '..': |
| 120 continue | 135 continue |
| 121 if filename == '.': | 136 if filename == '.': |
| 122 cache_directory_stat = stat | 137 cache_directory_stat = stat |
| 123 continue | 138 continue |
| 124 original_file = os.path.join(cache_directory, filename) | 139 original_file = os.path.join(_REMOTE_CACHE_DIRECTORY, filename) |
| 125 saved_file = os.path.join(save_target, filename) | 140 saved_file = os.path.join(save_target, filename) |
| 126 device.adb.Pull(original_file, saved_file) | 141 device.adb.Pull(original_file, saved_file) |
| 127 _UpdateTimestampFromAdbStat(saved_file, stat) | 142 _UpdateTimestampFromAdbStat(saved_file, stat) |
| 128 if filename == _INDEX_DIRECTORY_NAME: | 143 if filename == _INDEX_DIRECTORY_NAME: |
| 129 # The directory containing the index was pulled recursively, update the | 144 # The directory containing the index was pulled recursively, update the |
| 130 # timestamps for known files. They are ignored by cache backend, but may | 145 # timestamps for known files. They are ignored by cache backend, but may |
| 131 # be useful for debugging. | 146 # be useful for debugging. |
| 132 index_dir_stat = stat | 147 index_dir_stat = stat |
| 133 saved_index_dir = os.path.join(save_target, _INDEX_DIRECTORY_NAME) | 148 saved_index_dir = os.path.join(save_target, _INDEX_DIRECTORY_NAME) |
| 134 saved_index_file = os.path.join(saved_index_dir, _REAL_INDEX_FILE_NAME) | 149 saved_index_file = os.path.join(saved_index_dir, _REAL_INDEX_FILE_NAME) |
| 135 for sub_file, sub_stat in device.adb.Ls(original_file): | 150 for sub_file, sub_stat in device.adb.Ls(original_file): |
| 136 if sub_file == _REAL_INDEX_FILE_NAME: | 151 if sub_file == _REAL_INDEX_FILE_NAME: |
| 137 _UpdateTimestampFromAdbStat(saved_index_file, sub_stat) | 152 _UpdateTimestampFromAdbStat(saved_index_file, sub_stat) |
| 138 break | 153 break |
| 139 _UpdateTimestampFromAdbStat(saved_index_dir, index_dir_stat) | 154 _UpdateTimestampFromAdbStat(saved_index_dir, index_dir_stat) |
| 140 | 155 |
| 141 # Store the cache directory modification time. It is important to update it | 156 # Store the cache directory modification time. It is important to update it |
| 142 # after all files in it have been written. The timestamp is compared with | 157 # after all files in it have been written. The timestamp is compared with |
| 143 # the contents of the index file when freshness is determined. | 158 # the contents of the index file when freshness is determined. |
| 144 _UpdateTimestampFromAdbStat(save_target, cache_directory_stat) | 159 _UpdateTimestampFromAdbStat(save_target, cache_directory_stat) |
| 145 return save_target | 160 return save_target |
| 146 | 161 |
| 147 | 162 |
| 163 def _PushBrowserCache(device, local_cache_path): |
| 164 """Pushes the browser cache saved locally to the device. |
| 165 |
| 166 Args: |
| 167 device: Android device. |
| 168 local_cache_path: The directory's path containing the cache locally. |
| 169 """ |
| 170 # Clear previous cache. |
| 171 _AdbShell(device.adb, ['rm', '-rf', _REMOTE_CACHE_DIRECTORY]) |
| 172 _AdbShell(device.adb, ['mkdir', _REMOTE_CACHE_DIRECTORY]) |
| 173 |
| 174 # Push cache content. |
| 175 device.adb.Push(local_cache_path, _REMOTE_CACHE_DIRECTORY) |
| 176 |
| 177 # Walk through the local cache to update mtime on the device. |
| 178 def MirrorMtime(local_path): |
| 179 cache_relative_path = os.path.relpath(local_path, start=local_cache_path) |
| 180 remote_path = os.path.join(_REMOTE_CACHE_DIRECTORY, cache_relative_path) |
| 181 _AdbUtime(device.adb, remote_path, os.stat(local_path).st_mtime) |
| 182 |
| 183 for local_directory_path, dirnames, filenames in os.walk( |
| 184 local_cache_path, topdown=False): |
| 185 for filename in filenames: |
| 186 MirrorMtime(os.path.join(local_directory_path, filename)) |
| 187 for dirname in dirnames: |
| 188 MirrorMtime(os.path.join(local_directory_path, dirname)) |
| 189 MirrorMtime(local_cache_path) |
| 190 |
| 191 |
| 148 def _ZipDirectoryContent(root_directory_path, archive_dest_path): | 192 def _ZipDirectoryContent(root_directory_path, archive_dest_path): |
| 149 """Zip a directory's content recursively with all the directories' | 193 """Zip a directory's content recursively with all the directories' |
| 150 timestamps preserved. | 194 timestamps preserved. |
| 151 | 195 |
| 152 Args: | 196 Args: |
| 153 root_directory_path: The directory's path to archive. | 197 root_directory_path: The directory's path to archive. |
| 154 archive_dest_path: Archive destination's path. | 198 archive_dest_path: Archive destination's path. |
| 155 """ | 199 """ |
| 156 with zipfile.ZipFile(archive_dest_path, 'w') as zip_output: | 200 with zipfile.ZipFile(archive_dest_path, 'w') as zip_output: |
| 157 timestamps = {} | 201 timestamps = {} |
| 202 root_directory_stats = os.stat(root_directory_path) |
| 203 timestamps['.'] = { |
| 204 'atime': root_directory_stats.st_atime, |
| 205 'mtime': root_directory_stats.st_mtime} |
| 158 for directory_path, dirnames, filenames in os.walk(root_directory_path): | 206 for directory_path, dirnames, filenames in os.walk(root_directory_path): |
| 159 for dirname in dirnames: | 207 for dirname in dirnames: |
| 160 subdirectory_path = os.path.join(directory_path, dirname) | 208 subdirectory_path = os.path.join(directory_path, dirname) |
| 161 subdirectory_relative_path = os.path.relpath(subdirectory_path, | 209 subdirectory_relative_path = os.path.relpath(subdirectory_path, |
| 162 root_directory_path) | 210 root_directory_path) |
| 163 subdirectory_stats = os.stat(subdirectory_path) | 211 subdirectory_stats = os.stat(subdirectory_path) |
| 164 timestamps[subdirectory_relative_path] = { | 212 timestamps[subdirectory_relative_path] = { |
| 165 'atime': subdirectory_stats.st_atime, | 213 'atime': subdirectory_stats.st_atime, |
| 166 'mtime': subdirectory_stats.st_mtime} | 214 'mtime': subdirectory_stats.st_mtime} |
| 167 for filename in filenames: | 215 for filename in filenames: |
| (...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 203 f.write(zip_input.read(file_archive_name)) | 251 f.write(zip_input.read(file_archive_name)) |
| 204 | 252 |
| 205 assert timestamps | 253 assert timestamps |
| 206 for relative_path, stats in timestamps.iteritems(): | 254 for relative_path, stats in timestamps.iteritems(): |
| 207 output_path = os.path.join(directory_dest_path, relative_path) | 255 output_path = os.path.join(directory_dest_path, relative_path) |
| 208 if not os.path.exists(output_path): | 256 if not os.path.exists(output_path): |
| 209 os.makedirs(output_path) | 257 os.makedirs(output_path) |
| 210 os.utime(output_path, (stats['atime'], stats['mtime'])) | 258 os.utime(output_path, (stats['atime'], stats['mtime'])) |
| 211 | 259 |
| 212 | 260 |
| 261 def _CleanPreviousTraces(output_directories_path): |
| 262 """Cleans previous traces from the output directory. |
| 263 |
| 264 Args: |
| 265 output_directories_path: The output directory path where to clean the |
| 266 previous traces. |
| 267 """ |
| 268 for dirname in os.listdir(output_directories_path): |
| 269 directory_path = os.path.join(output_directories_path, dirname) |
| 270 if not os.path.isdir(directory_path): |
| 271 continue |
| 272 try: |
| 273 int(dirname) |
| 274 except ValueError: |
| 275 continue |
| 276 shutil.rmtree(directory_path) |
| 277 |
| 278 |
| 213 def main(): | 279 def main(): |
| 214 logging.basicConfig(level=logging.INFO) | 280 logging.basicConfig(level=logging.INFO) |
| 215 devil_chromium.Initialize() | 281 devil_chromium.Initialize() |
| 216 | 282 |
| 217 parser = argparse.ArgumentParser() | 283 parser = argparse.ArgumentParser() |
| 218 parser.add_argument('--job', required=True, | 284 parser.add_argument('--job', required=True, |
| 219 help='JSON file with job description.') | 285 help='JSON file with job description.') |
| 220 parser.add_argument('--output', required=True, | 286 parser.add_argument('--output', required=True, |
| 221 help='Name of output directory to create.') | 287 help='Name of output directory to create.') |
| 222 parser.add_argument('--repeat', default=1, type=int, | 288 parser.add_argument('--repeat', default=1, type=int, |
| 223 help='How many times to run the job') | 289 help='How many times to run the job') |
| 224 parser.add_argument('--save-cache', default=False, | 290 parser.add_argument('--cache-op', |
| 225 action='store_true', | 291 choices=['clear', 'save', 'push'], |
| 226 help='Clear HTTP cache before start,' + | 292 default='clear', |
| 227 'save cache before exit.') | 293 help='Configures cache operation to do before launching ' |
| 294 +'Chrome. (Default is clear).') |
| 228 parser.add_argument('--wpr-archive', default=None, type=str, | 295 parser.add_argument('--wpr-archive', default=None, type=str, |
| 229 help='Web page replay archive to load job\'s urls from.') | 296 help='Web page replay archive to load job\'s urls from.') |
| 230 parser.add_argument('--wpr-record', default=False, action='store_true', | 297 parser.add_argument('--wpr-record', default=False, action='store_true', |
| 231 help='Record web page replay archive.') | 298 help='Record web page replay archive.') |
| 232 args = parser.parse_args() | 299 args = parser.parse_args() |
| 233 | 300 |
| 234 try: | 301 if not os.path.isdir(args.output): |
| 235 os.makedirs(args.output) | 302 try: |
| 236 except OSError: | 303 os.makedirs(args.output) |
| 237 logging.error('Cannot create directory for results: %s' % args.output) | 304 except OSError: |
| 238 raise | 305 logging.error('Cannot create directory for results: %s' % args.output) |
| 306 raise |
| 307 else: |
| 308 _CleanPreviousTraces(args.output) |
| 239 | 309 |
| 240 job_urls = _ReadUrlsFromJobDescription(args.job) | 310 job_urls = _ReadUrlsFromJobDescription(args.job) |
| 241 device = device_utils.DeviceUtils.HealthyDevices()[0] | 311 device = device_utils.DeviceUtils.HealthyDevices()[0] |
| 312 local_cache_archive_path = os.path.join(args.output, 'cache.zip') |
| 313 local_cache_directory_path = None |
| 314 |
| 315 if args.cache_op == 'push': |
| 316 assert os.path.isfile(local_cache_archive_path) |
| 317 local_cache_directory_path = tempfile.mkdtemp(suffix='.cache') |
| 318 _UnzipDirectoryContent(local_cache_archive_path, local_cache_directory_path) |
| 242 | 319 |
| 243 with device_setup.WprHost(device, | 320 with device_setup.WprHost(device, |
| 244 args.wpr_archive, | 321 args.wpr_archive, |
| 245 args.wpr_record) as additional_flags: | 322 args.wpr_record) as additional_flags: |
| 246 pages_loaded = 0 | 323 pages_loaded = 0 |
| 247 for iteration in xrange(args.repeat): | 324 for _ in xrange(args.repeat): |
| 248 for url in job_urls: | 325 for url in job_urls: |
| 326 if args.cache_op == 'push': |
| 327 device.KillAll(_CHROME_PACKAGE, quiet=True) |
| 328 _PushBrowserCache(device, local_cache_directory_path) |
| 249 with device_setup.DeviceConnection( | 329 with device_setup.DeviceConnection( |
| 250 device=device, | 330 device=device, |
| 251 additional_flags=additional_flags) as connection: | 331 additional_flags=additional_flags) as connection: |
| 252 if iteration == 0 and pages_loaded == 0 and args.save_cache: | 332 if (pages_loaded == 0 and args.cache_op == 'save' or |
| 333 args.cache_op == 'clear'): |
| 253 connection.ClearCache() | 334 connection.ClearCache() |
| 254 page_track.PageTrack(connection) | 335 page_track.PageTrack(connection) |
| 255 tracing_track = tracing.TracingTrack(connection, | 336 tracing_track = tracing.TracingTrack(connection, |
| 256 categories=pull_sandwich_metrics.CATEGORIES) | 337 categories=pull_sandwich_metrics.CATEGORIES) |
| 257 connection.SetUpMonitoring() | 338 connection.SetUpMonitoring() |
| 258 connection.SendAndIgnoreResponse('Page.navigate', {'url': url}) | 339 connection.SendAndIgnoreResponse('Page.navigate', {'url': url}) |
| 259 connection.StartMonitoring() | 340 connection.StartMonitoring() |
| 260 pages_loaded += 1 | 341 pages_loaded += 1 |
| 261 _SaveChromeTrace(tracing_track.ToJsonDict(), args.output, | 342 _SaveChromeTrace(tracing_track.ToJsonDict(), args.output, |
| 262 str(pages_loaded)) | 343 str(pages_loaded)) |
| 263 | 344 |
| 264 if args.save_cache: | 345 if local_cache_directory_path: |
| 346 shutil.rmtree(local_cache_directory_path) |
| 347 |
| 348 if args.cache_op == 'save': |
| 265 # Move Chrome to background to allow it to flush the index. | 349 # Move Chrome to background to allow it to flush the index. |
| 266 device.adb.Shell('am start com.google.android.launcher') | 350 device.adb.Shell('am start com.google.android.launcher') |
| 267 time.sleep(_TIME_TO_DEVICE_IDLE_SECONDS) | 351 time.sleep(_TIME_TO_DEVICE_IDLE_SECONDS) |
| 268 device.KillAll(_CHROME_PACKAGE, quiet=True) | 352 device.KillAll(_CHROME_PACKAGE, quiet=True) |
| 269 time.sleep(_TIME_TO_DEVICE_IDLE_SECONDS) | 353 time.sleep(_TIME_TO_DEVICE_IDLE_SECONDS) |
| 270 | 354 |
| 271 cache_directory_path = _PullBrowserCache(device) | 355 cache_directory_path = _PullBrowserCache(device) |
| 272 _ZipDirectoryContent(cache_directory_path, | 356 _ZipDirectoryContent(cache_directory_path, local_cache_archive_path) |
| 273 os.path.join(args.output, 'cache.zip')) | |
| 274 shutil.rmtree(cache_directory_path) | 357 shutil.rmtree(cache_directory_path) |
| 275 | 358 |
| 276 | 359 |
| 277 if __name__ == '__main__': | 360 if __name__ == '__main__': |
| 278 sys.exit(main()) | 361 sys.exit(main()) |
| OLD | NEW |