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