chrome-devtools-frontend
Version:
Chrome DevTools UI
155 lines (131 loc) • 4.67 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 type * as Protocol from '../../generated/protocol.js';
import type {FragmentImpl, FrameImpl} from './StackTraceImpl.js';
/**
* Intentionally very close to a {@link Protocol.Runtime.CallFrame} but with optional `scriptId`.
*/
export interface RawFrame {
readonly scriptId?: Protocol.Runtime.ScriptId;
readonly url?: string;
readonly functionName?: string;
readonly lineNumber: number;
readonly columnNumber: number;
}
interface FrameNodeBase<ChildT, ParentT> {
readonly parent: ParentT;
readonly children: ChildT[];
}
type RootFrameNode = FrameNodeBase<WeakRef<FrameNode>, null>;
type AnyFrameNode = FrameNode|RootFrameNode;
export class FrameNode implements FrameNodeBase<FrameNode, AnyFrameNode> {
readonly parent: AnyFrameNode;
readonly children: FrameNode[] = [];
readonly rawFrame: RawFrame;
frames: FrameImpl[] = [];
fragment?: FragmentImpl;
constructor(rawFrame: RawFrame, parent: AnyFrameNode) {
this.rawFrame = rawFrame;
this.parent = parent;
}
/**
* Produces the ancestor chain. Including `this` but excluding the `RootFrameNode`.
*/
* getCallStack(): Generator<FrameNode> {
// The `RootFrameNode` doesn't have an actual frame attached, that's why we check for `node.parent` instead of `node`.
for (let node: AnyFrameNode|null = this; node.parent; node = node.parent) {
yield node;
}
}
}
/**
* Stores stack trace fragments in a trie, but does not own them/keep them alive.
*/
export class Trie {
readonly #root: RootFrameNode = {parent: null, children: []};
/**
* Most sources produce stack traces in "top-to-bottom" order, so that is what this method expects.
*
* @returns The {@link FrameNode} corresponding to the top-most stack frame.
*/
insert(frames: RawFrame[]): FrameNode {
if (frames.length === 0) {
throw new Error('Trie.insert called with an empty frames array.');
}
let currentNode: AnyFrameNode = this.#root;
for (let i = frames.length - 1; i >= 0; --i) {
currentNode = this.#insert(currentNode, frames[i]);
}
return currentNode as FrameNode;
}
/**
* Inserts `rawFrame` into the children of the provided node if not already there.
*
* @returns the child node corresponding to `rawFrame`.
*/
#insert(node: AnyFrameNode, rawFrame: RawFrame): FrameNode {
let i = 0;
for (; i < node.children.length; ++i) {
const maybeChild = node.children[i];
const child = maybeChild instanceof WeakRef ? maybeChild.deref() : maybeChild;
if (!child) {
continue;
}
const compareResult = compareRawFrames(child.rawFrame, rawFrame);
if (compareResult === 0) {
return child;
}
if (compareResult > 0) {
break;
}
}
const newNode = new FrameNode(rawFrame, node);
if (node.parent) {
node.children.splice(i, 0, newNode);
} else {
node.children.splice(i, 0, new WeakRef(newNode));
}
return newNode;
}
/**
* Traverses the trie in pre-order.
*
* @param node Start at `node` or `null` to start with the children of the root.
* @param visit Called on each node in the trie. Return `true` if the visitor should descend into child nodes of the provided node.
*/
walk(node: FrameNode|null, visit: (node: FrameNode) => boolean): void {
const stack =
node ? [node] : [...this.#root.children].map(ref => ref.deref()).filter(node => Boolean(node)) as FrameNode[];
for (let node = stack.pop(); node; node = stack.pop()) {
const visitChildren = visit(node);
if (visitChildren) {
// Pushing the children in reverse means the "left-most" child is visited first (i.e. pre-order).
for (let i = node.children.length - 1; i >= 0; --i) {
stack.push(node.children[i]);
}
}
}
}
}
/**
* @returns a number < 0, 0 or > 0, if the `a` is smaller then, equal or greater then `b`.
*/
export function compareRawFrames(a: RawFrame, b: RawFrame): number {
const scriptIdCompare = (a.scriptId ?? '').localeCompare(b.scriptId ?? '');
if (scriptIdCompare !== 0) {
return scriptIdCompare;
}
const urlCompare = (a.url ?? '').localeCompare(b.url ?? '');
if (urlCompare !== 0) {
return urlCompare;
}
const nameCompare = (a.functionName ?? '').localeCompare(b.functionName ?? '');
if (nameCompare !== 0) {
return nameCompare;
}
if (a.lineNumber !== b.lineNumber) {
return a.lineNumber - b.lineNumber;
}
return a.columnNumber - b.columnNumber;
}