@progen-labs/spectra 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 +21 -0
- package/README.md +99 -2
- package/bin/spectra.js +215 -0
- package/dist/assets/index-BvBsPok5.js +46 -0
- package/dist/assets/index-Da3Le5Px.css +1 -0
- package/dist/index.html +14 -0
- package/index.html +13 -0
- package/package.json +76 -4
- package/server/analysis/cpu.js +369 -0
- package/server/analysis/findings.js +551 -0
- package/server/analysis/locate.js +99 -0
- package/server/analysis/report.js +750 -0
- package/server/analysis/sources.js +249 -0
- package/server/analysis/trace.js +314 -0
- package/server/api.js +292 -0
- package/server/chrome.js +81 -0
- package/server/cli.js +94 -0
- package/server/config.js +59 -0
- package/server/dashboard.js +154 -0
- package/server/explore/boot.js +150 -0
- package/server/explore/classify.js +169 -0
- package/server/explore/discover.js +162 -0
- package/server/explore/evaluate-cli.js +101 -0
- package/server/explore/evaluate.js +165 -0
- package/server/explore/paths.js +105 -0
- package/server/explore/runner.js +94 -0
- package/server/explore/test-explore.js +139 -0
- package/server/explore/test-paths.js +75 -0
- package/server/framework.js +67 -0
- package/server/git.js +59 -0
- package/server/pdf/render.js +60 -0
- package/server/pdf/template.js +419 -0
- package/server/plugin.js +19 -0
- package/server/probe.browser.js +689 -0
- package/server/recorder.js +610 -0
- package/server/signin.js +27 -0
- package/server/test-analysis.js +424 -0
- package/server/test-compare.js +473 -0
- package/server/test-sources.js +136 -0
- package/shared/compare.js +977 -0
- package/shared/snapshot.js +77 -0
- package/src/App.jsx +361 -0
- package/src/api.js +51 -0
- package/src/components/AppearanceMenu.jsx +76 -0
- package/src/components/CodeRef.jsx +48 -0
- package/src/components/ComparePanel.jsx +143 -0
- package/src/components/CompareResults.jsx +473 -0
- package/src/components/ControlBar.jsx +336 -0
- package/src/components/CulpritSphere.jsx +218 -0
- package/src/components/Culprits.jsx +118 -0
- package/src/components/CulpritsHero.jsx +109 -0
- package/src/components/EvaluationPanel.jsx +160 -0
- package/src/components/FilePanel.jsx +225 -0
- package/src/components/FindingsList.jsx +381 -0
- package/src/components/HeatTrees.jsx +222 -0
- package/src/components/Icons.jsx +58 -0
- package/src/components/LongTasks.jsx +199 -0
- package/src/components/NetworkPanel.jsx +98 -0
- package/src/components/PageMap.jsx +329 -0
- package/src/components/Popover.jsx +51 -0
- package/src/components/ReactPanel.jsx +200 -0
- package/src/components/SidePicker.jsx +207 -0
- package/src/components/SnapshotButton.jsx +140 -0
- package/src/components/TimeBreakdown.jsx +45 -0
- package/src/components/TraceButton.jsx +76 -0
- package/src/components/TreeView.jsx +173 -0
- package/src/components/Vitals.jsx +163 -0
- package/src/culprits.js +127 -0
- package/src/download.js +18 -0
- package/src/format.js +26 -0
- package/src/heat.js +177 -0
- package/src/highlight.js +45 -0
- package/src/hooks/useCompare.js +187 -0
- package/src/hooks/useSession.js +73 -0
- package/src/layout.js +55 -0
- package/src/main.jsx +15 -0
- package/src/styles.css +2478 -0
- package/src/theme.js +75 -0
- package/src/themes.css +286 -0
- package/vite.config.js +8 -0
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* In-page probe, injected with Page.addScriptToEvaluateOnNewDocument so it
|
|
3
|
+
* runs before any application code. It is plain ES5-ish browser code: no
|
|
4
|
+
* imports, no build step.
|
|
5
|
+
*
|
|
6
|
+
* It collects what the trace cannot know:
|
|
7
|
+
* - React: per-component render counts, time, *why* each render happened
|
|
8
|
+
* and which props were recreated without changing (via the React DevTools
|
|
9
|
+
* global hook; React 16.9+ / 17 / 18 development builds).
|
|
10
|
+
* - Web vitals as the browser reported them: long tasks, long animation
|
|
11
|
+
* frames, layout shifts (with the shifting elements), slow interactions,
|
|
12
|
+
* LCP, FCP and resource timings.
|
|
13
|
+
*/
|
|
14
|
+
(function () {
|
|
15
|
+
if (window.__PERF_PROBE__) return;
|
|
16
|
+
// Runtime hint injected by the profiler: { framework, bundler, isSSR }
|
|
17
|
+
var SPECTRA_FRAMEWORK = typeof window.__SPECTRA_FRAMEWORK__ !== 'undefined' ? window.__SPECTRA_FRAMEWORK__ : null;
|
|
18
|
+
var WANT_REACT = !SPECTRA_FRAMEWORK || SPECTRA_FRAMEWORK.framework === 'react' || SPECTRA_FRAMEWORK.framework === 'unknown';
|
|
19
|
+
|
|
20
|
+
var PERFORMED_WORK = 1;
|
|
21
|
+
var TAG_FUNCTION = 0;
|
|
22
|
+
var TAG_CLASS = 1;
|
|
23
|
+
var TAG_FORWARD_REF = 11;
|
|
24
|
+
var TAG_SIMPLE_MEMO = 15;
|
|
25
|
+
var TAG_HOST_COMPONENT = 5;
|
|
26
|
+
var TAG_HOST_PORTAL = 4;
|
|
27
|
+
var MAX_COMMITS = 3000;
|
|
28
|
+
var DEEP_COMPARE_LIMIT = 300;
|
|
29
|
+
|
|
30
|
+
var probe = {
|
|
31
|
+
active: false,
|
|
32
|
+
startedAt: 0,
|
|
33
|
+
renderers: 0,
|
|
34
|
+
sawTimings: false,
|
|
35
|
+
reactVersion: '',
|
|
36
|
+
stats: {},
|
|
37
|
+
typeFns: [],
|
|
38
|
+
typeIds: new WeakMap(),
|
|
39
|
+
commits: [],
|
|
40
|
+
commitCount: 0,
|
|
41
|
+
overheadMs: 0,
|
|
42
|
+
roots: new Set(),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
var vitals = {
|
|
46
|
+
longtasks: [],
|
|
47
|
+
loafs: [],
|
|
48
|
+
shifts: [],
|
|
49
|
+
events: [],
|
|
50
|
+
lcp: null,
|
|
51
|
+
fcp: null,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------- helpers
|
|
55
|
+
|
|
56
|
+
function selectorOf(el) {
|
|
57
|
+
if (!el || el.nodeType !== 1) return '';
|
|
58
|
+
var parts = [];
|
|
59
|
+
for (
|
|
60
|
+
var n = el, i = 0;
|
|
61
|
+
n && n.nodeType === 1 && i < 4;
|
|
62
|
+
n = n.parentElement, i++
|
|
63
|
+
) {
|
|
64
|
+
var s = n.tagName.toLowerCase();
|
|
65
|
+
if (n.id) {
|
|
66
|
+
parts.unshift(s + '#' + n.id);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
var testId = n.getAttribute && n.getAttribute('data-test');
|
|
70
|
+
if (testId) s += '[data-test="' + testId + '"]';
|
|
71
|
+
else if (n.classList && n.classList.length) {
|
|
72
|
+
s += '.' + Array.prototype.slice.call(n.classList, 0, 2).join('.');
|
|
73
|
+
}
|
|
74
|
+
parts.unshift(s);
|
|
75
|
+
}
|
|
76
|
+
return parts.join(' > ');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function looseEqual(a, b, depth) {
|
|
80
|
+
if (a === b) return true;
|
|
81
|
+
if (
|
|
82
|
+
depth <= 0 ||
|
|
83
|
+
!a ||
|
|
84
|
+
!b ||
|
|
85
|
+
typeof a !== 'object' ||
|
|
86
|
+
typeof b !== 'object'
|
|
87
|
+
)
|
|
88
|
+
return false;
|
|
89
|
+
if (a.$$typeof || b.$$typeof) return false;
|
|
90
|
+
var ka = Object.keys(a);
|
|
91
|
+
if (ka.length !== Object.keys(b).length || ka.length > 50) return false;
|
|
92
|
+
for (var i = 0; i < ka.length; i++) {
|
|
93
|
+
if (!looseEqual(a[ka[i]], b[ka[i]], depth - 1)) return false;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function sameFunctionSource(a, b) {
|
|
99
|
+
try {
|
|
100
|
+
return (
|
|
101
|
+
a.length === b.length &&
|
|
102
|
+
Function.prototype.toString.call(a) ===
|
|
103
|
+
Function.prototype.toString.call(b)
|
|
104
|
+
);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function componentFn(fiber) {
|
|
111
|
+
var t = fiber.type;
|
|
112
|
+
if (!t) return null;
|
|
113
|
+
if (typeof t === 'function') return t;
|
|
114
|
+
return t.render || t.type || null; // forwardRef / memo wrappers
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function displayName(fiber, fn) {
|
|
118
|
+
var t = fiber.type;
|
|
119
|
+
return (
|
|
120
|
+
(t && t.displayName) ||
|
|
121
|
+
(fn && (fn.displayName || fn.name)) ||
|
|
122
|
+
(fiber.elementType && fiber.elementType.displayName) ||
|
|
123
|
+
'Anonymous'
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function typeIdOf(fn) {
|
|
128
|
+
var id = probe.typeIds.get(fn);
|
|
129
|
+
if (id === undefined) {
|
|
130
|
+
id = probe.typeFns.length;
|
|
131
|
+
probe.typeFns.push(fn);
|
|
132
|
+
probe.typeIds.set(fn, id);
|
|
133
|
+
}
|
|
134
|
+
return id;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ------------------------------------------------- why did this render?
|
|
138
|
+
|
|
139
|
+
function changedPropKeys(prev, next) {
|
|
140
|
+
var keys = [];
|
|
141
|
+
if (prev === next) return keys;
|
|
142
|
+
if (!prev || !next) return ['(props)'];
|
|
143
|
+
var seen = {};
|
|
144
|
+
var k;
|
|
145
|
+
for (k in next) {
|
|
146
|
+
seen[k] = true;
|
|
147
|
+
if (prev[k] !== next[k]) keys.push(k);
|
|
148
|
+
}
|
|
149
|
+
for (k in prev) if (!seen[k] && next[k] !== prev[k]) keys.push(k);
|
|
150
|
+
return keys;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function hooksStateChanged(prev, next) {
|
|
154
|
+
for (var a = prev, b = next; a && b; a = a.next, b = b.next) {
|
|
155
|
+
// Only useState/useReducer hooks own a queue; memo/effect/ref hooks
|
|
156
|
+
// change their memoizedState on every render and would be noise.
|
|
157
|
+
if (b.queue && a.memoizedState !== b.memoizedState) return true;
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function contextChanged(prevDeps, nextDeps) {
|
|
163
|
+
var a = prevDeps && prevDeps.firstContext;
|
|
164
|
+
var b = nextDeps && nextDeps.firstContext;
|
|
165
|
+
while (a && b) {
|
|
166
|
+
if (a.memoizedValue !== b.memoizedValue) return true;
|
|
167
|
+
a = a.next;
|
|
168
|
+
b = b.next;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function classifyPropChange(stat, key, prevValue, nextValue) {
|
|
174
|
+
var rec =
|
|
175
|
+
stat.props[key] ||
|
|
176
|
+
(stat.props[key] = { n: 0, sampled: 0, fn: 0, obj: 0 });
|
|
177
|
+
rec.n++;
|
|
178
|
+
if (stat.renders > DEEP_COMPARE_LIMIT) return;
|
|
179
|
+
rec.sampled++;
|
|
180
|
+
if (typeof prevValue === 'function' && typeof nextValue === 'function') {
|
|
181
|
+
if (sameFunctionSource(prevValue, nextValue)) rec.fn++;
|
|
182
|
+
} else if (
|
|
183
|
+
prevValue &&
|
|
184
|
+
nextValue &&
|
|
185
|
+
typeof prevValue === 'object' &&
|
|
186
|
+
typeof nextValue === 'object'
|
|
187
|
+
) {
|
|
188
|
+
if (looseEqual(prevValue, nextValue, 3)) rec.obj++;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function childrenDuration(fiber) {
|
|
193
|
+
var sum = 0;
|
|
194
|
+
for (var c = fiber.child; c; c = c.sibling) sum += c.actualDuration || 0;
|
|
195
|
+
return sum;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function record(fiber, alt, parentName, commit) {
|
|
199
|
+
var fn = componentFn(fiber);
|
|
200
|
+
if (!fn) return;
|
|
201
|
+
var id = typeIdOf(fn);
|
|
202
|
+
var stat = probe.stats[id];
|
|
203
|
+
if (!stat) {
|
|
204
|
+
stat = probe.stats[id] = {
|
|
205
|
+
id: id,
|
|
206
|
+
name: displayName(fiber, fn),
|
|
207
|
+
kind: fiber.tag === TAG_CLASS ? 'class' : 'function',
|
|
208
|
+
renders: 0,
|
|
209
|
+
mounts: 0,
|
|
210
|
+
wasted: 0,
|
|
211
|
+
selfMs: 0,
|
|
212
|
+
totalMs: 0,
|
|
213
|
+
maxMs: 0,
|
|
214
|
+
reasons: { props: 0, state: 0, hooks: 0, context: 0, parent: 0 },
|
|
215
|
+
props: {},
|
|
216
|
+
parents: {},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
stat.renders++;
|
|
220
|
+
commit.rendered++;
|
|
221
|
+
|
|
222
|
+
var total = fiber.actualDuration || 0;
|
|
223
|
+
var self = Math.max(0, total - childrenDuration(fiber));
|
|
224
|
+
stat.selfMs += self;
|
|
225
|
+
stat.totalMs += total;
|
|
226
|
+
if (self > stat.maxMs) stat.maxMs = self;
|
|
227
|
+
if (parentName)
|
|
228
|
+
stat.parents[parentName] = (stat.parents[parentName] || 0) + 1;
|
|
229
|
+
|
|
230
|
+
if (!alt) {
|
|
231
|
+
stat.mounts++;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
var keys = changedPropKeys(alt.memoizedProps, fiber.memoizedProps);
|
|
235
|
+
var stateChanged = false;
|
|
236
|
+
var hooksChanged = false;
|
|
237
|
+
if (fiber.tag === TAG_CLASS)
|
|
238
|
+
stateChanged = alt.memoizedState !== fiber.memoizedState;
|
|
239
|
+
else
|
|
240
|
+
hooksChanged = hooksStateChanged(alt.memoizedState, fiber.memoizedState);
|
|
241
|
+
var ctxChanged = contextChanged(alt.dependencies, fiber.dependencies);
|
|
242
|
+
|
|
243
|
+
if (keys.length) {
|
|
244
|
+
stat.reasons.props++;
|
|
245
|
+
for (var i = 0; i < keys.length; i++) {
|
|
246
|
+
classifyPropChange(
|
|
247
|
+
stat,
|
|
248
|
+
keys[i],
|
|
249
|
+
alt.memoizedProps && alt.memoizedProps[keys[i]],
|
|
250
|
+
fiber.memoizedProps && fiber.memoizedProps[keys[i]],
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (stateChanged) stat.reasons.state++;
|
|
255
|
+
if (hooksChanged) stat.reasons.hooks++;
|
|
256
|
+
if (ctxChanged) stat.reasons.context++;
|
|
257
|
+
if (!keys.length && !stateChanged && !hooksChanged && !ctxChanged) {
|
|
258
|
+
stat.wasted++;
|
|
259
|
+
stat.reasons.parent++;
|
|
260
|
+
commit.wasted++;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function isComposite(tag) {
|
|
265
|
+
return (
|
|
266
|
+
tag === TAG_FUNCTION ||
|
|
267
|
+
tag === TAG_CLASS ||
|
|
268
|
+
tag === TAG_FORWARD_REF ||
|
|
269
|
+
tag === TAG_SIMPLE_MEMO
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function walkCommit(rootFiber, commit) {
|
|
274
|
+
var stack = [rootFiber, ''];
|
|
275
|
+
while (stack.length) {
|
|
276
|
+
var parentName = stack.pop();
|
|
277
|
+
var fiber = stack.pop();
|
|
278
|
+
var alt = fiber.alternate;
|
|
279
|
+
var flags = fiber.flags !== undefined ? fiber.flags : fiber.effectTag;
|
|
280
|
+
var name = parentName;
|
|
281
|
+
if (isComposite(fiber.tag) && (!alt || flags & PERFORMED_WORK)) {
|
|
282
|
+
record(fiber, alt, parentName, commit);
|
|
283
|
+
name = displayName(fiber, componentFn(fiber));
|
|
284
|
+
} else if (isComposite(fiber.tag)) {
|
|
285
|
+
name = displayName(fiber, componentFn(fiber));
|
|
286
|
+
}
|
|
287
|
+
// A bailed-out subtree shares its child pointer with the previous tree.
|
|
288
|
+
if (fiber.child && (!alt || fiber.child !== alt.child)) {
|
|
289
|
+
for (var c = fiber.child; c; c = c.sibling) stack.push(c, name);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function onCommit(root) {
|
|
295
|
+
probe.roots.add(root);
|
|
296
|
+
// Only development/profiling builds of React maintain actualDuration.
|
|
297
|
+
if (typeof (root.current && root.current.actualDuration) === 'number')
|
|
298
|
+
probe.sawTimings = true;
|
|
299
|
+
if (!probe.active) return;
|
|
300
|
+
var t0 = performance.now();
|
|
301
|
+
var commit = {
|
|
302
|
+
t: t0,
|
|
303
|
+
dur: (root.current && root.current.actualDuration) || 0,
|
|
304
|
+
rendered: 0,
|
|
305
|
+
wasted: 0,
|
|
306
|
+
};
|
|
307
|
+
try {
|
|
308
|
+
walkCommit(root.current, commit);
|
|
309
|
+
} catch (e) {
|
|
310
|
+
probe.lastError = String(e && e.message);
|
|
311
|
+
}
|
|
312
|
+
probe.commitCount++;
|
|
313
|
+
if (probe.commits.length < MAX_COMMITS) probe.commits.push(commit);
|
|
314
|
+
probe.overheadMs += performance.now() - t0;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ------------------------------------------------ DevTools global hook
|
|
318
|
+
|
|
319
|
+
function installHook() {
|
|
320
|
+
var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
321
|
+
if (!hook) {
|
|
322
|
+
var nextId = 1;
|
|
323
|
+
hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
|
|
324
|
+
renderers: new Map(),
|
|
325
|
+
supportsFiber: true,
|
|
326
|
+
isDisabled: false,
|
|
327
|
+
inject: function (renderer) {
|
|
328
|
+
var id = nextId++;
|
|
329
|
+
hook.renderers.set(id, renderer);
|
|
330
|
+
return id;
|
|
331
|
+
},
|
|
332
|
+
onCommitFiberRoot: function () {},
|
|
333
|
+
onCommitFiberUnmount: function () {},
|
|
334
|
+
onScheduleFiberRoot: function () {},
|
|
335
|
+
onPostCommitFiberRoot: function () {},
|
|
336
|
+
checkDCE: function () {},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
var originalInject = hook.inject;
|
|
340
|
+
hook.inject = function (renderer) {
|
|
341
|
+
probe.renderers++;
|
|
342
|
+
probe.reactVersion = (renderer && renderer.version) || probe.reactVersion;
|
|
343
|
+
return originalInject.apply(this, arguments);
|
|
344
|
+
};
|
|
345
|
+
var originalCommit = hook.onCommitFiberRoot;
|
|
346
|
+
hook.onCommitFiberRoot = function (id, root) {
|
|
347
|
+
onCommit(root);
|
|
348
|
+
return originalCommit && originalCommit.apply(this, arguments);
|
|
349
|
+
};
|
|
350
|
+
if (hook.renderers && hook.renderers.size) probe.renderers = hook.renderers.size;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ------------------------------------------------------------ vitals
|
|
354
|
+
|
|
355
|
+
function observe(type, options, handler) {
|
|
356
|
+
try {
|
|
357
|
+
if (!PerformanceObserver.supportedEntryTypes.includes(type)) return;
|
|
358
|
+
var po = new PerformanceObserver(function (list) {
|
|
359
|
+
list.getEntries().forEach(handler);
|
|
360
|
+
});
|
|
361
|
+
options.type = type;
|
|
362
|
+
options.buffered = true;
|
|
363
|
+
po.observe(options);
|
|
364
|
+
} catch (e) {
|
|
365
|
+
/* unsupported entry type */
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function installObservers() {
|
|
370
|
+
observe('longtask', {}, function (e) {
|
|
371
|
+
vitals.longtasks.push({ start: e.startTime, dur: e.duration });
|
|
372
|
+
});
|
|
373
|
+
observe('long-animation-frame', {}, function (e) {
|
|
374
|
+
vitals.loafs.push({
|
|
375
|
+
start: e.startTime,
|
|
376
|
+
dur: e.duration,
|
|
377
|
+
blocking: e.blockingDuration || 0,
|
|
378
|
+
style: e.styleAndLayoutStart
|
|
379
|
+
? e.startTime + e.duration - e.styleAndLayoutStart
|
|
380
|
+
: 0,
|
|
381
|
+
scripts: (e.scripts || []).slice(0, 8).map(function (s) {
|
|
382
|
+
return {
|
|
383
|
+
url: s.sourceURL,
|
|
384
|
+
fn: s.sourceFunctionName,
|
|
385
|
+
pos: s.sourceCharPosition,
|
|
386
|
+
invoker: s.invoker,
|
|
387
|
+
type: s.invokerType,
|
|
388
|
+
dur: s.duration,
|
|
389
|
+
forced: s.forcedStyleAndLayoutDuration || 0,
|
|
390
|
+
};
|
|
391
|
+
}),
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
observe('layout-shift', {}, function (e) {
|
|
395
|
+
if (e.hadRecentInput) return;
|
|
396
|
+
vitals.shifts.push({
|
|
397
|
+
start: e.startTime,
|
|
398
|
+
value: e.value,
|
|
399
|
+
sources: (e.sources || []).slice(0, 4).map(function (s) {
|
|
400
|
+
return selectorOf(s.node);
|
|
401
|
+
}),
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
observe('largest-contentful-paint', {}, function (e) {
|
|
405
|
+
vitals.lcp = {
|
|
406
|
+
start: e.startTime,
|
|
407
|
+
size: e.size,
|
|
408
|
+
el: selectorOf(e.element),
|
|
409
|
+
url: e.url || '',
|
|
410
|
+
};
|
|
411
|
+
});
|
|
412
|
+
observe('paint', {}, function (e) {
|
|
413
|
+
if (e.name === 'first-contentful-paint') vitals.fcp = e.startTime;
|
|
414
|
+
});
|
|
415
|
+
observe('event', { durationThreshold: 40 }, function (e) {
|
|
416
|
+
vitals.events.push({
|
|
417
|
+
name: e.name,
|
|
418
|
+
start: e.startTime,
|
|
419
|
+
dur: e.duration,
|
|
420
|
+
input: e.processingStart - e.startTime,
|
|
421
|
+
processing: e.processingEnd - e.processingStart,
|
|
422
|
+
present: e.startTime + e.duration - e.processingEnd,
|
|
423
|
+
target: selectorOf(e.target),
|
|
424
|
+
interaction: e.interactionId || 0,
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// --------------------------------------------------------------- API
|
|
430
|
+
|
|
431
|
+
probe.start = function () {
|
|
432
|
+
probe.stats = {};
|
|
433
|
+
probe.commits = [];
|
|
434
|
+
probe.commitCount = 0;
|
|
435
|
+
probe.overheadMs = 0;
|
|
436
|
+
probe.startedAt = performance.now();
|
|
437
|
+
probe.active = true;
|
|
438
|
+
return probe.startedAt;
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
probe.stop = function () {
|
|
442
|
+
probe.active = false;
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// Outermost DOM nodes a component renders (stops at the first host element).
|
|
446
|
+
function hostRect(fiber, vw, vh) {
|
|
447
|
+
var left = Infinity;
|
|
448
|
+
var top = Infinity;
|
|
449
|
+
var right = -Infinity;
|
|
450
|
+
var bottom = -Infinity;
|
|
451
|
+
var stack = [];
|
|
452
|
+
for (var c = fiber.child; c; c = c.sibling) stack.push(c);
|
|
453
|
+
while (stack.length) {
|
|
454
|
+
var f = stack.pop();
|
|
455
|
+
if (f.tag === TAG_HOST_COMPONENT) {
|
|
456
|
+
var el = f.stateNode;
|
|
457
|
+
if (!el || !el.getBoundingClientRect) continue;
|
|
458
|
+
var r = el.getBoundingClientRect();
|
|
459
|
+
if (r.width <= 0 || r.height <= 0) continue;
|
|
460
|
+
left = Math.min(left, r.left);
|
|
461
|
+
top = Math.min(top, r.top);
|
|
462
|
+
right = Math.max(right, r.right);
|
|
463
|
+
bottom = Math.max(bottom, r.bottom);
|
|
464
|
+
} else if (f.tag !== TAG_HOST_PORTAL) {
|
|
465
|
+
for (var k = f.child; k; k = k.sibling) stack.push(k);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
left = Math.max(0, left);
|
|
469
|
+
top = Math.max(0, top);
|
|
470
|
+
right = Math.min(vw, right);
|
|
471
|
+
bottom = Math.min(vh, bottom);
|
|
472
|
+
if (right - left < 16 || bottom - top < 16) return null;
|
|
473
|
+
return [
|
|
474
|
+
Math.round(left),
|
|
475
|
+
Math.round(top),
|
|
476
|
+
Math.round(right - left),
|
|
477
|
+
Math.round(bottom - top),
|
|
478
|
+
];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Where every component that rendered during the recording sits in the viewport.
|
|
482
|
+
probe.layout = function () {
|
|
483
|
+
var vw = window.innerWidth;
|
|
484
|
+
var vh = window.innerHeight;
|
|
485
|
+
var byId = {};
|
|
486
|
+
probe.roots.forEach(function (root) {
|
|
487
|
+
var stack = [root.current];
|
|
488
|
+
while (stack.length) {
|
|
489
|
+
var f = stack.pop();
|
|
490
|
+
if (isComposite(f.tag)) {
|
|
491
|
+
var fn = componentFn(f);
|
|
492
|
+
var id = fn ? probe.typeIds.get(fn) : undefined;
|
|
493
|
+
if (id !== undefined && probe.stats[id]) {
|
|
494
|
+
var rect = hostRect(f, vw, vh);
|
|
495
|
+
if (rect) (byId[id] = byId[id] || []).push(rect);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
for (var c = f.child; c; c = c.sibling) stack.push(c);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
var entries = Object.keys(byId)
|
|
502
|
+
.map(function (id) {
|
|
503
|
+
var rects = byId[id]
|
|
504
|
+
.sort(function (a, b) {
|
|
505
|
+
return b[2] * b[3] - a[2] * a[3];
|
|
506
|
+
})
|
|
507
|
+
.slice(0, 6);
|
|
508
|
+
return { id: Number(id), rects: rects, count: byId[id].length };
|
|
509
|
+
})
|
|
510
|
+
.sort(function (a, b) {
|
|
511
|
+
return probe.stats[b.id].selfMs - probe.stats[a.id].selfMs;
|
|
512
|
+
})
|
|
513
|
+
.slice(0, 300);
|
|
514
|
+
return {
|
|
515
|
+
width: vw,
|
|
516
|
+
height: vh,
|
|
517
|
+
scrollX: Math.round(window.scrollX),
|
|
518
|
+
scrollY: Math.round(window.scrollY),
|
|
519
|
+
entries: entries,
|
|
520
|
+
};
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
probe.info = function () {
|
|
524
|
+
return {
|
|
525
|
+
react: probe.renderers > 0,
|
|
526
|
+
version: probe.reactVersion,
|
|
527
|
+
active: probe.active,
|
|
528
|
+
timings: probe.sawTimings,
|
|
529
|
+
};
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
probe.snapshot = function () {
|
|
533
|
+
var since = probe.startedAt;
|
|
534
|
+
var inWindow = function (e) {
|
|
535
|
+
return e.start >= since;
|
|
536
|
+
};
|
|
537
|
+
var resources = performance.getEntriesByType('resource').map(function (r) {
|
|
538
|
+
return {
|
|
539
|
+
url: r.name,
|
|
540
|
+
type: r.initiatorType,
|
|
541
|
+
start: r.startTime,
|
|
542
|
+
dur: r.duration,
|
|
543
|
+
transfer: r.transferSize,
|
|
544
|
+
encoded: r.encodedBodySize,
|
|
545
|
+
decoded: r.decodedBodySize,
|
|
546
|
+
blocking: r.renderBlockingStatus || '',
|
|
547
|
+
};
|
|
548
|
+
});
|
|
549
|
+
var nav = performance.getEntriesByType('navigation')[0];
|
|
550
|
+
return {
|
|
551
|
+
info: probe.info(),
|
|
552
|
+
now: performance.now(),
|
|
553
|
+
since: since,
|
|
554
|
+
overheadMs: probe.overheadMs,
|
|
555
|
+
commitCount: probe.commitCount,
|
|
556
|
+
commits: probe.commits,
|
|
557
|
+
components: Object.keys(probe.stats).map(function (k) {
|
|
558
|
+
return probe.stats[k];
|
|
559
|
+
}),
|
|
560
|
+
vitals: {
|
|
561
|
+
longtasks: vitals.longtasks.filter(inWindow),
|
|
562
|
+
loafs: vitals.loafs.filter(inWindow),
|
|
563
|
+
shifts: vitals.shifts.filter(inWindow),
|
|
564
|
+
events: vitals.events.filter(inWindow),
|
|
565
|
+
lcp: vitals.lcp,
|
|
566
|
+
fcp: vitals.fcp,
|
|
567
|
+
},
|
|
568
|
+
resources: resources,
|
|
569
|
+
navigation: nav
|
|
570
|
+
? {
|
|
571
|
+
ttfb: nav.responseStart,
|
|
572
|
+
domContentLoaded: nav.domContentLoadedEventEnd,
|
|
573
|
+
load: nav.loadEventEnd,
|
|
574
|
+
}
|
|
575
|
+
: null,
|
|
576
|
+
};
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
window.__PERF_PROBE__ = probe;
|
|
580
|
+
// Only attempt to hook into React when the runtime hint allows it.
|
|
581
|
+
try {
|
|
582
|
+
if (WANT_REACT) installHook();
|
|
583
|
+
} catch (e) {
|
|
584
|
+
// Fail silently: probe should still collect vitals even if hooking fails.
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ------------------------------------------------ Framework fallbacks
|
|
588
|
+
// Provide lightweight probes for Vue, Svelte and Angular when React is not detected.
|
|
589
|
+
function installFrameworkProbes() {
|
|
590
|
+
var fw = SPECTRA_FRAMEWORK && SPECTRA_FRAMEWORK.framework ? SPECTRA_FRAMEWORK.framework : null;
|
|
591
|
+
if (!fw) return;
|
|
592
|
+
|
|
593
|
+
// Shared storage on probe for framework-specific summaries
|
|
594
|
+
probe.framework = probe.framework || {
|
|
595
|
+
name: fw,
|
|
596
|
+
detected: false,
|
|
597
|
+
version: '',
|
|
598
|
+
commits: [],
|
|
599
|
+
components: [],
|
|
600
|
+
commitCount: 0,
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
// MutationObserver-based heuristic for UI frameworks (Vue, Svelte): treat DOM updates as "commits".
|
|
604
|
+
try {
|
|
605
|
+
var mo = new MutationObserver(function (mutations) {
|
|
606
|
+
if (!probe.active) return;
|
|
607
|
+
var t = performance.now();
|
|
608
|
+
var rendered = 0;
|
|
609
|
+
var seen = new Set();
|
|
610
|
+
for (var m of mutations) {
|
|
611
|
+
rendered += (m.addedNodes && m.addedNodes.length) || 0;
|
|
612
|
+
// detect Vue component instances on added nodes (Vue 2: __vue__, Vue 3: __vueParentComponent)
|
|
613
|
+
var nodes = m.addedNodes || [];
|
|
614
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
615
|
+
var n = nodes[i];
|
|
616
|
+
try {
|
|
617
|
+
if (n && n.__vue__) seen.add(n.__vue__);
|
|
618
|
+
if (n && n.__vueParentComponent) seen.add(n.__vueParentComponent);
|
|
619
|
+
// Svelte: look for nodes with attributes starting with 'data-svelte'
|
|
620
|
+
if (n && n.attributes) {
|
|
621
|
+
for (var j = 0; j < n.attributes.length; j++) {
|
|
622
|
+
var an = n.attributes[j].name || '';
|
|
623
|
+
if (an.indexOf('data-svelte') === 0) seen.add(an + (n.localName || ''));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
} catch (e) {}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
var commit = { t: t, dur: 0, rendered: rendered, components: seen.size };
|
|
630
|
+
probe.commits.push(commit);
|
|
631
|
+
probe.commitCount = (probe.commitCount || 0) + 1;
|
|
632
|
+
probe.framework.detected = true;
|
|
633
|
+
probe.framework.commitCount = probe.commitCount;
|
|
634
|
+
});
|
|
635
|
+
mo.observe(document, { childList: true, subtree: true });
|
|
636
|
+
} catch (e) {
|
|
637
|
+
// ignore
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Angular: if Zone.js is available, instrument Zone.run to measure change-detection durations
|
|
641
|
+
try {
|
|
642
|
+
if (fw === 'angular' && window.Zone && window.Zone.prototype && !window.__SPECTRA_ANGULAR_ZONE_PATCHED__) {
|
|
643
|
+
window.__SPECTRA_ANGULAR_ZONE_PATCHED__ = true;
|
|
644
|
+
var _origRun = window.Zone.prototype.run;
|
|
645
|
+
window.Zone.prototype.run = function () {
|
|
646
|
+
var start = performance.now();
|
|
647
|
+
try {
|
|
648
|
+
return _origRun.apply(this, arguments);
|
|
649
|
+
} finally {
|
|
650
|
+
try {
|
|
651
|
+
if (probe.active) {
|
|
652
|
+
var dur = performance.now() - start;
|
|
653
|
+
probe.commits.push({ t: start, dur: dur, rendered: 0, components: 0 });
|
|
654
|
+
probe.commitCount = (probe.commitCount || 0) + 1;
|
|
655
|
+
probe.framework.detected = true;
|
|
656
|
+
probe.framework.commitCount = probe.commitCount;
|
|
657
|
+
probe.framework.version = window.Zone.version || probe.framework.version;
|
|
658
|
+
}
|
|
659
|
+
} catch (e) {}
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
} catch (e) {}
|
|
664
|
+
|
|
665
|
+
// Next.js: treat as React unless React missing; try to detect Next router navigations for commits
|
|
666
|
+
try {
|
|
667
|
+
if (fw === 'next') {
|
|
668
|
+
// Next is usually React-based; provide basic navigational commit hints
|
|
669
|
+
if (window.next && window.next.router && window.next.router.events) {
|
|
670
|
+
var ev = window.next.router.events;
|
|
671
|
+
if (ev && ev.on) {
|
|
672
|
+
ev.on('routeChangeComplete', function () {
|
|
673
|
+
if (!probe.active) return;
|
|
674
|
+
var t = performance.now();
|
|
675
|
+
probe.commits.push({ t: t, dur: 0, rendered: 0, components: 0 });
|
|
676
|
+
probe.commitCount = (probe.commitCount || 0) + 1;
|
|
677
|
+
probe.framework.detected = true;
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
} catch (e) {}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
try {
|
|
686
|
+
if (!WANT_REACT) installFrameworkProbes();
|
|
687
|
+
} catch (e) {}
|
|
688
|
+
installObservers();
|
|
689
|
+
})();
|