| // 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.Target} target |
| * @param {!Timeline.TimelineController.Client} client |
| */ |
| constructor(target, client) { |
| this._target = target; |
| this._tracingManager = target.model(SDK.TracingManager); |
| this._performanceModel = new Timeline.PerformanceModel(); |
| this._performanceModel.setMainTarget(target); |
| this._client = client; |
| |
| const backingStorage = new Bindings.TempFileBackingStorage(); |
| this._tracingModel = new SDK.TracingModel(backingStorage); |
| |
| /** @type {!Array<!Timeline.ExtensionTracingSession>} */ |
| this._extensionSessions = []; |
| SDK.targetManager.observeModels(SDK.CPUProfilerModel, this); |
| } |
| |
| dispose() { |
| SDK.targetManager.unobserveModels(SDK.CPUProfilerModel, this); |
| } |
| |
| /** |
| * @return {!SDK.Target} |
| */ |
| mainTarget() { |
| return this._target; |
| } |
| |
| /** |
| * @param {!Timeline.TimelineController.RecordingOptions} options |
| * @param {!Array<!Extensions.ExtensionTraceProvider>} providers |
| * @return {!Promise<!Object>} |
| */ |
| async 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', disabledByDefault('devtools.timeline'), disabledByDefault('devtools.timeline.frame'), |
| 'v8.execute', TimelineModel.TimelineModel.Category.Console, TimelineModel.TimelineModel.Category.UserTiming |
| ]; |
| categoriesArray.push(TimelineModel.TimelineModel.Category.LatencyInfo); |
| |
| if (Root.Runtime.experiments.isEnabled('timelineFlowEvents')) { |
| categoriesArray.push('devtools.timeline.async'); |
| } |
| |
| if (Root.Runtime.experiments.isEnabled('timelineV8RuntimeCallStats') && options.enableJSSampling) { |
| categoriesArray.push(disabledByDefault('v8.runtime_stats_sampling')); |
| } |
| if (!Root.Runtime.queryParam('timelineTracingJSProfileDisabled') && 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 (Root.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()); |
| this._performanceModel.setRecordStartTime(Date.now()); |
| const response = await this._startRecordingWithCategories(categoriesArray.join(','), options.enableJSSampling); |
| if (response[Protocol.Error]) { |
| await this._waitForTracingToStop(false); |
| } |
| return response; |
| } |
| |
| /** |
| * @return {!Promise<!Timeline.PerformanceModel>} |
| */ |
| async stopRecording() { |
| if (this._tracingManager) { |
| this._tracingManager.stop(); |
| } |
| |
| this._client.loadingStarted(); |
| await this._waitForTracingToStop(true); |
| this._allSourcesFinished(); |
| return this._performanceModel; |
| } |
| |
| |
| /** |
| * @param {boolean} awaitTracingCompleteCallback - Whether to wait for the _tracingCompleteCallback to happen |
| * @return {!Promise} |
| */ |
| _waitForTracingToStop(awaitTracingCompleteCallback) { |
| const tracingStoppedPromises = []; |
| if (this._tracingManager && awaitTracingCompleteCallback) { |
| tracingStoppedPromises.push(new Promise(resolve => this._tracingCompleteCallback = resolve)); |
| } |
| tracingStoppedPromises.push(this._stopProfilingOnAllModels()); |
| |
| const extensionCompletionPromises = this._extensionSessions.map(session => session.stop()); |
| if (extensionCompletionPromises.length) { |
| tracingStoppedPromises.push( |
| Promise.race([Promise.all(extensionCompletionPromises), new Promise(r => setTimeout(r, 5000))])); |
| } |
| return Promise.all(tracingStoppedPromises); |
| } |
| |
| /** |
| * @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; |
| const 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() { |
| const models = this._profiling ? SDK.targetManager.models(SDK.CPUProfilerModel) : []; |
| this._profiling = false; |
| const promises = []; |
| for (const model of models) { |
| const targetId = model.target().id(); |
| const modelPromise = model.stopRecording().then(this._addCpuProfile.bind(this, targetId)); |
| promises.push(modelPromise); |
| } |
| return Promise.all(promises); |
| } |
| |
| /** |
| * @param {string} categories |
| * @param {boolean=} enableJSSampling |
| * @return {!Promise<!Object|undefined>} |
| */ |
| async _startRecordingWithCategories(categories, enableJSSampling) { |
| // There might be a significant delay in the beginning of timeline recording |
| // caused by starting CPU profiler, that needs to traverse JS heap to collect |
| // all the functions data. |
| await SDK.targetManager.suspendAllTargets('performance-timeline'); |
| if (enableJSSampling && Root.Runtime.queryParam('timelineTracingJSProfileDisabled')) { |
| await this._startProfilingOnAllModels(); |
| } |
| if (!this._tracingManager) { |
| return; |
| } |
| |
| const samplingFrequencyHz = Common.moduleSetting('highResolutionCpuProfiling').get() ? 10000 : 1000; |
| const options = 'sampling-frequency=' + samplingFrequencyHz; |
| return 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); |
| } |
| |
| /** |
| * @return {!Promise<undefined>} |
| */ |
| async _finalizeTrace() { |
| this._injectCpuProfileEvents(); |
| await SDK.targetManager.resumeAllTargets(); |
| this._tracingModel.tracingComplete(); |
| this._client.loadingComplete(this._tracingModel); |
| } |
| |
| /** |
| * @param {number} pid |
| * @param {number} tid |
| * @param {?Protocol.Profiler.Profile} cpuProfile |
| */ |
| _injectCpuProfileEvent(pid, tid, cpuProfile) { |
| if (!cpuProfile) { |
| return; |
| } |
| const 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]); |
| } |
| |
| /** |
| * @return {?Map<string, number>} |
| */ |
| _buildTargetToProcessIdMap() { |
| const metadataEventTypes = TimelineModel.TimelineModel.DevToolsMetadataEvent; |
| const metadataEvents = this._tracingModel.devToolsMetadataEvents(); |
| const browserMetaEvent = metadataEvents.find(e => e.name === metadataEventTypes.TracingStartedInBrowser); |
| if (!browserMetaEvent) { |
| return null; |
| } |
| |
| /** @type {!Platform.Multimap<string, string>} */ |
| const pseudoPidToFrames = new Platform.Multimap(); |
| /** @type {!Map<string, number>} */ |
| const targetIdToPid = new Map(); |
| const frames = browserMetaEvent.args.data['frames']; |
| for (const frameInfo of frames) { |
| targetIdToPid.set(frameInfo.frame, frameInfo.processId); |
| } |
| for (const event of metadataEvents) { |
| const data = event.args.data; |
| switch (event.name) { |
| case metadataEventTypes.FrameCommittedInBrowser: |
| if (data.processId) { |
| targetIdToPid.set(data.frame, data.processId); |
| } else { |
| pseudoPidToFrames.set(data.processPseudoId, data.frame); |
| } |
| break; |
| case metadataEventTypes.ProcessReadyInBrowser: |
| for (const frame of pseudoPidToFrames.get(data.processPseudoId) || []) { |
| targetIdToPid.set(frame, data.processId); |
| } |
| break; |
| } |
| } |
| const mainFrame = frames.find(frame => !frame.parent); |
| const mainRendererProcessId = mainFrame.processId; |
| const mainProcess = this._tracingModel.processById(mainRendererProcessId); |
| if (mainProcess) { |
| targetIdToPid.set(SDK.targetManager.mainTarget().id(), mainProcess.id()); |
| } |
| return targetIdToPid; |
| } |
| |
| _injectCpuProfileEvents() { |
| if (!this._cpuProfiles) { |
| return; |
| } |
| |
| const metadataEventTypes = TimelineModel.TimelineModel.DevToolsMetadataEvent; |
| const metadataEvents = this._tracingModel.devToolsMetadataEvents(); |
| |
| const targetIdToPid = this._buildTargetToProcessIdMap(); |
| if (targetIdToPid) { |
| for (const [id, profile] of this._cpuProfiles) { |
| const pid = targetIdToPid.get(id); |
| if (!pid) { |
| continue; |
| } |
| const process = this._tracingModel.processById(pid); |
| const thread = process && process.threadByName(TimelineModel.TimelineModel.RendererMainThreadName); |
| if (thread) { |
| this._injectCpuProfileEvent(pid, thread.id(), profile); |
| } |
| } |
| } else { |
| // Legacy backends support. |
| const mainMetaEvent = |
| metadataEvents.filter(event => event.name === metadataEventTypes.TracingStartedInPage).peekLast(); |
| if (mainMetaEvent) { |
| const pid = mainMetaEvent.thread.process().id(); |
| const mainCpuProfile = this._cpuProfiles.get(this._tracingManager.target().id()); |
| this._injectCpuProfileEvent(pid, mainMetaEvent.thread.id(), mainCpuProfile); |
| } else { |
| // Or there was no tracing manager in the main target at all, in this case build the model full |
| // of cpu profiles. |
| let tid = 0; |
| for (const pair of this._cpuProfiles) { |
| const target = SDK.targetManager.targetById(pair[0]); |
| const name = target && target.name(); |
| this._tracingModel.addEvents(TimelineModel.TimelineJSProfileProcessor.buildTraceProfileFromCpuProfile( |
| pair[1], ++tid, /* injectPageEvent */ tid === 1, name)); |
| } |
| } |
| } |
| |
| const workerMetaEvents = |
| metadataEvents.filter(event => event.name === metadataEventTypes.TracingSessionIdForWorker); |
| for (const metaEvent of workerMetaEvents) { |
| const workerId = metaEvent.args['data']['workerId']; |
| const 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), |
| * startCoverage: (boolean|undefined) |
| * }} |
| */ |
| Timeline.TimelineController.RecordingOptions; |