trace-engine-cli 0.0.0-stage → 0.1.0
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/LICENSE +202 -0
- package/README.md +342 -2
- package/dist/bin/trace-cli.d.ts +2 -0
- package/dist/bin/trace-cli.js +19 -0
- package/dist/src/cli.d.ts +1 -0
- package/dist/src/cli.js +477 -0
- package/dist/src/engine/adapter.d.ts +13 -0
- package/dist/src/engine/adapter.js +51 -0
- package/dist/src/engine/extractors/diff.d.ts +12 -0
- package/dist/src/engine/extractors/diff.js +100 -0
- package/dist/src/engine/extractors/events.d.ts +27 -0
- package/dist/src/engine/extractors/events.js +84 -0
- package/dist/src/engine/extractors/find.d.ts +12 -0
- package/dist/src/engine/extractors/find.js +100 -0
- package/dist/src/engine/extractors/insights.d.ts +15 -0
- package/dist/src/engine/extractors/insights.js +153 -0
- package/dist/src/engine/extractors/interactions.d.ts +7 -0
- package/dist/src/engine/extractors/interactions.js +55 -0
- package/dist/src/engine/extractors/navigations.d.ts +10 -0
- package/dist/src/engine/extractors/navigations.js +82 -0
- package/dist/src/engine/extractors/network.d.ts +10 -0
- package/dist/src/engine/extractors/network.js +105 -0
- package/dist/src/engine/extractors/overview.d.ts +11 -0
- package/dist/src/engine/extractors/overview.js +232 -0
- package/dist/src/engine/extractors/profile.d.ts +12 -0
- package/dist/src/engine/extractors/profile.js +83 -0
- package/dist/src/engine/extractors/scan.d.ts +37 -0
- package/dist/src/engine/extractors/scan.js +218 -0
- package/dist/src/engine/extractors/scheduling.d.ts +10 -0
- package/dist/src/engine/extractors/scheduling.js +89 -0
- package/dist/src/engine/extractors/shifts.d.ts +11 -0
- package/dist/src/engine/extractors/shifts.js +118 -0
- package/dist/src/engine/extractors/tasks.d.ts +13 -0
- package/dist/src/engine/extractors/tasks.js +156 -0
- package/dist/src/engine/helpers.d.ts +157 -0
- package/dist/src/engine/helpers.js +95 -0
- package/dist/src/engine/loader.d.ts +34 -0
- package/dist/src/engine/loader.js +142 -0
- package/dist/src/engine/types.d.ts +452 -0
- package/dist/src/engine/types.js +14 -0
- package/dist/src/formatters/index.d.ts +6 -0
- package/dist/src/formatters/index.js +66 -0
- package/dist/src/formatters/markdown.d.ts +15 -0
- package/dist/src/formatters/markdown.js +474 -0
- package/dist/src/formatters/toon.d.ts +1 -0
- package/dist/src/formatters/toon.js +17 -0
- package/dist/src/utils/filter.d.ts +47 -0
- package/dist/src/utils/filter.js +136 -0
- package/dist/src/utils/units.d.ts +24 -0
- package/dist/src/utils/units.js +57 -0
- package/package.json +45 -4
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type TimeRange } from '../../utils/filter.js';
|
|
2
|
+
import type { LoadedTrace } from '../loader.js';
|
|
3
|
+
import type { ShiftsResult } from '../types.js';
|
|
4
|
+
export interface ExtractShiftsOptions {
|
|
5
|
+
timeRange?: TimeRange;
|
|
6
|
+
includeEvents?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Extracts layout shift clusters, scores, and culprit elements.
|
|
10
|
+
*/
|
|
11
|
+
export declare function extractShifts(loadedTrace: LoadedTrace, options?: ExtractShiftsOptions): ShiftsResult;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Copyright 2026 Google LLC
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
import { isEventInBounds } from '../../utils/filter.js';
|
|
15
|
+
import { microToMilli, round } from '../../utils/units.js';
|
|
16
|
+
import { getPrimaryInsightSet } from '../helpers.js';
|
|
17
|
+
/**
|
|
18
|
+
* Extracts layout shift clusters, scores, and culprit elements.
|
|
19
|
+
*/
|
|
20
|
+
export function extractShifts(loadedTrace, options) {
|
|
21
|
+
const { parsedTrace } = loadedTrace;
|
|
22
|
+
const timeRange = options?.timeRange;
|
|
23
|
+
const rawClusters = (parsedTrace.data.LayoutShifts?.clusters || []);
|
|
24
|
+
const clusters = [];
|
|
25
|
+
for (const cluster of rawClusters) {
|
|
26
|
+
const startMs = microToMilli(cluster.clusterWindow?.min ?? cluster.ts ?? 0);
|
|
27
|
+
const durMs = microToMilli(cluster.clusterWindow?.range ?? cluster.dur ?? 0);
|
|
28
|
+
const score = cluster.clusterCumulativeScore ?? 0;
|
|
29
|
+
if (!isEventInBounds(startMs, durMs, timeRange)) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const culpritMap = new Map();
|
|
33
|
+
const individualEvents = [];
|
|
34
|
+
if (cluster.events) {
|
|
35
|
+
for (const ev of cluster.events) {
|
|
36
|
+
const scoreDelta = ev.args?.data?.weighted_score_delta;
|
|
37
|
+
const nodes = ev.args?.data?.impacted_nodes || [];
|
|
38
|
+
for (const n of nodes) {
|
|
39
|
+
const key = n.selector ||
|
|
40
|
+
(n.node_id ? `node:${n.node_id}` : n.debug_name || n.tag_name || 'unknown');
|
|
41
|
+
const existing = culpritMap.get(key);
|
|
42
|
+
if (existing) {
|
|
43
|
+
if (scoreDelta !== undefined) {
|
|
44
|
+
existing.scoreDelta = round((existing.scoreDelta ?? 0) + scoreDelta, 4);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
culpritMap.set(key, {
|
|
49
|
+
nodeId: n.node_id,
|
|
50
|
+
tagName: n.tag_name,
|
|
51
|
+
selector: n.selector || (n.node_id ? `Node #${n.node_id}` : undefined),
|
|
52
|
+
debugName: n.debug_name,
|
|
53
|
+
scoreDelta: scoreDelta !== undefined ? round(scoreDelta, 4) : undefined,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (options?.includeEvents) {
|
|
58
|
+
const evTsMs = microToMilli(ev.ts ?? 0);
|
|
59
|
+
const evScore = ev.args?.data?.score ?? ev.args?.data?.weighted_score_delta ?? 0;
|
|
60
|
+
const evWeighted = ev.args?.data?.weighted_score_delta;
|
|
61
|
+
const evNavId = ev.args?.data?.navigationId;
|
|
62
|
+
const hadRecentInput = ev.args?.data?.had_recent_input;
|
|
63
|
+
const impactedNodes = nodes.map((node) => ({
|
|
64
|
+
nodeId: node.node_id,
|
|
65
|
+
tagName: node.tag_name,
|
|
66
|
+
selector: node.selector,
|
|
67
|
+
debugName: node.debug_name,
|
|
68
|
+
oldRect: node.old_rect,
|
|
69
|
+
newRect: node.new_rect,
|
|
70
|
+
}));
|
|
71
|
+
individualEvents.push({
|
|
72
|
+
timestampMs: round(evTsMs),
|
|
73
|
+
score: round(evScore, 4),
|
|
74
|
+
weightedScoreDelta: evWeighted !== undefined ? round(evWeighted, 4) : undefined,
|
|
75
|
+
navigationId: evNavId,
|
|
76
|
+
hadRecentInput,
|
|
77
|
+
impactedNodes: impactedNodes.length > 0 ? impactedNodes : undefined,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const clusterNavId = cluster.navigationId ||
|
|
83
|
+
(cluster.events ? cluster.events[0]?.args?.data?.navigationId : undefined);
|
|
84
|
+
clusters.push({
|
|
85
|
+
timestampMs: round(startMs),
|
|
86
|
+
durationMs: round(durMs),
|
|
87
|
+
cumulativeScore: round(score, 4),
|
|
88
|
+
navigationId: clusterNavId,
|
|
89
|
+
culprits: Array.from(culpritMap.values()),
|
|
90
|
+
...(options?.includeEvents ? { events: individualEvents } : {}),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// If no clusters in handler, check insights
|
|
94
|
+
if (clusters.length === 0) {
|
|
95
|
+
const insightSet = getPrimaryInsightSet(parsedTrace);
|
|
96
|
+
const clsModel = insightSet?.CLSCulprits;
|
|
97
|
+
if (clsModel?.clusters && clsModel.clusters.length > 0) {
|
|
98
|
+
for (const c of clsModel.clusters) {
|
|
99
|
+
const startMs = microToMilli(c.clusterWindow?.min ?? 0);
|
|
100
|
+
const durMs = microToMilli(c.clusterWindow?.range ?? 0);
|
|
101
|
+
const score = c.clusterCumulativeScore ?? 0;
|
|
102
|
+
if (isEventInBounds(startMs, durMs, timeRange)) {
|
|
103
|
+
clusters.push({
|
|
104
|
+
timestampMs: round(startMs),
|
|
105
|
+
durationMs: round(durMs),
|
|
106
|
+
cumulativeScore: round(score, 4),
|
|
107
|
+
culprits: [],
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Sort by score descending
|
|
114
|
+
clusters.sort((a, b) => b.cumulativeScore - a.cumulativeScore);
|
|
115
|
+
return {
|
|
116
|
+
clusters,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type TimeRange } from '../../utils/filter.js';
|
|
2
|
+
import type { LoadedTrace } from '../loader.js';
|
|
3
|
+
import type { TasksResult } from '../types.js';
|
|
4
|
+
export interface ExtractTasksOptions {
|
|
5
|
+
minDurationMs?: number;
|
|
6
|
+
tree?: 'bottom-up' | 'top-down';
|
|
7
|
+
top?: number;
|
|
8
|
+
timeRange?: TimeRange;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Extracts long main thread tasks and aggregated top-down / bottom-up call trees.
|
|
12
|
+
*/
|
|
13
|
+
export declare function extractTasks(loadedTrace: LoadedTrace, options?: ExtractTasksOptions): TasksResult;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Copyright 2026 Google LLC
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
import { isEventInBounds } from '../../utils/filter.js';
|
|
15
|
+
import { microToMilli, round } from '../../utils/units.js';
|
|
16
|
+
import { getMainRendererThread } from '../helpers.js';
|
|
17
|
+
/**
|
|
18
|
+
* Extracts long main thread tasks and aggregated top-down / bottom-up call trees.
|
|
19
|
+
*/
|
|
20
|
+
export function extractTasks(loadedTrace, options) {
|
|
21
|
+
const { engine, parsedTrace } = loadedTrace;
|
|
22
|
+
const minDurationMs = options?.minDurationMs ?? 50;
|
|
23
|
+
const top = options?.top ?? 20;
|
|
24
|
+
const treeMode = options?.tree;
|
|
25
|
+
const timeRange = options?.timeRange;
|
|
26
|
+
const mainThread = getMainRendererThread(parsedTrace.data);
|
|
27
|
+
if (!mainThread) {
|
|
28
|
+
return {
|
|
29
|
+
mode: treeMode || 'list',
|
|
30
|
+
tasks: [],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const traceBounds = parsedTrace.data.Meta.traceBounds;
|
|
34
|
+
const startMs = timeRange ? timeRange.startMs : microToMilli(traceBounds.min);
|
|
35
|
+
const endMs = timeRange ? timeRange.endMs : microToMilli(traceBounds.max);
|
|
36
|
+
if (treeMode === 'bottom-up') {
|
|
37
|
+
const bottomUp = new engine.Extras.TraceTree.BottomUpRootNode(mainThread.entries, {
|
|
38
|
+
textFilter: { accept: () => true },
|
|
39
|
+
filters: [],
|
|
40
|
+
startTime: engine.Types.Timing.Milli(startMs),
|
|
41
|
+
endTime: engine.Types.Timing.Milli(endMs),
|
|
42
|
+
});
|
|
43
|
+
const tasks = [];
|
|
44
|
+
const children = bottomUp.children();
|
|
45
|
+
for (const node of children.values()) {
|
|
46
|
+
if (node.selfTime <= 0)
|
|
47
|
+
continue;
|
|
48
|
+
// biome-ignore lint/suspicious/noExplicitAny: generic trace event
|
|
49
|
+
const frame = engine.Extras.TraceTree.eventStackFrame(node.event);
|
|
50
|
+
tasks.push({
|
|
51
|
+
startTimeMs: round(startMs),
|
|
52
|
+
durationMs: round(node.totalTime),
|
|
53
|
+
selfTimeMs: round(node.selfTime),
|
|
54
|
+
functionName: frame?.functionName || node.event?.name || String(node.id) || 'Unknown',
|
|
55
|
+
url: frame?.url || undefined,
|
|
56
|
+
lineNumber: frame?.lineNumber,
|
|
57
|
+
columnNumber: frame?.columnNumber,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
tasks.sort((a, b) => b.selfTimeMs - a.selfTimeMs);
|
|
61
|
+
return {
|
|
62
|
+
mode: 'bottom-up',
|
|
63
|
+
tasks: tasks.slice(0, top),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (treeMode === 'top-down') {
|
|
67
|
+
const topDown = new engine.Extras.TraceTree.TopDownRootNode(mainThread.entries, {
|
|
68
|
+
filters: [],
|
|
69
|
+
startTime: engine.Types.Timing.Milli(startMs),
|
|
70
|
+
endTime: engine.Types.Timing.Milli(endMs),
|
|
71
|
+
});
|
|
72
|
+
const tasks = [];
|
|
73
|
+
const children = topDown.children();
|
|
74
|
+
for (const node of children.values()) {
|
|
75
|
+
// biome-ignore lint/suspicious/noExplicitAny: generic trace event
|
|
76
|
+
const frame = engine.Extras.TraceTree.eventStackFrame(node.event);
|
|
77
|
+
tasks.push({
|
|
78
|
+
startTimeMs: round(startMs),
|
|
79
|
+
durationMs: round(node.totalTime),
|
|
80
|
+
selfTimeMs: round(node.selfTime),
|
|
81
|
+
functionName: frame?.functionName || node.event?.name || String(node.id) || 'Unknown',
|
|
82
|
+
url: frame?.url || undefined,
|
|
83
|
+
lineNumber: frame?.lineNumber,
|
|
84
|
+
columnNumber: frame?.columnNumber,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
tasks.sort((a, b) => b.durationMs - a.durationMs);
|
|
88
|
+
return {
|
|
89
|
+
mode: 'top-down',
|
|
90
|
+
tasks: tasks.slice(0, top),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// Default List Mode
|
|
94
|
+
const tasks = [];
|
|
95
|
+
const entryToNode = parsedTrace.data.Renderer?.entryToNode;
|
|
96
|
+
for (const entry of mainThread.entries) {
|
|
97
|
+
if (!entry.dur)
|
|
98
|
+
continue;
|
|
99
|
+
const durMs = microToMilli(entry.dur);
|
|
100
|
+
const startEntryMs = microToMilli(entry.ts);
|
|
101
|
+
if (durMs < minDurationMs)
|
|
102
|
+
continue;
|
|
103
|
+
if (!isEventInBounds(startEntryMs, durMs, timeRange))
|
|
104
|
+
continue;
|
|
105
|
+
// biome-ignore lint/suspicious/noExplicitAny: trace helper accepts generic event
|
|
106
|
+
const isTop = entry.name === 'RunTask' || engine.Helpers.Trace.isTopLevelEvent(entry);
|
|
107
|
+
if (isTop) {
|
|
108
|
+
let functionName = entry.name;
|
|
109
|
+
let url;
|
|
110
|
+
let lineNumber;
|
|
111
|
+
let columnNumber;
|
|
112
|
+
let selfTimeMs = durMs;
|
|
113
|
+
// biome-ignore lint/suspicious/noExplicitAny: stack frame extraction accepts generic event
|
|
114
|
+
const frame = engine.Extras.TraceTree.eventStackFrame(entry);
|
|
115
|
+
if (frame?.functionName) {
|
|
116
|
+
functionName = frame.functionName;
|
|
117
|
+
url = frame.url;
|
|
118
|
+
lineNumber = frame.lineNumber;
|
|
119
|
+
columnNumber = frame.columnNumber;
|
|
120
|
+
}
|
|
121
|
+
if (entryToNode) {
|
|
122
|
+
const node = entryToNode.get(entry);
|
|
123
|
+
if (node?.children && node.children.length > 0) {
|
|
124
|
+
let childDurationSum = 0;
|
|
125
|
+
for (const c of node.children) {
|
|
126
|
+
childDurationSum += microToMilli(c.entry.dur ?? 0);
|
|
127
|
+
}
|
|
128
|
+
selfTimeMs = Math.max(0, durMs - childDurationSum);
|
|
129
|
+
if (!frame?.functionName) {
|
|
130
|
+
const mainChild = node.children[0].entry;
|
|
131
|
+
// biome-ignore lint/suspicious/noExplicitAny: stack frame extraction accepts generic event
|
|
132
|
+
const childFrame = engine.Extras.TraceTree.eventStackFrame(mainChild);
|
|
133
|
+
functionName = `${entry.name} > ${childFrame?.functionName || mainChild.name}`;
|
|
134
|
+
url = childFrame?.url;
|
|
135
|
+
lineNumber = childFrame?.lineNumber;
|
|
136
|
+
columnNumber = childFrame?.columnNumber;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
tasks.push({
|
|
141
|
+
startTimeMs: round(startEntryMs),
|
|
142
|
+
durationMs: round(durMs),
|
|
143
|
+
selfTimeMs: round(selfTimeMs),
|
|
144
|
+
functionName,
|
|
145
|
+
url,
|
|
146
|
+
lineNumber,
|
|
147
|
+
columnNumber,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
tasks.sort((a, b) => b.durationMs - a.durationMs);
|
|
152
|
+
return {
|
|
153
|
+
mode: 'list',
|
|
154
|
+
tasks: tasks.slice(0, top),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
export interface RawThreadEntry {
|
|
2
|
+
name: string;
|
|
3
|
+
ts: number;
|
|
4
|
+
dur?: number;
|
|
5
|
+
args?: {
|
|
6
|
+
data?: Record<string, unknown>;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export interface RawRendererThread {
|
|
10
|
+
name: string;
|
|
11
|
+
entries: RawThreadEntry[];
|
|
12
|
+
tree?: unknown;
|
|
13
|
+
layoutEvents?: RawThreadEntry[];
|
|
14
|
+
recalcStyleEvents?: RawThreadEntry[];
|
|
15
|
+
}
|
|
16
|
+
export interface RawRendererProcess {
|
|
17
|
+
isOnMainFrame?: boolean;
|
|
18
|
+
url?: string;
|
|
19
|
+
threads: Map<number, RawRendererThread>;
|
|
20
|
+
}
|
|
21
|
+
export interface RawInsightModel {
|
|
22
|
+
LCPBreakdown?: {
|
|
23
|
+
lcpMs?: number;
|
|
24
|
+
lcpTs?: number;
|
|
25
|
+
subparts?: {
|
|
26
|
+
ttfb?: {
|
|
27
|
+
range?: number;
|
|
28
|
+
};
|
|
29
|
+
loadDelay?: {
|
|
30
|
+
range?: number;
|
|
31
|
+
};
|
|
32
|
+
loadDuration?: {
|
|
33
|
+
range?: number;
|
|
34
|
+
};
|
|
35
|
+
renderDelay?: {
|
|
36
|
+
range?: number;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
lcpRequest?: {
|
|
40
|
+
args?: {
|
|
41
|
+
data?: {
|
|
42
|
+
url?: string;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
lcpEvent?: {
|
|
47
|
+
args?: {
|
|
48
|
+
data?: {
|
|
49
|
+
nodeName?: string;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
INPBreakdown?: {
|
|
55
|
+
longestInteractionEvent?: {
|
|
56
|
+
dur: number;
|
|
57
|
+
ts?: number;
|
|
58
|
+
type?: string;
|
|
59
|
+
name?: string;
|
|
60
|
+
inputDelay?: number;
|
|
61
|
+
mainThreadHandling?: number;
|
|
62
|
+
processingDuration?: number;
|
|
63
|
+
presentationDelay?: number;
|
|
64
|
+
args?: {
|
|
65
|
+
data?: {
|
|
66
|
+
nodeName?: string;
|
|
67
|
+
beginEvent?: {
|
|
68
|
+
args?: {
|
|
69
|
+
data?: {
|
|
70
|
+
nodeName?: string;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
CLSCulprits?: {
|
|
79
|
+
clusters?: Array<{
|
|
80
|
+
clusterCumulativeScore?: number;
|
|
81
|
+
ts?: number;
|
|
82
|
+
dur?: number;
|
|
83
|
+
clusterWindow?: {
|
|
84
|
+
min?: number;
|
|
85
|
+
range?: number;
|
|
86
|
+
};
|
|
87
|
+
events?: Array<{
|
|
88
|
+
args?: {
|
|
89
|
+
data?: {
|
|
90
|
+
weighted_score_delta?: number;
|
|
91
|
+
impacted_nodes?: Array<{
|
|
92
|
+
node_id?: number;
|
|
93
|
+
tag_name?: string;
|
|
94
|
+
selector?: string;
|
|
95
|
+
debug_name?: string;
|
|
96
|
+
old_rect?: [number, number, number, number];
|
|
97
|
+
new_rect?: [number, number, number, number];
|
|
98
|
+
}>;
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
}>;
|
|
102
|
+
}>;
|
|
103
|
+
};
|
|
104
|
+
RenderBlocking?: {
|
|
105
|
+
renderBlockingRequests?: Array<{
|
|
106
|
+
dur?: number;
|
|
107
|
+
args?: {
|
|
108
|
+
data?: {
|
|
109
|
+
url?: string;
|
|
110
|
+
encodedDataLength?: number;
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
}>;
|
|
114
|
+
};
|
|
115
|
+
ThirdParties?: {
|
|
116
|
+
entitySummaries?: Array<{
|
|
117
|
+
entity?: {
|
|
118
|
+
name?: string;
|
|
119
|
+
};
|
|
120
|
+
transferSize?: number;
|
|
121
|
+
mainThreadTime?: number;
|
|
122
|
+
}>;
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Finds the primary main renderer thread (CrRendererMain) in the trace.
|
|
127
|
+
*/
|
|
128
|
+
export declare function getMainRendererThread(data: any): RawRendererThread | null;
|
|
129
|
+
export interface ParsedTraceWithInsights {
|
|
130
|
+
insights?: Map<string, {
|
|
131
|
+
id?: string;
|
|
132
|
+
model?: RawInsightModel;
|
|
133
|
+
navigation?: {
|
|
134
|
+
args?: {
|
|
135
|
+
data?: {
|
|
136
|
+
navigationId?: string;
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
}>;
|
|
141
|
+
}
|
|
142
|
+
export interface InsightEntryResult {
|
|
143
|
+
insightSetId: string;
|
|
144
|
+
model: RawInsightModel;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Finds an insight set entry matching a navigation ID or key.
|
|
148
|
+
*/
|
|
149
|
+
export declare function findInsightEntry(parsedTrace: ParsedTraceWithInsights, navigationId?: string): InsightEntryResult | null;
|
|
150
|
+
/**
|
|
151
|
+
* Retrieves the insight model set from the parsed trace, optionally filtered by navigation ID.
|
|
152
|
+
*/
|
|
153
|
+
export declare function getPrimaryInsightSet(parsedTrace: ParsedTraceWithInsights, navigationId?: string): RawInsightModel | null;
|
|
154
|
+
/**
|
|
155
|
+
* Traverses an object along a dot-separated property path.
|
|
156
|
+
*/
|
|
157
|
+
export declare function getPropertyByPath(obj: unknown, path: string): unknown;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Copyright 2026 Google LLC
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
/**
|
|
15
|
+
* Finds the primary main renderer thread (CrRendererMain) in the trace.
|
|
16
|
+
*/
|
|
17
|
+
// biome-ignore lint/suspicious/noExplicitAny: generic trace data
|
|
18
|
+
export function getMainRendererThread(data) {
|
|
19
|
+
const processes = data?.Renderer?.processes;
|
|
20
|
+
if (!processes)
|
|
21
|
+
return null;
|
|
22
|
+
// 1. Prefer main frame process
|
|
23
|
+
for (const proc of processes.values()) {
|
|
24
|
+
if (proc.isOnMainFrame && proc.threads) {
|
|
25
|
+
for (const thread of proc.threads.values()) {
|
|
26
|
+
if (thread.name === 'CrRendererMain' && thread.entries?.length > 0) {
|
|
27
|
+
return thread;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// 2. Fallback to any process with CrRendererMain
|
|
33
|
+
for (const proc of processes.values()) {
|
|
34
|
+
if (proc.threads) {
|
|
35
|
+
for (const thread of proc.threads.values()) {
|
|
36
|
+
if (thread.name === 'CrRendererMain' && thread.entries?.length > 0) {
|
|
37
|
+
return thread;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Finds an insight set entry matching a navigation ID or key.
|
|
46
|
+
*/
|
|
47
|
+
export function findInsightEntry(parsedTrace, navigationId) {
|
|
48
|
+
if (!parsedTrace?.insights || parsedTrace.insights.size === 0) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (navigationId) {
|
|
52
|
+
const directMatch = parsedTrace.insights.get(navigationId);
|
|
53
|
+
if (directMatch) {
|
|
54
|
+
const model = (directMatch.model ||
|
|
55
|
+
directMatch);
|
|
56
|
+
return { insightSetId: navigationId, model };
|
|
57
|
+
}
|
|
58
|
+
for (const [key, val] of parsedTrace.insights.entries()) {
|
|
59
|
+
const rawVal = val;
|
|
60
|
+
if (key === navigationId ||
|
|
61
|
+
rawVal?.id === navigationId ||
|
|
62
|
+
rawVal?.navigation?.args?.data?.navigationId === navigationId) {
|
|
63
|
+
const model = (rawVal?.model || val);
|
|
64
|
+
return { insightSetId: key || rawVal?.id || navigationId, model };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
for (const [key, val] of parsedTrace.insights.entries()) {
|
|
70
|
+
const rawVal = val;
|
|
71
|
+
const model = (rawVal?.model || val);
|
|
72
|
+
return { insightSetId: key || rawVal?.id || '', model };
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Retrieves the insight model set from the parsed trace, optionally filtered by navigation ID.
|
|
78
|
+
*/
|
|
79
|
+
export function getPrimaryInsightSet(parsedTrace, navigationId) {
|
|
80
|
+
return findInsightEntry(parsedTrace, navigationId)?.model ?? null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Traverses an object along a dot-separated property path.
|
|
84
|
+
*/
|
|
85
|
+
export function getPropertyByPath(obj, path) {
|
|
86
|
+
const parts = path.split('.');
|
|
87
|
+
let current = obj;
|
|
88
|
+
for (const part of parts) {
|
|
89
|
+
if (current === null || current === undefined || typeof current !== 'object') {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
current = current[part];
|
|
93
|
+
}
|
|
94
|
+
return current;
|
|
95
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type TraceEngine } from './adapter.js';
|
|
2
|
+
export interface LoadedTrace {
|
|
3
|
+
engine: TraceEngine;
|
|
4
|
+
parsedTrace: any;
|
|
5
|
+
rawEvents: any[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Checks if the buffer starts with the gzip magic header bytes (0x1f, 0x8b).
|
|
9
|
+
*/
|
|
10
|
+
export declare function isGzipBuffer(buffer: Buffer): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Decompresses a trace buffer if it is gzip-compressed or has a .gz extension.
|
|
13
|
+
*/
|
|
14
|
+
export declare function decompressTraceBuffer(buffer: Buffer, filePath?: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Parses raw JSON string into an array of trace events.
|
|
17
|
+
* Accepts both raw arrays `[...]` and wrapped objects `{"traceEvents": [...]}`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseTraceJson(jsonString: string): unknown[];
|
|
20
|
+
/**
|
|
21
|
+
* Loads, decompresses, and parses a Chromium performance trace file using DevTools Trace Engine.
|
|
22
|
+
*
|
|
23
|
+
* Capabilities:
|
|
24
|
+
* - Transparently gunzips files with `.gz` extensions or gzip magic bytes (`0x1f 0x8b`).
|
|
25
|
+
* - Supports raw event arrays (`[...]`) and standard Chrome wrapper objects (`{ traceEvents: [...] }`).
|
|
26
|
+
* - Normalizes `parsedTrace` retrieval across different DevTools engine versions (`parsedTrace(0)` vs `traceParsedData(0)`).
|
|
27
|
+
* - Filters internal non-fatal Lantern simulation errors during model parsing.
|
|
28
|
+
*
|
|
29
|
+
* @param filePath - Relative or absolute path to the trace file.
|
|
30
|
+
* @param customEnginePath - Optional custom Trace Engine bundle path.
|
|
31
|
+
* @returns Object containing the TraceEngine instance, parsedTrace data, and rawEvents.
|
|
32
|
+
* @throws {Error} If the file does not exist, is invalid JSON, or lacks trace events.
|
|
33
|
+
*/
|
|
34
|
+
export declare function loadTraceFromFile(filePath: string, customEnginePath?: string): Promise<LoadedTrace>;
|