blob: ac9c4b86b7b861e1ba8dc6ed734112630df3092c [file] [log] [blame]
// Copyright 2016 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.
/** @typedef {!{range: !Protocol.CSS.SourceRange, styleSheetId: !Protocol.CSS.StyleSheetId, wasUsed: boolean}} */
SDK.CSSModel.RuleUsage;
/**
* @implements {SDK.TargetManager.Observer}
* @implements {SDK.TracingManagerClient}
* @unrestricted
*/
Timeline.TimelineController = class {
/**
* @param {!SDK.Target} target
* @param {!Timeline.TimelineLifecycleDelegate} delegate
* @param {!SDK.TracingModel} tracingModel
*/
constructor(target, delegate, tracingModel) {
this._delegate = delegate;
this._target = target;
this._tracingModel = tracingModel;
this._targets = [];
SDK.targetManager.observeTargets(this);
if (Runtime.experiments.isEnabled('timelineRuleUsageRecording'))
this._markUnusedCSS = Common.settings.createSetting('timelineMarkUnusedCSS', false);
}
/**
* @param {boolean} captureCauses
* @param {boolean} enableJSSampling
* @param {boolean} captureMemory
* @param {boolean} capturePictures
* @param {boolean} captureFilmStrip
*/
startRecording(captureCauses, enableJSSampling, captureMemory, capturePictures, captureFilmStrip) {
this._extensionTraceProviders = Extensions.extensionServer.traceProviders().slice();
function disabledByDefault(category) {
return 'disabled-by-default-' + category;
}
var categoriesArray = [
'-*', 'devtools.timeline', 'v8.execute', disabledByDefault('devtools.timeline'),
disabledByDefault('devtools.timeline.frame'), SDK.TracingModel.TopLevelEventCategory,
TimelineModel.TimelineModel.Category.Console, TimelineModel.TimelineModel.Category.UserTiming
];
categoriesArray.push(TimelineModel.TimelineModel.Category.LatencyInfo);
if (Runtime.experiments.isEnabled('timelineFlowEvents'))
categoriesArray.push(disabledByDefault('toplevel.flow'), disabledByDefault('ipc.flow'));
if (Runtime.experiments.isEnabled('timelineV8RuntimeCallStats') && enableJSSampling)
categoriesArray.push(disabledByDefault('v8.runtime_stats_sampling'));
if (Runtime.experiments.isEnabled('timelineTracingJSProfile') && enableJSSampling) {
categoriesArray.push(disabledByDefault('v8.cpu_profiler'));
if (Common.moduleSetting('highResolutionCpuProfiling').get())
categoriesArray.push(disabledByDefault('v8.cpu_profiler.hires'));
}
if (captureCauses || enableJSSampling)
categoriesArray.push(disabledByDefault('devtools.timeline.stack'));
if (captureCauses && Runtime.experiments.isEnabled('timelineInvalidationTracking'))
categoriesArray.push(disabledByDefault('devtools.timeline.invalidationTracking'));
if (capturePictures) {
categoriesArray.push(
disabledByDefault('devtools.timeline.layers'), disabledByDefault('devtools.timeline.picture'),
disabledByDefault('blink.graphics_context_annotations'));
}
if (captureFilmStrip)
categoriesArray.push(disabledByDefault('devtools.screenshot'));
for (var traceProvider of this._extensionTraceProviders)
traceProvider.start();
var categories = categoriesArray.join(',');
this._startRecordingWithCategories(categories, enableJSSampling);
}
stopRecording() {
var tracingStoppedPromises = [];
tracingStoppedPromises.push(new Promise(resolve => this._tracingCompleteCallback = resolve));
tracingStoppedPromises.push(this._stopProfilingOnAllTargets());
this._target.tracingManager.stop();
if (!Runtime.experiments.isEnabled('timelineRuleUsageRecording') || !this._markUnusedCSS.get())
tracingStoppedPromises.push(SDK.targetManager.resumeAllTargets());
else
this._addUnusedRulesToCoverage();
Promise.all(tracingStoppedPromises).then(() => this._allSourcesFinished());
this._delegate.loadingStarted();
for (var traceProvider of this._extensionTraceProviders)
traceProvider.stop();
}
/**
* @override
* @param {!SDK.Target} target
*/
targetAdded(target) {
this._targets.push(target);
if (this._profiling)
this._startProfilingOnTarget(target);
}
/**
* @override
* @param {!SDK.Target} target
*/
targetRemoved(target) {
this._targets.remove(target, true);
// FIXME: We'd like to stop profiling on the target and retrieve a profile
// but it's too late. Backend connection is closed.
}
_addUnusedRulesToCoverage() {
var mainTarget = SDK.targetManager.mainTarget();
if (!mainTarget)
return;
var cssModel = SDK.CSSModel.fromTarget(mainTarget);
/**
* @param {!Array<!SDK.CSSModel.RuleUsage>} ruleUsageList
*/
function ruleListReceived(ruleUsageList) {
for (var rule of ruleUsageList) {
if (rule.wasUsed)
continue;
var styleSheetHeader = cssModel.styleSheetHeaderForId(rule.styleSheetId);
var url = styleSheetHeader.sourceURL;
Components.CoverageProfile.instance().appendUnusedRule(url, rule.range);
}
}
cssModel.ruleListPromise().then(ruleListReceived);
}
/**
* @param {!SDK.Target} target
* @return {!Promise}
*/
_startProfilingOnTarget(target) {
return target.hasJSCapability() ? target.profilerAgent().start() : Promise.resolve();
}
/**
* @return {!Promise}
*/
_startProfilingOnAllTargets() {
var intervalUs = Common.moduleSetting('highResolutionCpuProfiling').get() ? 100 : 1000;
this._target.profilerAgent().setSamplingInterval(intervalUs);
this._profiling = true;
return Promise.all(this._targets.map(this._startProfilingOnTarget));
}
/**
* @param {!SDK.Target} target
* @return {!Promise}
*/
_stopProfilingOnTarget(target) {
return target.hasJSCapability() ? target.profilerAgent().stop(this._addCpuProfile.bind(this, target.id())) :
Promise.resolve();
}
/**
* @param {number} targetId
* @param {?Protocol.Error} error
* @param {?Protocol.Profiler.Profile} cpuProfile
*/
_addCpuProfile(targetId, error, cpuProfile) {
if (!cpuProfile) {
Common.console.warn(Common.UIString('CPU profile for a target is not available. %s', error || ''));
return;
}
if (!this._cpuProfiles)
this._cpuProfiles = new Map();
this._cpuProfiles.set(targetId, cpuProfile);
}
/**
* @return {!Promise}
*/
_stopProfilingOnAllTargets() {
var targets = this._profiling ? this._targets : [];
this._profiling = false;
return Promise.all(targets.map(this._stopProfilingOnTarget, this));
}
/**
* @param {string} categories
* @param {boolean=} enableJSSampling
* @param {function(?string)=} callback
*/
_startRecordingWithCategories(categories, enableJSSampling, callback) {
if (!Runtime.experiments.isEnabled('timelineRuleUsageRecording') || !this._markUnusedCSS.get())
SDK.targetManager.suspendAllTargets();
var profilingStartedPromise = enableJSSampling && !Runtime.experiments.isEnabled('timelineTracingJSProfile') ?
this._startProfilingOnAllTargets() :
Promise.resolve();
var samplingFrequencyHz = Common.moduleSetting('highResolutionCpuProfiling').get() ? 10000 : 1000;
var options = 'sampling-frequency=' + samplingFrequencyHz;
var target = this._target;
var tracingManager = target.tracingManager;
SDK.targetManager.suspendReload(target);
profilingStartedPromise.then(tracingManager.start.bind(tracingManager, this, categories, options, onTraceStarted));
/**
* @param {?string} error
*/
function onTraceStarted(error) {
SDK.targetManager.resumeReload(target);
if (callback)
callback(error);
}
}
/**
* @override
*/
tracingStarted() {
this._tracingModel.reset();
this._delegate.recordingStarted();
}
/**
* @param {!Array.<!SDK.TracingManager.EventPayload>} events
* @override
*/
traceEventsCollected(events) {
this._tracingModel.addEvents(events);
}
/**
* @override
*/
tracingComplete() {
this._tracingCompleteCallback();
this._tracingCompleteCallback = null;
}
_allSourcesFinished() {
this._injectCpuProfileEvents();
this._tracingModel.tracingComplete();
this._delegate.loadingComplete(true);
}
/**
* @param {number} pid
* @param {number} tid
* @param {?Protocol.Profiler.Profile} cpuProfile
*/
_injectCpuProfileEvent(pid, tid, cpuProfile) {
if (!cpuProfile)
return;
var cpuProfileEvent = /** @type {!SDK.TracingManager.EventPayload} */ ({
cat: SDK.TracingModel.DevToolsMetadataEventCategory,
ph: SDK.TracingModel.Phase.Instant,
ts: this._tracingModel.maximumRecordTime() * 1000,
pid: pid,
tid: tid,
name: TimelineModel.TimelineModel.RecordType.CpuProfile,
args: {data: {cpuProfile: cpuProfile}}
});
this._tracingModel.addEvents([cpuProfileEvent]);
}
_injectCpuProfileEvents() {
if (!this._cpuProfiles)
return;
var metadataEventTypes = TimelineModel.TimelineModel.DevToolsMetadataEvent;
var metadataEvents = this._tracingModel.devToolsMetadataEvents();
var mainMetaEvent =
metadataEvents.filter(event => event.name === metadataEventTypes.TracingStartedInPage).peekLast();
if (!mainMetaEvent)
return;
var pid = mainMetaEvent.thread.process().id();
var mainCpuProfile = this._cpuProfiles.get(this._target.id());
this._injectCpuProfileEvent(pid, mainMetaEvent.thread.id(), mainCpuProfile);
var workerMetaEvents = metadataEvents.filter(event => event.name === metadataEventTypes.TracingSessionIdForWorker);
for (var metaEvent of workerMetaEvents) {
var workerId = metaEvent.args['data']['workerId'];
var workerTarget = this._target.subTargetsManager ? this._target.subTargetsManager.targetForId(workerId) : null;
if (!workerTarget)
continue;
var cpuProfile = this._cpuProfiles.get(workerTarget.id());
this._injectCpuProfileEvent(pid, metaEvent.args['data']['workerThreadId'], cpuProfile);
}
this._cpuProfiles = null;
}
/**
* @param {number} usage
* @override
*/
tracingBufferUsage(usage) {
this._delegate.recordingProgress(usage);
}
/**
* @param {number} progress
* @override
*/
eventsRetrievalProgress(progress) {
this._delegate.loadingProgress(progress);
}
};