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
5from telemetry.internal.util import atexit_with_log
6import json
7import logging
8import os
9import shutil
10import stat
11import sys
12import tempfile
13import traceback
14
15from py_trace_event import trace_time
16from telemetry.internal.platform import tracing_agent
17from telemetry.internal.platform.tracing_agent import (
18    chrome_tracing_devtools_manager)
19
20_DESKTOP_OS_NAMES = ['linux', 'mac', 'win']
21_STARTUP_TRACING_OS_NAMES = _DESKTOP_OS_NAMES + ['android', 'chromeos']
22
23# The trace config file path should be the same as specified in
24# src/components/tracing/trace_config_file.[h|cc]
25_CHROME_TRACE_CONFIG_DIR_ANDROID = '/data/local/'
26_CHROME_TRACE_CONFIG_DIR_CROS = '/tmp/'
27_CHROME_TRACE_CONFIG_FILE_NAME = 'chrome-trace-config.json'
28
29
30def ClearStarupTracingStateIfNeeded(platform_backend):
31  # Trace config file has fixed path on Android and temporary path on desktop.
32  if platform_backend.GetOSName() == 'android':
33    trace_config_file = os.path.join(_CHROME_TRACE_CONFIG_DIR_ANDROID,
34                                     _CHROME_TRACE_CONFIG_FILE_NAME)
35    platform_backend.device.RunShellCommand(
36        ['rm', '-f', trace_config_file], check_return=True, as_root=True)
37
38
39class ChromeTracingStartedError(Exception):
40  pass
41
42
43class ChromeTracingStoppedError(Exception):
44  pass
45
46
47class ChromeClockSyncError(Exception):
48  pass
49
50
51class ChromeTracingAgent(tracing_agent.TracingAgent):
52  def __init__(self, platform_backend):
53    super(ChromeTracingAgent, self).__init__(platform_backend)
54    self._trace_config = None
55    self._trace_config_file = None
56    self._previously_responsive_devtools = []
57
58  @property
59  def trace_config(self):
60    # Trace config is also used to check if Chrome tracing is running or not.
61    return self._trace_config
62
63  @property
64  def trace_config_file(self):
65    return self._trace_config_file
66
67  @classmethod
68  def IsStartupTracingSupported(cls, platform_backend):
69    if platform_backend.GetOSName() in _STARTUP_TRACING_OS_NAMES:
70      return True
71    else:
72      return False
73
74  @classmethod
75  def IsSupported(cls, platform_backend):
76    if cls.IsStartupTracingSupported(platform_backend):
77      return True
78    else:
79      return chrome_tracing_devtools_manager.IsSupported(platform_backend)
80
81  def _StartStartupTracing(self, config):
82    if not self.IsStartupTracingSupported(self._platform_backend):
83      return False
84    self._CreateTraceConfigFile(config)
85    return True
86
87  def _StartDevToolsTracing(self, config, timeout):
88    if not chrome_tracing_devtools_manager.IsSupported(self._platform_backend):
89      return False
90    devtools_clients = (chrome_tracing_devtools_manager
91        .GetActiveDevToolsClients(self._platform_backend))
92    if not devtools_clients:
93      return False
94    for client in devtools_clients:
95      if client.is_tracing_running:
96        raise ChromeTracingStartedError(
97            'Tracing is already running on devtools at port %s on platform'
98            'backend %s.' % (client.remote_port, self._platform_backend))
99      client.StartChromeTracing(config, timeout)
100    return True
101
102  def StartAgentTracing(self, config, timeout):
103    if not config.enable_chrome_trace:
104      return False
105
106    if self._trace_config:
107      raise ChromeTracingStartedError(
108          'Tracing is already running on platform backend %s.'
109          % self._platform_backend)
110
111    if (config.enable_android_graphics_memtrack and
112        self._platform_backend.GetOSName() == 'android'):
113      self._platform_backend.SetGraphicsMemoryTrackingEnabled(True)
114
115    # Chrome tracing Agent needs to start tracing for chrome browsers that are
116    # not yet started, and for the ones that already are. For the former, we
117    # first setup the trace_config_file, which allows browsers that starts after
118    # this point to use it for enabling tracing upon browser startup. For the
119    # latter, we invoke start tracing command through devtools for browsers that
120    # are already started and tracked by chrome_tracing_devtools_manager.
121    started_startup_tracing = self._StartStartupTracing(config)
122    started_devtools_tracing = self._StartDevToolsTracing(config, timeout)
123    if started_startup_tracing or started_devtools_tracing:
124      self._trace_config = config
125      return True
126    return False
127
128  def SupportsExplicitClockSync(self):
129    return True
130
131  def _RecordClockSyncMarkerDevTools(
132      self, sync_id, record_controller_clock_sync_marker_callback,
133      devtools_clients):
134    has_clock_synced = False
135    for client in devtools_clients:
136      try:
137        timestamp = trace_time.Now()
138        client.RecordChromeClockSyncMarker(sync_id)
139        # We only need one successful clock sync.
140        has_clock_synced = True
141        break
142      except Exception:
143        logging.exception('Failed to record clock sync marker with sync_id=%r '
144                          'via DevTools client %r:' % (sync_id, client))
145    if not has_clock_synced:
146      raise ChromeClockSyncError(
147          'Failed to issue clock sync to devtools client')
148    record_controller_clock_sync_marker_callback(sync_id, timestamp)
149
150  def _RecordClockSyncMarkerAsyncEvent(
151      self, sync_id, record_controller_clock_sync_marker_callback):
152    has_clock_synced = False
153    for backend in self._IterInspectorBackends():
154      try:
155        timestamp = trace_time.Now()
156        event = 'ClockSyncEvent.%s' % sync_id
157        backend.EvaluateJavaScript(
158            "console.time({{ event }});", event=event)
159        backend.EvaluateJavaScript(
160            "console.timeEnd({{ event }});", event=event)
161        has_clock_synced = True
162        break
163      except Exception:
164        logging.exception('Failed to record clock sync marker with sync_id=%r '
165                          'via inspector backend %r:' % (sync_id, backend))
166    if not has_clock_synced:
167      raise ChromeClockSyncError(
168          'Failed to issue clock sync to devtools client')
169    record_controller_clock_sync_marker_callback(sync_id, timestamp)
170
171  def RecordClockSyncMarker(self, sync_id,
172                            record_controller_clock_sync_marker_callback):
173    devtools_clients = (chrome_tracing_devtools_manager
174        .GetActiveDevToolsClients(self._platform_backend))
175    if not devtools_clients:
176      raise ChromeClockSyncError('Cannot issue clock sync. No devtools clients')
177    version = None
178    for client in devtools_clients:
179      version = client.GetChromeBranchNumber()
180      break
181    logging.info('Chrome version: %s', version)
182    # Note, we aren't sure whether 2744 is the correct cut-off point which
183    # Chrome will support clock sync marker, however we verified that 2743 does
184    # not support clock sync (catapult/issues/2804) hence we use it here.
185    # On the next update of Chrome ref build, if testTBM2ForSmoke still fails,
186    # the cut-off branch number will need to be bumped up again.
187    if version and int(version) > 2743:
188      self._RecordClockSyncMarkerDevTools(
189          sync_id, record_controller_clock_sync_marker_callback,
190          devtools_clients)
191    else:  # TODO(rnephew): Remove once chrome stable is past branch 2743.
192      self._RecordClockSyncMarkerAsyncEvent(
193          sync_id, record_controller_clock_sync_marker_callback)
194
195  def StopAgentTracing(self):
196    if not self._trace_config:
197      raise ChromeTracingStoppedError(
198          'Tracing is not running on platform backend %s.'
199          % self._platform_backend)
200
201    if self.IsStartupTracingSupported(self._platform_backend):
202      self._RemoveTraceConfigFile()
203
204    # We get all DevTools clients including the stale ones, so that we get an
205    # exception if there is a stale client. This is because we will potentially
206    # lose data if there is a stale client.
207    devtools_clients = (chrome_tracing_devtools_manager
208        .GetDevToolsClients(self._platform_backend))
209    raised_exception_messages = []
210    assert len(self._previously_responsive_devtools) == 0
211    for client in devtools_clients:
212      try:
213        client.StopChromeTracing()
214        self._previously_responsive_devtools.append(client)
215
216      except Exception:
217        raised_exception_messages.append(
218          'Error when trying to stop Chrome tracing on devtools at port %s:\n%s'
219          % (client.remote_port,
220             ''.join(traceback.format_exception(*sys.exc_info()))))
221
222    if (self._trace_config.enable_android_graphics_memtrack and
223        self._platform_backend.GetOSName() == 'android'):
224      self._platform_backend.SetGraphicsMemoryTrackingEnabled(False)
225
226    self._trace_config = None
227    if raised_exception_messages:
228      raise ChromeTracingStoppedError(
229          'Exceptions raised when trying to stop Chrome devtool tracing:\n' +
230          '\n'.join(raised_exception_messages))
231
232  def CollectAgentTraceData(self, trace_data_builder, timeout=None):
233    raised_exception_messages = []
234    for client in self._previously_responsive_devtools:
235      try:
236        client.CollectChromeTracingData(trace_data_builder)
237      except Exception:
238        raised_exception_messages.append(
239          'Error when collecting Chrome tracing on devtools at port %s:\n%s'
240          % (client.remote_port,
241             ''.join(traceback.format_exception(*sys.exc_info()))))
242    self._previously_responsive_devtools = []
243
244    if raised_exception_messages:
245      raise ChromeTracingStoppedError(
246          'Exceptions raised when trying to collect Chrome devtool tracing:\n' +
247          '\n'.join(raised_exception_messages))
248
249  def _CreateTraceConfigFileString(self, config):
250    # See src/components/tracing/trace_config_file.h for the format
251    result = {
252      'trace_config':
253        config.chrome_trace_config.GetChromeTraceConfigForStartupTracing()
254    }
255    return json.dumps(result, sort_keys=True)
256
257  def _CreateTraceConfigFile(self, config):
258    assert not self._trace_config_file
259    if self._platform_backend.GetOSName() == 'android':
260      self._trace_config_file = os.path.join(_CHROME_TRACE_CONFIG_DIR_ANDROID,
261                                             _CHROME_TRACE_CONFIG_FILE_NAME)
262      self._platform_backend.device.WriteFile(self._trace_config_file,
263          self._CreateTraceConfigFileString(config), as_root=True)
264      # The config file has fixed path on Android. We need to ensure it is
265      # always cleaned up.
266      atexit_with_log.Register(self._RemoveTraceConfigFile)
267    elif self._platform_backend.GetOSName() == 'chromeos':
268      self._trace_config_file = os.path.join(_CHROME_TRACE_CONFIG_DIR_CROS,
269                                             _CHROME_TRACE_CONFIG_FILE_NAME)
270      cri = self._platform_backend.cri
271      cri.PushContents(self._CreateTraceConfigFileString(config),
272                       self._trace_config_file)
273      cri.Chown(self._trace_config_file)
274      # The config file has fixed path on CrOS. We need to ensure it is
275      # always cleaned up.
276      atexit_with_log.Register(self._RemoveTraceConfigFile)
277    elif self._platform_backend.GetOSName() in _DESKTOP_OS_NAMES:
278      self._trace_config_file = os.path.join(tempfile.mkdtemp(),
279                                             _CHROME_TRACE_CONFIG_FILE_NAME)
280      with open(self._trace_config_file, 'w') as f:
281        trace_config_string = self._CreateTraceConfigFileString(config)
282        logging.info('Trace config file string: %s', trace_config_string)
283        f.write(trace_config_string)
284      os.chmod(self._trace_config_file,
285               os.stat(self._trace_config_file).st_mode | stat.S_IROTH)
286    else:
287      raise NotImplementedError
288
289  def _RemoveTraceConfigFile(self):
290    if not self._trace_config_file:
291      return
292    if self._platform_backend.GetOSName() == 'android':
293      self._platform_backend.device.RunShellCommand(
294          ['rm', '-f', self._trace_config_file], check_return=True,
295          as_root=True)
296    elif self._platform_backend.GetOSName() == 'chromeos':
297      self._platform_backend.cri.RmRF(self._trace_config_file)
298    elif self._platform_backend.GetOSName() in _DESKTOP_OS_NAMES:
299      if os.path.exists(self._trace_config_file):
300        os.remove(self._trace_config_file)
301      shutil.rmtree(os.path.dirname(self._trace_config_file))
302    else:
303      raise NotImplementedError
304    self._trace_config_file = None
305
306  def SupportsFlushingAgentTracing(self):
307    return True
308
309  def FlushAgentTracing(self, config, timeout, trace_data_builder):
310    if not self._trace_config:
311      raise ChromeTracingStoppedError(
312          'Tracing is not running on platform backend %s.'
313          % self._platform_backend)
314
315    for backend in self._IterInspectorBackends():
316      backend.EvaluateJavaScript("console.time('flush-tracing');")
317
318    self.StopAgentTracing()
319    self.CollectAgentTraceData(trace_data_builder)
320    self.StartAgentTracing(config, timeout)
321
322    for backend in self._IterInspectorBackends():
323      backend.EvaluateJavaScript("console.timeEnd('flush-tracing');")
324
325  def _IterInspectorBackends(self):
326    for client in chrome_tracing_devtools_manager.GetDevToolsClients(
327        self._platform_backend):
328      context_map = client.GetUpdatedInspectableContexts()
329      for context in context_map.contexts:
330        if context['type'] in ['iframe', 'page', 'webview']:
331          yield context_map.GetInspectorBackend(context['id'])
332