Skip to content

Commit 31818f4

Browse files
szuendDevtools-frontend LUCI CQ
authored andcommitted
[stack_trace] Implement insertion into fragment trie
[email protected] Bug: 433162438 Change-Id: I8b292f6ac1a3d71f49aabdd8ed34a347c202263c Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6839778 Reviewed-by: Philip Pfaffe <[email protected]> Commit-Queue: Simon Zünd <[email protected]>
1 parent dfc9ef6 commit 31818f4

File tree

5 files changed

+138
-3
lines changed

5 files changed

+138
-3
lines changed

front_end/models/stack_trace/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,6 @@ ts_library("unittests") {
5555
deps = [
5656
":bundle",
5757
":impl",
58+
"../../testing",
5859
]
5960
}

front_end/models/stack_trace/Trie.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import {protocolCallFrame} from '../../testing/StackTraceHelpers.js';
6+
57
import * as StackTraceImpl from './stack_trace_impl.js';
68

79
describe('Trie', () => {
@@ -11,5 +13,47 @@ describe('Trie', () => {
1113

1214
assert.throws(() => trie.insert([]));
1315
});
16+
17+
it('returns the same node when inserting the same frame twice', () => {
18+
const trie = new StackTraceImpl.Trie.Trie();
19+
const frame = protocolCallFrame('foo.js:1:foo:1:10');
20+
21+
const node1 = trie.insert([frame]);
22+
const node2 = trie.insert([frame]);
23+
24+
assert.strictEqual(node1, node2);
25+
assert.strictEqual(StackTraceImpl.Trie.compareRawFrames(frame, node1.rawFrame), 0);
26+
});
27+
28+
it('returns different nodes when inserting different frames', () => {
29+
const trie = new StackTraceImpl.Trie.Trie();
30+
const frame1 = protocolCallFrame('foo.js:1:foo:1:10');
31+
const frame2 = protocolCallFrame('foo.js:1:bar:2:20');
32+
33+
const node1 = trie.insert([frame1]);
34+
const node2 = trie.insert([frame2]);
35+
36+
assert.notStrictEqual(node1, node2);
37+
assert.strictEqual(StackTraceImpl.Trie.compareRawFrames(frame1, node1.rawFrame), 0);
38+
assert.strictEqual(StackTraceImpl.Trie.compareRawFrames(frame2, node2.rawFrame), 0);
39+
});
40+
41+
it('creates 3 nodes for 2 stack traces with 1 shared parent call frame', () => {
42+
const trie = new StackTraceImpl.Trie.Trie();
43+
44+
const node1 = trie.insert([
45+
'foo.js::x:1:10',
46+
'foo.js::y:2:20',
47+
].map(protocolCallFrame));
48+
const node2 = trie.insert([
49+
'foo.js::x:1:15',
50+
'foo.js::y:2:20',
51+
].map(protocolCallFrame));
52+
53+
assert.strictEqual(node1.rawFrame.columnNumber, 10);
54+
assert.strictEqual(node2.rawFrame.columnNumber, 15);
55+
56+
assert.strictEqual(node1.parent, node2.parent);
57+
});
1458
});
1559
});

front_end/models/stack_trace/Trie.ts

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,85 @@ export interface FrameNode extends FrameNodeBase<FrameNode, AnyFrameNode> {
3434
* Stores stack trace fragments in a trie, but does not own them/keep them alive.
3535
*/
3636
export class Trie {
37-
// eslint-disable-next-line no-unused-private-class-members
3837
readonly #root: RootFrameNode = {parent: null, children: []};
3938

39+
/**
40+
* Most sources produce stack traces in "top-to-bottom" order, so that is what this method expects.
41+
*
42+
* @returns The {@link FrameNode} corresponding to the top-most stack frame.
43+
*/
4044
insert(frames: RawFrame[]): FrameNode {
4145
if (frames.length === 0) {
4246
throw new Error('Trie.insert called with an empty frames array.');
4347
}
4448

45-
// TODO(crbug.com/433162438): Implement it.
46-
throw new Error('Not implemented');
49+
let currentNode: AnyFrameNode = this.#root;
50+
for (let i = frames.length - 1; i >= 0; --i) {
51+
currentNode = this.#insert(currentNode, frames[i]);
52+
}
53+
return currentNode as FrameNode;
54+
}
55+
56+
/**
57+
* Inserts `rawFrame` into the children of the provided node if not already there.
58+
*
59+
* @returns the child node corresponding to `rawFrame`.
60+
*/
61+
#insert(node: AnyFrameNode, rawFrame: RawFrame): FrameNode {
62+
let i = 0;
63+
for (; i < node.children.length; ++i) {
64+
const maybeChild = node.children[i];
65+
const child = maybeChild instanceof WeakRef ? maybeChild.deref() : maybeChild;
66+
if (!child) {
67+
continue;
68+
}
69+
70+
const compareResult = compareRawFrames(child.rawFrame, rawFrame);
71+
if (compareResult === 0) {
72+
return child;
73+
}
74+
if (compareResult > 0) {
75+
break;
76+
}
77+
}
78+
79+
const newNode: FrameNode = {
80+
parent: node,
81+
children: [],
82+
rawFrame,
83+
frames: [],
84+
};
85+
if (node.parent) {
86+
node.children.splice(i, 0, newNode);
87+
} else {
88+
node.children.splice(i, 0, new WeakRef(newNode));
89+
}
90+
return newNode;
4791
}
4892
}
93+
94+
/**
95+
* @returns a number < 0, 0 or > 0, if the `a` is smaller then, equal or greater then `b`.
96+
*/
97+
export function compareRawFrames(a: RawFrame, b: RawFrame): number {
98+
const scriptIdCompare = (a.scriptId ?? '').localeCompare(b.scriptId ?? '');
99+
if (scriptIdCompare !== 0) {
100+
return scriptIdCompare;
101+
}
102+
103+
const urlCompare = (a.url ?? '').localeCompare(b.url ?? '');
104+
if (urlCompare !== 0) {
105+
return urlCompare;
106+
}
107+
108+
const nameCompare = (a.functionName ?? '').localeCompare(b.functionName ?? '');
109+
if (nameCompare !== 0) {
110+
return nameCompare;
111+
}
112+
113+
if (a.lineNumber !== b.lineNumber) {
114+
return a.lineNumber - b.lineNumber;
115+
}
116+
117+
return a.columnNumber - b.columnNumber;
118+
}

front_end/testing/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ ts_library("testing") {
4141
"SourceMapEncoder.test.ts",
4242
"SourceMapEncoder.ts",
4343
"SourceMapHelpers.ts",
44+
"StackTraceHelpers.ts",
4445
"StorageItemsViewHelpers.ts",
4546
"StubIssue.ts",
4647
"StyleHelpers.ts",
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import type * as Protocol from '../generated/protocol.js';
6+
7+
/**
8+
* Easily create `Protocol.Runtime.CallFrame`s by passing a string of the format: `<url>:<scriptId>:<name>:<line>:<column>`
9+
*/
10+
export function protocolCallFrame(descriptor: string): Protocol.Runtime.CallFrame {
11+
const parts = descriptor.split(':', 5);
12+
return {
13+
url: parts[0],
14+
scriptId: parts[1] as Protocol.Runtime.ScriptId,
15+
functionName: parts[2],
16+
lineNumber: parts[3] ? Number.parseInt(parts[3], 10) : -1,
17+
columnNumber: parts[4] ? Number.parseInt(parts[4], 10) : -1,
18+
};
19+
}

0 commit comments

Comments
 (0)