1# Copyright 2014 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 ast
6from telemetry.internal.util import atexit_with_log
7import contextlib
8import gc
9import logging
10import os
11import sys
12import tempfile
13import traceback
14import uuid
15
16from py_trace_event import trace_event
17from telemetry.core import discover
18from telemetry.core import exceptions
19from telemetry.core import util
20from telemetry.internal.platform import tracing_agent
21from telemetry.internal.platform.tracing_agent import chrome_tracing_agent
22from telemetry.timeline import tracing_config
23from tracing.trace_data import trace_data as trace_data_module
24
25
26def _IterAllTracingAgentClasses():
27  tracing_agent_dir = os.path.join(
28      os.path.dirname(os.path.realpath(__file__)), 'tracing_agent')
29  return discover.DiscoverClasses(
30      tracing_agent_dir, util.GetTelemetryDir(),
31      tracing_agent.TracingAgent).itervalues()
32
33
34class _TracingState(object):
35
36  def __init__(self, config, timeout):
37    self._builder = trace_data_module.TraceDataBuilder()
38    self._config = config
39    self._timeout = timeout
40
41  @property
42  def builder(self):
43    return self._builder
44
45  @property
46  def config(self):
47    return self._config
48
49  @property
50  def timeout(self):
51    return self._timeout
52
53
54class TracingControllerBackend(object):
55  def __init__(self, platform_backend):
56    self._platform_backend = platform_backend
57    self._current_state = None
58    self._supported_agents_classes = [
59        agent_classes for agent_classes in _IterAllTracingAgentClasses() if
60        agent_classes.IsSupported(platform_backend)]
61    self._active_agents_instances = []
62    self._trace_log = None
63    self._is_tracing_controllable = True
64    self._telemetry_info = None
65
66  def StartTracing(self, config, timeout):
67    if self.is_tracing_running:
68      return False
69
70    assert isinstance(config, tracing_config.TracingConfig)
71    assert len(self._active_agents_instances) == 0
72
73    self._current_state = _TracingState(config, timeout)
74    # Hack: chrome tracing agent may only depend on the number of alive chrome
75    # devtools processes, rather platform (when startup tracing is not
76    # supported), hence we add it to the list of supported agents here if it was
77    # not added.
78    if (chrome_tracing_agent.ChromeTracingAgent.IsSupported(
79        self._platform_backend) and
80        not chrome_tracing_agent.ChromeTracingAgent in
81        self._supported_agents_classes):
82      self._supported_agents_classes.append(
83          chrome_tracing_agent.ChromeTracingAgent)
84
85    self.StartAgentTracing(config, timeout)
86    for agent_class in self._supported_agents_classes:
87      agent = agent_class(self._platform_backend)
88      with trace_event.trace(
89          'StartAgentTracing',
90          agent=str(agent.__class__.__name__)):
91        started = agent.StartAgentTracing(config, timeout)
92      if started:
93        self._active_agents_instances.append(agent)
94    return True
95
96  def _GenerateClockSyncId(self):
97    return str(uuid.uuid4())
98
99  @contextlib.contextmanager
100  def _DisableGarbageCollection(self):
101    try:
102      gc.disable()
103      yield
104    finally:
105      gc.enable()
106
107  def StopTracing(self):
108    assert self.is_tracing_running, 'Can only stop tracing when tracing is on.'
109    self._IssueClockSyncMarker()
110    builder = self._current_state.builder
111
112    raised_exception_messages = []
113    for agent in self._active_agents_instances + [self]:
114      try:
115        with trace_event.trace(
116            'StopAgentTracing',
117            agent=str(agent.__class__.__name__)):
118          agent.StopAgentTracing()
119      except Exception: # pylint: disable=broad-except
120        raised_exception_messages.append(
121            ''.join(traceback.format_exception(*sys.exc_info())))
122
123    for agent in self._active_agents_instances + [self]:
124      try:
125        with trace_event.trace(
126            'CollectAgentTraceData',
127            agent=str(agent.__class__.__name__)):
128          agent.CollectAgentTraceData(builder)
129      except Exception: # pylint: disable=broad-except
130        raised_exception_messages.append(
131            ''.join(traceback.format_exception(*sys.exc_info())))
132
133    self._telemetry_info = None
134    self._active_agents_instances = []
135    self._current_state = None
136
137    if raised_exception_messages:
138      raise exceptions.Error(
139          'Exceptions raised when trying to stop tracing:\n' +
140          '\n'.join(raised_exception_messages))
141
142    return builder.AsData()
143
144  def FlushTracing(self):
145    assert self.is_tracing_running, 'Can only flush tracing when tracing is on.'
146    self._IssueClockSyncMarker()
147
148    raised_exception_messages = []
149    # Flushing the controller's pytrace is not supported.
150    for agent in self._active_agents_instances:
151      try:
152        if agent.SupportsFlushingAgentTracing():
153          with trace_event.trace(
154              'FlushAgentTracing',
155              agent=str(agent.__class__.__name__)):
156            agent.FlushAgentTracing(self._current_state.config,
157                                    self._current_state.timeout,
158                                    self._current_state.builder)
159      except Exception: # pylint: disable=broad-except
160        raised_exception_messages.append(
161            ''.join(traceback.format_exception(*sys.exc_info())))
162
163    if raised_exception_messages:
164      raise exceptions.Error(
165          'Exceptions raised when trying to flush tracing:\n' +
166          '\n'.join(raised_exception_messages))
167
168  def StartAgentTracing(self, config, timeout):
169    self._is_tracing_controllable = self._IsTracingControllable()
170    if not self._is_tracing_controllable:
171      return False
172
173    tf = tempfile.NamedTemporaryFile(delete=False)
174    self._trace_log = tf.name
175    tf.close()
176    del config # unused
177    del timeout # unused
178    assert not trace_event.trace_is_enabled(), 'Tracing already running.'
179    trace_event.trace_enable(self._trace_log)
180    assert trace_event.trace_is_enabled(), 'Tracing didn\'t enable properly.'
181    return True
182
183  def StopAgentTracing(self):
184    if not self._is_tracing_controllable:
185      return
186    assert trace_event.trace_is_enabled(), 'Tracing not running'
187    trace_event.trace_disable()
188    assert not trace_event.trace_is_enabled(), 'Tracing didnt disable properly.'
189
190  def SupportsExplicitClockSync(self):
191    return True
192
193  def _RecordIssuerClockSyncMarker(self, sync_id, issue_ts):
194    """ Record clock sync event.
195
196    Args:
197      sync_id: Unqiue id for sync event.
198      issue_ts: timestamp before issuing clocksync to agent.
199    """
200    if self._is_tracing_controllable:
201      trace_event.clock_sync(sync_id, issue_ts=issue_ts)
202
203  def _IssueClockSyncMarker(self):
204    with self._DisableGarbageCollection():
205      for agent in self._active_agents_instances:
206        if agent.SupportsExplicitClockSync():
207          sync_id = self._GenerateClockSyncId()
208          with trace_event.trace(
209              'RecordClockSyncMarker',
210              agent=str(agent.__class__.__name__),
211              sync_id=sync_id):
212            agent.RecordClockSyncMarker(sync_id,
213                                        self._RecordIssuerClockSyncMarker)
214
215  def IsChromeTracingSupported(self):
216    return chrome_tracing_agent.ChromeTracingAgent.IsSupported(
217        self._platform_backend)
218
219  @property
220  def is_tracing_running(self):
221    return self._current_state is not None
222
223  def _GetActiveChromeTracingAgent(self):
224    if not self.is_tracing_running:
225      return None
226    if not self._current_state.config.enable_chrome_trace:
227      return None
228    for agent in self._active_agents_instances:
229      if isinstance(agent, chrome_tracing_agent.ChromeTracingAgent):
230        return agent
231    return None
232
233  def GetChromeTraceConfig(self):
234    agent = self._GetActiveChromeTracingAgent()
235    if agent:
236      return agent.trace_config
237    return None
238
239  def GetChromeTraceConfigFile(self):
240    agent = self._GetActiveChromeTracingAgent()
241    if agent:
242      return agent.trace_config_file
243    return None
244
245  def _IsTracingControllable(self):
246    return trace_event.is_tracing_controllable()
247
248  def ClearStateIfNeeded(self):
249    chrome_tracing_agent.ClearStarupTracingStateIfNeeded(self._platform_backend)
250
251  @property
252  def telemetry_info(self):
253    return self._telemetry_info
254
255  @telemetry_info.setter
256  def telemetry_info(self, ii):
257    self._telemetry_info = ii
258
259  def CollectAgentTraceData(self, trace_data_builder):
260    if not self._is_tracing_controllable:
261      return
262    assert not trace_event.trace_is_enabled(), 'Stop tracing before collection.'
263    with open(self._trace_log, 'r') as fp:
264      data = ast.literal_eval(fp.read() + ']')
265    trace_data_builder.AddTraceFor(trace_data_module.TELEMETRY_PART, {
266        "traceEvents": data,
267        "metadata": {
268            # TODO(charliea): For right now, we use "TELEMETRY" as the clock
269            # domain to guarantee that Telemetry is given its own clock
270            # domain. Telemetry isn't really a clock domain, though: it's a
271            # system that USES a clock domain like LINUX_CLOCK_MONOTONIC or
272            # WIN_QPC. However, there's a chance that a Telemetry controller
273            # running on Linux (using LINUX_CLOCK_MONOTONIC) is interacting with
274            # an Android phone (also using LINUX_CLOCK_MONOTONIC, but on a
275            # different machine). The current logic collapses clock domains
276            # based solely on the clock domain string, but we really should to
277            # collapse based on some (device ID, clock domain ID) tuple. Giving
278            # Telemetry its own clock domain is a work-around for this.
279            "clock-domain": "TELEMETRY",
280            "telemetry": (self._telemetry_info.AsDict()
281                if self._telemetry_info else {}),
282        }
283    })
284    try:
285      os.remove(self._trace_log)
286      self._trace_log = None
287    except OSError:
288      logging.exception('Error when deleting %s, will try again at exit.',
289                        self._trace_log)
290      def DeleteAtExit(path):
291        os.remove(path)
292      atexit_with_log.Register(DeleteAtExit, self._trace_log)
293    self._trace_log = None
294