mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-06 20:11:38 +08:00
experiment is enabled. Currently, this experiment reveals a few more events that were not allowlisted. But this change will expand the tracing category selection to all non-high-overhead categories. Driveby: fix undefined property exception. Change-Id: I236aad44e40724d82d8c37055686e03148059d2b Bug: 1321307 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3616460 Reviewed-by: Jack Franklin <jacktfranklin@chromium.org> Commit-Queue: Paul Irish <paulirish@chromium.org>
391 lines
16 KiB
TypeScript
391 lines
16 KiB
TypeScript
// 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.
|
|
|
|
import * as Common from '../../core/common/common.js';
|
|
import * as i18n from '../../core/i18n/i18n.js';
|
|
import * as Platform from '../../core/platform/platform.js';
|
|
import * as Root from '../../core/root/root.js';
|
|
import * as SDK from '../../core/sdk/sdk.js';
|
|
import * as Bindings from '../../models/bindings/bindings.js';
|
|
import * as Extensions from '../../models/extensions/extensions.js';
|
|
import * as TimelineModel from '../../models/timeline_model/timeline_model.js';
|
|
import type * as Protocol from '../../generated/protocol.js';
|
|
|
|
import {ExtensionTracingSession} from './ExtensionTracingSession.js';
|
|
import {PerformanceModel} from './PerformanceModel.js';
|
|
|
|
const UIStrings = {
|
|
/**
|
|
* @description Text in Timeline Controller of the Performance panel.
|
|
* A "CPU profile" is a recorded performance measurement how a specific target behaves.
|
|
* "Target" in this context can mean a web page, service or normal worker.
|
|
* "Not available" is used as there are multiple things that can go wrong, but we do not
|
|
* know what exactly, just that the CPU profile was not correctly recorded.
|
|
*/
|
|
cpuProfileForATargetIsNot: 'CPU profile for a target is not available.',
|
|
/**
|
|
*@description Text in Timeline Controller of the Performance panel indicating that the Performance Panel cannot
|
|
* record a performance trace because the type of target (where possible types are page, service worker and shared
|
|
* worker) doesn't support it.
|
|
*/
|
|
tracingNotSupported: 'Performance trace recording not supported for this type of target',
|
|
};
|
|
const str_ = i18n.i18n.registerUIStrings('panels/timeline/TimelineController.ts', UIStrings);
|
|
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
|
export class TimelineController implements SDK.TargetManager.SDKModelObserver<SDK.CPUProfilerModel.CPUProfilerModel>,
|
|
SDK.TracingManager.TracingManagerClient {
|
|
private readonly target: SDK.Target.Target;
|
|
private tracingManager: SDK.TracingManager.TracingManager|null;
|
|
private performanceModel: PerformanceModel;
|
|
private readonly client: Client;
|
|
private readonly tracingModel: SDK.TracingModel.TracingModel;
|
|
private extensionSessions: ExtensionTracingSession[];
|
|
private extensionTraceProviders?: Extensions.ExtensionTraceProvider.ExtensionTraceProvider[];
|
|
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
private tracingCompleteCallback?: ((value: any) => void)|null;
|
|
private profiling?: boolean;
|
|
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
private cpuProfiles?: Map<any, any>|null;
|
|
|
|
constructor(target: SDK.Target.Target, client: Client) {
|
|
this.target = target;
|
|
this.tracingManager = target.model(SDK.TracingManager.TracingManager);
|
|
this.performanceModel = new PerformanceModel();
|
|
this.performanceModel.setMainTarget(target);
|
|
this.client = client;
|
|
|
|
const backingStorage = new Bindings.TempFile.TempFileBackingStorage();
|
|
this.tracingModel = new SDK.TracingModel.TracingModel(backingStorage);
|
|
|
|
this.extensionSessions = [];
|
|
SDK.TargetManager.TargetManager.instance().observeModels(SDK.CPUProfilerModel.CPUProfilerModel, this);
|
|
}
|
|
|
|
dispose(): void {
|
|
SDK.TargetManager.TargetManager.instance().unobserveModels(SDK.CPUProfilerModel.CPUProfilerModel, this);
|
|
}
|
|
|
|
mainTarget(): SDK.Target.Target {
|
|
return this.target;
|
|
}
|
|
|
|
async startRecording(
|
|
options: RecordingOptions, providers: Extensions.ExtensionTraceProvider.ExtensionTraceProvider[]):
|
|
Promise<Protocol.ProtocolResponseWithError> {
|
|
this.extensionTraceProviders = Extensions.ExtensionServer.ExtensionServer.instance().traceProviders().slice();
|
|
|
|
function disabledByDefault(category: string): string {
|
|
return 'disabled-by-default-' + category;
|
|
}
|
|
|
|
// The following categories are also used in other tools, but this panel
|
|
// offers the possibility of turning them off (see below).
|
|
// 'disabled-by-default-devtools.screenshot'
|
|
// └ default: on, option: captureFilmStrip
|
|
// 'disabled-by-default-devtools.timeline.invalidationTracking'
|
|
// └ default: off, experiment: timelineInvalidationTracking
|
|
// 'disabled-by-default-v8.cpu_profiler'
|
|
// └ default: on, option: enableJSSampling
|
|
const categoriesArray = [
|
|
Root.Runtime.experiments.isEnabled('timelineShowAllEvents') ? '*' : '-*',
|
|
TimelineModel.TimelineModel.TimelineModelImpl.Category.Console,
|
|
TimelineModel.TimelineModel.TimelineModelImpl.Category.UserTiming,
|
|
'devtools.timeline',
|
|
disabledByDefault('devtools.timeline'),
|
|
disabledByDefault('devtools.timeline.frame'),
|
|
disabledByDefault('devtools.timeline.stack'),
|
|
disabledByDefault('v8.compile'),
|
|
disabledByDefault('v8.cpu_profiler.hires'),
|
|
TimelineModel.TimelineModel.TimelineModelImpl.Category.LatencyInfo,
|
|
TimelineModel.TimelineModel.TimelineModelImpl.Category.Loading,
|
|
disabledByDefault('lighthouse'),
|
|
'v8.execute',
|
|
'v8',
|
|
];
|
|
|
|
if (Root.Runtime.experiments.isEnabled('timelineV8RuntimeCallStats') && options.enableJSSampling) {
|
|
categoriesArray.push(disabledByDefault('v8.runtime_stats_sampling'));
|
|
}
|
|
if (!Root.Runtime.Runtime.queryParam('timelineTracingJSProfileDisabled') && options.enableJSSampling) {
|
|
categoriesArray.push(disabledByDefault('v8.cpu_profiler'));
|
|
}
|
|
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 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.getError()) {
|
|
await this.waitForTracingToStop(false);
|
|
await SDK.TargetManager.TargetManager.instance().resumeAllTargets();
|
|
}
|
|
return response;
|
|
}
|
|
|
|
async stopRecording(): Promise<PerformanceModel> {
|
|
if (this.tracingManager) {
|
|
this.tracingManager.stop();
|
|
}
|
|
|
|
this.client.loadingStarted();
|
|
await this.waitForTracingToStop(true);
|
|
this.allSourcesFinished();
|
|
return this.performanceModel;
|
|
}
|
|
|
|
private async waitForTracingToStop(awaitTracingCompleteCallback: boolean): Promise<void> {
|
|
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 => window.setTimeout(r, 5000))]));
|
|
}
|
|
await Promise.all(tracingStoppedPromises);
|
|
}
|
|
|
|
modelAdded(cpuProfilerModel: SDK.CPUProfilerModel.CPUProfilerModel): void {
|
|
if (this.profiling) {
|
|
void cpuProfilerModel.startRecording();
|
|
}
|
|
}
|
|
|
|
modelRemoved(_cpuProfilerModel: SDK.CPUProfilerModel.CPUProfilerModel): void {
|
|
// FIXME: We'd like to stop profiling on the target and retrieve a profile
|
|
// but it's too late. Backend connection is closed.
|
|
}
|
|
|
|
private async startProfilingOnAllModels(): Promise<void> {
|
|
this.profiling = true;
|
|
const models = SDK.TargetManager.TargetManager.instance().models(SDK.CPUProfilerModel.CPUProfilerModel);
|
|
await Promise.all(models.map(model => model.startRecording()));
|
|
}
|
|
|
|
private addCpuProfile(targetId: Protocol.Target.TargetID|'main', cpuProfile: Protocol.Profiler.Profile|null): void {
|
|
if (!cpuProfile) {
|
|
Common.Console.Console.instance().warn(i18nString(UIStrings.cpuProfileForATargetIsNot));
|
|
return;
|
|
}
|
|
if (!this.cpuProfiles) {
|
|
this.cpuProfiles = new Map();
|
|
}
|
|
this.cpuProfiles.set(targetId, cpuProfile);
|
|
}
|
|
|
|
private async stopProfilingOnAllModels(): Promise<void> {
|
|
const models =
|
|
this.profiling ? SDK.TargetManager.TargetManager.instance().models(SDK.CPUProfilerModel.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);
|
|
}
|
|
await Promise.all(promises);
|
|
}
|
|
|
|
private async startRecordingWithCategories(categories: string, enableJSSampling?: boolean):
|
|
Promise<Protocol.ProtocolResponseWithError> {
|
|
if (!this.tracingManager) {
|
|
throw new Error(UIStrings.tracingNotSupported);
|
|
}
|
|
// 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.TargetManager.instance().suspendAllTargets('performance-timeline');
|
|
if (enableJSSampling && Root.Runtime.Runtime.queryParam('timelineTracingJSProfileDisabled')) {
|
|
await this.startProfilingOnAllModels();
|
|
}
|
|
|
|
return this.tracingManager.start(this, categories, '');
|
|
}
|
|
|
|
traceEventsCollected(events: SDK.TracingManager.EventPayload[]): void {
|
|
this.tracingModel.addEvents(events);
|
|
}
|
|
|
|
tracingComplete(): void {
|
|
if (!this.tracingCompleteCallback) {
|
|
return;
|
|
}
|
|
this.tracingCompleteCallback(undefined);
|
|
this.tracingCompleteCallback = null;
|
|
}
|
|
|
|
private allSourcesFinished(): void {
|
|
this.client.processingStarted();
|
|
window.setTimeout(() => this.finalizeTrace(), 0);
|
|
}
|
|
|
|
private async finalizeTrace(): Promise<void> {
|
|
this.injectCpuProfileEvents();
|
|
await SDK.TargetManager.TargetManager.instance().resumeAllTargets();
|
|
this.tracingModel.tracingComplete();
|
|
this.client.loadingComplete(this.tracingModel);
|
|
}
|
|
|
|
private injectCpuProfileEvent(pid: number, tid: number, cpuProfile: Protocol.Profiler.Profile|null): void {
|
|
if (!cpuProfile) {
|
|
return;
|
|
}
|
|
// TODO(crbug/1011811): This event type is not compatible with the SDK.TracingManager.EventPayload.
|
|
// EventPayload requires many properties to be defined but it's not clear if they will have
|
|
// any side effects.
|
|
const cpuProfileEvent = ({
|
|
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}},
|
|
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} as any);
|
|
this.tracingModel.addEvents([cpuProfileEvent]);
|
|
}
|
|
|
|
private buildTargetToProcessIdMap(): Map<string, number>|null {
|
|
const metadataEventTypes = TimelineModel.TimelineModel.TimelineModelImpl.DevToolsMetadataEvent;
|
|
const metadataEvents = this.tracingModel.devToolsMetadataEvents();
|
|
const browserMetaEvent = metadataEvents.find(e => e.name === metadataEventTypes.TracingStartedInBrowser);
|
|
if (!browserMetaEvent) {
|
|
return null;
|
|
}
|
|
|
|
const pseudoPidToFrames = new Platform.MapUtilities.Multimap<string, string>();
|
|
const targetIdToPid = new Map<string, number>();
|
|
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const frames: any[] = 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.getProcessById(mainRendererProcessId);
|
|
if (mainProcess) {
|
|
const target = SDK.TargetManager.TargetManager.instance().mainTarget();
|
|
if (target) {
|
|
targetIdToPid.set(target.id(), mainProcess.id());
|
|
}
|
|
}
|
|
return targetIdToPid;
|
|
}
|
|
|
|
private injectCpuProfileEvents(): void {
|
|
if (!this.cpuProfiles) {
|
|
return;
|
|
}
|
|
|
|
const metadataEventTypes = TimelineModel.TimelineModel.TimelineModelImpl.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.getProcessById(pid);
|
|
const thread =
|
|
process && process.threadByName(TimelineModel.TimelineModel.TimelineModelImpl.RendererMainThreadName);
|
|
if (thread) {
|
|
this.injectCpuProfileEvent(pid, thread.id(), profile);
|
|
}
|
|
}
|
|
} else {
|
|
// Legacy backends support.
|
|
const filteredEvents = metadataEvents.filter(event => event.name === metadataEventTypes.TracingStartedInPage);
|
|
const mainMetaEvent = filteredEvents[filteredEvents.length - 1];
|
|
if (mainMetaEvent) {
|
|
const pid = mainMetaEvent.thread.process().id();
|
|
if (this.tracingManager) {
|
|
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.TargetManager.instance().targetById(pair[0]);
|
|
const name = target && target.name();
|
|
this.tracingModel.addEvents(
|
|
TimelineModel.TimelineJSProfile.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;
|
|
}
|
|
|
|
tracingBufferUsage(usage: number): void {
|
|
this.client.recordingProgress(usage);
|
|
}
|
|
|
|
eventsRetrievalProgress(progress: number): void {
|
|
this.client.loadingProgress(progress);
|
|
}
|
|
}
|
|
|
|
export interface Client {
|
|
recordingProgress(usage: number): void;
|
|
loadingStarted(): void;
|
|
processingStarted(): void;
|
|
loadingProgress(progress?: number): void;
|
|
loadingComplete(tracingModel: SDK.TracingModel.TracingModel|null): void;
|
|
}
|
|
export interface RecordingOptions {
|
|
enableJSSampling?: boolean;
|
|
capturePictures?: boolean;
|
|
captureFilmStrip?: boolean;
|
|
startCoverage?: boolean;
|
|
}
|