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