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 os
6
7from telemetry import decorators
8from telemetry.core import browser_credentials
9from telemetry.core import exceptions
10from telemetry.core import extension_dict
11from telemetry.core import local_server
12from telemetry.core import memory_cache_http_server
13from telemetry.core import tab_list
14from telemetry.core import wpr_modes
15from telemetry.core import wpr_server
16from telemetry.core.backends import browser_backend
17
18
19class Browser(object):
20  """A running browser instance that can be controlled in a limited way.
21
22  To create a browser instance, use browser_finder.FindBrowser.
23
24  Be sure to clean up after yourself by calling Close() when you are done with
25  the browser. Or better yet:
26    browser_to_create = FindBrowser(options)
27    with browser_to_create.Create() as browser:
28      ... do all your operations on browser here
29  """
30  def __init__(self, backend, platform_backend, archive_path,
31               append_to_existing_wpr, make_javascript_deterministic,
32               credentials_path):
33    assert platform_backend.platform != None
34
35    self._browser_backend = backend
36    self._platform_backend = platform_backend
37    self._wpr_server = None
38    self._local_server_controller = local_server.LocalServerController(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
44    self.SetReplayArchivePath(archive_path,
45                              append_to_existing_wpr,
46                              make_javascript_deterministic)
47
48    browser_options = self._browser_backend.browser_options
49    self.platform.FlushDnsCache()
50    if browser_options.clear_sytem_cache_for_browser_and_profile_on_start:
51      if self.platform.CanFlushIndividualFilesFromSystemCache():
52        self.platform.FlushSystemCacheForDirectory(
53            self._browser_backend.profile_directory)
54        self.platform.FlushSystemCacheForDirectory(
55            self._browser_backend.browser_directory)
56      else:
57        self.platform.FlushEntireSystemCache()
58
59    self._browser_backend.SetBrowser(self)
60    self._browser_backend.Start()
61    self._platform_backend.DidStartBrowser(self, self._browser_backend)
62
63  def __enter__(self):
64    return self
65
66  def __exit__(self, *args):
67    self.Close()
68
69  @property
70  def platform(self):
71    return self._platform_backend.platform
72
73  @property
74  def browser_type(self):
75    return self._browser_backend.browser_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 synthetic_gesture_source_type(self):
87    return self._browser_backend.browser_options.synthetic_gesture_source_type
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 _GetStatsCommon(self, pid_stats_function):
114    browser_pid = self._browser_backend.pid
115    result = {
116        'Browser': dict(pid_stats_function(browser_pid), **{'ProcessCount': 1}),
117        'Renderer': {'ProcessCount': 0},
118        'Gpu': {'ProcessCount': 0},
119        'Other': {'ProcessCount': 0}
120    }
121    process_count = 1
122    for child_pid in self._platform_backend.GetChildPids(browser_pid):
123      try:
124        child_cmd_line = self._platform_backend.GetCommandLine(child_pid)
125        child_stats = pid_stats_function(child_pid)
126      except exceptions.ProcessGoneException:
127        # It is perfectly fine for a process to have gone away between calling
128        # GetChildPids() and then further examining it.
129        continue
130      child_process_name = self._browser_backend.GetProcessName(child_cmd_line)
131      process_name_type_key_map = {'gpu-process': 'Gpu', 'renderer': 'Renderer'}
132      if child_process_name in process_name_type_key_map:
133        child_process_type_key = process_name_type_key_map[child_process_name]
134      else:
135        # TODO: identify other process types (zygote, plugin, etc), instead of
136        # lumping them in a single category.
137        child_process_type_key = 'Other'
138      result[child_process_type_key]['ProcessCount'] += 1
139      for k, v in child_stats.iteritems():
140        if k in result[child_process_type_key]:
141          result[child_process_type_key][k] += v
142        else:
143          result[child_process_type_key][k] = v
144      process_count += 1
145    for v in result.itervalues():
146      if v['ProcessCount'] > 1:
147        for k in v.keys():
148          if k.endswith('Peak'):
149            del v[k]
150      del v['ProcessCount']
151    result['ProcessCount'] = process_count
152    return result
153
154  @property
155  def memory_stats(self):
156    """Returns a dict of memory statistics for the browser:
157    { 'Browser': {
158        'VM': R,
159        'VMPeak': S,
160        'WorkingSetSize': T,
161        'WorkingSetSizePeak': U,
162        'ProportionalSetSize': V,
163        'PrivateDirty': W
164      },
165      'Gpu': {
166        'VM': R,
167        'VMPeak': S,
168        'WorkingSetSize': T,
169        'WorkingSetSizePeak': U,
170        'ProportionalSetSize': V,
171        'PrivateDirty': W
172      },
173      'Renderer': {
174        'VM': R,
175        'VMPeak': S,
176        'WorkingSetSize': T,
177        'WorkingSetSizePeak': U,
178        'ProportionalSetSize': V,
179        'PrivateDirty': W
180      },
181      'SystemCommitCharge': X,
182      'SystemTotalPhysicalMemory': Y,
183      'ProcessCount': Z,
184    }
185    Any of the above keys may be missing on a per-platform basis.
186    """
187    self._platform_backend.PurgeUnpinnedMemory()
188    result = self._GetStatsCommon(self._platform_backend.GetMemoryStats)
189    commit_charge = self._platform_backend.GetSystemCommitCharge()
190    if commit_charge:
191      result['SystemCommitCharge'] = commit_charge
192    total = self._platform_backend.GetSystemTotalPhysicalMemory()
193    if total:
194      result['SystemTotalPhysicalMemory'] = total
195    return result
196
197  @property
198  def cpu_stats(self):
199    """Returns a dict of cpu statistics for the system.
200    { 'Browser': {
201        'CpuProcessTime': S,
202        'TotalTime': T
203      },
204      'Gpu': {
205        'CpuProcessTime': S,
206        'TotalTime': T
207      },
208      'Renderer': {
209        'CpuProcessTime': S,
210        'TotalTime': T
211      }
212    }
213    Any of the above keys may be missing on a per-platform basis.
214    """
215    result = self._GetStatsCommon(self._platform_backend.GetCpuStats)
216    del result['ProcessCount']
217
218    # We want a single time value, not the sum for all processes.
219    cpu_timestamp = self._platform_backend.GetCpuTimestamp()
220    for process_type in result:
221      # Skip any process_types that are empty
222      if not len(result[process_type]):
223        continue
224      result[process_type].update(cpu_timestamp)
225    return result
226
227  @property
228  def io_stats(self):
229    """Returns a dict of IO statistics for the browser:
230    { 'Browser': {
231        'ReadOperationCount': W,
232        'WriteOperationCount': X,
233        'ReadTransferCount': Y,
234        'WriteTransferCount': Z
235      },
236      'Gpu': {
237        'ReadOperationCount': W,
238        'WriteOperationCount': X,
239        'ReadTransferCount': Y,
240        'WriteTransferCount': Z
241      },
242      'Renderer': {
243        'ReadOperationCount': W,
244        'WriteOperationCount': X,
245        'ReadTransferCount': Y,
246        'WriteTransferCount': Z
247      }
248    }
249    """
250    result = self._GetStatsCommon(self._platform_backend.GetIOStats)
251    del result['ProcessCount']
252    return result
253
254  def Close(self):
255    """Closes this browser."""
256    if self._browser_backend.IsBrowserRunning():
257      self._platform_backend.WillCloseBrowser(self, self._browser_backend)
258
259    if self._wpr_server:
260      self._wpr_server.Close()
261      self._wpr_server = None
262
263    self._local_server_controller.Close()
264    self._browser_backend.Close()
265    self.credentials = None
266
267  @property
268  def http_server(self):
269    return self._local_server_controller.GetRunningServer(
270        memory_cache_http_server.MemoryCacheHTTPServer, None)
271
272  def SetHTTPServerDirectories(self, paths):
273    """Returns True if the HTTP server was started, False otherwise."""
274    if isinstance(paths, basestring):
275      paths = set([paths])
276    paths = set(os.path.realpath(p) for p in paths)
277
278    # If any path is in a subdirectory of another, remove the subdirectory.
279    duplicates = set()
280    for parent_path in paths:
281      for sub_path in paths:
282        if parent_path == sub_path:
283          continue
284        if os.path.commonprefix((parent_path, sub_path)) == parent_path:
285          duplicates.add(sub_path)
286    paths -= duplicates
287
288    if self.http_server:
289      if paths and self.http_server.paths == paths:
290        return False
291
292      self.http_server.Close()
293
294    if not paths:
295      return False
296
297    server = memory_cache_http_server.MemoryCacheHTTPServer(paths)
298    self.StartLocalServer(server)
299    return True
300
301  def StartLocalServer(self, server):
302    """Starts a LocalServer and associates it with this browser.
303
304    It will be closed when the browser closes.
305    """
306    self._local_server_controller.StartServer(server)
307
308  @property
309  def local_servers(self):
310    """Returns the currently running local servers."""
311    return self._local_server_controller.local_servers
312
313  def SetReplayArchivePath(self, archive_path, append_to_existing_wpr=False,
314                           make_javascript_deterministic=True):
315    if self._wpr_server:
316      self._wpr_server.Close()
317      self._wpr_server = None
318
319    if not archive_path:
320      return None
321
322    if self._browser_backend.wpr_mode == wpr_modes.WPR_OFF:
323      return
324
325    use_record_mode = self._browser_backend.wpr_mode == wpr_modes.WPR_RECORD
326    if not use_record_mode:
327      assert os.path.isfile(archive_path)
328
329    self._wpr_server = wpr_server.ReplayServer(
330        self._browser_backend,
331        archive_path,
332        use_record_mode,
333        append_to_existing_wpr,
334        make_javascript_deterministic)
335
336  def GetStandardOutput(self):
337    return self._browser_backend.GetStandardOutput()
338
339  def GetStackTrace(self):
340    return self._browser_backend.GetStackTrace()
341
342  @property
343  def supports_system_info(self):
344    return self._browser_backend.supports_system_info
345
346  def GetSystemInfo(self):
347    """Returns low-level information about the system, if available.
348
349       See the documentation of the SystemInfo class for more details."""
350    return self._browser_backend.GetSystemInfo()
351
352  # TODO: Remove after call to Start() has been removed from
353  # related authotest files.
354  def Start(self):
355    pass
356