blob: ccedeba2ad3919fae09a93a9e842f722e6d4ee2a [file] [log] [blame]
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from telemetry.internal.util import atexit_with_log
import json
import logging
import os
import shutil
import stat
import sys
import tempfile
import traceback
from py_trace_event import trace_time
from telemetry.internal.platform import tracing_agent
from telemetry.internal.platform.tracing_agent import (
chrome_tracing_devtools_manager)
_DESKTOP_OS_NAMES = ['linux', 'mac', 'win']
_STARTUP_TRACING_OS_NAMES = _DESKTOP_OS_NAMES + ['android', 'chromeos']
# The trace config file path should be the same as specified in
# src/components/tracing/trace_config_file.[h|cc]
_CHROME_TRACE_CONFIG_DIR_ANDROID = '/data/local/'
_CHROME_TRACE_CONFIG_DIR_CROS = '/tmp/'
_CHROME_TRACE_CONFIG_FILE_NAME = 'chrome-trace-config.json'
def ClearStarupTracingStateIfNeeded(platform_backend):
# Trace config file has fixed path on Android and temporary path on desktop.
if platform_backend.GetOSName() == 'android':
trace_config_file = os.path.join(_CHROME_TRACE_CONFIG_DIR_ANDROID,
_CHROME_TRACE_CONFIG_FILE_NAME)
platform_backend.device.RunShellCommand(
['rm', '-f', trace_config_file], check_return=True, as_root=True)
class ChromeTracingStartedError(Exception):
pass
class ChromeTracingStoppedError(Exception):
pass
class ChromeClockSyncError(Exception):
pass
class ChromeTracingAgent(tracing_agent.TracingAgent):
def __init__(self, platform_backend):
super(ChromeTracingAgent, self).__init__(platform_backend)
self._trace_config = None
self._trace_config_file = None
self._previously_responsive_devtools = []
@property
def trace_config(self):
# Trace config is also used to check if Chrome tracing is running or not.
return self._trace_config
@property
def trace_config_file(self):
return self._trace_config_file
@classmethod
def IsStartupTracingSupported(cls, platform_backend):
if platform_backend.GetOSName() in _STARTUP_TRACING_OS_NAMES:
return True
else:
return False
@classmethod
def IsSupported(cls, platform_backend):
if cls.IsStartupTracingSupported(platform_backend):
return True
else:
return chrome_tracing_devtools_manager.IsSupported(platform_backend)
def _StartStartupTracing(self, config):
if not self.IsStartupTracingSupported(self._platform_backend):
return False
self._CreateTraceConfigFile(config)
return True
def _StartDevToolsTracing(self, config, timeout):
if not chrome_tracing_devtools_manager.IsSupported(self._platform_backend):
return False
devtools_clients = (chrome_tracing_devtools_manager
.GetActiveDevToolsClients(self._platform_backend))
if not devtools_clients:
return False
for client in devtools_clients:
if client.is_tracing_running:
raise ChromeTracingStartedError(
'Tracing is already running on devtools at port %s on platform'
'backend %s.' % (client.remote_port, self._platform_backend))
client.StartChromeTracing(config, timeout)
return True
def StartAgentTracing(self, config, timeout):
if not config.enable_chrome_trace:
return False
if self._trace_config:
raise ChromeTracingStartedError(
'Tracing is already running on platform backend %s.'
% self._platform_backend)
if (config.enable_android_graphics_memtrack and
self._platform_backend.GetOSName() == 'android'):
self._platform_backend.SetGraphicsMemoryTrackingEnabled(True)
# Chrome tracing Agent needs to start tracing for chrome browsers that are
# not yet started, and for the ones that already are. For the former, we
# first setup the trace_config_file, which allows browsers that starts after
# this point to use it for enabling tracing upon browser startup. For the
# latter, we invoke start tracing command through devtools for browsers that
# are already started and tracked by chrome_tracing_devtools_manager.
started_startup_tracing = self._StartStartupTracing(config)
started_devtools_tracing = self._StartDevToolsTracing(config, timeout)
if started_startup_tracing or started_devtools_tracing:
self._trace_config = config
return True
return False
def SupportsExplicitClockSync(self):
return True
def _RecordClockSyncMarkerDevTools(
self, sync_id, record_controller_clock_sync_marker_callback,
devtools_clients):
has_clock_synced = False
if not devtools_clients:
raise ChromeClockSyncError('Cannot issue clock sync. No devtools clients')
for client in devtools_clients:
try:
timestamp = trace_time.Now()
client.RecordChromeClockSyncMarker(sync_id)
# We only need one successful clock sync.
has_clock_synced = True
break
except Exception:
logging.exception('Failed to record clock sync marker with sync_id=%r '
'via DevTools client %r:' % (sync_id, client))
if not has_clock_synced:
raise ChromeClockSyncError(
'Failed to issue clock sync to devtools client')
record_controller_clock_sync_marker_callback(sync_id, timestamp)
def _RecordClockSyncMarkerAsyncEvent(
self, sync_id, record_controller_clock_sync_marker_callback):
has_clock_synced = False
for backend in self._IterInspectorBackends():
try:
timestamp = trace_time.Now()
backend.EvaluateJavaScript(
"console.time('ClockSyncEvent.%s');" % sync_id)
backend.EvaluateJavaScript(
"console.timeEnd('ClockSyncEvent.%s');" % sync_id)
has_clock_synced = True
break
except Exception:
logging.exception('Failed to record clock sync marker with sync_id=%r '
'via inspector backend %r:' % (sync_id, backend))
if not has_clock_synced:
raise ChromeClockSyncError(
'Failed to issue clock sync to devtools client')
record_controller_clock_sync_marker_callback(sync_id, timestamp)
def RecordClockSyncMarker(self, sync_id,
record_controller_clock_sync_marker_callback):
devtools_clients = (chrome_tracing_devtools_manager
.GetActiveDevToolsClients(self._platform_backend))
version = None
for client in devtools_clients:
version = client.GetChromeBranchNumber()
break
logging.info('Chrome version: %s', version)
# Note, we aren't sure whether 2744 is the correct cut-off point which
# Chrome will support clock sync marker, however we verified that 2743 does
# not support clock sync (catapult/issues/2804) hence we use it here.
# On the next update of Chrome ref build, if testTBM2ForSmoke still fails,
# the cut-off branch number will need to be bumped up again.
if version and int(version) > 2743:
self._RecordClockSyncMarkerDevTools(
sync_id, record_controller_clock_sync_marker_callback,
devtools_clients)
else: # TODO(rnephew): Remove once chrome stable is past branch 2743.
self._RecordClockSyncMarkerAsyncEvent(
sync_id, record_controller_clock_sync_marker_callback)
def StopAgentTracing(self):
if not self._trace_config:
raise ChromeTracingStoppedError(
'Tracing is not running on platform backend %s.'
% self._platform_backend)
if self.IsStartupTracingSupported(self._platform_backend):
self._RemoveTraceConfigFile()
# We get all DevTools clients including the stale ones, so that we get an
# exception if there is a stale client. This is because we will potentially
# lose data if there is a stale client.
devtools_clients = (chrome_tracing_devtools_manager
.GetDevToolsClients(self._platform_backend))
raised_exception_messages = []
assert len(self._previously_responsive_devtools) == 0
for client in devtools_clients:
try:
client.StopChromeTracing()
self._previously_responsive_devtools.append(client)
except Exception:
raised_exception_messages.append(
'Error when trying to stop Chrome tracing on devtools at port %s:\n%s'
% (client.remote_port,
''.join(traceback.format_exception(*sys.exc_info()))))
if (self._trace_config.enable_android_graphics_memtrack and
self._platform_backend.GetOSName() == 'android'):
self._platform_backend.SetGraphicsMemoryTrackingEnabled(False)
self._trace_config = None
if raised_exception_messages:
raise ChromeTracingStoppedError(
'Exceptions raised when trying to stop Chrome devtool tracing:\n' +
'\n'.join(raised_exception_messages))
def CollectAgentTraceData(self, trace_data_builder, timeout=None):
raised_exception_messages = []
for client in self._previously_responsive_devtools:
try:
client.CollectChromeTracingData(trace_data_builder)
except Exception:
raised_exception_messages.append(
'Error when collecting Chrome tracing on devtools at port %s:\n%s'
% (client.remote_port,
''.join(traceback.format_exception(*sys.exc_info()))))
self._previously_responsive_devtools = []
if raised_exception_messages:
raise ChromeTracingStoppedError(
'Exceptions raised when trying to collect Chrome devtool tracing:\n' +
'\n'.join(raised_exception_messages))
def _CreateTraceConfigFileString(self, config):
# See src/components/tracing/trace_config_file.h for the format
result = {
'trace_config':
config.chrome_trace_config.GetChromeTraceConfigForStartupTracing()
}
return json.dumps(result, sort_keys=True)
def _CreateTraceConfigFile(self, config):
assert not self._trace_config_file
if self._platform_backend.GetOSName() == 'android':
self._trace_config_file = os.path.join(_CHROME_TRACE_CONFIG_DIR_ANDROID,
_CHROME_TRACE_CONFIG_FILE_NAME)
self._platform_backend.device.WriteFile(self._trace_config_file,
self._CreateTraceConfigFileString(config), as_root=True)
# The config file has fixed path on Android. We need to ensure it is
# always cleaned up.
atexit_with_log.Register(self._RemoveTraceConfigFile)
elif self._platform_backend.GetOSName() == 'chromeos':
self._trace_config_file = os.path.join(_CHROME_TRACE_CONFIG_DIR_CROS,
_CHROME_TRACE_CONFIG_FILE_NAME)
cri = self._platform_backend.cri
cri.PushContents(self._CreateTraceConfigFileString(config),
self._trace_config_file)
cri.Chown(self._trace_config_file)
# The config file has fixed path on CrOS. We need to ensure it is
# always cleaned up.
atexit_with_log.Register(self._RemoveTraceConfigFile)
elif self._platform_backend.GetOSName() in _DESKTOP_OS_NAMES:
self._trace_config_file = os.path.join(tempfile.mkdtemp(),
_CHROME_TRACE_CONFIG_FILE_NAME)
with open(self._trace_config_file, 'w') as f:
trace_config_string = self._CreateTraceConfigFileString(config)
logging.info('Trace config file string: %s', trace_config_string)
f.write(trace_config_string)
os.chmod(self._trace_config_file,
os.stat(self._trace_config_file).st_mode | stat.S_IROTH)
else:
raise NotImplementedError
def _RemoveTraceConfigFile(self):
if not self._trace_config_file:
return
if self._platform_backend.GetOSName() == 'android':
self._platform_backend.device.RunShellCommand(
['rm', '-f', self._trace_config_file], check_return=True,
as_root=True)
elif self._platform_backend.GetOSName() == 'chromeos':
self._platform_backend.cri.RmRF(self._trace_config_file)
elif self._platform_backend.GetOSName() in _DESKTOP_OS_NAMES:
if os.path.exists(self._trace_config_file):
os.remove(self._trace_config_file)
shutil.rmtree(os.path.dirname(self._trace_config_file))
else:
raise NotImplementedError
self._trace_config_file = None
def SupportsFlushingAgentTracing(self):
return True
def FlushAgentTracing(self, config, timeout, trace_data_builder):
if not self._trace_config:
raise ChromeTracingStoppedError(
'Tracing is not running on platform backend %s.'
% self._platform_backend)
for backend in self._IterInspectorBackends():
backend.EvaluateJavaScript("console.time('flush-tracing');")
self.StopAgentTracing()
self.CollectAgentTraceData(trace_data_builder)
self.StartAgentTracing(config, timeout)
for backend in self._IterInspectorBackends():
backend.EvaluateJavaScript("console.timeEnd('flush-tracing');")
def _IterInspectorBackends(self):
for client in chrome_tracing_devtools_manager.GetDevToolsClients(
self._platform_backend):
context_map = client.GetUpdatedInspectableContexts()
for context in context_map.contexts:
if context['type'] in ['iframe', 'page', 'webview']:
yield context_map.GetInspectorBackend(context['id'])