mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-10 13:44:39 +08:00
This is a reland of 250e7ef199a3aa19ed838120b9ab64c7b5fcfb55 Original change's description: > Reland "DevTools: Support CPU profiles in OOPIFs" > > This is a reland of 91d5ec53554dafd2ab13ad1908ba47b16842468f > > Original change's description: > > DevTools: Support CPU profiles in OOPIFs > > > > BUG=832313,820250 > > > > Change-Id: I53d730d4ff7e747eb0ccfe9a055738c1fe9858cf > > Reviewed-on: https://chromium-review.googlesource.com/1011350 > > Commit-Queue: Alexei Filippov <alph@chromium.org> > > Reviewed-by: Dmitry Gozman <dgozman@chromium.org> > > Cr-Commit-Position: refs/heads/master@{#553688} > > TBR=dgozman > > Bug: 832313, 820250 > Change-Id: I177e0d404b8efe595ddc680368b21c32160cd5de > Reviewed-on: https://chromium-review.googlesource.com/1065230 > Commit-Queue: Alexei Filippov <alph@chromium.org> > Reviewed-by: Alexei Filippov <alph@chromium.org> > Cr-Commit-Position: refs/heads/master@{#560049} TBR=dgozman Bug: 832313, 820250 Change-Id: I7eee078d51e23034ac8c08ce315949c4d78c7092 Reviewed-on: https://chromium-review.googlesource.com/1079729 Reviewed-by: Alexei Filippov <alph@chromium.org> Commit-Queue: Alexei Filippov <alph@chromium.org> Cr-Original-Commit-Position: refs/heads/master@{#563032} Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src Cr-Mirrored-Commit: d0a2c26539a8a4e3a5352de72be82ade08fcdf24
349 lines
12 KiB
JavaScript
349 lines
12 KiB
JavaScript
// 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._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._tracingManager.target();
|
|
}
|
|
|
|
/**
|
|
* @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('timelineFlowEvents'))
|
|
categoriesArray.push('devtools.timeline.async');
|
|
|
|
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());
|
|
const startPromise = this._startRecordingWithCategories(categoriesArray.join(','), options.enableJSSampling);
|
|
this._performanceModel.setRecordStartTime(Date.now());
|
|
return startPromise;
|
|
}
|
|
|
|
/**
|
|
* @return {!Promise<!Timeline.PerformanceModel>}
|
|
*/
|
|
async stopRecording() {
|
|
const tracingStoppedPromises = [];
|
|
tracingStoppedPromises.push(new Promise(resolve => this._tracingCompleteCallback = resolve));
|
|
tracingStoppedPromises.push(this._stopProfilingOnAllModels());
|
|
this._tracingManager.stop();
|
|
tracingStoppedPromises.push(SDK.targetManager.resumeAllTargets());
|
|
|
|
this._client.loadingStarted();
|
|
|
|
const extensionCompletionPromises = this._extensionSessions.map(session => session.stop());
|
|
if (extensionCompletionPromises.length) {
|
|
tracingStoppedPromises.push(
|
|
Promise.race([Promise.all(extensionCompletionPromises), new Promise(r => setTimeout(r, 5000))]));
|
|
}
|
|
await Promise.all(tracingStoppedPromises);
|
|
this._allSourcesFinished();
|
|
return this._performanceModel;
|
|
}
|
|
|
|
/**
|
|
* @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}
|
|
*/
|
|
_startRecordingWithCategories(categories, enableJSSampling) {
|
|
SDK.targetManager.suspendAllTargets();
|
|
const profilingStartedPromise = enableJSSampling && !Runtime.experiments.isEnabled('timelineTracingJSProfile') ?
|
|
this._startProfilingOnAllModels() :
|
|
Promise.resolve();
|
|
const samplingFrequencyHz = Common.moduleSetting('highResolutionCpuProfiling').get() ? 10000 : 1000;
|
|
const 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);
|
|
}
|
|
|
|
/**
|
|
* @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 {!Multimap<string, string>} */
|
|
const pseudoPidToFrames = new 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);
|
|
}
|
|
}
|
|
|
|
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)
|
|
* }}
|
|
*/
|
|
Timeline.TimelineController.RecordingOptions;
|