1# Copyright 2015 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 atexit
6import logging
7import os
8import shutil
9import stat
10import sys
11import tempfile
12import traceback
13
14from telemetry.internal.platform import tracing_agent
15from telemetry.internal.platform.tracing_agent import (
16    chrome_tracing_devtools_manager)
17
18_DESKTOP_OS_NAMES = ['linux', 'mac', 'win']
19_STARTUP_TRACING_OS_NAMES = _DESKTOP_OS_NAMES + ['android']
20
21# The trace config file path should be the same as specified in
22# src/components/tracing/trace_config_file.[h|cc]
23_CHROME_TRACE_CONFIG_DIR_ANDROID = '/data/local/'
24_CHROME_TRACE_CONFIG_FILE_NAME = 'chrome-trace-config.json'
25
26
27def ClearStarupTracingStateIfNeeded(platform_backend):
28  # Trace config file has fixed path on Android and temporary path on desktop.
29  if platform_backend.GetOSName() == 'android':
30    trace_config_file = os.path.join(_CHROME_TRACE_CONFIG_DIR_ANDROID,
31                                     _CHROME_TRACE_CONFIG_FILE_NAME)
32    platform_backend.device.RunShellCommand(
33        ['rm', '-f', trace_config_file], check_return=True, as_root=True)
34
35
36class ChromeTracingStartedError(Exception):
37  pass
38
39
40class ChromeTracingStoppedError(Exception):
41  pass
42
43
44class ChromeTracingAgent(tracing_agent.TracingAgent):
45  def __init__(self, platform_backend):
46    super(ChromeTracingAgent, self).__init__(platform_backend)
47    self._trace_config = None
48    self._trace_config_file = None
49
50  @property
51  def trace_config(self):
52    # Trace config is also used to check if Chrome tracing is running or not.
53    return self._trace_config
54
55  @property
56  def trace_config_file(self):
57    return self._trace_config_file
58
59  @classmethod
60  def IsStartupTracingSupported(cls, platform_backend):
61    if platform_backend.GetOSName() in _STARTUP_TRACING_OS_NAMES:
62      return True
63    else:
64      return False
65
66  @classmethod
67  def IsSupported(cls, platform_backend):
68    if cls.IsStartupTracingSupported(platform_backend):
69      return True
70    else:
71      return chrome_tracing_devtools_manager.IsSupported(platform_backend)
72
73  def _StartStartupTracing(self, config):
74    if not self.IsStartupTracingSupported(self._platform_backend):
75      return False
76    self._CreateTraceConfigFile(config)
77    return True
78
79  def _StartDevToolsTracing(self, config, timeout):
80    if not chrome_tracing_devtools_manager.IsSupported(self._platform_backend):
81      return False
82    devtools_clients = (chrome_tracing_devtools_manager
83        .GetActiveDevToolsClients(self._platform_backend))
84    if not devtools_clients:
85      return False
86    for client in devtools_clients:
87      if client.is_tracing_running:
88        raise ChromeTracingStartedError(
89            'Tracing is already running on devtools at port %s on platform'
90            'backend %s.' % (client.remote_port, self._platform_backend))
91      client.StartChromeTracing(
92          config, config.tracing_category_filter.filter_string, timeout)
93    return True
94
95  def StartAgentTracing(self, config, timeout):
96    if not config.enable_chrome_trace:
97      return False
98
99    if self._trace_config:
100      raise ChromeTracingStartedError(
101          'Tracing is already running on platform backend %s.'
102          % self._platform_backend)
103
104    if (config.enable_android_graphics_memtrack and
105        self._platform_backend.GetOSName() == 'android'):
106      self._platform_backend.SetGraphicsMemoryTrackingEnabled(True)
107
108    # Chrome tracing Agent needs to start tracing for chrome browsers that are
109    # not yet started, and for the ones that already are. For the former, we
110    # first setup the trace_config_file, which allows browsers that starts after
111    # this point to use it for enabling tracing upon browser startup. For the
112    # latter, we invoke start tracing command through devtools for browsers that
113    # are already started and tracked by chrome_tracing_devtools_manager.
114    started_startup_tracing = self._StartStartupTracing(config)
115    started_devtools_tracing = self._StartDevToolsTracing(config, timeout)
116    if started_startup_tracing or started_devtools_tracing:
117      self._trace_config = config
118      return True
119    return False
120
121  def StopAgentTracing(self, trace_data_builder):
122    if not self._trace_config:
123      raise ChromeTracingStoppedError(
124          'Tracing is not running on platform backend %s.'
125          % self._platform_backend)
126
127    if self.IsStartupTracingSupported(self._platform_backend):
128      self._RemoveTraceConfigFile()
129
130    # We get all DevTools clients including the stale ones, so that we get an
131    # exception if there is a stale client. This is because we will potentially
132    # lose data if there is a stale client.
133    devtools_clients = (chrome_tracing_devtools_manager
134        .GetDevToolsClients(self._platform_backend))
135    raised_execption_messages = []
136    for client in devtools_clients:
137      try:
138        client.StopChromeTracing(trace_data_builder)
139      except Exception:
140        raised_execption_messages.append(
141          'Error when trying to stop Chrome tracing on devtools at port %s:\n%s'
142          % (client.remote_port,
143             ''.join(traceback.format_exception(*sys.exc_info()))))
144
145    if (self._trace_config.enable_android_graphics_memtrack and
146        self._platform_backend.GetOSName() == 'android'):
147      self._platform_backend.SetGraphicsMemoryTrackingEnabled(False)
148
149    self._trace_config = None
150    if raised_execption_messages:
151      raise ChromeTracingStoppedError(
152          'Exceptions raised when trying to stop Chrome devtool tracing:\n' +
153          '\n'.join(raised_execption_messages))
154
155  def _CreateTraceConfigFileString(self, config):
156    # See src/components/tracing/trace_config_file.h for the format
157    trace_config_str = config.GetChromeTraceConfigJsonString()
158    return '{"trace_config":' + trace_config_str + '}'
159
160  def _CreateTraceConfigFile(self, config):
161    assert not self._trace_config_file
162    if self._platform_backend.GetOSName() == 'android':
163      self._trace_config_file = os.path.join(_CHROME_TRACE_CONFIG_DIR_ANDROID,
164                                             _CHROME_TRACE_CONFIG_FILE_NAME)
165      self._platform_backend.device.WriteFile(self._trace_config_file,
166          self._CreateTraceConfigFileString(config), as_root=True)
167      # The config file has fixed path on Android. We need to ensure it is
168      # always cleaned up.
169      atexit.register(self._RemoveTraceConfigFile)
170    elif self._platform_backend.GetOSName() in _DESKTOP_OS_NAMES:
171      self._trace_config_file = os.path.join(tempfile.mkdtemp(),
172                                             _CHROME_TRACE_CONFIG_FILE_NAME)
173      with open(self._trace_config_file, 'w') as f:
174        trace_config_string = self._CreateTraceConfigFileString(config)
175        logging.info('Trace config file string: %s', trace_config_string)
176        f.write(trace_config_string)
177      os.chmod(self._trace_config_file,
178               os.stat(self._trace_config_file).st_mode | stat.S_IROTH)
179    else:
180      raise NotImplementedError
181
182  def _RemoveTraceConfigFile(self):
183    if not self._trace_config_file:
184      return
185    if self._platform_backend.GetOSName() == 'android':
186      self._platform_backend.device.RunShellCommand(
187          ['rm', '-f', self._trace_config_file], check_return=True,
188          as_root=True)
189    elif self._platform_backend.GetOSName() in _DESKTOP_OS_NAMES:
190      if os.path.exists(self._trace_config_file):
191        os.remove(self._trace_config_file)
192      shutil.rmtree(os.path.dirname(self._trace_config_file))
193    else:
194      raise NotImplementedError
195    self._trace_config_file = None
196
197  def SupportsFlushingAgentTracing(self):
198    return True
199
200  def FlushAgentTracing(self, config, timeout, trace_data_builder):
201    if not self._trace_config:
202      raise ChromeTracingStoppedError(
203          'Tracing is not running on platform backend %s.'
204          % self._platform_backend)
205
206    for backend in self._IterInspectorBackends():
207      backend.EvaluateJavaScript("console.time('flush-tracing');")
208
209    self.StopAgentTracing(trace_data_builder)
210    self.StartAgentTracing(config, timeout)
211
212    for backend in self._IterInspectorBackends():
213      backend.EvaluateJavaScript("console.timeEnd('flush-tracing');")
214
215  def _IterInspectorBackends(self):
216    for client in chrome_tracing_devtools_manager.GetDevToolsClients(
217        self._platform_backend):
218      context_map = client.GetUpdatedInspectableContexts()
219      for context in context_map.contexts:
220        if context['type'] in ['iframe', 'page', 'webview']:
221          yield context_map.GetInspectorBackend(context['id'])
222