chrome-devtools-frontend
Version:
Chrome DevTools UI
249 lines (231 loc) • 9.46 kB
text/typescript
// Copyright 2024 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 type * as Protocol from '../../generated/protocol.js';
import {UserVisibleError} from '../platform/platform.js';
import type {
HydratingDataPerTarget, RehydratingExecutionContext, RehydratingScript, RehydratingTarget} from
'./RehydratingObject.js';
interface RehydratingTraceBase {
cat: string;
pid: number;
args: {data: object};
}
interface TraceEventTargetRundown extends RehydratingTraceBase {
cat: 'disabled-by-default-devtools.target-rundown';
args: {
data: {
frame: Protocol.Page.FrameId,
frameType: string,
url: string,
isolate: string,
v8context: string,
origin: string,
scriptId: Protocol.Runtime.ScriptId,
isDefault?: boolean,
contextType?: string,
},
};
}
interface TraceEventScriptRundown extends RehydratingTraceBase {
cat: 'disabled-by-default-devtools.v8-source-rundown';
args: {
data: {
isolate: string,
executionContextId: Protocol.Runtime.ExecutionContextId,
scriptId: Protocol.Runtime.ScriptId,
startLine: number,
startColumn: number,
endLine: number,
endColumn: number,
url: string,
hash: string,
isModule: boolean,
hasSourceUrl: boolean,
sourceMapUrl?: string,
},
};
}
interface TraceEventScriptRundownSource extends RehydratingTraceBase {
cat: 'disabled-by-default-devtools.v8-source-rundown-sources';
args: {
data: {
isolate: string,
scriptId: Protocol.Runtime.ScriptId,
length?: number,
sourceText?: string,
},
};
}
export class EnhancedTracesParser {
#scriptRundownEvents: TraceEventScriptRundown[] = [];
#scriptToV8Context: Map<string, string> = new Map<string, string>();
#scriptToScriptSource: Map<string, string> = new Map<string, string>();
#largeScriptToScriptSource: Map<string, string[]> = new Map<string, string[]>();
#scriptToSourceLength: Map<string, number> = new Map<string, number>();
#targets: RehydratingTarget[] = [];
#executionContexts: RehydratingExecutionContext[] = [];
#scripts: RehydratingScript[] = [];
static readonly enhancedTraceVersion: number = 1;
constructor(traceEvents: unknown[]) {
// Initialize with the trace events provided.
try {
this.parseEnhancedTrace(traceEvents);
} catch (e) {
throw new UserVisibleError.UserVisibleError(e);
}
}
parseEnhancedTrace(traceEvents: unknown[]): void {
for (const event of traceEvents) {
if (this.isTargetRundownEvent(event)) {
// Set up script to v8 context mapping
const data = event.args?.data;
this.#scriptToV8Context.set(this.getScriptIsolateId(data.isolate, data.scriptId), data.v8context);
// Add target
if (!this.#targets.find(target => target.targetId === data.frame)) {
this.#targets.push({
targetId: data.frame,
type: data.frameType,
isolate: data.isolate,
pid: event.pid,
url: data.url,
});
}
// Add execution context, need to put back execution context id with info from other traces
if (!this.#executionContexts.find(executionContext => executionContext.v8Context === data.v8context)) {
this.#executionContexts.push({
id: -1 as Protocol.Runtime.ExecutionContextId,
origin: data.origin,
v8Context: data.v8context,
auxData: {
frameId: data.frame,
isDefault: data.isDefault,
type: data.contextType,
},
isolate: data.isolate,
});
}
} else if (this.isScriptRundownEvent(event)) {
this.#scriptRundownEvents.push(event);
const data = event.args.data;
// Add script
if (!this.#scripts.find(script => script.scriptId === data.scriptId && script.isolate === data.isolate)) {
this.#scripts.push({
scriptId: data.scriptId,
isolate: data.isolate,
executionContextId: data.executionContextId,
startLine: data.startLine,
startColumn: data.startColumn,
endLine: data.endLine,
endColumn: data.endColumn,
hash: data.hash,
isModule: data.isModule,
url: data.url,
hasSourceUrl: data.hasSourceUrl,
sourceMapUrl: data.sourceMapUrl,
});
}
} else if (this.isScriptRundownSourceEvent(event)) {
// Set up script to source text and length mapping
const data = event.args.data;
const scriptIsolateId = this.getScriptIsolateId(data.isolate, data.scriptId);
if ('splitIndex' in data && 'splitCount' in data) {
if (!this.#largeScriptToScriptSource.has(scriptIsolateId)) {
this.#largeScriptToScriptSource.set(scriptIsolateId, new Array(data.splitCount).fill('') as string[]);
}
const splittedSource = this.#largeScriptToScriptSource.get(scriptIsolateId);
if (splittedSource && data.sourceText) {
splittedSource[data.splitIndex as number] = data.sourceText;
}
} else {
if (data.sourceText) {
this.#scriptToScriptSource.set(scriptIsolateId, data.sourceText);
}
if (data.length) {
this.#scriptToSourceLength.set(scriptIsolateId, data.length);
}
}
}
}
}
data(): HydratingDataPerTarget {
// Put back execution context id
const v8ContextToExecutionContextId: Map<string, Protocol.Runtime.ExecutionContextId> =
new Map<string, Protocol.Runtime.ExecutionContextId>();
this.#scriptRundownEvents.forEach(scriptRundownEvent => {
const data = scriptRundownEvent.args.data;
const v8Context = this.#scriptToV8Context.get(this.getScriptIsolateId(data.isolate, data.scriptId));
if (v8Context) {
v8ContextToExecutionContextId.set(v8Context, data.executionContextId);
}
});
this.#executionContexts.forEach(executionContext => {
if (executionContext.v8Context) {
const id = v8ContextToExecutionContextId.get(executionContext.v8Context);
if (id) {
executionContext.id = id;
}
}
});
// Put back script source text and length
this.#scripts.forEach(script => {
const scriptIsolateId = this.getScriptIsolateId(script.isolate, script.scriptId);
if (this.#scriptToScriptSource.has(scriptIsolateId)) {
script.sourceText = this.#scriptToScriptSource.get(scriptIsolateId);
script.length = this.#scriptToSourceLength.get(scriptIsolateId);
} else if (this.#largeScriptToScriptSource.has(scriptIsolateId)) {
const splittedSources = this.#largeScriptToScriptSource.get(scriptIsolateId);
if (splittedSources) {
script.sourceText = splittedSources.join('');
script.length = script.sourceText.length;
}
}
// put in the aux data
script.auxData =
this.#executionContexts
.find(context => context.id === script.executionContextId && context.isolate === script.isolate)
?.auxData;
});
const data = new Map<RehydratingTarget, [RehydratingExecutionContext[], RehydratingScript[]]>();
for (const target of this.#targets) {
data.set(target, this.groupContextsAndScriptsUnderTarget(target, this.#executionContexts, this.#scripts));
}
return data;
}
private getScriptIsolateId(isolate: string, scriptId: Protocol.Runtime.ScriptId): string {
return scriptId + '@' + isolate;
}
private isTraceEvent(event: unknown): event is RehydratingTraceBase {
return 'cat' in (event as RehydratingTraceBase) && 'pid' in (event as RehydratingTraceBase) &&
'args' in (event as RehydratingTraceBase) && 'data' in (event as RehydratingTraceBase).args;
}
private isTargetRundownEvent(event: unknown): event is TraceEventTargetRundown {
return this.isTraceEvent(event) && event.cat === 'disabled-by-default-devtools.target-rundown';
}
private isScriptRundownEvent(event: unknown): event is TraceEventScriptRundown {
return this.isTraceEvent(event) && event.cat === 'disabled-by-default-devtools.v8-source-rundown';
}
private isScriptRundownSourceEvent(event: unknown): event is TraceEventScriptRundownSource {
return this.isTraceEvent(event) && event.cat === 'disabled-by-default-devtools.v8-source-rundown-sources';
}
private groupContextsAndScriptsUnderTarget(
target: RehydratingTarget, executionContexts: RehydratingExecutionContext[],
scripts: RehydratingScript[]): [RehydratingExecutionContext[], RehydratingScript[]] {
const filteredExecutionContexts: RehydratingExecutionContext[] = [];
const filteredScripts: RehydratingScript[] = [];
for (const executionContext of executionContexts) {
if (executionContext.auxData?.frameId === target.targetId) {
filteredExecutionContexts.push(executionContext);
}
}
for (const script of scripts) {
if (script.auxData === null) {
console.error(script + ' missing aux data');
}
if (script.auxData?.frameId === target.targetId) {
filteredScripts.push(script);
}
}
return [filteredExecutionContexts, filteredScripts];
}
}