@crouton-kit/capture 1.4.2 → 1.4.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +187 -0
- package/bin/capture +31278 -7312
- package/package.json +3 -1
- package/src/capture.ts +21 -18
- package/src/cdp/args.ts +269 -49
- package/src/cdp/bridge/server.ts +6 -5
- package/src/cdp/bridge/spawn.ts +41 -97
- package/src/cdp/commands/bridge-serve.ts +11 -21
- package/src/cdp/commands/cdp.ts +44 -22
- package/src/cdp/commands/heap/census.ts +61 -0
- package/src/cdp/commands/heap/common.ts +77 -0
- package/src/cdp/commands/heap/diff.ts +92 -0
- package/src/cdp/commands/heap/index.ts +38 -0
- package/src/cdp/commands/heap/objects.ts +42 -0
- package/src/cdp/commands/heap/retainers.ts +52 -0
- package/src/cdp/commands/heap/snapshot.ts +94 -0
- package/src/cdp/commands/lib.ts +9 -17
- package/src/cdp/commands/lighthouse.ts +414 -0
- package/src/cdp/commands/measure/census.ts +2 -1
- package/src/cdp/commands/measure/check.ts +70 -16
- package/src/cdp/commands/measure/diff.ts +1 -2
- package/src/cdp/commands/measure/explain.ts +44 -7
- package/src/cdp/commands/measure/index.ts +24 -32
- package/src/cdp/commands/measure/map-ax.ts +2 -1
- package/src/cdp/commands/measure/map-focus.ts +2 -1
- package/src/cdp/commands/measure/map-layers.ts +5 -3
- package/src/cdp/commands/measure/map-scroll.ts +2 -1
- package/src/cdp/commands/measure/snap.ts +5 -4
- package/src/cdp/commands/measure/sweep.ts +16 -11
- package/src/cdp/commands/measure/text.ts +147 -0
- package/src/cdp/commands/motion/index.ts +10 -18
- package/src/cdp/commands/motion/jank.ts +13 -22
- package/src/cdp/commands/motion/mask.ts +10 -9
- package/src/cdp/commands/motion/rec.ts +228 -26
- package/src/cdp/commands/motion/timeline.ts +1 -1
- package/src/cdp/commands/page/click.ts +15 -5
- package/src/cdp/commands/page/elements.ts +43 -11
- package/src/cdp/commands/page/exec.ts +53 -18
- package/src/cdp/commands/page/index.ts +20 -20
- package/src/cdp/commands/page/inspect.ts +338 -0
- package/src/cdp/commands/page/navigate.ts +83 -38
- package/src/cdp/commands/page/repeat.ts +176 -0
- package/src/cdp/commands/page/scroll.ts +10 -5
- package/src/cdp/commands/page/shot.ts +66 -31
- package/src/cdp/commands/page/type.ts +6 -2
- package/src/cdp/commands/perf/index.ts +32 -0
- package/src/cdp/commands/perf/insights.ts +214 -0
- package/src/cdp/commands/perf/trace.ts +235 -0
- package/src/cdp/commands/perf/vitals.ts +76 -0
- package/src/cdp/commands/session/collectors.ts +54 -0
- package/src/cdp/commands/stub.ts +10 -0
- package/src/cdp/commands/tab/close.ts +2 -2
- package/src/cdp/commands/tab/index.ts +16 -23
- package/src/cdp/commands/tab/launch.ts +1 -1
- package/src/cdp/commands/tab/mock/index.ts +24 -0
- package/src/cdp/commands/tab/mock/lifecycle.ts +88 -0
- package/src/cdp/commands/tab/mock/start.ts +65 -0
- package/src/cdp/commands/tab/mock/stop.ts +39 -0
- package/src/cdp/commands/tab/network.ts +22 -13
- package/src/cdp/commands/tab/reset.ts +11 -12
- package/src/cdp/connection.ts +19 -39
- package/src/cdp/detect.ts +27 -7
- package/src/cdp/dispatch.ts +25 -4
- package/src/cdp/domains.ts +2 -2
- package/src/cdp/har-recorder.ts +0 -1
- package/src/cdp/heap-snapshot.ts +647 -0
- package/src/cdp/host/client.ts +22 -0
- package/src/cdp/host/collector.ts +74 -0
- package/src/cdp/host/collectors/heap.ts +79 -0
- package/src/cdp/host/collectors/intercept.ts +247 -0
- package/src/cdp/{recorder-bridge.ts → host/collectors/motion.ts} +388 -473
- package/src/cdp/host/collectors/trace.ts +108 -0
- package/src/cdp/host/core.ts +217 -0
- package/src/cdp/host/handle.ts +47 -0
- package/src/cdp/host/held-client.ts +91 -0
- package/src/cdp/host/kinds.ts +97 -0
- package/src/cdp/host/lifecycle.ts +123 -0
- package/src/cdp/host/server.ts +88 -0
- package/src/cdp/measure/check.ts +181 -16
- package/src/cdp/measure/collectors/hittest.ts +16 -10
- package/src/cdp/measure/collectors/scroll.ts +15 -7
- package/src/cdp/measure/collectors/style-provenance.ts +11 -0
- package/src/cdp/measure/collectors/styles.ts +4 -4
- package/src/cdp/measure/explain.ts +14 -2
- package/src/cdp/measure/map-layers.ts +52 -8
- package/src/cdp/measure/snapshot.ts +5 -3
- package/src/cdp/measure/sweep.ts +29 -6
- package/src/cdp/measure/text.ts +227 -0
- package/src/cdp/motion/jank.ts +29 -83
- package/src/cdp/motion/mask.ts +18 -9
- package/src/cdp/motion/recorder.ts +86 -731
- package/src/cdp/scope-lock.ts +9 -9
- package/src/cdp/screenshot.ts +51 -21
- package/src/cdp/timing.ts +5 -5
- package/src/cdp/trace-analysis.ts +320 -0
- package/src/cdp/types.ts +31 -1
- package/src/cdp.ts +4 -6
- package/src/har-manager.ts +2 -4
- package/src/interact.ts +50 -23
- package/src/output/artifact.ts +49 -5
- package/src/output/render.ts +60 -9
- package/src/output/selection.ts +6 -0
- package/src/session/artifacts.ts +122 -12
- package/src/session/commands.ts +348 -113
- package/src/session/coordinator.ts +0 -93
- package/src/session/har-redact.ts +29 -23
- package/src/session-context.ts +13 -9
- package/src/cdp/bridge/protocol.ts +0 -224
- package/src/cdp/recorder-client.ts +0 -326
- package/src/cdp/tab-lock.ts +0 -132
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
export interface RawHeapSnapshot {
|
|
2
|
+
snapshot: {
|
|
3
|
+
meta: {
|
|
4
|
+
node_fields: string[];
|
|
5
|
+
node_types: unknown[];
|
|
6
|
+
edge_fields: string[];
|
|
7
|
+
edge_types: unknown[];
|
|
8
|
+
};
|
|
9
|
+
node_count?: number;
|
|
10
|
+
edge_count?: number;
|
|
11
|
+
};
|
|
12
|
+
nodes: number[];
|
|
13
|
+
edges: number[];
|
|
14
|
+
strings: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface HeapNode {
|
|
18
|
+
index: number;
|
|
19
|
+
type: string;
|
|
20
|
+
name: string;
|
|
21
|
+
id: number;
|
|
22
|
+
selfSize: number;
|
|
23
|
+
edgeCount: number;
|
|
24
|
+
/** Chrome's snapshot detachedness metadata: 0 unknown, 1 attached, 2 detached. */
|
|
25
|
+
detachedness: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface HeapEdge {
|
|
29
|
+
index: number;
|
|
30
|
+
type: string;
|
|
31
|
+
nameOrIndex: number;
|
|
32
|
+
name: string | number;
|
|
33
|
+
from: number;
|
|
34
|
+
to: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RetainerPath {
|
|
38
|
+
nodes: HeapNode[];
|
|
39
|
+
edges: HeapEdge[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface RetainingPaths {
|
|
43
|
+
/** Paths are shortest edge-count paths to root, in stable source-edge order. */
|
|
44
|
+
selection: 'shortest-paths';
|
|
45
|
+
rootIndex: number;
|
|
46
|
+
maxPaths: number;
|
|
47
|
+
truncated: boolean;
|
|
48
|
+
paths: RetainerPath[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ApplicationRetainingPaths {
|
|
52
|
+
/** Paths start at the nearest application-owned object and end at the selected object. */
|
|
53
|
+
selection: 'nearest-application-owners';
|
|
54
|
+
maxPaths: number;
|
|
55
|
+
truncated: boolean;
|
|
56
|
+
paths: RetainerPath[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface DominatorTree {
|
|
60
|
+
rootIndex: number;
|
|
61
|
+
/** -1 means the node is not reachable from rootIndex. */
|
|
62
|
+
immediateDominators: Int32Array;
|
|
63
|
+
/** Chrome's self_size summed over every dominator-tree descendant. */
|
|
64
|
+
retainedSizes: Float64Array;
|
|
65
|
+
reachableNodeCount: number;
|
|
66
|
+
sizeQualification: 'retained size is the sum of Chrome self_size in the dominator subtree';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface DuplicateString {
|
|
70
|
+
value: string;
|
|
71
|
+
count: number;
|
|
72
|
+
totalSelfBytes: number;
|
|
73
|
+
/** Estimated as total self_size less one smallest instance; backing-store sharing is not modeled. */
|
|
74
|
+
wastedBytes: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface DuplicateStringAnalysis {
|
|
78
|
+
duplicates: DuplicateString[];
|
|
79
|
+
sizeQualification: 'self_size is Chrome-reported shallow size; wastedBytes assumes one smallest equal-content string is needed and does not model backing-store sharing';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ConstructorTotals {
|
|
83
|
+
nodeCount: number;
|
|
84
|
+
/** Nodes in this total whose Chrome detachedness metadata is detached (2). */
|
|
85
|
+
detachedNodeCount: number;
|
|
86
|
+
retainedSize: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface DetachedSnapshotNode {
|
|
90
|
+
objectId: number;
|
|
91
|
+
constructorName: string;
|
|
92
|
+
type: string;
|
|
93
|
+
detachedness: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ConstructorComparison {
|
|
97
|
+
constructorName: string;
|
|
98
|
+
added: ConstructorTotals;
|
|
99
|
+
removed: ConstructorTotals;
|
|
100
|
+
/** Nodes matched by Chrome snapshot object id whose retained size increased. retainedSize is the increase, not the current size. */
|
|
101
|
+
grown: ConstructorTotals;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface SnapshotComparison {
|
|
105
|
+
matching: 'Chrome snapshot node id';
|
|
106
|
+
constructors: ConstructorComparison[];
|
|
107
|
+
/** After-snapshot nodes that Chrome marks detached, sorted by constructor then object id. */
|
|
108
|
+
afterDetachedNodes: DetachedSnapshotNode[];
|
|
109
|
+
retainedSizeQualification: 'retained sizes are independently computed dominator-subtree sums, so constructor totals overlap and are not heap-total deltas';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface ReverseEdges {
|
|
113
|
+
starts: Int32Array;
|
|
114
|
+
edges: Int32Array;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function requiredField(fields: unknown, field: string, schema: string): number {
|
|
118
|
+
if (!Array.isArray(fields)) throw new Error(`Heap snapshot ${schema} must be an array.`);
|
|
119
|
+
const index = fields.indexOf(field);
|
|
120
|
+
if (index < 0) throw new Error(`Heap snapshot ${schema} is missing required field ${JSON.stringify(field)}.`);
|
|
121
|
+
return index;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function requiredEnum(types: unknown, index: number, schema: string): string[] {
|
|
125
|
+
if (!Array.isArray(types) || !Array.isArray(types[index]) || !types[index].every((value) => typeof value === 'string')) {
|
|
126
|
+
throw new Error(`Heap snapshot ${schema} must declare string values for its type field.`);
|
|
127
|
+
}
|
|
128
|
+
return types[index] as string[];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function checkedInteger(value: unknown, description: string): number {
|
|
132
|
+
if (!Number.isSafeInteger(value) || (value as number) < 0) throw new Error(`Heap snapshot ${description} must be a non-negative safe integer.`);
|
|
133
|
+
return value as number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A compact, schema-driven view of Chrome's JSON .heapsnapshot graph. */
|
|
137
|
+
export class HeapSnapshot {
|
|
138
|
+
readonly nodeCount: number;
|
|
139
|
+
readonly edgeCount: number;
|
|
140
|
+
private readonly strings: string[];
|
|
141
|
+
private readonly nodeTypes: string[];
|
|
142
|
+
private readonly edgeTypes: string[];
|
|
143
|
+
private readonly nodeTypeIndexes: Int32Array;
|
|
144
|
+
private readonly nameIndexes: Int32Array;
|
|
145
|
+
private readonly ids: Float64Array;
|
|
146
|
+
private readonly selfSizes: Float64Array;
|
|
147
|
+
private readonly detachedness: Int32Array;
|
|
148
|
+
private readonly edgeStarts: Int32Array;
|
|
149
|
+
private readonly targets: Int32Array;
|
|
150
|
+
private readonly sources: Int32Array;
|
|
151
|
+
private readonly edgeTypeIndexes: Int32Array;
|
|
152
|
+
private readonly edgeNameOrIndexes: Int32Array;
|
|
153
|
+
private reverseEdges?: ReverseEdges;
|
|
154
|
+
|
|
155
|
+
private constructor(data: {
|
|
156
|
+
strings: string[];
|
|
157
|
+
nodeTypes: string[];
|
|
158
|
+
edgeTypes: string[];
|
|
159
|
+
nodeTypeIndexes: Int32Array;
|
|
160
|
+
nameIndexes: Int32Array;
|
|
161
|
+
ids: Float64Array;
|
|
162
|
+
selfSizes: Float64Array;
|
|
163
|
+
detachedness: Int32Array;
|
|
164
|
+
edgeStarts: Int32Array;
|
|
165
|
+
targets: Int32Array;
|
|
166
|
+
sources: Int32Array;
|
|
167
|
+
edgeTypeIndexes: Int32Array;
|
|
168
|
+
edgeNameOrIndexes: Int32Array;
|
|
169
|
+
}) {
|
|
170
|
+
this.strings = data.strings;
|
|
171
|
+
this.nodeTypes = data.nodeTypes;
|
|
172
|
+
this.edgeTypes = data.edgeTypes;
|
|
173
|
+
this.nodeTypeIndexes = data.nodeTypeIndexes;
|
|
174
|
+
this.nameIndexes = data.nameIndexes;
|
|
175
|
+
this.ids = data.ids;
|
|
176
|
+
this.selfSizes = data.selfSizes;
|
|
177
|
+
this.detachedness = data.detachedness;
|
|
178
|
+
this.edgeStarts = data.edgeStarts;
|
|
179
|
+
this.targets = data.targets;
|
|
180
|
+
this.sources = data.sources;
|
|
181
|
+
this.edgeTypeIndexes = data.edgeTypeIndexes;
|
|
182
|
+
this.edgeNameOrIndexes = data.edgeNameOrIndexes;
|
|
183
|
+
this.nodeCount = data.nodeTypeIndexes.length;
|
|
184
|
+
this.edgeCount = data.targets.length;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
static parse(input: string | RawHeapSnapshot): HeapSnapshot {
|
|
188
|
+
let raw: RawHeapSnapshot;
|
|
189
|
+
try {
|
|
190
|
+
raw = typeof input === 'string' ? JSON.parse(input) : input;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
throw new Error(`Heap snapshot is not valid JSON: ${(error as Error).message}`);
|
|
193
|
+
}
|
|
194
|
+
if (!raw || typeof raw !== 'object' || !raw.snapshot?.meta) throw new Error('Heap snapshot is missing snapshot.meta.');
|
|
195
|
+
if (!Array.isArray(raw.nodes) || !Array.isArray(raw.edges) || !Array.isArray(raw.strings)) throw new Error('Heap snapshot must contain nodes, edges, and strings arrays.');
|
|
196
|
+
if (!raw.strings.every((value) => typeof value === 'string')) throw new Error('Heap snapshot strings must contain only strings.');
|
|
197
|
+
|
|
198
|
+
const meta = raw.snapshot.meta;
|
|
199
|
+
const nodeTypeField = requiredField(meta.node_fields, 'type', 'snapshot.meta.node_fields');
|
|
200
|
+
const nodeNameField = requiredField(meta.node_fields, 'name', 'snapshot.meta.node_fields');
|
|
201
|
+
const nodeIdField = requiredField(meta.node_fields, 'id', 'snapshot.meta.node_fields');
|
|
202
|
+
const nodeSelfSizeField = requiredField(meta.node_fields, 'self_size', 'snapshot.meta.node_fields');
|
|
203
|
+
const nodeEdgeCountField = requiredField(meta.node_fields, 'edge_count', 'snapshot.meta.node_fields');
|
|
204
|
+
const nodeDetachednessField = meta.node_fields.indexOf('detachedness');
|
|
205
|
+
const edgeTypeField = requiredField(meta.edge_fields, 'type', 'snapshot.meta.edge_fields');
|
|
206
|
+
const edgeNameField = requiredField(meta.edge_fields, 'name_or_index', 'snapshot.meta.edge_fields');
|
|
207
|
+
const edgeTargetField = requiredField(meta.edge_fields, 'to_node', 'snapshot.meta.edge_fields');
|
|
208
|
+
const nodeTypes = requiredEnum(meta.node_types, nodeTypeField, 'snapshot.meta.node_types');
|
|
209
|
+
const edgeTypes = requiredEnum(meta.edge_types, edgeTypeField, 'snapshot.meta.edge_types');
|
|
210
|
+
const nodeWidth = meta.node_fields.length;
|
|
211
|
+
const edgeWidth = meta.edge_fields.length;
|
|
212
|
+
if (nodeWidth === 0 || raw.nodes.length % nodeWidth !== 0) throw new Error(`Heap snapshot nodes length ${raw.nodes.length} is not divisible by declared node width ${nodeWidth}.`);
|
|
213
|
+
if (edgeWidth === 0 || raw.edges.length % edgeWidth !== 0) throw new Error(`Heap snapshot edges length ${raw.edges.length} is not divisible by declared edge width ${edgeWidth}.`);
|
|
214
|
+
const nodeCount = raw.nodes.length / nodeWidth;
|
|
215
|
+
const edgeCount = raw.edges.length / edgeWidth;
|
|
216
|
+
if (raw.snapshot.node_count !== undefined && raw.snapshot.node_count !== nodeCount) throw new Error(`Heap snapshot declares ${raw.snapshot.node_count} nodes but its nodes array contains ${nodeCount}.`);
|
|
217
|
+
if (raw.snapshot.edge_count !== undefined && raw.snapshot.edge_count !== edgeCount) throw new Error(`Heap snapshot declares ${raw.snapshot.edge_count} edges but its edges array contains ${edgeCount}.`);
|
|
218
|
+
|
|
219
|
+
const nodeTypeIndexes = new Int32Array(nodeCount);
|
|
220
|
+
const nameIndexes = new Int32Array(nodeCount);
|
|
221
|
+
const ids = new Float64Array(nodeCount);
|
|
222
|
+
const selfSizes = new Float64Array(nodeCount);
|
|
223
|
+
const detachedness = new Int32Array(nodeCount);
|
|
224
|
+
const edgeStarts = new Int32Array(nodeCount + 1);
|
|
225
|
+
let expectedEdges = 0;
|
|
226
|
+
for (let node = 0; node < nodeCount; node += 1) {
|
|
227
|
+
const offset = node * nodeWidth;
|
|
228
|
+
const typeIndex = checkedInteger(raw.nodes[offset + nodeTypeField], `node ${node} type`);
|
|
229
|
+
const nameIndex = checkedInteger(raw.nodes[offset + nodeNameField], `node ${node} name`);
|
|
230
|
+
const id = checkedInteger(raw.nodes[offset + nodeIdField], `node ${node} id`);
|
|
231
|
+
const selfSize = checkedInteger(raw.nodes[offset + nodeSelfSizeField], `node ${node} self_size`);
|
|
232
|
+
const edgeCountForNode = checkedInteger(raw.nodes[offset + nodeEdgeCountField], `node ${node} edge_count`);
|
|
233
|
+
const detachednessForNode = nodeDetachednessField < 0 ? 0 : checkedInteger(raw.nodes[offset + nodeDetachednessField], `node ${node} detachedness`);
|
|
234
|
+
if (typeIndex >= nodeTypes.length) throw new Error(`Heap snapshot node ${node} type index ${typeIndex} is outside snapshot.meta.node_types.`);
|
|
235
|
+
if (nameIndex >= raw.strings.length) throw new Error(`Heap snapshot node ${node} name index ${nameIndex} is outside strings.`);
|
|
236
|
+
if (expectedEdges + edgeCountForNode > edgeCount) throw new Error(`Heap snapshot node ${node} declares edges beyond the edges array.`);
|
|
237
|
+
nodeTypeIndexes[node] = typeIndex;
|
|
238
|
+
nameIndexes[node] = nameIndex;
|
|
239
|
+
ids[node] = id;
|
|
240
|
+
selfSizes[node] = selfSize;
|
|
241
|
+
detachedness[node] = detachednessForNode;
|
|
242
|
+
edgeStarts[node] = expectedEdges;
|
|
243
|
+
expectedEdges += edgeCountForNode;
|
|
244
|
+
}
|
|
245
|
+
edgeStarts[nodeCount] = expectedEdges;
|
|
246
|
+
if (expectedEdges !== edgeCount) throw new Error(`Heap snapshot node edge_count values total ${expectedEdges}, but edges array contains ${edgeCount}.`);
|
|
247
|
+
|
|
248
|
+
const targets = new Int32Array(edgeCount);
|
|
249
|
+
const sources = new Int32Array(edgeCount);
|
|
250
|
+
for (let node = 0; node < nodeCount; node += 1) {
|
|
251
|
+
for (let edge = edgeStarts[node]; edge < edgeStarts[node + 1]; edge += 1) sources[edge] = node;
|
|
252
|
+
}
|
|
253
|
+
const edgeTypeIndexes = new Int32Array(edgeCount);
|
|
254
|
+
const edgeNameOrIndexes = new Int32Array(edgeCount);
|
|
255
|
+
for (let edge = 0; edge < edgeCount; edge += 1) {
|
|
256
|
+
const offset = edge * edgeWidth;
|
|
257
|
+
const typeIndex = checkedInteger(raw.edges[offset + edgeTypeField], `edge ${edge} type`);
|
|
258
|
+
const nameOrIndex = checkedInteger(raw.edges[offset + edgeNameField], `edge ${edge} name_or_index`);
|
|
259
|
+
const targetOffset = checkedInteger(raw.edges[offset + edgeTargetField], `edge ${edge} to_node`);
|
|
260
|
+
if (typeIndex >= edgeTypes.length) throw new Error(`Heap snapshot edge ${edge} type index ${typeIndex} is outside snapshot.meta.edge_types.`);
|
|
261
|
+
if (targetOffset % nodeWidth !== 0 || targetOffset / nodeWidth >= nodeCount) throw new Error(`Heap snapshot edge ${edge} to_node ${targetOffset} is not a valid node-record offset.`);
|
|
262
|
+
targets[edge] = targetOffset / nodeWidth;
|
|
263
|
+
edgeTypeIndexes[edge] = typeIndex;
|
|
264
|
+
edgeNameOrIndexes[edge] = nameOrIndex;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return new HeapSnapshot({ strings: raw.strings, nodeTypes, edgeTypes, nodeTypeIndexes, nameIndexes, ids, selfSizes, detachedness, edgeStarts, targets, sources, edgeTypeIndexes, edgeNameOrIndexes });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
nodeAt(index: number): HeapNode {
|
|
271
|
+
this.assertNodeIndex(index);
|
|
272
|
+
return {
|
|
273
|
+
index,
|
|
274
|
+
type: this.nodeTypes[this.nodeTypeIndexes[index]],
|
|
275
|
+
name: this.strings[this.nameIndexes[index]],
|
|
276
|
+
id: this.ids[index],
|
|
277
|
+
selfSize: this.selfSizes[index],
|
|
278
|
+
edgeCount: this.edgeStarts[index + 1] - this.edgeStarts[index],
|
|
279
|
+
detachedness: this.detachedness[index],
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
edgeAt(index: number): HeapEdge {
|
|
284
|
+
if (!Number.isInteger(index) || index < 0 || index >= this.edgeCount) throw new Error(`Heap snapshot edge index ${index} is outside 0..${this.edgeCount - 1}.`);
|
|
285
|
+
const from = this.sourceForEdge(index);
|
|
286
|
+
const nameOrIndex = this.edgeNameOrIndexes[index];
|
|
287
|
+
return {
|
|
288
|
+
index,
|
|
289
|
+
type: this.edgeTypes[this.edgeTypeIndexes[index]],
|
|
290
|
+
nameOrIndex,
|
|
291
|
+
name: this.edgeTypes[this.edgeTypeIndexes[index]] === 'element' || this.edgeTypes[this.edgeTypeIndexes[index]] === 'hidden' ? nameOrIndex : this.strings[nameOrIndex] ?? nameOrIndex,
|
|
292
|
+
from,
|
|
293
|
+
to: this.targets[index],
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
outgoingEdges(nodeIndex: number): HeapEdge[] {
|
|
298
|
+
this.assertNodeIndex(nodeIndex);
|
|
299
|
+
const edges: HeapEdge[] = [];
|
|
300
|
+
for (let edge = this.edgeStarts[nodeIndex]; edge < this.edgeStarts[nodeIndex + 1]; edge += 1) edges.push(this.edgeAt(edge));
|
|
301
|
+
return edges;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
retainingPaths(nodeIndex: number, { rootIndex = 0, maxPaths = 1 }: { rootIndex?: number; maxPaths?: number } = {}): RetainingPaths {
|
|
305
|
+
this.assertNodeIndex(nodeIndex);
|
|
306
|
+
this.assertNodeIndex(rootIndex);
|
|
307
|
+
if (!Number.isInteger(maxPaths) || maxPaths < 1) throw new Error('maxPaths must be a positive integer.');
|
|
308
|
+
if (nodeIndex === rootIndex) return { selection: 'shortest-paths', rootIndex, maxPaths, truncated: false, paths: [{ nodes: [this.nodeAt(rootIndex)], edges: [] }] };
|
|
309
|
+
|
|
310
|
+
const reverse = this.getReverseEdges();
|
|
311
|
+
const distance = new Int32Array(this.nodeCount);
|
|
312
|
+
distance.fill(-1);
|
|
313
|
+
const queue = new Int32Array(this.nodeCount);
|
|
314
|
+
let head = 0;
|
|
315
|
+
let tail = 0;
|
|
316
|
+
distance[nodeIndex] = 0;
|
|
317
|
+
queue[tail++] = nodeIndex;
|
|
318
|
+
let rootDistance = -1;
|
|
319
|
+
while (head < tail) {
|
|
320
|
+
const current = queue[head++];
|
|
321
|
+
if (rootDistance >= 0 && distance[current] >= rootDistance) continue;
|
|
322
|
+
for (let cursor = reverse.starts[current]; cursor < reverse.starts[current + 1]; cursor += 1) {
|
|
323
|
+
const source = this.sourceForEdge(reverse.edges[cursor]);
|
|
324
|
+
if (distance[source] >= 0) continue;
|
|
325
|
+
distance[source] = distance[current] + 1;
|
|
326
|
+
if (source === rootIndex) rootDistance = distance[source];
|
|
327
|
+
queue[tail++] = source;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (rootDistance < 0) return { selection: 'shortest-paths', rootIndex, maxPaths, truncated: false, paths: [] };
|
|
331
|
+
|
|
332
|
+
// Count root-reaching shortest paths, capped so this remains linear even
|
|
333
|
+
// when a diamond-shaped graph has exponentially many possible paths.
|
|
334
|
+
const pathCounts = new Int32Array(this.nodeCount);
|
|
335
|
+
pathCounts[rootIndex] = 1;
|
|
336
|
+
for (let position = tail - 1; position >= 0; position -= 1) {
|
|
337
|
+
const current = queue[position];
|
|
338
|
+
if (current === rootIndex || distance[current] >= rootDistance) continue;
|
|
339
|
+
let count = 0;
|
|
340
|
+
for (let cursor = reverse.starts[current]; cursor < reverse.starts[current + 1]; cursor += 1) {
|
|
341
|
+
const source = this.sourceForEdge(reverse.edges[cursor]);
|
|
342
|
+
if (distance[source] === distance[current] + 1) count = Math.min(maxPaths + 1, count + pathCounts[source]);
|
|
343
|
+
}
|
|
344
|
+
pathCounts[current] = count;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const paths: RetainerPath[] = [];
|
|
348
|
+
const nodePath = [nodeIndex];
|
|
349
|
+
const edgePath: number[] = [];
|
|
350
|
+
const cursors = [reverse.starts[nodeIndex]];
|
|
351
|
+
while (nodePath.length > 0 && paths.length < maxPaths) {
|
|
352
|
+
const current = nodePath[nodePath.length - 1];
|
|
353
|
+
if (current === rootIndex) {
|
|
354
|
+
paths.push({ nodes: [...nodePath].reverse().map((index) => this.nodeAt(index)), edges: [...edgePath].reverse().map((index) => this.edgeAt(index)) });
|
|
355
|
+
nodePath.pop();
|
|
356
|
+
cursors.pop();
|
|
357
|
+
edgePath.pop();
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const cursor = cursors[cursors.length - 1];
|
|
361
|
+
if (cursor >= reverse.starts[current + 1]) {
|
|
362
|
+
nodePath.pop();
|
|
363
|
+
cursors.pop();
|
|
364
|
+
edgePath.pop();
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
cursors[cursors.length - 1] = cursor + 1;
|
|
368
|
+
const edge = reverse.edges[cursor];
|
|
369
|
+
const source = this.sourceForEdge(edge);
|
|
370
|
+
if (distance[source] !== distance[current] + 1 || pathCounts[source] === 0) continue;
|
|
371
|
+
nodePath.push(source);
|
|
372
|
+
edgePath.push(edge);
|
|
373
|
+
cursors.push(reverse.starts[source]);
|
|
374
|
+
}
|
|
375
|
+
return { selection: 'shortest-paths', rootIndex, maxPaths, truncated: pathCounts[nodeIndex] > maxPaths, paths };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
applicationRetainingPaths(nodeIndex: number, { maxPaths = 1 }: { maxPaths?: number } = {}): ApplicationRetainingPaths {
|
|
379
|
+
this.assertNodeIndex(nodeIndex);
|
|
380
|
+
if (!Number.isInteger(maxPaths) || maxPaths < 1) throw new Error('maxPaths must be a positive integer.');
|
|
381
|
+
|
|
382
|
+
const reverse = this.getReverseEdges();
|
|
383
|
+
const distance = new Int32Array(this.nodeCount);
|
|
384
|
+
distance.fill(-1);
|
|
385
|
+
const queue = new Int32Array(this.nodeCount);
|
|
386
|
+
let head = 0;
|
|
387
|
+
let tail = 0;
|
|
388
|
+
let ownerDistance = -1;
|
|
389
|
+
const owners: number[] = [];
|
|
390
|
+
distance[nodeIndex] = 0;
|
|
391
|
+
queue[tail++] = nodeIndex;
|
|
392
|
+
|
|
393
|
+
while (head < tail) {
|
|
394
|
+
const current = queue[head++];
|
|
395
|
+
const currentDistance = distance[current];
|
|
396
|
+
if (ownerDistance >= 0 && currentDistance > ownerDistance) break;
|
|
397
|
+
if (current !== nodeIndex && this.isApplicationOwner(current)) {
|
|
398
|
+
ownerDistance = currentDistance;
|
|
399
|
+
owners.push(current);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (ownerDistance >= 0) continue;
|
|
403
|
+
for (let cursor = reverse.starts[current]; cursor < reverse.starts[current + 1]; cursor += 1) {
|
|
404
|
+
const edge = reverse.edges[cursor];
|
|
405
|
+
const source = this.sourceForEdge(edge);
|
|
406
|
+
if (distance[source] >= 0) continue;
|
|
407
|
+
distance[source] = currentDistance + 1;
|
|
408
|
+
queue[tail++] = source;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const paths: RetainerPath[] = [];
|
|
413
|
+
let truncated = false;
|
|
414
|
+
for (const owner of owners) {
|
|
415
|
+
const nodePath = [owner];
|
|
416
|
+
const edgePath: number[] = [];
|
|
417
|
+
const cursors = [this.edgeStarts[owner]];
|
|
418
|
+
while (nodePath.length > 0) {
|
|
419
|
+
const current = nodePath[nodePath.length - 1];
|
|
420
|
+
if (current === nodeIndex) {
|
|
421
|
+
if (paths.length === maxPaths) {
|
|
422
|
+
truncated = true;
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
paths.push({ nodes: nodePath.map(index => this.nodeAt(index)), edges: edgePath.map(index => this.edgeAt(index)) });
|
|
426
|
+
nodePath.pop();
|
|
427
|
+
cursors.pop();
|
|
428
|
+
edgePath.pop();
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const cursor = cursors[cursors.length - 1];
|
|
432
|
+
if (cursor >= this.edgeStarts[current + 1]) {
|
|
433
|
+
nodePath.pop();
|
|
434
|
+
cursors.pop();
|
|
435
|
+
edgePath.pop();
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
cursors[cursors.length - 1] = cursor + 1;
|
|
439
|
+
const target = this.targets[cursor];
|
|
440
|
+
if (distance[target] !== distance[current] - 1) continue;
|
|
441
|
+
nodePath.push(target);
|
|
442
|
+
edgePath.push(cursor);
|
|
443
|
+
cursors.push(this.edgeStarts[target]);
|
|
444
|
+
}
|
|
445
|
+
if (truncated) break;
|
|
446
|
+
}
|
|
447
|
+
return { selection: 'nearest-application-owners', maxPaths, truncated, paths };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
computeDominators(rootIndex = 0): DominatorTree {
|
|
451
|
+
this.assertNodeIndex(rootIndex);
|
|
452
|
+
const dfsNumber = new Int32Array(this.nodeCount);
|
|
453
|
+
const vertex = new Int32Array(this.nodeCount + 1);
|
|
454
|
+
const parent = new Int32Array(this.nodeCount + 1);
|
|
455
|
+
const stackNodes = new Int32Array(this.nodeCount);
|
|
456
|
+
const stackEdges = new Int32Array(this.nodeCount);
|
|
457
|
+
let reachable = 1;
|
|
458
|
+
dfsNumber[rootIndex] = 1;
|
|
459
|
+
vertex[1] = rootIndex;
|
|
460
|
+
let depth = 0;
|
|
461
|
+
stackNodes[0] = rootIndex;
|
|
462
|
+
stackEdges[0] = this.edgeStarts[rootIndex];
|
|
463
|
+
while (depth >= 0) {
|
|
464
|
+
const node = stackNodes[depth];
|
|
465
|
+
const edge = stackEdges[depth];
|
|
466
|
+
if (edge >= this.edgeStarts[node + 1]) {
|
|
467
|
+
depth -= 1;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
stackEdges[depth] = edge + 1;
|
|
471
|
+
const target = this.targets[edge];
|
|
472
|
+
if (dfsNumber[target] !== 0) continue;
|
|
473
|
+
reachable += 1;
|
|
474
|
+
dfsNumber[target] = reachable;
|
|
475
|
+
vertex[reachable] = target;
|
|
476
|
+
parent[reachable] = dfsNumber[node];
|
|
477
|
+
depth += 1;
|
|
478
|
+
stackNodes[depth] = target;
|
|
479
|
+
stackEdges[depth] = this.edgeStarts[target];
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const semi = new Int32Array(reachable + 1);
|
|
483
|
+
const label = new Int32Array(reachable + 1);
|
|
484
|
+
const ancestor = new Int32Array(reachable + 1);
|
|
485
|
+
const idom = new Int32Array(reachable + 1);
|
|
486
|
+
const bucketHead = new Int32Array(reachable + 1);
|
|
487
|
+
const bucketNext = new Int32Array(reachable + 1);
|
|
488
|
+
const compressPath = new Int32Array(reachable + 1);
|
|
489
|
+
for (let i = 1; i <= reachable; i += 1) {
|
|
490
|
+
semi[i] = i;
|
|
491
|
+
label[i] = i;
|
|
492
|
+
}
|
|
493
|
+
const reverse = this.getReverseEdges();
|
|
494
|
+
const compress = (node: number): void => {
|
|
495
|
+
let length = 0;
|
|
496
|
+
let current = node;
|
|
497
|
+
while (ancestor[current] !== 0 && ancestor[ancestor[current]] !== 0) {
|
|
498
|
+
compressPath[length++] = current;
|
|
499
|
+
current = ancestor[current];
|
|
500
|
+
}
|
|
501
|
+
while (length > 0) {
|
|
502
|
+
current = compressPath[--length];
|
|
503
|
+
const parentNode = ancestor[current];
|
|
504
|
+
if (semi[label[parentNode]] < semi[label[current]]) label[current] = label[parentNode];
|
|
505
|
+
ancestor[current] = ancestor[parentNode];
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
const evaluate = (node: number): number => {
|
|
509
|
+
if (ancestor[node] === 0) return label[node];
|
|
510
|
+
compress(node);
|
|
511
|
+
return label[node];
|
|
512
|
+
};
|
|
513
|
+
for (let current = reachable; current >= 2; current -= 1) {
|
|
514
|
+
const node = vertex[current];
|
|
515
|
+
for (let cursor = reverse.starts[node]; cursor < reverse.starts[node + 1]; cursor += 1) {
|
|
516
|
+
const predecessor = dfsNumber[this.sourceForEdge(reverse.edges[cursor])];
|
|
517
|
+
if (predecessor === 0) continue;
|
|
518
|
+
const candidate = evaluate(predecessor);
|
|
519
|
+
if (semi[candidate] < semi[current]) semi[current] = semi[candidate];
|
|
520
|
+
}
|
|
521
|
+
bucketNext[current] = bucketHead[semi[current]];
|
|
522
|
+
bucketHead[semi[current]] = current;
|
|
523
|
+
ancestor[current] = parent[current];
|
|
524
|
+
let member = bucketHead[parent[current]];
|
|
525
|
+
bucketHead[parent[current]] = 0;
|
|
526
|
+
while (member !== 0) {
|
|
527
|
+
const next = bucketNext[member];
|
|
528
|
+
const candidate = evaluate(member);
|
|
529
|
+
idom[member] = semi[candidate] < semi[member] ? candidate : parent[current];
|
|
530
|
+
member = next;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
for (let current = 2; current <= reachable; current += 1) {
|
|
534
|
+
if (idom[current] !== semi[current]) idom[current] = idom[idom[current]];
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const immediateDominators = new Int32Array(this.nodeCount);
|
|
538
|
+
immediateDominators.fill(-1);
|
|
539
|
+
immediateDominators[rootIndex] = rootIndex;
|
|
540
|
+
const retainedSizes = new Float64Array(this.nodeCount);
|
|
541
|
+
for (let current = 1; current <= reachable; current += 1) retainedSizes[vertex[current]] = this.selfSizes[vertex[current]];
|
|
542
|
+
for (let current = reachable; current >= 2; current -= 1) {
|
|
543
|
+
const node = vertex[current];
|
|
544
|
+
const dominator = vertex[idom[current]];
|
|
545
|
+
immediateDominators[node] = dominator;
|
|
546
|
+
retainedSizes[dominator] += retainedSizes[node];
|
|
547
|
+
}
|
|
548
|
+
return { rootIndex, immediateDominators, retainedSizes, reachableNodeCount: reachable, sizeQualification: 'retained size is the sum of Chrome self_size in the dominator subtree' };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
duplicateStrings(): DuplicateStringAnalysis {
|
|
552
|
+
const groups = new Map<number, { count: number; totalSelfBytes: number; smallestSelfBytes: number }>();
|
|
553
|
+
for (let node = 0; node < this.nodeCount; node += 1) {
|
|
554
|
+
if (this.nodeTypes[this.nodeTypeIndexes[node]] !== 'string') continue;
|
|
555
|
+
const nameIndex = this.nameIndexes[node];
|
|
556
|
+
const group = groups.get(nameIndex) ?? { count: 0, totalSelfBytes: 0, smallestSelfBytes: Number.POSITIVE_INFINITY };
|
|
557
|
+
group.count += 1;
|
|
558
|
+
group.totalSelfBytes += this.selfSizes[node];
|
|
559
|
+
group.smallestSelfBytes = Math.min(group.smallestSelfBytes, this.selfSizes[node]);
|
|
560
|
+
groups.set(nameIndex, group);
|
|
561
|
+
}
|
|
562
|
+
const duplicates: DuplicateString[] = [];
|
|
563
|
+
for (const [nameIndex, group] of groups) {
|
|
564
|
+
if (group.count > 1) duplicates.push({ value: this.strings[nameIndex], count: group.count, totalSelfBytes: group.totalSelfBytes, wastedBytes: group.totalSelfBytes - group.smallestSelfBytes });
|
|
565
|
+
}
|
|
566
|
+
return { duplicates, sizeQualification: 'self_size is Chrome-reported shallow size; wastedBytes assumes one smallest equal-content string is needed and does not model backing-store sharing' };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
static compare(before: HeapSnapshot, after: HeapSnapshot, rootIndex = 0): SnapshotComparison {
|
|
570
|
+
const beforeDominators = before.computeDominators(rootIndex);
|
|
571
|
+
const afterDominators = after.computeDominators(rootIndex);
|
|
572
|
+
const beforeById = new Map<number, number>();
|
|
573
|
+
for (let node = 0; node < before.nodeCount; node += 1) beforeById.set(before.ids[node], node);
|
|
574
|
+
const groups = new Map<string, ConstructorComparison>();
|
|
575
|
+
const groupFor = (name: string): ConstructorComparison => {
|
|
576
|
+
let group = groups.get(name);
|
|
577
|
+
if (!group) {
|
|
578
|
+
group = { constructorName: name, added: { nodeCount: 0, detachedNodeCount: 0, retainedSize: 0 }, removed: { nodeCount: 0, detachedNodeCount: 0, retainedSize: 0 }, grown: { nodeCount: 0, detachedNodeCount: 0, retainedSize: 0 } };
|
|
579
|
+
groups.set(name, group);
|
|
580
|
+
}
|
|
581
|
+
return group;
|
|
582
|
+
};
|
|
583
|
+
const matchedBefore = new Uint8Array(before.nodeCount);
|
|
584
|
+
const afterDetachedNodes: DetachedSnapshotNode[] = [];
|
|
585
|
+
for (let node = 0; node < after.nodeCount; node += 1) {
|
|
586
|
+
if (after.detachedness[node] === 2) afterDetachedNodes.push({ objectId: after.ids[node], constructorName: after.strings[after.nameIndexes[node]], type: after.nodeTypes[after.nodeTypeIndexes[node]], detachedness: after.detachedness[node] });
|
|
587
|
+
const beforeNode = beforeById.get(after.ids[node]);
|
|
588
|
+
if (beforeNode === undefined) {
|
|
589
|
+
const added = groupFor(after.strings[after.nameIndexes[node]]).added;
|
|
590
|
+
added.nodeCount += 1;
|
|
591
|
+
if (after.detachedness[node] === 2) added.detachedNodeCount += 1;
|
|
592
|
+
added.retainedSize += afterDominators.retainedSizes[node];
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
matchedBefore[beforeNode] = 1;
|
|
596
|
+
const increase = afterDominators.retainedSizes[node] - beforeDominators.retainedSizes[beforeNode];
|
|
597
|
+
if (increase > 0) {
|
|
598
|
+
const grown = groupFor(after.strings[after.nameIndexes[node]]).grown;
|
|
599
|
+
grown.nodeCount += 1;
|
|
600
|
+
if (after.detachedness[node] === 2) grown.detachedNodeCount += 1;
|
|
601
|
+
grown.retainedSize += increase;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
for (let node = 0; node < before.nodeCount; node += 1) {
|
|
605
|
+
if (matchedBefore[node]) continue;
|
|
606
|
+
const removed = groupFor(before.strings[before.nameIndexes[node]]).removed;
|
|
607
|
+
removed.nodeCount += 1;
|
|
608
|
+
if (before.detachedness[node] === 2) removed.detachedNodeCount += 1;
|
|
609
|
+
removed.retainedSize += beforeDominators.retainedSizes[node];
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
matching: 'Chrome snapshot node id',
|
|
613
|
+
constructors: [...groups.values()],
|
|
614
|
+
afterDetachedNodes: afterDetachedNodes.sort((a, b) => a.constructorName.localeCompare(b.constructorName) || a.objectId - b.objectId),
|
|
615
|
+
retainedSizeQualification: 'retained sizes are independently computed dominator-subtree sums, so constructor totals overlap and are not heap-total deltas',
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private isApplicationOwner(index: number): boolean {
|
|
620
|
+
const type = this.nodeTypes[this.nodeTypeIndexes[index]];
|
|
621
|
+
if (type === 'closure') return true;
|
|
622
|
+
if (!['object', 'array'].includes(type)) return false;
|
|
623
|
+
const name = this.strings[this.nameIndexes[index]];
|
|
624
|
+
return !/^(\(|NativeContext$|system \/ Context$|Window(?: \(global\*\))?(?: \/.*)?$|global handles$|traced handles$)/.test(name);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
private getReverseEdges(): ReverseEdges {
|
|
628
|
+
if (this.reverseEdges) return this.reverseEdges;
|
|
629
|
+
const starts = new Int32Array(this.nodeCount + 1);
|
|
630
|
+
for (let edge = 0; edge < this.edgeCount; edge += 1) starts[this.targets[edge] + 1] += 1;
|
|
631
|
+
for (let node = 1; node <= this.nodeCount; node += 1) starts[node] += starts[node - 1];
|
|
632
|
+
const cursor = starts.slice(0, this.nodeCount);
|
|
633
|
+
const edges = new Int32Array(this.edgeCount);
|
|
634
|
+
for (let edge = 0; edge < this.edgeCount; edge += 1) edges[cursor[this.targets[edge]]++] = edge;
|
|
635
|
+
this.reverseEdges = { starts, edges };
|
|
636
|
+
return this.reverseEdges;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private sourceForEdge(edgeIndex: number): number {
|
|
640
|
+
return this.sources[edgeIndex];
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
private assertNodeIndex(index: number): void {
|
|
645
|
+
if (!Number.isInteger(index) || index < 0 || index >= this.nodeCount) throw new Error(`Heap snapshot node index ${index} is outside 0..${this.nodeCount - 1}.`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as net from 'node:net';
|
|
2
|
+
|
|
3
|
+
let requestId = 0;
|
|
4
|
+
export interface HostResponse { reqId: number; ok: boolean; type: string; error?: string; [key: string]: unknown; }
|
|
5
|
+
|
|
6
|
+
export function sendHostRequest(socketPath: string, request: Record<string, unknown>, timeoutMs = 15_000): Promise<HostResponse> {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const socket = net.createConnection(socketPath);
|
|
9
|
+
let buffer = '';
|
|
10
|
+
const timer = setTimeout(() => { socket.destroy(); reject(new Error(`collector host request timed out after ${timeoutMs}ms`)); }, timeoutMs);
|
|
11
|
+
socket.once('connect', () => socket.write(JSON.stringify({ ...request, reqId: ++requestId }) + '\n'));
|
|
12
|
+
socket.on('data', chunk => {
|
|
13
|
+
buffer += chunk.toString('utf8');
|
|
14
|
+
const newline = buffer.indexOf('\n');
|
|
15
|
+
if (newline === -1) return;
|
|
16
|
+
clearTimeout(timer);
|
|
17
|
+
socket.end();
|
|
18
|
+
try { resolve(JSON.parse(buffer.slice(0, newline)) as HostResponse); } catch (error) { reject(error); }
|
|
19
|
+
});
|
|
20
|
+
socket.once('error', error => { clearTimeout(timer); reject(error); });
|
|
21
|
+
});
|
|
22
|
+
}
|