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 catapult_base 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._platform_backend.DidStartBrowser(self, self._browser_backend)
57      self._profiling_controller = profiling_controller.ProfilingController(
58          self._browser_backend.profiling_controller_backend)
59    except Exception:
60      exc_info = sys.exc_info()
61      logging.exception('Failure while starting browser backend.')
62      try:
63        self._platform_backend.WillCloseBrowser(self, self._browser_backend)
64      except Exception:
65        exception_formatter.PrintFormattedException(
66            msg='Exception raised while closing platform backend')
67      raise exc_info[0], exc_info[1], exc_info[2]
68
69  @property
70  def profiling_controller(self):
71    return self._profiling_controller
72
73  @property
74  def browser_type(self):
75    return self.app_type
76
77  @property
78  def supports_extensions(self):
79    return self._browser_backend.supports_extensions
80
81  @property
82  def supports_tab_control(self):
83    return self._browser_backend.supports_tab_control
84
85  @property
86  def tabs(self):
87    return self._tabs
88
89  @property
90  def foreground_tab(self):
91    for i in xrange(len(self._tabs)):
92      # The foreground tab is the first (only) one that isn't hidden.
93      # This only works through luck on Android, due to crbug.com/322544
94      # which means that tabs that have never been in the foreground return
95      # document.hidden as false; however in current code the Android foreground
96      # tab is always tab 0, which will be the first one that isn't hidden
97      if self._tabs[i].EvaluateJavaScript('!document.hidden'):
98        return self._tabs[i]
99    raise Exception("No foreground tab found")
100
101  @property
102  @decorators.Cache
103  def extensions(self):
104    if not self.supports_extensions:
105      raise browser_backend.ExtensionsNotSupportedException(
106          'Extensions not supported')
107    return extension_dict.ExtensionDict(self._browser_backend.extension_backend)
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    commit_charge = self._platform_backend.GetSystemCommitCharge()
186    if commit_charge:
187      result['SystemCommitCharge'] = commit_charge
188    total = self._platform_backend.GetSystemTotalPhysicalMemory()
189    if total:
190      result['SystemTotalPhysicalMemory'] = total
191    return result
192
193  @property
194  def cpu_stats(self):
195    """Returns a dict of cpu statistics for the system.
196    { 'Browser': {
197        'CpuProcessTime': S,
198        'TotalTime': T
199      },
200      'Gpu': {
201        'CpuProcessTime': S,
202        'TotalTime': T
203      },
204      'Renderer': {
205        'CpuProcessTime': S,
206        'TotalTime': T
207      }
208    }
209    Any of the above keys may be missing on a per-platform basis.
210    """
211    result = self._GetStatsCommon(self._platform_backend.GetCpuStats)
212    del result['ProcessCount']
213
214    # We want a single time value, not the sum for all processes.
215    cpu_timestamp = self._platform_backend.GetCpuTimestamp()
216    for process_type in result:
217      # Skip any process_types that are empty
218      if not len(result[process_type]):
219        continue
220      result[process_type].update(cpu_timestamp)
221    return result
222
223  def Close(self):
224    """Closes this browser."""
225    try:
226      if self._browser_backend.IsBrowserRunning():
227        self._platform_backend.WillCloseBrowser(self, self._browser_backend)
228
229      self._browser_backend.profiling_controller_backend.WillCloseBrowser()
230      if self._browser_backend.supports_uploading_logs:
231        try:
232          self._browser_backend.UploadLogsToCloudStorage()
233        except cloud_storage.CloudStorageError as e:
234          logging.error('Cannot upload browser log: %s' % str(e))
235    finally:
236      self._browser_backend.Close()
237      self.credentials = None
238
239
240  def GetStandardOutput(self):
241    return self._browser_backend.GetStandardOutput()
242
243  def GetStackTrace(self):
244    return self._browser_backend.GetStackTrace()
245
246  @property
247  def supports_system_info(self):
248    return self._browser_backend.supports_system_info
249
250  def GetSystemInfo(self):
251    """Returns low-level information about the system, if available.
252
253       See the documentation of the SystemInfo class for more details."""
254    return self._browser_backend.GetSystemInfo()
255
256  @property
257  def supports_memory_dumping(self):
258    return self._browser_backend.supports_memory_dumping
259
260  def DumpMemory(self, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT):
261    return self._browser_backend.DumpMemory(timeout)
262
263  @property
264  def supports_overriding_memory_pressure_notifications(self):
265    return (
266        self._browser_backend.supports_overriding_memory_pressure_notifications)
267
268  def SetMemoryPressureNotificationsSuppressed(
269      self, suppressed, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT):
270    self._browser_backend.SetMemoryPressureNotificationsSuppressed(
271        suppressed, timeout)
272
273  def SimulateMemoryPressureNotification(
274      self, pressure_level, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT):
275    self._browser_backend.SimulateMemoryPressureNotification(
276        pressure_level, timeout)
277
278  @property
279  def supports_cpu_metrics(self):
280    return self._browser_backend.supports_cpu_metrics
281
282  @property
283  def supports_memory_metrics(self):
284    return self._browser_backend.supports_memory_metrics
285
286  @property
287  def supports_power_metrics(self):
288    return self._browser_backend.supports_power_metrics
289