1# Copyright 2012 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import os 6 7from telemetry import decorators 8from telemetry.core import browser_credentials 9from telemetry.core import exceptions 10from telemetry.core import extension_dict 11from telemetry.core import local_server 12from telemetry.core import memory_cache_http_server 13from telemetry.core import platform 14from telemetry.core import tab_list 15from telemetry.core import wpr_modes 16from telemetry.core import wpr_server 17from telemetry.core.backends import browser_backend 18from telemetry.core.platform.profiler import profiler_finder 19 20 21class Browser(object): 22 """A running browser instance that can be controlled in a limited way. 23 24 To create a browser instance, use browser_finder.FindBrowser. 25 26 Be sure to clean up after yourself by calling Close() when you are done with 27 the browser. Or better yet: 28 browser_to_create = FindBrowser(options) 29 with browser_to_create.Create() as browser: 30 ... do all your operations on browser here 31 """ 32 def __init__(self, backend, platform_backend): 33 self._browser_backend = backend 34 self._http_server = None 35 self._wpr_server = None 36 self._platform_backend = platform_backend 37 self._platform = platform.Platform(platform_backend) 38 self._active_profilers = [] 39 self._profilers_states = {} 40 self._local_server_controller = local_server.LocalServerController(backend) 41 self._tabs = tab_list.TabList(backend.tab_list_backend) 42 self.credentials = browser_credentials.BrowserCredentials() 43 self._platform.SetFullPerformanceModeEnabled(True) 44 45 def __enter__(self): 46 self.Start() 47 return self 48 49 def __exit__(self, *args): 50 self.Close() 51 52 @property 53 def platform(self): 54 return self._platform 55 56 @property 57 def browser_type(self): 58 return self._browser_backend.browser_type 59 60 @property 61 def is_content_shell(self): 62 """Returns whether this browser is a content shell, only.""" 63 return self._browser_backend.is_content_shell 64 65 @property 66 def supports_extensions(self): 67 return self._browser_backend.supports_extensions 68 69 @property 70 def supports_tab_control(self): 71 return self._browser_backend.supports_tab_control 72 73 @property 74 def synthetic_gesture_source_type(self): 75 return self._browser_backend.browser_options.synthetic_gesture_source_type 76 77 @property 78 def tabs(self): 79 return self._tabs 80 81 @property 82 def foreground_tab(self): 83 for i in xrange(len(self._tabs)): 84 # The foreground tab is the first (only) one that isn't hidden. 85 # This only works through luck on Android, due to crbug.com/322544 86 # which means that tabs that have never been in the foreground return 87 # document.hidden as false; however in current code the Android foreground 88 # tab is always tab 0, which will be the first one that isn't hidden 89 if self._tabs[i].EvaluateJavaScript('!document.hidden'): 90 return self._tabs[i] 91 raise Exception("No foreground tab found") 92 93 @property 94 @decorators.Cache 95 def extensions(self): 96 if not self.supports_extensions: 97 raise browser_backend.ExtensionsNotSupportedException( 98 'Extensions not supported') 99 return extension_dict.ExtensionDict(self._browser_backend.extension_backend) 100 101 @property 102 def supports_tracing(self): 103 return self._browser_backend.supports_tracing 104 105 def is_profiler_active(self, profiler_name): 106 return profiler_name in [profiler.name() for 107 profiler in self._active_profilers] 108 109 def _GetStatsCommon(self, pid_stats_function): 110 browser_pid = self._browser_backend.pid 111 result = { 112 'Browser': dict(pid_stats_function(browser_pid), **{'ProcessCount': 1}), 113 'Renderer': {'ProcessCount': 0}, 114 'Gpu': {'ProcessCount': 0}, 115 'Other': {'ProcessCount': 0} 116 } 117 process_count = 1 118 for child_pid in self._platform_backend.GetChildPids(browser_pid): 119 try: 120 child_cmd_line = self._platform_backend.GetCommandLine(child_pid) 121 child_stats = pid_stats_function(child_pid) 122 except exceptions.ProcessGoneException: 123 # It is perfectly fine for a process to have gone away between calling 124 # GetChildPids() and then further examining it. 125 continue 126 child_process_name = self._browser_backend.GetProcessName(child_cmd_line) 127 process_name_type_key_map = {'gpu-process': 'Gpu', 'renderer': 'Renderer'} 128 if child_process_name in process_name_type_key_map: 129 child_process_type_key = process_name_type_key_map[child_process_name] 130 else: 131 # TODO: identify other process types (zygote, plugin, etc), instead of 132 # lumping them in a single category. 133 child_process_type_key = 'Other' 134 result[child_process_type_key]['ProcessCount'] += 1 135 for k, v in child_stats.iteritems(): 136 if k in result[child_process_type_key]: 137 result[child_process_type_key][k] += v 138 else: 139 result[child_process_type_key][k] = v 140 process_count += 1 141 for v in result.itervalues(): 142 if v['ProcessCount'] > 1: 143 for k in v.keys(): 144 if k.endswith('Peak'): 145 del v[k] 146 del v['ProcessCount'] 147 result['ProcessCount'] = process_count 148 return result 149 150 @property 151 def memory_stats(self): 152 """Returns a dict of memory statistics for the browser: 153 { 'Browser': { 154 'VM': R, 155 'VMPeak': S, 156 'WorkingSetSize': T, 157 'WorkingSetSizePeak': U, 158 'ProportionalSetSize': V, 159 'PrivateDirty': W 160 }, 161 'Gpu': { 162 'VM': R, 163 'VMPeak': S, 164 'WorkingSetSize': T, 165 'WorkingSetSizePeak': U, 166 'ProportionalSetSize': V, 167 'PrivateDirty': W 168 }, 169 'Renderer': { 170 'VM': R, 171 'VMPeak': S, 172 'WorkingSetSize': T, 173 'WorkingSetSizePeak': U, 174 'ProportionalSetSize': V, 175 'PrivateDirty': W 176 }, 177 'SystemCommitCharge': X, 178 'SystemTotalPhysicalMemory': Y, 179 'ProcessCount': Z, 180 } 181 Any of the above keys may be missing on a per-platform basis. 182 """ 183 self._platform_backend.PurgeUnpinnedMemory() 184 result = self._GetStatsCommon(self._platform_backend.GetMemoryStats) 185 result['SystemCommitCharge'] = \ 186 self._platform_backend.GetSystemCommitCharge() 187 result['SystemTotalPhysicalMemory'] = \ 188 self._platform_backend.GetSystemTotalPhysicalMemory() 189 return result 190 191 @property 192 def cpu_stats(self): 193 """Returns a dict of cpu statistics for the system. 194 { 'Browser': { 195 'CpuProcessTime': S, 196 'TotalTime': T 197 }, 198 'Gpu': { 199 'CpuProcessTime': S, 200 'TotalTime': T 201 }, 202 'Renderer': { 203 'CpuProcessTime': S, 204 'TotalTime': T 205 } 206 } 207 Any of the above keys may be missing on a per-platform basis. 208 """ 209 result = self._GetStatsCommon(self._platform_backend.GetCpuStats) 210 del result['ProcessCount'] 211 212 # We want a single time value, not the sum for all processes. 213 cpu_timestamp = self._platform_backend.GetCpuTimestamp() 214 for process_type in result: 215 # Skip any process_types that are empty 216 if not len(result[process_type]): 217 continue 218 result[process_type].update(cpu_timestamp) 219 return result 220 221 @property 222 def io_stats(self): 223 """Returns a dict of IO statistics for the browser: 224 { 'Browser': { 225 'ReadOperationCount': W, 226 'WriteOperationCount': X, 227 'ReadTransferCount': Y, 228 'WriteTransferCount': Z 229 }, 230 'Gpu': { 231 'ReadOperationCount': W, 232 'WriteOperationCount': X, 233 'ReadTransferCount': Y, 234 'WriteTransferCount': Z 235 }, 236 'Renderer': { 237 'ReadOperationCount': W, 238 'WriteOperationCount': X, 239 'ReadTransferCount': Y, 240 'WriteTransferCount': Z 241 } 242 } 243 """ 244 result = self._GetStatsCommon(self._platform_backend.GetIOStats) 245 del result['ProcessCount'] 246 return result 247 248 def StartProfiling(self, profiler_name, base_output_file): 249 """Starts profiling using |profiler_name|. Results are saved to 250 |base_output_file|.<process_name>.""" 251 assert not self._active_profilers, 'Already profiling. Must stop first.' 252 253 profiler_class = profiler_finder.FindProfiler(profiler_name) 254 255 if not profiler_class.is_supported(self._browser_backend.browser_type): 256 raise Exception('The %s profiler is not ' 257 'supported on this platform.' % profiler_name) 258 259 if not profiler_class in self._profilers_states: 260 self._profilers_states[profiler_class] = {} 261 262 self._active_profilers.append( 263 profiler_class(self._browser_backend, self._platform_backend, 264 base_output_file, self._profilers_states[profiler_class])) 265 266 def StopProfiling(self): 267 """Stops all active profilers and saves their results. 268 269 Returns: 270 A list of filenames produced by the profiler. 271 """ 272 output_files = [] 273 for profiler in self._active_profilers: 274 output_files.extend(profiler.CollectProfile()) 275 self._active_profilers = [] 276 return output_files 277 278 def StartTracing(self, custom_categories=None, timeout=10): 279 return self._browser_backend.StartTracing(custom_categories, timeout) 280 281 @property 282 def is_tracing_running(self): 283 return self._browser_backend.is_tracing_running 284 285 def StopTracing(self): 286 """ Stops tracing and returns the result as TimelineData object. """ 287 return self._browser_backend.StopTracing() 288 289 def Start(self): 290 browser_options = self._browser_backend.browser_options 291 self.platform.FlushDnsCache() 292 if browser_options.clear_sytem_cache_for_browser_and_profile_on_start: 293 if self.platform.CanFlushIndividualFilesFromSystemCache(): 294 self.platform.FlushSystemCacheForDirectory( 295 self._browser_backend.profile_directory) 296 self.platform.FlushSystemCacheForDirectory( 297 self._browser_backend.browser_directory) 298 else: 299 self.platform.FlushEntireSystemCache() 300 301 self._browser_backend.SetBrowser(self) 302 self._browser_backend.Start() 303 304 def Close(self): 305 """Closes this browser.""" 306 for profiler_class in self._profilers_states: 307 profiler_class.WillCloseBrowser(self._browser_backend, 308 self._platform_backend) 309 310 self.platform.SetFullPerformanceModeEnabled(False) 311 312 if self._wpr_server: 313 self._wpr_server.Close() 314 self._wpr_server = None 315 316 if self._http_server: 317 self._http_server.Close() 318 self._http_server = None 319 320 self._local_server_controller.Close() 321 self._browser_backend.Close() 322 self.credentials = None 323 324 @property 325 def http_server(self): 326 return self._local_server_controller.GetRunningServer( 327 memory_cache_http_server.MemoryCacheHTTPServer, None) 328 329 def SetHTTPServerDirectories(self, paths): 330 """Returns True if the HTTP server was started, False otherwise.""" 331 if isinstance(paths, basestring): 332 paths = set([paths]) 333 paths = set(os.path.realpath(p) for p in paths) 334 335 # If any path is in a subdirectory of another, remove the subdirectory. 336 duplicates = set() 337 for parent_path in paths: 338 for sub_path in paths: 339 if parent_path == sub_path: 340 continue 341 if os.path.commonprefix((parent_path, sub_path)) == parent_path: 342 duplicates.add(sub_path) 343 paths -= duplicates 344 345 if self.http_server: 346 if paths and self.http_server.paths == paths: 347 return False 348 349 self.http_server.Close() 350 351 if not paths: 352 return False 353 354 server = memory_cache_http_server.MemoryCacheHTTPServer(paths) 355 self.StartLocalServer(server) 356 return True 357 358 def StartLocalServer(self, server): 359 """Starts a LocalServer and associates it with this browser. 360 361 It will be closed when the browser closes. 362 """ 363 self._local_server_controller.StartServer(server) 364 365 @property 366 def local_servers(self): 367 """Returns the currently running local servers.""" 368 return self._local_server_controller.local_servers 369 370 def SetReplayArchivePath(self, archive_path, append_to_existing_wpr=False, 371 make_javascript_deterministic=True): 372 if self._wpr_server: 373 self._wpr_server.Close() 374 self._wpr_server = None 375 376 if not archive_path: 377 return None 378 379 if self._browser_backend.wpr_mode == wpr_modes.WPR_OFF: 380 return 381 382 use_record_mode = self._browser_backend.wpr_mode == wpr_modes.WPR_RECORD 383 if not use_record_mode: 384 assert os.path.isfile(archive_path) 385 386 self._wpr_server = wpr_server.ReplayServer( 387 self._browser_backend, 388 archive_path, 389 use_record_mode, 390 append_to_existing_wpr, 391 make_javascript_deterministic) 392 393 def GetStandardOutput(self): 394 return self._browser_backend.GetStandardOutput() 395 396 def GetStackTrace(self): 397 return self._browser_backend.GetStackTrace() 398 399 @property 400 def supports_system_info(self): 401 return self._browser_backend.supports_system_info 402 403 def GetSystemInfo(self): 404 """Returns low-level information about the system, if available. 405 406 See the documentation of the SystemInfo class for more details.""" 407 return self._browser_backend.GetSystemInfo() 408