chrome-devtools-frontend
Version:
Chrome DevTools UI
180 lines (155 loc) • 7.83 kB
text/typescript
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
// eslint-disable-next-line @devtools/es-modules-import
import * as StackTrace from './stack_trace.js';
import {
type AnyStackTraceImpl,
AsyncFragmentImpl,
DebuggableFragmentImpl,
FragmentImpl,
FrameImpl,
StackTraceImpl
} from './StackTraceImpl.js';
import {type FrameNode, type RawFrame, Trie} from './Trie.js';
/**
* A stack trace translation function.
*
* Any implementation must return an array with the same length as `frames`.
*/
export type TranslateRawFrames = (frames: readonly RawFrame[], target: SDK.Target.Target) => Promise<
Array<Array<Pick<StackTrace.StackTrace.Frame, 'url'|'uiSourceCode'|'name'|'line'|'column'|'missingDebugInfo'>>>>;
/**
* The {@link StackTraceModel} is a thin wrapper around a fragment trie.
*
* We want to store stack trace fragments per target so a SDKModel is the natural choice.
*/
export class StackTraceModel extends SDK.SDKModel.SDKModel<unknown> {
readonly #trie = new Trie();
/** @returns the {@link StackTraceModel} for the target, or the model for the primaryPageTarget when passing null/undefined */
static #modelForTarget(target: SDK.Target.Target|null|undefined): StackTraceModel {
const model = (target ?? SDK.TargetManager.TargetManager.instance().primaryPageTarget())?.model(StackTraceModel);
if (!model) {
throw new Error('Unable to find StackTraceModel');
}
return model;
}
async createFromProtocolRuntime(stackTrace: Protocol.Runtime.StackTrace, rawFramesToUIFrames: TranslateRawFrames):
Promise<StackTrace.StackTrace.StackTrace> {
const [syncFragment, asyncFragments] = await Promise.all([
this.#createSyncFragment(stackTrace, rawFramesToUIFrames),
this.#createAsyncFragments(stackTrace, rawFramesToUIFrames),
]);
return new StackTraceImpl(syncFragment, asyncFragments);
}
async createFromDebuggerPaused(
pausedDetails: SDK.DebuggerModel.DebuggerPausedDetails,
rawFramesToUIFrames: TranslateRawFrames): Promise<StackTrace.StackTrace.DebuggableStackTrace> {
const [syncFragment, asyncFragments] = await Promise.all([
this.#createDebuggableFragment(pausedDetails, rawFramesToUIFrames),
this.#createAsyncFragments(pausedDetails, rawFramesToUIFrames),
]);
return new StackTraceImpl(syncFragment, asyncFragments);
}
/** Trigger re-translation of all fragments with the provide script in their call stack */
async scriptInfoChanged(script: SDK.Script.Script, translateRawFrames: TranslateRawFrames): Promise<void> {
const translatePromises: Array<Promise<unknown>> = [];
let stackTracesToUpdate = new Set<AnyStackTraceImpl>();
for (const fragment of this.#affectedFragments(script)) {
// We trigger re-translation only for fragments of leaf-nodes. Any fragment along the ancestor-chain
// is re-translated as a side-effect.
// We just need to remember the stack traces of the skipped over fragments, so we can send the
// UPDATED event also to them.
if (fragment.node.children.length === 0) {
translatePromises.push(this.#translateFragment(fragment, translateRawFrames));
}
stackTracesToUpdate = stackTracesToUpdate.union(fragment.stackTraces);
}
await Promise.all(translatePromises);
for (const stackTrace of stackTracesToUpdate) {
stackTrace.dispatchEventToListeners(StackTrace.StackTrace.Events.UPDATED);
}
}
async #createSyncFragment(stackTrace: Protocol.Runtime.StackTrace, rawFramesToUIFrames: TranslateRawFrames):
Promise<FragmentImpl> {
const fragment = this.#createFragment(stackTrace.callFrames);
await this.#translateFragment(fragment, rawFramesToUIFrames);
return fragment;
}
async #createDebuggableFragment(
pausedDetails: SDK.DebuggerModel.DebuggerPausedDetails,
rawFramesToUIFrames: TranslateRawFrames): Promise<DebuggableFragmentImpl> {
const fragment = this.#createFragment(pausedDetails.callFrames.map(frame => ({
scriptId: frame.script.scriptId,
url: frame.script.sourceURL,
functionName: frame.functionName,
lineNumber: frame.location().lineNumber,
columnNumber: frame.location().columnNumber,
})));
await this.#translateFragment(fragment, rawFramesToUIFrames);
return new DebuggableFragmentImpl(fragment, pausedDetails.callFrames);
}
async #createAsyncFragments(
stackTraceOrPausedEvent: Protocol.Runtime.StackTrace|SDK.DebuggerModel.DebuggerPausedDetails,
rawFramesToUIFrames: TranslateRawFrames): Promise<AsyncFragmentImpl[]> {
const asyncFragments: AsyncFragmentImpl[] = [];
const translatePromises: Array<Promise<unknown>> = [];
const debuggerModel = this.target().model(SDK.DebuggerModel.DebuggerModel);
if (debuggerModel) {
for await (
const {stackTrace: asyncStackTrace, target} of debuggerModel.iterateAsyncParents(stackTraceOrPausedEvent)) {
if (asyncStackTrace.callFrames.length === 0) {
// Skip empty async fragments, they don't add value.
continue;
}
const model = StackTraceModel.#modelForTarget(target);
const fragment = model.#createFragment(asyncStackTrace.callFrames);
translatePromises.push(model.#translateFragment(fragment, rawFramesToUIFrames));
asyncFragments.push(new AsyncFragmentImpl(asyncStackTrace.description ?? '', fragment));
}
}
await Promise.all(translatePromises);
return asyncFragments;
}
#createFragment(frames: RawFrame[]): FragmentImpl {
return FragmentImpl.getOrCreate(this.#trie.insert(frames));
}
async #translateFragment(fragment: FragmentImpl, rawFramesToUIFrames: TranslateRawFrames): Promise<void> {
const rawFrames = fragment.node.getCallStack().map(node => node.rawFrame).toArray();
const uiFrames = await rawFramesToUIFrames(rawFrames, this.target());
console.assert(rawFrames.length === uiFrames.length, 'Broken rawFramesToUIFrames implementation');
let i = 0;
for (const node of fragment.node.getCallStack()) {
node.frames = uiFrames[i++].map(
frame => new FrameImpl(
frame.url, frame.uiSourceCode, frame.name, frame.line, frame.column, frame.missingDebugInfo));
}
}
#affectedFragments(script: SDK.Script.Script): Set<FragmentImpl> {
// 1. Collect branches with the matching script.
const affectedBranches = new Set<FrameNode>();
this.#trie.walk(null, node => {
// scriptId has precedence, but if the frame does not have one, check the URL.
if (node.rawFrame.scriptId === script.scriptId ||
(!node.rawFrame.scriptId && node.rawFrame.url === script.sourceURL)) {
affectedBranches.add(node);
return false;
}
return true;
});
// 2. For each branch collect all the fragments.
const fragments = new Set<FragmentImpl>();
for (const branch of affectedBranches) {
this.#trie.walk(branch, node => {
if (node.fragment) {
fragments.add(node.fragment);
}
return true;
});
}
return fragments;
}
}
SDK.SDKModel.SDKModel.register(StackTraceModel, {capabilities: SDK.Target.Capability.NONE, autostart: false});