blob: dbfd06d3acab82c3be03da5a05b449a27b5615c1 [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.
/**
* @implements {SDK.SDKModelObserver<!SDK.CPUProfilerModel>}
* @implements {SDK.TracingManagerClient}
* @unrestricted
*/
Timeline.TimelineController = class {
/**
* @param {!SDK.TracingManager} tracingManager
* @param {!Timeline.PerformanceModel} performanceModel
* @param {!Timeline.TimelineController.Client} client
*/
constructor(tracingManager, performanceModel, client) {
this._tracingManager = tracingManager;
this._performanceModel = performanceModel;
this._client = client;
this._tracingModelBackingStorage = new Bindings.TempFileBackingStorage('tracing');
this._tracingModel = new SDK.TracingModel(this._tracingModelBackingStorage);
this._performanceModel.setMainTarget(tracingManager.target());
/** @type {!Array<!Timeline.ExtensionTracingSession>} */
this._extensionSessions = [];
SDK.targetManager.observeModels(SDK.CPUProfilerModel, this);
}
/**
* @param {!Timeline.TimelineController.RecordingOptions} options
* @param {!Array<!Extensions.ExtensionTraceProvider>} providers
* @return {!Promise}
*/
startRecording(options, providers) {
this._extensionTraceProviders = Extensions.extensionServer.traceProviders().slice();
/**
* @param {string} category
* @return {string}
*/
function disabledByDefault(category) {
return 'disabled-by-default-' + category;
}
const 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('timelineV8RuntimeCallStats') && options.enableJSSampling)
categoriesArray.push(disabledByDefault('v8.runtime_stats_sampling'));
if (Runtime.experiments.isEnabled('timelineTracingJSProfile') && options.enableJSSampling) {
categoriesArray.push(disabledByDefault('v8.cpu_profiler'));
if (Common.moduleSetting('highResolutionCpuProfiling').get())
categoriesArray.push(disabledByDefault('v8.cpu_profiler.hires'));
}
categoriesArray.push(disabledByDefault('devtools.timeline.stack'));
if (Runtime.experiments.isEnabled('timelineInvalidationTracking'))
categoriesArray.push(disabledByDefault('devtools.timeline.invalidationTracking'));
if (options.capturePictures) {
categoriesArray.push(
disabledByDefault('devtools.timeline.layers'), disabledByDefault('devtools.timeline.picture'),
disabledByDefault('blink.graphics_context_annotations'));
}
if (options.captureFilmStrip)
categoriesArray.push(disabledByDefault('devtools.screenshot'));
this._extensionSessions =
providers.map(provider => new Timeline.ExtensionTracingSession(provider, this._performanceModel));
this._extensionSessions.forEach(session => session.start());
var startPromise = this._startRecordingWithCategories(categoriesArray.join(','), options.enableJSSampling);
this._performanceModel.setRecordStartTime(Date.now());
return startPromise;
}
stopRecording() {
var tracingStoppedPromises = [];
tracingStoppedPromises.push(new Promise(resolve => this._tracingCompleteCallback = resolve));
tracingStoppedPromises.push(this._stopProfilingOnAllModels());
this._tracingManager.stop();
tracingStoppedPromises.push(SDK.targetManager.resumeAllTargets());
this._client.loadingStarted();
var extensionCompletionPromises = this._extensionSessions.map(session => session.stop());
if (extensionCompletionPromises.length) {
var timerId;
var timeoutPromise = new Promise(fulfill => timerId = setTimeout(fulfill, 5000));
tracingStoppedPromises.push(
Promise.race([Promise.all(extensionCompletionPromises).then(() => clearTimeout(timerId)), timeoutPromise]));
}
Promise.all(tracingStoppedPromises).then(() => this._allSourcesFinished());
}
/**
* @override
* @param {!SDK.CPUProfilerModel} cpuProfilerModel
*/
modelAdded(cpuProfilerModel) {
if (this._profiling)
cpuProfilerModel.startRecording();
}
/**
* @override
* @param {!SDK.CPUProfilerModel} cpuProfilerModel
*/
modelRemoved(cpuProfilerModel) {
// FIXME: We'd like to stop profiling on the target and retrieve a profile
// but it's too late. Backend connection is closed.
}
/**
* @return {!Promise}
*/
_startProfilingOnAllModels() {
this._profiling = true;
var models = SDK.targetManager.models(SDK.CPUProfilerModel);
return Promise.all(models.map(model => model.startRecording()));
}
/**
* @param {string} targetId
* @param {?Protocol.Profiler.Profile} cpuProfile
*/
_addCpuProfile(targetId, cpuProfile) {
if (!cpuProfile) {
Common.console.warn(Common.UIString('CPU profile for a target is not available.'));
return;
}
if (!this._cpuProfiles)
this._cpuProfiles = new Map();
this._cpuProfiles.set(targetId, cpuProfile);
}
/**
* @return {!Promise}
*/
_stopProfilingOnAllModels() {
var models = this._profiling ? SDK.targetManager.models(SDK.CPUProfilerModel) : [];
this._profiling = false;
var promises = [];
for (var model of models) {
var targetId = model.target().id();
var modelPromise = model.stopRecording().then(this._addCpuProfile.bind(this, targetId));
promises.push(modelPromise);
}
return Promise.all(promises);
}
/**
* @param {string} categories
* @param {boolean=} enableJSSampling
* @return {!Promise}
*/
_startRecordingWithCategories(categories, enableJSSampling) {
SDK.targetManager.suspendAllTargets();
var profilingStartedPromise = enableJSSampling && !Runtime.experiments.isEnabled('timelineTracingJSProfile') ?
this._startProfilingOnAllModels() :
Promise.resolve();
var samplingFrequencyHz = Common.moduleSetting('highResolutionCpuProfiling').get() ? 10000 : 1000;
var options = 'sampling-frequency=' + samplingFrequencyHz;
return profilingStartedPromise.then(() => this._tracingManager.start(this, categories, options));
}
/**
* @param {!Array.<!SDK.TracingManager.EventPayload>} events
* @override
*/
traceEventsCollected(events) {
this._tracingModel.addEvents(events);
}
/**
* @override
*/
tracingComplete() {
this._tracingCompleteCallback();
this._tracingCompleteCallback = null;
}
_allSourcesFinished() {
this._client.processingStarted();
setTimeout(() => this._finalizeTrace(), 0);
}
_finalizeTrace() {
this._injectCpuProfileEvents();
this._tracingModel.tracingComplete();
this._client.loadingComplete(this._tracingModel, this._tracingModelBackingStorage);
}
/**
* @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._tracingManager.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 cpuProfile = this._cpuProfiles.get(workerId);
this._injectCpuProfileEvent(
metaEvent.thread.process().id(), metaEvent.args['data']['workerThreadId'], cpuProfile);
}
this._cpuProfiles = null;
}
/**
* @param {number} usage
* @override
*/
tracingBufferUsage(usage) {
this._client.recordingProgress(usage);
}
/**
* @param {number} progress
* @override
*/
eventsRetrievalProgress(progress) {
this._client.loadingProgress(progress);
}
};
/**
* @interface
* @extends {Timeline.TimelineLoader.Client}
*/
Timeline.TimelineController.Client = function() {};
Timeline.TimelineController.Client.prototype = {
/**
* @param {number} usage
*/
recordingProgress(usage) {},
};
/**
* @typedef {!{
* enableJSSampling: (boolean|undefined),
* capturePictures: (boolean|undefined),
* captureFilmStrip: (boolean|undefined)
* }}
*/
Timeline.TimelineController.RecordingOptions;