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,474 @@
|
|
|
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 { formatBytes, formatMs, formatScore } from '../utils/units.js';
|
|
15
|
+
export function formatOverviewMarkdown(overview) {
|
|
16
|
+
const lines = [];
|
|
17
|
+
lines.push('# Trace Overview');
|
|
18
|
+
lines.push('');
|
|
19
|
+
lines.push(`- **Main URL**: \`${overview.mainUrl || 'N/A'}\``);
|
|
20
|
+
lines.push(`- **Duration**: \`${formatMs(overview.bounds.durationMs)}\` (Range: \`${formatMs(overview.bounds.minMs)}\` - \`${formatMs(overview.bounds.maxMs)}\`)`);
|
|
21
|
+
if (overview.navigationStartMs !== null) {
|
|
22
|
+
lines.push(`- **Navigation Start**: \`${formatMs(overview.navigationStartMs)}\``);
|
|
23
|
+
}
|
|
24
|
+
lines.push('');
|
|
25
|
+
lines.push('## Core Web Vitals');
|
|
26
|
+
lines.push('');
|
|
27
|
+
lines.push('| Metric | Value | Rating | Details |');
|
|
28
|
+
lines.push('|---|---|---|---|');
|
|
29
|
+
const { fcp, lcp, cls, inp } = overview.coreWebVitals;
|
|
30
|
+
lines.push(`| FCP | ${fcp ? formatMs(fcp.valueMs) : 'N/A'} | ${fcp ? fcp.rating : 'N/A'} | - |`);
|
|
31
|
+
lines.push(`| LCP | ${lcp ? formatMs(lcp.valueMs) : 'N/A'} | ${lcp ? lcp.rating : 'N/A'} | ${lcp?.element || lcp?.url || '-'} |`);
|
|
32
|
+
lines.push(`| CLS | ${cls ? formatScore(cls.value) : 'N/A'} | ${cls ? cls.rating : 'N/A'} | ${cls?.worstClusterScore ? `Worst cluster: ${formatScore(cls.worstClusterScore)}` : '-'} |`);
|
|
33
|
+
lines.push(`| INP | ${inp ? formatMs(inp.valueMs) : 'N/A'} | ${inp ? inp.rating : 'N/A'} | ${inp?.interactionType || '-'} |`);
|
|
34
|
+
lines.push('');
|
|
35
|
+
lines.push('## Main Thread Time Breakdown');
|
|
36
|
+
lines.push('');
|
|
37
|
+
lines.push('| Category | Duration (ms) | Share |');
|
|
38
|
+
lines.push('|---|---|---|');
|
|
39
|
+
const b = overview.mainThreadBreakdown;
|
|
40
|
+
const total = b.totalMs > 0 ? b.totalMs : 1;
|
|
41
|
+
const rows = [
|
|
42
|
+
{ name: 'Scripting', ms: b.scriptingMs },
|
|
43
|
+
{ name: 'Layout', ms: b.layoutMs },
|
|
44
|
+
{ name: 'Rendering', ms: b.renderingMs },
|
|
45
|
+
{ name: 'Garbage Collection', ms: b.gcMs },
|
|
46
|
+
{ name: 'System', ms: b.systemMs },
|
|
47
|
+
{ name: 'Idle', ms: b.idleMs },
|
|
48
|
+
];
|
|
49
|
+
for (const row of rows) {
|
|
50
|
+
const pct = ((row.ms / total) * 100).toFixed(1);
|
|
51
|
+
lines.push(`| ${row.name} | ${formatMs(row.ms)} | ${pct}% |`);
|
|
52
|
+
}
|
|
53
|
+
lines.push(`| **Total** | **${formatMs(b.totalMs)}** | **100%** |`);
|
|
54
|
+
lines.push('');
|
|
55
|
+
if (overview.warnings.length > 0) {
|
|
56
|
+
lines.push('## Warnings & Diagnostics');
|
|
57
|
+
lines.push('');
|
|
58
|
+
for (const w of overview.warnings) {
|
|
59
|
+
lines.push(`- ${w}`);
|
|
60
|
+
}
|
|
61
|
+
lines.push('');
|
|
62
|
+
}
|
|
63
|
+
return lines.join('\n');
|
|
64
|
+
}
|
|
65
|
+
export function formatInsightsMarkdown(insights) {
|
|
66
|
+
const lines = ['# Diagnostic Insights', ''];
|
|
67
|
+
if (insights.navigationId) {
|
|
68
|
+
lines.push(`- **Navigation ID**: \`${insights.navigationId}\``, '');
|
|
69
|
+
}
|
|
70
|
+
if (insights.lcpBreakdown) {
|
|
71
|
+
const l = insights.lcpBreakdown;
|
|
72
|
+
lines.push('## LCP Breakdown');
|
|
73
|
+
lines.push('');
|
|
74
|
+
lines.push(`- **Total LCP**: \`${formatMs(l.totalLcpMs)}\``);
|
|
75
|
+
lines.push(`- **TTFB**: \`${formatMs(l.ttfbMs)}\``);
|
|
76
|
+
lines.push(`- **Resource Load Delay**: \`${formatMs(l.loadDelayMs)}\``);
|
|
77
|
+
lines.push(`- **Resource Load Duration**: \`${formatMs(l.loadDurationMs)}\``);
|
|
78
|
+
lines.push(`- **Element Render Delay**: \`${formatMs(l.renderDelayMs)}\``);
|
|
79
|
+
if (l.lcpElement)
|
|
80
|
+
lines.push(`- **Element**: \`${l.lcpElement}\``);
|
|
81
|
+
if (l.lcpUrl)
|
|
82
|
+
lines.push(`- **Resource URL**: \`${l.lcpUrl}\``);
|
|
83
|
+
lines.push('');
|
|
84
|
+
}
|
|
85
|
+
if (insights.inpBreakdown) {
|
|
86
|
+
const i = insights.inpBreakdown;
|
|
87
|
+
lines.push('## INP Breakdown');
|
|
88
|
+
lines.push('');
|
|
89
|
+
lines.push(`- **Event Type**: \`${i.eventType}\``);
|
|
90
|
+
lines.push(`- **Total Duration**: \`${formatMs(i.totalDurationMs)}\``);
|
|
91
|
+
lines.push(`- **Input Delay**: \`${formatMs(i.inputDelayMs)}\``);
|
|
92
|
+
lines.push(`- **Processing Duration**: \`${formatMs(i.processingDurationMs)}\``);
|
|
93
|
+
lines.push(`- **Presentation Delay**: \`${formatMs(i.presentationDelayMs)}\``);
|
|
94
|
+
if (i.target)
|
|
95
|
+
lines.push(`- **Target**: \`${i.target}\``);
|
|
96
|
+
lines.push('');
|
|
97
|
+
}
|
|
98
|
+
if (insights.clsCulprits && insights.clsCulprits.length > 0) {
|
|
99
|
+
lines.push('## CLS Culprits');
|
|
100
|
+
lines.push('');
|
|
101
|
+
lines.push('| Timestamp (ms) | Score | Culprit Elements |');
|
|
102
|
+
lines.push('|---|---|---|');
|
|
103
|
+
for (const c of insights.clsCulprits) {
|
|
104
|
+
lines.push(`| ${formatMs(c.timestampMs)} | ${formatScore(c.score)} | ${c.culpritNodes.join(', ') || 'N/A'} |`);
|
|
105
|
+
}
|
|
106
|
+
lines.push('');
|
|
107
|
+
}
|
|
108
|
+
if (insights.renderBlocking && insights.renderBlocking.length > 0) {
|
|
109
|
+
lines.push('## Render Blocking Resources');
|
|
110
|
+
lines.push('');
|
|
111
|
+
lines.push('| URL | Duration (ms) | Transfer Size |');
|
|
112
|
+
lines.push('|---|---|---|');
|
|
113
|
+
for (const r of insights.renderBlocking) {
|
|
114
|
+
lines.push(`| \`${r.url}\` | ${formatMs(r.durationMs)} | ${formatBytes(r.transferSizeBytes)} |`);
|
|
115
|
+
}
|
|
116
|
+
lines.push('');
|
|
117
|
+
}
|
|
118
|
+
if (insights.thirdParties && insights.thirdParties.length > 0) {
|
|
119
|
+
lines.push('## Third-Party Entities');
|
|
120
|
+
lines.push('');
|
|
121
|
+
lines.push('| Entity | Main Thread CPU (ms) | Transfer Size |');
|
|
122
|
+
lines.push('|---|---|---|');
|
|
123
|
+
for (const tp of insights.thirdParties) {
|
|
124
|
+
lines.push(`| ${tp.entityName} | ${formatMs(tp.mainThreadTimeMs)} | ${formatBytes(tp.transferSizeBytes)} |`);
|
|
125
|
+
}
|
|
126
|
+
lines.push('');
|
|
127
|
+
}
|
|
128
|
+
if (insights.warnings && insights.warnings.length > 0) {
|
|
129
|
+
lines.push('## Warnings');
|
|
130
|
+
lines.push('');
|
|
131
|
+
for (const w of insights.warnings) {
|
|
132
|
+
lines.push(`- ${w}`);
|
|
133
|
+
}
|
|
134
|
+
lines.push('');
|
|
135
|
+
}
|
|
136
|
+
return lines.join('\n');
|
|
137
|
+
}
|
|
138
|
+
export function formatTasksMarkdown(tasksResult) {
|
|
139
|
+
const lines = [`# Tasks (${tasksResult.mode})`, ''];
|
|
140
|
+
if (tasksResult.tasks.length === 0) {
|
|
141
|
+
lines.push('No tasks found matching criteria.');
|
|
142
|
+
return lines.join('\n');
|
|
143
|
+
}
|
|
144
|
+
lines.push('| Start (ms) | Duration (ms) | Self Time (ms) | Function / Task | Location |');
|
|
145
|
+
lines.push('|---|---|---|---|---|');
|
|
146
|
+
for (const t of tasksResult.tasks) {
|
|
147
|
+
const loc = t.url ? `${t.url}${t.lineNumber !== undefined ? `:${t.lineNumber}` : ''}` : '-';
|
|
148
|
+
lines.push(`| ${formatMs(t.startTimeMs)} | ${formatMs(t.durationMs)} | ${formatMs(t.selfTimeMs)} | \`${t.functionName}\` | \`${loc}\` |`);
|
|
149
|
+
}
|
|
150
|
+
return lines.join('\n');
|
|
151
|
+
}
|
|
152
|
+
export function formatNetworkMarkdown(network) {
|
|
153
|
+
const lines = ['# Network Requests', ''];
|
|
154
|
+
if (network.initiatorChain && network.initiatorChain.length > 0) {
|
|
155
|
+
lines.push('## Initiator Chain');
|
|
156
|
+
lines.push('');
|
|
157
|
+
for (let idx = 0; idx < network.initiatorChain.length; idx++) {
|
|
158
|
+
const item = network.initiatorChain[idx];
|
|
159
|
+
const indent = ' '.repeat(idx);
|
|
160
|
+
lines.push(`${indent}- \`${item.url}\` (${item.type || 'request'})`);
|
|
161
|
+
}
|
|
162
|
+
lines.push('');
|
|
163
|
+
}
|
|
164
|
+
if (network.requests.length === 0) {
|
|
165
|
+
lines.push('No network requests found matching criteria.');
|
|
166
|
+
return lines.join('\n');
|
|
167
|
+
}
|
|
168
|
+
lines.push('| URL | MIME | Priority | Queue | TTFB | Download | Total | Size | Blocking |');
|
|
169
|
+
lines.push('|---|---|---|---|---|---|---|---|---|');
|
|
170
|
+
for (const r of network.requests) {
|
|
171
|
+
lines.push(`| \`${r.url}\` | ${r.mimeType || '-'} | ${r.priority} | ${formatMs(r.timing.queueingMs)} | ${formatMs(r.timing.ttfbMs)} | ${formatMs(r.timing.downloadMs)} | ${formatMs(r.timing.totalDurationMs)} | ${formatBytes(r.transferSizeBytes)} | ${r.renderBlocking ? 'Yes' : 'No'} |`);
|
|
172
|
+
}
|
|
173
|
+
return lines.join('\n');
|
|
174
|
+
}
|
|
175
|
+
export function formatInteractionsMarkdown(interactions) {
|
|
176
|
+
const lines = ['# User Interactions', ''];
|
|
177
|
+
if (interactions.interactions.length === 0) {
|
|
178
|
+
lines.push('No user interactions found in trace.');
|
|
179
|
+
return lines.join('\n');
|
|
180
|
+
}
|
|
181
|
+
lines.push('| Event Type | Start (ms) | Total (ms) | Input Delay | Processing | Presentation Delay | Target |');
|
|
182
|
+
lines.push('|---|---|---|---|---|---|---|');
|
|
183
|
+
for (const i of interactions.interactions) {
|
|
184
|
+
lines.push(`| \`${i.eventType}\` | ${formatMs(i.startTimeMs)} | ${formatMs(i.durationMs)} | ${formatMs(i.inputDelayMs)} | ${formatMs(i.processingDurationMs)} | ${formatMs(i.presentationDelayMs)} | \`${i.targetElement || '-'}\` |`);
|
|
185
|
+
}
|
|
186
|
+
return lines.join('\n');
|
|
187
|
+
}
|
|
188
|
+
function formatCulpritLabel(item) {
|
|
189
|
+
const detail = item.selector ||
|
|
190
|
+
item.tagName ||
|
|
191
|
+
(item.nodeId !== undefined ? `Node #${item.nodeId}` : undefined);
|
|
192
|
+
if (item.debugName && detail)
|
|
193
|
+
return `${item.debugName} (${detail})`;
|
|
194
|
+
return item.debugName || detail || 'unknown';
|
|
195
|
+
}
|
|
196
|
+
export function formatShiftsMarkdown(shifts) {
|
|
197
|
+
const lines = ['# Layout Shifts', ''];
|
|
198
|
+
if (shifts.clusters.length === 0) {
|
|
199
|
+
lines.push('No layout shifts or clusters detected in trace.');
|
|
200
|
+
return lines.join('\n');
|
|
201
|
+
}
|
|
202
|
+
const hasNavIds = shifts.clusters.some((c) => c.navigationId);
|
|
203
|
+
if (hasNavIds) {
|
|
204
|
+
lines.push('| Start (ms) | Duration (ms) | Score | Navigation ID | Culprit Elements |');
|
|
205
|
+
lines.push('|---|---|---|---|---|');
|
|
206
|
+
for (const c of shifts.clusters) {
|
|
207
|
+
const culprits = c.culprits.map(formatCulpritLabel).join(', ');
|
|
208
|
+
lines.push(`| ${formatMs(c.timestampMs)} | ${formatMs(c.durationMs)} | ${formatScore(c.cumulativeScore)} | \`${c.navigationId || '-'}\` | ${culprits || '-'} |`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
lines.push('| Start (ms) | Duration (ms) | Score | Culprit Elements |');
|
|
213
|
+
lines.push('|---|---|---|---|');
|
|
214
|
+
for (const c of shifts.clusters) {
|
|
215
|
+
const culprits = c.culprits.map(formatCulpritLabel).join(', ');
|
|
216
|
+
lines.push(`| ${formatMs(c.timestampMs)} | ${formatMs(c.durationMs)} | ${formatScore(c.cumulativeScore)} | ${culprits || '-'} |`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const hasIndividualEvents = shifts.clusters.some((c) => c.events && c.events.length > 0);
|
|
220
|
+
if (hasIndividualEvents) {
|
|
221
|
+
lines.push('');
|
|
222
|
+
lines.push('## Individual Shift Events');
|
|
223
|
+
lines.push('');
|
|
224
|
+
lines.push('| Timestamp (ms) | Score | Navigation ID | Recent Input | Impacted Elements |');
|
|
225
|
+
lines.push('|---|---|---|---|---|');
|
|
226
|
+
for (const c of shifts.clusters) {
|
|
227
|
+
if (c.events) {
|
|
228
|
+
for (const ev of c.events) {
|
|
229
|
+
const nodes = (ev.impactedNodes || []).map(formatCulpritLabel).join(', ');
|
|
230
|
+
lines.push(`| ${formatMs(ev.timestampMs)} | ${formatScore(ev.score)} | \`${ev.navigationId || '-'}\` | ${ev.hadRecentInput ? 'Yes' : 'No'} | ${nodes || '-'} |`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return lines.join('\n');
|
|
236
|
+
}
|
|
237
|
+
export function formatFindMarkdown(findResult) {
|
|
238
|
+
const lines = [`# Function Execution: \`${findResult.functionQuery}\``, ''];
|
|
239
|
+
lines.push(`- **Total Calls**: \`${findResult.totalCalls}\``);
|
|
240
|
+
lines.push(`- **Total Duration**: \`${formatMs(findResult.totalDurationMs)}\``);
|
|
241
|
+
lines.push(`- **Average Duration**: \`${formatMs(findResult.avgDurationMs)}\``);
|
|
242
|
+
if (findResult.firstCallMs !== undefined) {
|
|
243
|
+
lines.push(`- **First Call**: \`${formatMs(findResult.firstCallMs)}\``);
|
|
244
|
+
}
|
|
245
|
+
if (findResult.lastCallMs !== undefined) {
|
|
246
|
+
lines.push(`- **Last Call**: \`${formatMs(findResult.lastCallMs)}\``);
|
|
247
|
+
}
|
|
248
|
+
lines.push('');
|
|
249
|
+
if (findResult.intervals && findResult.intervals.length > 0) {
|
|
250
|
+
lines.push('## Call Frequency by Time Interval');
|
|
251
|
+
lines.push('');
|
|
252
|
+
lines.push('| Interval (ms) | Calls | Duration (ms) |');
|
|
253
|
+
lines.push('|---|---|---|');
|
|
254
|
+
for (const inv of findResult.intervals) {
|
|
255
|
+
lines.push(`| ${formatMs(inv.startMs)} - ${formatMs(inv.endMs)} | ${inv.calls} | ${formatMs(inv.totalDurationMs)} |`);
|
|
256
|
+
}
|
|
257
|
+
lines.push('');
|
|
258
|
+
}
|
|
259
|
+
if (findResult.calls.length > 0) {
|
|
260
|
+
lines.push('## Call Invocations');
|
|
261
|
+
lines.push('');
|
|
262
|
+
lines.push('| Timestamp (ms) | Duration (ms) | Location |');
|
|
263
|
+
lines.push('|---|---|---|');
|
|
264
|
+
for (const c of findResult.calls) {
|
|
265
|
+
const loc = c.url ? `${c.url}${c.lineNumber !== undefined ? `:${c.lineNumber}` : ''}` : '-';
|
|
266
|
+
lines.push(`| ${formatMs(c.timestampMs)} | ${formatMs(c.durationMs)} | \`${loc}\` |`);
|
|
267
|
+
}
|
|
268
|
+
lines.push('');
|
|
269
|
+
}
|
|
270
|
+
return lines.join('\n');
|
|
271
|
+
}
|
|
272
|
+
export function formatProfileMarkdown(profileResult) {
|
|
273
|
+
const lines = [
|
|
274
|
+
`# CPU Profile Progression (${formatMs(profileResult.bucketSizeMs)} buckets)`,
|
|
275
|
+
'',
|
|
276
|
+
];
|
|
277
|
+
if (profileResult.buckets.length === 0) {
|
|
278
|
+
lines.push('No CPU activity found in the specified range.');
|
|
279
|
+
return lines.join('\n');
|
|
280
|
+
}
|
|
281
|
+
lines.push('| Time Bucket | Active CPU (ms) | Top Functions (% of slice) |');
|
|
282
|
+
lines.push('|---|---|---|');
|
|
283
|
+
for (const b of profileResult.buckets) {
|
|
284
|
+
const topSummary = b.topFunctions.map((f) => `${f.name} (${f.selfTimePct}%)`).join(', ');
|
|
285
|
+
lines.push(`| ${formatMs(b.startMs)} - ${formatMs(b.endMs)} | ${formatMs(b.totalActiveMs)} | ${topSummary || 'None (Idle)'} |`);
|
|
286
|
+
}
|
|
287
|
+
return lines.join('\n');
|
|
288
|
+
}
|
|
289
|
+
export function formatDiffMarkdown(diffResult) {
|
|
290
|
+
const lines = ['# Differential Profile', ''];
|
|
291
|
+
lines.push(`- **Window A**: \`${formatMs(diffResult.windowA.startMs)} - ${formatMs(diffResult.windowA.endMs)}\` (${formatMs(diffResult.windowA.durationMs)})`);
|
|
292
|
+
lines.push(`- **Window B**: \`${formatMs(diffResult.windowB.startMs)} - ${formatMs(diffResult.windowB.endMs)}\` (${formatMs(diffResult.windowB.durationMs)})`);
|
|
293
|
+
lines.push('');
|
|
294
|
+
if (diffResult.entries.length === 0) {
|
|
295
|
+
lines.push('No significant differences detected between the two windows.');
|
|
296
|
+
return lines.join('\n');
|
|
297
|
+
}
|
|
298
|
+
lines.push('| Function | Window A (%) | Window B (%) | Shift (\u0394) | Location |');
|
|
299
|
+
lines.push('|---|---|---|---|---|');
|
|
300
|
+
for (const d of diffResult.entries) {
|
|
301
|
+
const sign = d.deltaPct > 0 ? '+' : '';
|
|
302
|
+
lines.push(`| \`${d.functionName}\` | ${d.selfTimePctA}% (${formatMs(d.selfTimeMsA)}) | ${d.selfTimePctB}% (${formatMs(d.selfTimeMsB)}) | **${sign}${d.deltaPct}%** | \`${d.url || '-'}\` |`);
|
|
303
|
+
}
|
|
304
|
+
return lines.join('\n');
|
|
305
|
+
}
|
|
306
|
+
export function formatSchedulingMarkdown(sched) {
|
|
307
|
+
const lines = ['# Async Task & Animation Scheduling', ''];
|
|
308
|
+
lines.push('## Animation Frames (`requestAnimationFrame`)');
|
|
309
|
+
lines.push('');
|
|
310
|
+
lines.push(`- **Scheduled**: \`${sched.animationFrames.scheduledCount}\``);
|
|
311
|
+
lines.push(`- **Canceled**: \`${sched.animationFrames.canceledCount}\``);
|
|
312
|
+
lines.push(`- **Executed**: \`${sched.animationFrames.executedCount}\``);
|
|
313
|
+
lines.push(`- **Cancellation Rate**: \`${sched.animationFrames.cancellationRatePct}%\``);
|
|
314
|
+
lines.push('');
|
|
315
|
+
lines.push('## Timers (`setTimeout` / `setInterval`)');
|
|
316
|
+
lines.push('');
|
|
317
|
+
lines.push(`- **Installed**: \`${sched.timers.scheduledCount}\``);
|
|
318
|
+
lines.push(`- **Removed**: \`${sched.timers.canceledCount}\``);
|
|
319
|
+
lines.push(`- **Fired**: \`${sched.timers.executedCount}\``);
|
|
320
|
+
lines.push(`- **Cancellation Rate**: \`${sched.timers.cancellationRatePct}%\``);
|
|
321
|
+
lines.push('');
|
|
322
|
+
if (sched.warnings.length > 0) {
|
|
323
|
+
lines.push('## Scheduling Churn Warnings');
|
|
324
|
+
lines.push('');
|
|
325
|
+
for (const w of sched.warnings) {
|
|
326
|
+
lines.push(`- ${w}`);
|
|
327
|
+
}
|
|
328
|
+
lines.push('');
|
|
329
|
+
}
|
|
330
|
+
return lines.join('\n');
|
|
331
|
+
}
|
|
332
|
+
export function formatEventsMarkdown(result) {
|
|
333
|
+
const lines = ['# Raw Trace Events', ''];
|
|
334
|
+
lines.push(`- **Matched**: \`${result.totalMatched}\``);
|
|
335
|
+
lines.push(`- **Returned**: \`${result.returned}\``);
|
|
336
|
+
lines.push('');
|
|
337
|
+
if (result.events.length === 0) {
|
|
338
|
+
lines.push('No matching trace events found.');
|
|
339
|
+
return lines.join('\n');
|
|
340
|
+
}
|
|
341
|
+
const hasSelectedFields = result.events.some((e) => e.selectedFields);
|
|
342
|
+
if (hasSelectedFields) {
|
|
343
|
+
const fieldSet = new Set();
|
|
344
|
+
for (const ev of result.events) {
|
|
345
|
+
if (ev.selectedFields) {
|
|
346
|
+
for (const k of Object.keys(ev.selectedFields)) {
|
|
347
|
+
fieldSet.add(k);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const fieldList = Array.from(fieldSet);
|
|
352
|
+
lines.push(`| Name | Timestamp (ms) | ${fieldList.join(' | ')} |`);
|
|
353
|
+
lines.push(`|---|---|${fieldList.map(() => '---|').join('')}`);
|
|
354
|
+
for (const ev of result.events) {
|
|
355
|
+
const fieldVals = fieldList.map((f) => {
|
|
356
|
+
const val = ev.selectedFields?.[f];
|
|
357
|
+
if (val === undefined)
|
|
358
|
+
return '-';
|
|
359
|
+
const raw = typeof val === 'object' ? JSON.stringify(val) : String(val);
|
|
360
|
+
const sanitized = raw.replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
|
|
361
|
+
return `\`${sanitized}\``;
|
|
362
|
+
});
|
|
363
|
+
lines.push(`| \`${ev.name}\` | ${formatMs(ev.tsMs)} | ${fieldVals.join(' | ')} |`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
lines.push('| Name | Timestamp (ms) | Duration (ms) | Category | Phase | PID | TID |');
|
|
368
|
+
lines.push('|---|---|---|---|---|---|---|');
|
|
369
|
+
for (const ev of result.events) {
|
|
370
|
+
const dur = ev.durMs !== undefined ? formatMs(ev.durMs) : '-';
|
|
371
|
+
lines.push(`| \`${ev.name}\` | ${formatMs(ev.tsMs)} | ${dur} | ${ev.cat || '-'} | ${ev.ph || '-'} | ${ev.pid ?? '-'} | ${ev.tid ?? '-'} |`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return lines.join('\n');
|
|
375
|
+
}
|
|
376
|
+
export function formatNavigationsMarkdown(result) {
|
|
377
|
+
const lines = ['# Trace Navigations', ''];
|
|
378
|
+
lines.push(`- **Total Navigations**: \`${result.total}\``);
|
|
379
|
+
if (result.insightSetIds.length > 0) {
|
|
380
|
+
lines.push(`- **Insight Set IDs**: \`${result.insightSetIds.join('`, `')}\``);
|
|
381
|
+
}
|
|
382
|
+
lines.push('');
|
|
383
|
+
if (result.navigations.length === 0) {
|
|
384
|
+
lines.push('No navigation events found in trace.');
|
|
385
|
+
return lines.join('\n');
|
|
386
|
+
}
|
|
387
|
+
lines.push('| Navigation ID | Insight Set ID | URL | Timestamp (ms) | Frame ID | Main Frame | Has Insights |');
|
|
388
|
+
lines.push('|---|---|---|---|---|---|---|');
|
|
389
|
+
for (const nav of result.navigations) {
|
|
390
|
+
lines.push(`| \`${nav.navigationId}\` | \`${nav.insightSetId || '-'}\` | \`${nav.url || '-'}\` | ${formatMs(nav.timestampMs)} | \`${nav.frameId || '-'}\` | ${nav.isMainFrame ? 'Yes' : 'No'} | ${nav.hasInsights ? 'Yes' : 'No'} |`);
|
|
391
|
+
}
|
|
392
|
+
return lines.join('\n');
|
|
393
|
+
}
|
|
394
|
+
export function formatScanMarkdown(result) {
|
|
395
|
+
const lines = [];
|
|
396
|
+
lines.push('# Trace Scan Results');
|
|
397
|
+
lines.push('');
|
|
398
|
+
const queryItems = [];
|
|
399
|
+
if (result.query.names && result.query.names.length > 0) {
|
|
400
|
+
queryItems.push(`name: \`${result.query.names.join(', ')}\``);
|
|
401
|
+
}
|
|
402
|
+
if (result.query.category) {
|
|
403
|
+
queryItems.push(`category: \`${result.query.category}\``);
|
|
404
|
+
}
|
|
405
|
+
if (result.query.phase) {
|
|
406
|
+
queryItems.push(`phase: \`${result.query.phase}\``);
|
|
407
|
+
}
|
|
408
|
+
if (result.query.grep) {
|
|
409
|
+
queryItems.push(`grep: "${result.query.grep}"`);
|
|
410
|
+
}
|
|
411
|
+
if (result.query.fields && result.query.fields.length > 0) {
|
|
412
|
+
queryItems.push(`fields: \`${result.query.fields.join(', ')}\``);
|
|
413
|
+
}
|
|
414
|
+
if (queryItems.length > 0) {
|
|
415
|
+
lines.push(`- **Query**: ${queryItems.join(', ')}`);
|
|
416
|
+
}
|
|
417
|
+
lines.push(`- **Files Scanned**: ${result.totalFilesScanned}`);
|
|
418
|
+
if (result.totalFilesMatched > result.matchedFiles.length) {
|
|
419
|
+
lines.push(`- **Files Matched**: ${result.totalFilesMatched} (showing top ${result.matchedFiles.length})`);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
lines.push(`- **Files Matched**: ${result.totalFilesMatched}`);
|
|
423
|
+
}
|
|
424
|
+
lines.push(`- **Total Events Matched**: ${result.totalEventsMatched}`);
|
|
425
|
+
lines.push('');
|
|
426
|
+
if (result.matchedFiles.length === 0) {
|
|
427
|
+
lines.push('No matching trace files found.');
|
|
428
|
+
return lines.join('\n');
|
|
429
|
+
}
|
|
430
|
+
lines.push('| File | Matches | Categories | First Event | First Event Timestamp |');
|
|
431
|
+
lines.push('|---|---|---|---|---|');
|
|
432
|
+
for (const item of result.matchedFiles) {
|
|
433
|
+
const safeFile = item.file.replace(/\|/g, '\\|');
|
|
434
|
+
const cats = item.categories.length > 0 ? item.categories.join(', ').replace(/\|/g, '\\|') : '-';
|
|
435
|
+
const firstEvent = item.firstEventName
|
|
436
|
+
? `\`${item.firstEventName.replace(/\|/g, '\\|')}\``
|
|
437
|
+
: '-';
|
|
438
|
+
const firstTs = item.firstEventTsMs !== undefined ? formatMs(item.firstEventTsMs) : '-';
|
|
439
|
+
lines.push(`| \`${safeFile}\` | ${item.matchCount} | ${cats} | ${firstEvent} | ${firstTs} |`);
|
|
440
|
+
}
|
|
441
|
+
return lines.join('\n');
|
|
442
|
+
}
|
|
443
|
+
export function formatMarkdown(data, type) {
|
|
444
|
+
switch (type) {
|
|
445
|
+
case 'overview':
|
|
446
|
+
return formatOverviewMarkdown(data);
|
|
447
|
+
case 'insights':
|
|
448
|
+
return formatInsightsMarkdown(data);
|
|
449
|
+
case 'tasks':
|
|
450
|
+
return formatTasksMarkdown(data);
|
|
451
|
+
case 'network':
|
|
452
|
+
return formatNetworkMarkdown(data);
|
|
453
|
+
case 'interactions':
|
|
454
|
+
return formatInteractionsMarkdown(data);
|
|
455
|
+
case 'shifts':
|
|
456
|
+
return formatShiftsMarkdown(data);
|
|
457
|
+
case 'find':
|
|
458
|
+
return formatFindMarkdown(data);
|
|
459
|
+
case 'profile':
|
|
460
|
+
return formatProfileMarkdown(data);
|
|
461
|
+
case 'diff':
|
|
462
|
+
return formatDiffMarkdown(data);
|
|
463
|
+
case 'scheduling':
|
|
464
|
+
return formatSchedulingMarkdown(data);
|
|
465
|
+
case 'events':
|
|
466
|
+
return formatEventsMarkdown(data);
|
|
467
|
+
case 'navigations':
|
|
468
|
+
return formatNavigationsMarkdown(data);
|
|
469
|
+
case 'scan':
|
|
470
|
+
return formatScanMarkdown(data);
|
|
471
|
+
default:
|
|
472
|
+
return `\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\``;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function formatToon(data: unknown): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
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 { encode } from '@toon-format/toon';
|
|
15
|
+
export function formatToon(data) {
|
|
16
|
+
return encode(data);
|
|
17
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a time window in milliseconds.
|
|
3
|
+
*/
|
|
4
|
+
export interface TimeRange {
|
|
5
|
+
startMs: number;
|
|
6
|
+
endMs: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Trace boundary timestamps in microseconds (DevTools Meta.traceBounds).
|
|
10
|
+
*/
|
|
11
|
+
export interface RawTraceBounds {
|
|
12
|
+
min: number;
|
|
13
|
+
max: number;
|
|
14
|
+
range?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parses a time string with optional unit suffix into milliseconds.
|
|
18
|
+
* Supported units: 's' (seconds), 'ms' (milliseconds), 'us'/'µs' (microseconds).
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* parseTimeValue("3.5s") // 3500
|
|
22
|
+
* parseTimeValue("50ms") // 50
|
|
23
|
+
* parseTimeValue("5000us") // 5
|
|
24
|
+
* parseTimeValue("100") // 100
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseTimeValue(valueStr: string): number;
|
|
27
|
+
/**
|
|
28
|
+
* Parses a time range string in formats such as:
|
|
29
|
+
* - "100,500" or "100:500"
|
|
30
|
+
* - "3.5s,4.2s" or "3.5s..4.2s"
|
|
31
|
+
* - "100ms-500ms"
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseTimeRange(rangeStr?: string): TimeRange | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Resolves and validates a TimeRange against trace bounds.
|
|
36
|
+
* Defaults to relative timestamps (0ms = trace origin).
|
|
37
|
+
* When `isAbsolute` is true, values are treated as raw trace timestamps.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveTimeRange(range: TimeRange | undefined, traceBounds: RawTraceBounds, isAbsolute?: boolean): TimeRange | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Checks if a timestamp in milliseconds falls within a time range.
|
|
42
|
+
*/
|
|
43
|
+
export declare function isTimestampInBounds(timestampMs: number, range?: TimeRange): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Checks if an event with start time and duration overlaps with a time range.
|
|
46
|
+
*/
|
|
47
|
+
export declare function isEventInBounds(startMs: number, durationMs: number | undefined, range?: TimeRange): boolean;
|
|
@@ -0,0 +1,136 @@
|
|
|
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 { microToMilli, round } from './units.js';
|
|
15
|
+
/**
|
|
16
|
+
* Parses a time string with optional unit suffix into milliseconds.
|
|
17
|
+
* Supported units: 's' (seconds), 'ms' (milliseconds), 'us'/'µs' (microseconds).
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* parseTimeValue("3.5s") // 3500
|
|
21
|
+
* parseTimeValue("50ms") // 50
|
|
22
|
+
* parseTimeValue("5000us") // 5
|
|
23
|
+
* parseTimeValue("100") // 100
|
|
24
|
+
*/
|
|
25
|
+
export function parseTimeValue(valueStr) {
|
|
26
|
+
const trimmed = valueStr.trim();
|
|
27
|
+
const match = trimmed.match(/^([+-]?\d+(?:\.\d+)?)\s*(s|ms|us|µs)?$/i);
|
|
28
|
+
if (!match) {
|
|
29
|
+
throw new Error(`Invalid time value: "${valueStr}". Expected number with optional unit (e.g. "3.5s", "50ms", "5000us").`);
|
|
30
|
+
}
|
|
31
|
+
const num = Number.parseFloat(match[1]);
|
|
32
|
+
const unit = (match[2] || 'ms').toLowerCase();
|
|
33
|
+
if (Number.isNaN(num)) {
|
|
34
|
+
throw new Error(`Invalid numeric value: "${valueStr}".`);
|
|
35
|
+
}
|
|
36
|
+
if (unit === 's') {
|
|
37
|
+
return num * 1000;
|
|
38
|
+
}
|
|
39
|
+
if (unit === 'us' || unit === 'µs') {
|
|
40
|
+
return num / 1000;
|
|
41
|
+
}
|
|
42
|
+
return num;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Parses a time range string in formats such as:
|
|
46
|
+
* - "100,500" or "100:500"
|
|
47
|
+
* - "3.5s,4.2s" or "3.5s..4.2s"
|
|
48
|
+
* - "100ms-500ms"
|
|
49
|
+
*/
|
|
50
|
+
export function parseTimeRange(rangeStr) {
|
|
51
|
+
if (!rangeStr?.trim())
|
|
52
|
+
return undefined;
|
|
53
|
+
const trimmed = rangeStr.trim();
|
|
54
|
+
let parts = [];
|
|
55
|
+
if (trimmed.includes('..')) {
|
|
56
|
+
parts = trimmed.split('..');
|
|
57
|
+
}
|
|
58
|
+
else if (trimmed.includes(',')) {
|
|
59
|
+
parts = trimmed.split(',');
|
|
60
|
+
}
|
|
61
|
+
else if (trimmed.includes(':')) {
|
|
62
|
+
parts = trimmed.split(':');
|
|
63
|
+
}
|
|
64
|
+
else if (trimmed.includes('-') && !trimmed.startsWith('-')) {
|
|
65
|
+
// Split on '-' only if not a single negative number
|
|
66
|
+
const idx = trimmed.indexOf('-', 1);
|
|
67
|
+
if (idx !== -1) {
|
|
68
|
+
parts = [trimmed.slice(0, idx), trimmed.slice(idx + 1)];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (parts.length !== 2) {
|
|
72
|
+
throw new Error(`Invalid time range format: "${rangeStr}". Expected format: "<start>,<end>" (e.g. "3500,4200" or "3.5s,4.2s").`);
|
|
73
|
+
}
|
|
74
|
+
const startMs = parseTimeValue(parts[0]);
|
|
75
|
+
const endMs = parseTimeValue(parts[1]);
|
|
76
|
+
if (startMs > endMs) {
|
|
77
|
+
throw new Error(`Invalid time range values: "${rangeStr}". Start (${startMs}ms) must be <= end (${endMs}ms).`);
|
|
78
|
+
}
|
|
79
|
+
return { startMs, endMs };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Resolves and validates a TimeRange against trace bounds.
|
|
83
|
+
* Defaults to relative timestamps (0ms = trace origin).
|
|
84
|
+
* When `isAbsolute` is true, values are treated as raw trace timestamps.
|
|
85
|
+
*/
|
|
86
|
+
export function resolveTimeRange(range, traceBounds, isAbsolute = false) {
|
|
87
|
+
if (!range)
|
|
88
|
+
return undefined;
|
|
89
|
+
const boundsMinMs = microToMilli(traceBounds.min);
|
|
90
|
+
const boundsMaxMs = microToMilli(traceBounds.max);
|
|
91
|
+
const traceDurationMs = round(boundsMaxMs - boundsMinMs);
|
|
92
|
+
let startMs;
|
|
93
|
+
let endMs;
|
|
94
|
+
if (isAbsolute) {
|
|
95
|
+
startMs = range.startMs;
|
|
96
|
+
endMs = range.endMs;
|
|
97
|
+
// Check if range is completely outside trace bounds
|
|
98
|
+
if (endMs < boundsMinMs || startMs > boundsMaxMs) {
|
|
99
|
+
throw new Error(`Time range [${round(startMs)}ms, ${round(endMs)}ms] is outside trace bounds [${round(boundsMinMs)}ms, ${round(boundsMaxMs)}ms]. Trace duration is ${traceDurationMs}ms. Did you mean to omit --absolute-timestamps?`);
|
|
100
|
+
}
|
|
101
|
+
startMs = Math.max(boundsMinMs, startMs);
|
|
102
|
+
endMs = Math.min(boundsMaxMs, endMs);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// Relative to trace start
|
|
106
|
+
startMs = boundsMinMs + range.startMs;
|
|
107
|
+
endMs = boundsMinMs + range.endMs;
|
|
108
|
+
// Check if relative range is completely outside trace duration
|
|
109
|
+
if (range.startMs > traceDurationMs || range.endMs < 0) {
|
|
110
|
+
throw new Error(`Time range [${round(range.startMs)}ms, ${round(range.endMs)}ms] is outside trace duration [0ms, ${traceDurationMs}ms] (trace bounds: [${round(boundsMinMs)}ms, ${round(boundsMaxMs)}ms]). Did you mean to add --absolute-timestamps?`);
|
|
111
|
+
}
|
|
112
|
+
startMs = Math.max(boundsMinMs, startMs);
|
|
113
|
+
endMs = Math.min(boundsMaxMs, endMs);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
startMs: round(startMs),
|
|
117
|
+
endMs: round(endMs),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Checks if a timestamp in milliseconds falls within a time range.
|
|
122
|
+
*/
|
|
123
|
+
export function isTimestampInBounds(timestampMs, range) {
|
|
124
|
+
if (!range)
|
|
125
|
+
return true;
|
|
126
|
+
return timestampMs >= range.startMs && timestampMs <= range.endMs;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Checks if an event with start time and duration overlaps with a time range.
|
|
130
|
+
*/
|
|
131
|
+
export function isEventInBounds(startMs, durationMs, range) {
|
|
132
|
+
if (!range)
|
|
133
|
+
return true;
|
|
134
|
+
const endMs = durationMs !== undefined ? startMs + durationMs : startMs;
|
|
135
|
+
return endMs >= range.startMs && startMs <= range.endMs;
|
|
136
|
+
}
|