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 logging 6import sys 7 8from py_utils import cloud_storage # pylint: disable=import-error 9 10from telemetry.core import exceptions 11from telemetry.core import profiling_controller 12from telemetry import decorators 13from telemetry.internal import app 14from telemetry.internal.backends import browser_backend 15from telemetry.internal.browser import browser_credentials 16from telemetry.internal.browser import extension_dict 17from telemetry.internal.browser import tab_list 18from telemetry.internal.browser import web_contents 19from telemetry.internal.util import exception_formatter 20 21 22class Browser(app.App): 23 """A running browser instance that can be controlled in a limited way. 24 25 To create a browser instance, use browser_finder.FindBrowser. 26 27 Be sure to clean up after yourself by calling Close() when you are done with 28 the browser. Or better yet: 29 browser_to_create = FindBrowser(options) 30 with browser_to_create.Create(options) as browser: 31 ... do all your operations on browser here 32 """ 33 def __init__(self, backend, platform_backend, credentials_path): 34 super(Browser, self).__init__(app_backend=backend, 35 platform_backend=platform_backend) 36 try: 37 self._browser_backend = backend 38 self._platform_backend = platform_backend 39 self._tabs = tab_list.TabList(backend.tab_list_backend) 40 self.credentials = browser_credentials.BrowserCredentials() 41 self.credentials.credentials_path = credentials_path 42 self._platform_backend.DidCreateBrowser(self, self._browser_backend) 43 browser_options = self._browser_backend.browser_options 44 self.platform.FlushDnsCache() 45 if browser_options.clear_sytem_cache_for_browser_and_profile_on_start: 46 if self.platform.CanFlushIndividualFilesFromSystemCache(): 47 self.platform.FlushSystemCacheForDirectory( 48 self._browser_backend.profile_directory) 49 self.platform.FlushSystemCacheForDirectory( 50 self._browser_backend.browser_directory) 51 else: 52 self.platform.FlushEntireSystemCache() 53 54 self._browser_backend.SetBrowser(self) 55 self._browser_backend.Start() 56 self._LogBrowserInfo() 57 self._platform_backend.DidStartBrowser(self, self._browser_backend) 58 self._profiling_controller = profiling_controller.ProfilingController( 59 self._browser_backend.profiling_controller_backend) 60 except Exception: 61 exc_info = sys.exc_info() 62 logging.exception('Failure while starting browser backend.') 63 try: 64 self._platform_backend.WillCloseBrowser(self, self._browser_backend) 65 except Exception: 66 exception_formatter.PrintFormattedException( 67 msg='Exception raised while closing platform backend') 68 raise exc_info[0], exc_info[1], exc_info[2] 69 70 @property 71 def profiling_controller(self): 72 return self._profiling_controller 73 74 @property 75 def browser_type(self): 76 return self.app_type 77 78 @property 79 def supports_extensions(self): 80 return self._browser_backend.supports_extensions 81 82 @property 83 def supports_tab_control(self): 84 return self._browser_backend.supports_tab_control 85 86 @property 87 def tabs(self): 88 return self._tabs 89 90 @property 91 def foreground_tab(self): 92 for i in xrange(len(self._tabs)): 93 # The foreground tab is the first (only) one that isn't hidden. 94 # This only works through luck on Android, due to crbug.com/322544 95 # which means that tabs that have never been in the foreground return 96 # document.hidden as false; however in current code the Android foreground 97 # tab is always tab 0, which will be the first one that isn't hidden 98 if self._tabs[i].EvaluateJavaScript('!document.hidden'): 99 return self._tabs[i] 100 raise Exception("No foreground tab found") 101 102 @property 103 @decorators.Cache 104 def extensions(self): 105 if not self.supports_extensions: 106 raise browser_backend.ExtensionsNotSupportedException( 107 'Extensions not supported') 108 return extension_dict.ExtensionDict(self._browser_backend.extension_backend) 109 110 def _LogBrowserInfo(self): 111 logging.info('OS: %s %s', 112 self._platform_backend.platform.GetOSName(), 113 self._platform_backend.platform.GetOSVersionName()) 114 if self.supports_system_info: 115 system_info = self.GetSystemInfo() 116 if system_info.model_name: 117 logging.info('Model: %s', system_info.model_name) 118 if system_info.gpu: 119 for i, device in enumerate(system_info.gpu.devices): 120 logging.info('GPU device %d: %s', i, device) 121 if system_info.gpu.aux_attributes: 122 logging.info('GPU Attributes:') 123 for k, v in sorted(system_info.gpu.aux_attributes.iteritems()): 124 logging.info(' %-20s: %s', k, v) 125 if system_info.gpu.feature_status: 126 logging.info('Feature Status:') 127 for k, v in sorted(system_info.gpu.feature_status.iteritems()): 128 logging.info(' %-20s: %s', k, v) 129 if system_info.gpu.driver_bug_workarounds: 130 logging.info('Driver Bug Workarounds:') 131 for workaround in system_info.gpu.driver_bug_workarounds: 132 logging.info(' %s', workaround) 133 else: 134 logging.info('No GPU devices') 135 else: 136 logging.warning('System info not supported') 137 138 def _GetStatsCommon(self, pid_stats_function): 139 browser_pid = self._browser_backend.pid 140 result = { 141 'Browser': dict(pid_stats_function(browser_pid), **{'ProcessCount': 1}), 142 'Renderer': {'ProcessCount': 0}, 143 'Gpu': {'ProcessCount': 0}, 144 'Other': {'ProcessCount': 0} 145 } 146 process_count = 1 147 for child_pid in self._platform_backend.GetChildPids(browser_pid): 148 try: 149 child_cmd_line = self._platform_backend.GetCommandLine(child_pid) 150 child_stats = pid_stats_function(child_pid) 151 except exceptions.ProcessGoneException: 152 # It is perfectly fine for a process to have gone away between calling 153 # GetChildPids() and then further examining it. 154 continue 155 child_process_name = self._browser_backend.GetProcessName(child_cmd_line) 156 process_name_type_key_map = {'gpu-process': 'Gpu', 'renderer': 'Renderer'} 157 if child_process_name in process_name_type_key_map: 158 child_process_type_key = process_name_type_key_map[child_process_name] 159 else: 160 # TODO: identify other process types (zygote, plugin, etc), instead of 161 # lumping them in a single category. 162 child_process_type_key = 'Other' 163 result[child_process_type_key]['ProcessCount'] += 1 164 for k, v in child_stats.iteritems(): 165 if k in result[child_process_type_key]: 166 result[child_process_type_key][k] += v 167 else: 168 result[child_process_type_key][k] = v 169 process_count += 1 170 for v in result.itervalues(): 171 if v['ProcessCount'] > 1: 172 for k in v.keys(): 173 if k.endswith('Peak'): 174 del v[k] 175 del v['ProcessCount'] 176 result['ProcessCount'] = process_count 177 return result 178 179 @property 180 def memory_stats(self): 181 """Returns a dict of memory statistics for the browser: 182 { 'Browser': { 183 'VM': R, 184 'VMPeak': S, 185 'WorkingSetSize': T, 186 'WorkingSetSizePeak': U, 187 'ProportionalSetSize': V, 188 'PrivateDirty': W 189 }, 190 'Gpu': { 191 'VM': R, 192 'VMPeak': S, 193 'WorkingSetSize': T, 194 'WorkingSetSizePeak': U, 195 'ProportionalSetSize': V, 196 'PrivateDirty': W 197 }, 198 'Renderer': { 199 'VM': R, 200 'VMPeak': S, 201 'WorkingSetSize': T, 202 'WorkingSetSizePeak': U, 203 'ProportionalSetSize': V, 204 'PrivateDirty': W 205 }, 206 'SystemCommitCharge': X, 207 'SystemTotalPhysicalMemory': Y, 208 'ProcessCount': Z, 209 } 210 Any of the above keys may be missing on a per-platform basis. 211 """ 212 self._platform_backend.PurgeUnpinnedMemory() 213 result = self._GetStatsCommon(self._platform_backend.GetMemoryStats) 214 commit_charge = self._platform_backend.GetSystemCommitCharge() 215 if commit_charge: 216 result['SystemCommitCharge'] = commit_charge 217 total = self._platform_backend.GetSystemTotalPhysicalMemory() 218 if total: 219 result['SystemTotalPhysicalMemory'] = total 220 return result 221 222 @property 223 def cpu_stats(self): 224 """Returns a dict of cpu statistics for the system. 225 { 'Browser': { 226 'CpuProcessTime': S, 227 'TotalTime': T 228 }, 229 'Gpu': { 230 'CpuProcessTime': S, 231 'TotalTime': T 232 }, 233 'Renderer': { 234 'CpuProcessTime': S, 235 'TotalTime': T 236 } 237 } 238 Any of the above keys may be missing on a per-platform basis. 239 """ 240 result = self._GetStatsCommon(self._platform_backend.GetCpuStats) 241 del result['ProcessCount'] 242 243 # We want a single time value, not the sum for all processes. 244 cpu_timestamp = self._platform_backend.GetCpuTimestamp() 245 for process_type in result: 246 # Skip any process_types that are empty 247 if not len(result[process_type]): 248 continue 249 result[process_type].update(cpu_timestamp) 250 return result 251 252 def Close(self): 253 """Closes this browser.""" 254 try: 255 if self._browser_backend.IsBrowserRunning(): 256 self._platform_backend.WillCloseBrowser(self, self._browser_backend) 257 258 self._browser_backend.profiling_controller_backend.WillCloseBrowser() 259 if self._browser_backend.supports_uploading_logs: 260 try: 261 self._browser_backend.UploadLogsToCloudStorage() 262 except cloud_storage.CloudStorageError as e: 263 logging.error('Cannot upload browser log: %s' % str(e)) 264 finally: 265 self._browser_backend.Close() 266 self.credentials = None 267 268 def Foreground(self): 269 """Ensure the browser application is moved to the foreground.""" 270 return self._browser_backend.Foreground() 271 272 def GetStandardOutput(self): 273 return self._browser_backend.GetStandardOutput() 274 275 def GetLogFileContents(self): 276 return self._browser_backend.GetLogFileContents() 277 278 def GetStackTrace(self): 279 return self._browser_backend.GetStackTrace() 280 281 def GetMostRecentMinidumpPath(self): 282 """Returns the path to the most recent minidump.""" 283 return self._browser_backend.GetMostRecentMinidumpPath() 284 285 def GetAllMinidumpPaths(self): 286 """Returns all minidump paths available in the backend.""" 287 return self._browser_backend.GetAllMinidumpPaths() 288 289 def GetAllUnsymbolizedMinidumpPaths(self): 290 """Returns paths to all minidumps that have not already been 291 symbolized.""" 292 return self._browser_backend.GetAllUnsymbolizedMinidumpPaths() 293 294 def SymbolizeMinidump(self, minidump_path): 295 """Given a minidump path, this method returns a tuple with the 296 first value being whether or not the minidump was able to be 297 symbolized and the second being that symbolized dump when true 298 and error message when false.""" 299 return self._browser_backend.SymbolizeMinidump(minidump_path) 300 301 @property 302 def supports_system_info(self): 303 return self._browser_backend.supports_system_info 304 305 def GetSystemInfo(self): 306 """Returns low-level information about the system, if available. 307 308 See the documentation of the SystemInfo class for more details.""" 309 return self._browser_backend.GetSystemInfo() 310 311 @property 312 def supports_memory_dumping(self): 313 return self._browser_backend.supports_memory_dumping 314 315 def DumpMemory(self, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT): 316 return self._browser_backend.DumpMemory(timeout) 317 318 @property 319 def supports_overriding_memory_pressure_notifications(self): 320 return ( 321 self._browser_backend.supports_overriding_memory_pressure_notifications) 322 323 def SetMemoryPressureNotificationsSuppressed( 324 self, suppressed, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT): 325 self._browser_backend.SetMemoryPressureNotificationsSuppressed( 326 suppressed, timeout) 327 328 def SimulateMemoryPressureNotification( 329 self, pressure_level, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT): 330 self._browser_backend.SimulateMemoryPressureNotification( 331 pressure_level, timeout) 332 333 @property 334 def supports_cpu_metrics(self): 335 return self._browser_backend.supports_cpu_metrics 336 337 @property 338 def supports_memory_metrics(self): 339 return self._browser_backend.supports_memory_metrics 340 341 @property 342 def supports_power_metrics(self): 343 return self._browser_backend.supports_power_metrics 344 345 def DumpStateUponFailure(self): 346 logging.info('*************** BROWSER STANDARD OUTPUT ***************') 347 try: # pylint: disable=broad-except 348 logging.info(self.GetStandardOutput()) 349 except Exception: 350 logging.exception('Failed to get browser standard output:') 351 logging.info('*********** END OF BROWSER STANDARD OUTPUT ************') 352 353 logging.info('********************* BROWSER LOG *********************') 354 try: # pylint: disable=broad-except 355 logging.info(self.GetLogFileContents()) 356 except Exception: 357 logging.exception('Failed to get browser log:') 358 logging.info('***************** END OF BROWSER LOG ******************') 359