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