aipeek 0.2.9 → 0.2.10
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/dist/{chunk-7NJSWR7E.js → chunk-FBY5P5KH.js} +5 -4
- package/dist/{chunk-7ALIH3JX.cjs → chunk-NJ3KST7P.cjs} +17 -16
- package/dist/index.cjs +2 -2
- package/dist/index.js +1 -1
- package/dist/plugin.cjs +2 -2
- package/dist/plugin.js +1 -1
- package/package.json +1 -1
- package/src/client/client-patch.ts +7 -4
- package/src/client/client.ts +34 -9
- package/src/core/perf.ts +16 -9
|
@@ -127,8 +127,8 @@ function detailPerformance(p) {
|
|
|
127
127
|
const windowSec = (p.windowMs / 1e3).toFixed(1);
|
|
128
128
|
lines.push(`=== Performance (${windowSec}s window) ===`);
|
|
129
129
|
if (p.hiddenFrames > 0) {
|
|
130
|
-
lines.push(`\u26A0 Tab was backgrounded \u2014 ${p.hiddenFrames}
|
|
131
|
-
lines.push("For accurate profiling: keep tab FOREGROUND + run /profile/reset before reproducing");
|
|
130
|
+
lines.push(`\u26A0 Tab was backgrounded or unfocused \u2014 ${p.hiddenFrames} throttled frames skipped (rAF throttling lies about timing)`);
|
|
131
|
+
lines.push("For accurate profiling: keep tab FOREGROUND + FOCUSED + run /profile/reset before reproducing");
|
|
132
132
|
}
|
|
133
133
|
lines.push(`MobX attribution: ${p.mobxPatched ? "ON" : "OFF"}`);
|
|
134
134
|
if (!p.mobxPatched) {
|
|
@@ -182,7 +182,7 @@ function diffPerformance(before, after) {
|
|
|
182
182
|
const bothSampledFrames = (beforeAll?.frames.total ?? 0) > 0 && (afterAll?.frames.total ?? 0) > 0;
|
|
183
183
|
const sawDrops = (beforeAll?.frames.long ?? 0) > 0 || (afterAll?.frames.long ?? 0) > 0;
|
|
184
184
|
const framesExercised = bothSampledFrames && sawDrops;
|
|
185
|
-
const afterBlind = (afterAll?.frames.total ?? 0) === 0
|
|
185
|
+
const afterBlind = afterSelf === 0 && (beforeSelf > 0 || (afterAll?.frames.total ?? 0) === 0);
|
|
186
186
|
let verdict;
|
|
187
187
|
if (afterBlind) {
|
|
188
188
|
verdict = "NO DATA";
|
|
@@ -198,7 +198,8 @@ function diffPerformance(before, after) {
|
|
|
198
198
|
}
|
|
199
199
|
out.push(`=== Perf diff: ${verdict} ===`);
|
|
200
200
|
if (afterBlind) {
|
|
201
|
-
|
|
201
|
+
const why = (afterAll?.frames.total ?? 0) === 0 ? "recorded no frames and no re-renders" : `recorded frames but zero re-renders (was ${Math.round(beforeSelf)}ms self-time)`;
|
|
202
|
+
out.push(`after-window ${why} \u2014 tab backgrounded or workload not reproduced. Re-run with the tab focused and the interaction repeated.`);
|
|
202
203
|
out.push("");
|
|
203
204
|
return out.join("\n").trimEnd();
|
|
204
205
|
}
|
|
@@ -131,8 +131,8 @@ function detailPerformance(p) {
|
|
|
131
131
|
const windowSec = (p.windowMs / 1e3).toFixed(1);
|
|
132
132
|
lines.push(`=== Performance (${windowSec}s window) ===`);
|
|
133
133
|
if (p.hiddenFrames > 0) {
|
|
134
|
-
lines.push(`\u26A0 Tab was backgrounded \u2014 ${p.hiddenFrames}
|
|
135
|
-
lines.push("For accurate profiling: keep tab FOREGROUND + run /profile/reset before reproducing");
|
|
134
|
+
lines.push(`\u26A0 Tab was backgrounded or unfocused \u2014 ${p.hiddenFrames} throttled frames skipped (rAF throttling lies about timing)`);
|
|
135
|
+
lines.push("For accurate profiling: keep tab FOREGROUND + FOCUSED + run /profile/reset before reproducing");
|
|
136
136
|
}
|
|
137
137
|
lines.push(`MobX attribution: ${p.mobxPatched ? "ON" : "OFF"}`);
|
|
138
138
|
if (!p.mobxPatched) {
|
|
@@ -186,7 +186,7 @@ function diffPerformance(before, after) {
|
|
|
186
186
|
const bothSampledFrames = (_nullishCoalesce(_optionalChain([beforeAll, 'optionalAccess', _ => _.frames, 'access', _2 => _2.total]), () => ( 0))) > 0 && (_nullishCoalesce(_optionalChain([afterAll, 'optionalAccess', _3 => _3.frames, 'access', _4 => _4.total]), () => ( 0))) > 0;
|
|
187
187
|
const sawDrops = (_nullishCoalesce(_optionalChain([beforeAll, 'optionalAccess', _5 => _5.frames, 'access', _6 => _6.long]), () => ( 0))) > 0 || (_nullishCoalesce(_optionalChain([afterAll, 'optionalAccess', _7 => _7.frames, 'access', _8 => _8.long]), () => ( 0))) > 0;
|
|
188
188
|
const framesExercised = bothSampledFrames && sawDrops;
|
|
189
|
-
const afterBlind = (_nullishCoalesce(_optionalChain([afterAll, 'optionalAccess', _9 => _9.frames, 'access', _10 => _10.total]), () => ( 0))) === 0
|
|
189
|
+
const afterBlind = afterSelf === 0 && (beforeSelf > 0 || (_nullishCoalesce(_optionalChain([afterAll, 'optionalAccess', _9 => _9.frames, 'access', _10 => _10.total]), () => ( 0))) === 0);
|
|
190
190
|
let verdict;
|
|
191
191
|
if (afterBlind) {
|
|
192
192
|
verdict = "NO DATA";
|
|
@@ -202,7 +202,8 @@ function diffPerformance(before, after) {
|
|
|
202
202
|
}
|
|
203
203
|
out.push(`=== Perf diff: ${verdict} ===`);
|
|
204
204
|
if (afterBlind) {
|
|
205
|
-
|
|
205
|
+
const why = (_nullishCoalesce(_optionalChain([afterAll, 'optionalAccess', _11 => _11.frames, 'access', _12 => _12.total]), () => ( 0))) === 0 ? "recorded no frames and no re-renders" : `recorded frames but zero re-renders (was ${Math.round(beforeSelf)}ms self-time)`;
|
|
206
|
+
out.push(`after-window ${why} \u2014 tab backgrounded or workload not reproduced. Re-run with the tab focused and the interaction repeated.`);
|
|
206
207
|
out.push("");
|
|
207
208
|
return out.join("\n").trimEnd();
|
|
208
209
|
}
|
|
@@ -214,13 +215,13 @@ function diffPerformance(before, after) {
|
|
|
214
215
|
out.push(`Longtasks: ${before.longtasks.count} \u2192 ${after.longtasks.count}`);
|
|
215
216
|
}
|
|
216
217
|
out.push("");
|
|
217
|
-
const compDelta = keyedTotalDelta(_nullishCoalesce(_optionalChain([beforeAll, 'optionalAccess',
|
|
218
|
+
const compDelta = keyedTotalDelta(_nullishCoalesce(_optionalChain([beforeAll, 'optionalAccess', _13 => _13.components]), () => ( {})), _nullishCoalesce(_optionalChain([afterAll, 'optionalAccess', _14 => _14.components]), () => ( {})));
|
|
218
219
|
if (compDelta.length > 0) {
|
|
219
220
|
out.push("Components (\u0394 total self-time):");
|
|
220
221
|
for (const d of compDelta) out.push(" " + formatDelta(d));
|
|
221
222
|
out.push("");
|
|
222
223
|
}
|
|
223
|
-
const lineDelta = keyedTotalDelta(_nullishCoalesce(_optionalChain([beforeAll, 'optionalAccess',
|
|
224
|
+
const lineDelta = keyedTotalDelta(_nullishCoalesce(_optionalChain([beforeAll, 'optionalAccess', _15 => _15.lines]), () => ( {})), _nullishCoalesce(_optionalChain([afterAll, 'optionalAccess', _16 => _16.lines]), () => ( {})));
|
|
224
225
|
if (lineDelta.length > 0) {
|
|
225
226
|
out.push("Hot lines (\u0394 total inclusive time):");
|
|
226
227
|
for (const d of lineDelta) out.push(" " + formatDelta(d));
|
|
@@ -231,8 +232,8 @@ function keyedTotalDelta(a, b) {
|
|
|
231
232
|
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
232
233
|
const rows = [];
|
|
233
234
|
for (const key of keys) {
|
|
234
|
-
const beforeT = _nullishCoalesce(_optionalChain([a, 'access',
|
|
235
|
-
const afterT = _nullishCoalesce(_optionalChain([b, 'access',
|
|
235
|
+
const beforeT = _nullishCoalesce(_optionalChain([a, 'access', _17 => _17[key], 'optionalAccess', _18 => _18.total]), () => ( 0));
|
|
236
|
+
const afterT = _nullishCoalesce(_optionalChain([b, 'access', _19 => _19[key], 'optionalAccess', _20 => _20.total]), () => ( 0));
|
|
236
237
|
const delta = afterT - beforeT;
|
|
237
238
|
if (Math.round(delta) === 0) continue;
|
|
238
239
|
rows.push({ key, before: beforeT, after: afterT, delta, isNew: !a[key], gone: !b[key] });
|
|
@@ -349,7 +350,7 @@ function formatActions(entries, anchorTs) {
|
|
|
349
350
|
}
|
|
350
351
|
function appendAction(log, tab, entry, max) {
|
|
351
352
|
const last = log[log.length - 1];
|
|
352
|
-
if (entry.type === "input" && _optionalChain([last, 'optionalAccess',
|
|
353
|
+
if (entry.type === "input" && _optionalChain([last, 'optionalAccess', _21 => _21.type]) === "input" && last.tab === tab && last.target === entry.target) {
|
|
353
354
|
log[log.length - 1] = { ...entry, tab };
|
|
354
355
|
return;
|
|
355
356
|
}
|
|
@@ -360,7 +361,7 @@ function appendAction(log, tab, entry, max) {
|
|
|
360
361
|
function uiChange(prev, cur) {
|
|
361
362
|
if (cur.modal === void 0)
|
|
362
363
|
return "";
|
|
363
|
-
const prevModal = _nullishCoalesce(_optionalChain([prev, 'optionalAccess',
|
|
364
|
+
const prevModal = _nullishCoalesce(_optionalChain([prev, 'optionalAccess', _22 => _22.modal]), () => ( "none"));
|
|
364
365
|
if (cur.modal !== prevModal) {
|
|
365
366
|
if (cur.modal === "none")
|
|
366
367
|
return "\u5F39\u7A97\u5173\u95ED";
|
|
@@ -910,7 +911,7 @@ function emit(state) {
|
|
|
910
911
|
for (const key of SECTIONS) {
|
|
911
912
|
if (state[key]) {
|
|
912
913
|
const countKey = COUNTED_SECTIONS[key];
|
|
913
|
-
const count = countKey ? _nullishCoalesce(_optionalChain([state, 'access',
|
|
914
|
+
const count = countKey ? _nullishCoalesce(_optionalChain([state, 'access', _23 => _23.counts, 'optionalAccess', _24 => _24[countKey]]), () => ( 0)) : 0;
|
|
914
915
|
const attr = count ? ` count="${count}"` : "";
|
|
915
916
|
sections.push(`<${key}${attr}>
|
|
916
917
|
${state[key]}
|
|
@@ -1265,9 +1266,9 @@ function aipeekPlugin() {
|
|
|
1265
1266
|
const prev = tabs.get(data.tab);
|
|
1266
1267
|
tabs.set(data.tab, {
|
|
1267
1268
|
id: data.tab,
|
|
1268
|
-
url: _nullishCoalesce(_nullishCoalesce(data.url, () => ( _optionalChain([prev, 'optionalAccess',
|
|
1269
|
-
title: _nullishCoalesce(_nullishCoalesce(data.title, () => ( _optionalChain([prev, 'optionalAccess',
|
|
1270
|
-
visible: _nullishCoalesce(_nullishCoalesce(data.visible, () => ( _optionalChain([prev, 'optionalAccess',
|
|
1269
|
+
url: _nullishCoalesce(_nullishCoalesce(data.url, () => ( _optionalChain([prev, 'optionalAccess', _25 => _25.url]))), () => ( "")),
|
|
1270
|
+
title: _nullishCoalesce(_nullishCoalesce(data.title, () => ( _optionalChain([prev, 'optionalAccess', _26 => _26.title]))), () => ( "")),
|
|
1271
|
+
visible: _nullishCoalesce(_nullishCoalesce(data.visible, () => ( _optionalChain([prev, 'optionalAccess', _27 => _27.visible]))), () => ( false)),
|
|
1271
1272
|
lastSeen: Date.now()
|
|
1272
1273
|
});
|
|
1273
1274
|
}
|
|
@@ -1276,7 +1277,7 @@ function aipeekPlugin() {
|
|
|
1276
1277
|
const actionLog = [];
|
|
1277
1278
|
const BOOT_ID = Date.now().toString(36);
|
|
1278
1279
|
const mergeActions = (tab, actions) => {
|
|
1279
|
-
if (!tab || !_optionalChain([actions, 'optionalAccess',
|
|
1280
|
+
if (!tab || !_optionalChain([actions, 'optionalAccess', _28 => _28.length]))
|
|
1280
1281
|
return;
|
|
1281
1282
|
for (const entry of actions) {
|
|
1282
1283
|
if (!actionLog.some((e) => e.tab === tab && e.ts === entry.ts))
|
|
@@ -1521,7 +1522,7 @@ function aipeekPlugin() {
|
|
|
1521
1522
|
const r = await fetch(target, { method: req.method, body });
|
|
1522
1523
|
send(res, r.status, await r.text());
|
|
1523
1524
|
} catch (e) {
|
|
1524
|
-
const code = _optionalChain([e, 'access',
|
|
1525
|
+
const code = _optionalChain([e, 'access', _29 => _29.cause, 'optionalAccess', _30 => _30.code]);
|
|
1525
1526
|
const why = code === "ECONNREFUSED" ? `nothing is listening on ${host} \u2014 its dev server isn't running (start it), or the port is wrong.` : code === "ENOTFOUND" ? `host '${host}' doesn't resolve \u2014 check the hostname.` : code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT" ? `connection to ${host} timed out \u2014 it's unreachable (firewall, or wrong host).` : `${e.message} (code ${_nullishCoalesce(code, () => ( "unknown"))}).`;
|
|
1526
1527
|
send(res, 502, `cannot reach aipeek peer at ${host}: ${why}`);
|
|
1527
1528
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
var
|
|
8
|
+
var _chunkNJ3KST7Pcjs = require('./chunk-NJ3KST7P.cjs');
|
|
9
9
|
require('./chunk-Z2Y65YOY.cjs');
|
|
10
10
|
|
|
11
11
|
|
|
@@ -14,4 +14,4 @@ require('./chunk-Z2Y65YOY.cjs');
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
exports.aipeekPlugin =
|
|
17
|
+
exports.aipeekPlugin = _chunkNJ3KST7Pcjs.aipeekPlugin; exports.check = _chunkNJ3KST7Pcjs.check; exports.diffState = _chunkNJ3KST7Pcjs.diffState; exports.emitCheck = _chunkNJ3KST7Pcjs.emitCheck; exports.emitDiff = _chunkNJ3KST7Pcjs.emitDiff; exports.emitSummary = _chunkNJ3KST7Pcjs.emitSummary;
|
package/dist/index.js
CHANGED
package/dist/plugin.cjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
var
|
|
7
|
+
var _chunkNJ3KST7Pcjs = require('./chunk-NJ3KST7P.cjs');
|
|
8
8
|
require('./chunk-Z2Y65YOY.cjs');
|
|
9
9
|
|
|
10
10
|
|
|
@@ -12,4 +12,4 @@ require('./chunk-Z2Y65YOY.cjs');
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
exports.END_TAG =
|
|
15
|
+
exports.END_TAG = _chunkNJ3KST7Pcjs.END_TAG; exports.START_TAG = _chunkNJ3KST7Pcjs.START_TAG; exports.aipeekPlugin = _chunkNJ3KST7Pcjs.aipeekPlugin; exports.injectClaudeMd = _chunkNJ3KST7Pcjs.injectClaudeMd; exports.renderClaudeMd = _chunkNJ3KST7Pcjs.renderClaudeMd;
|
package/dist/plugin.js
CHANGED
package/package.json
CHANGED
|
@@ -129,16 +129,19 @@ function hitFrame(dt: number) {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
// (b) Frame loop — zero-coupling, holds for any app.
|
|
133
|
-
// throttles to ~1fps, so each
|
|
134
|
-
//
|
|
132
|
+
// (b) Frame loop — zero-coupling, holds for any app. Throttle guard: a backgrounded OR unfocused
|
|
133
|
+
// window throttles rAF to ~1fps, so each such frame looks like a ~1000ms "dropped" frame and lies.
|
|
134
|
+
// document.hidden only fires when the TAB is switched away; a window that's merely unfocused
|
|
135
|
+
// (another app has focus, minimized, fully occluded) keeps document.hidden=false yet the OS still
|
|
136
|
+
// throttles its rAF — same lie, different probe. document.hasFocus() catches that second case.
|
|
137
|
+
// Don't count dt while throttled; tally hiddenFrames so the report can flag it.
|
|
135
138
|
const WARN_THRESHOLD = 20 // dropped% — push warning when ≥20% frames jank
|
|
136
139
|
const WARN_COOLDOWN = 5000 // ms — don't spam, at most one warn per 5s
|
|
137
140
|
let lastFrame = performance.now()
|
|
138
141
|
let lastWarn = 0
|
|
139
142
|
function frameLoop() {
|
|
140
143
|
const now = performance.now()
|
|
141
|
-
if (document.hidden) {
|
|
144
|
+
if (document.hidden || !document.hasFocus()) {
|
|
142
145
|
perf.hiddenFrames += 1
|
|
143
146
|
lastFrame = now
|
|
144
147
|
requestAnimationFrame(frameLoop)
|
package/src/client/client.ts
CHANGED
|
@@ -775,16 +775,41 @@ if (import.meta.hot) {
|
|
|
775
775
|
hello()
|
|
776
776
|
addEventListener('visibilitychange', hello)
|
|
777
777
|
|
|
778
|
-
//
|
|
779
|
-
//
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
//
|
|
784
|
-
//
|
|
785
|
-
|
|
778
|
+
// Self-heal a dropped HMR socket. The whole action chain rides this WS; when it drops,
|
|
779
|
+
// Vite's own reconnect (client.mjs waitForSuccessfulPing) is GATED on visibilityState ===
|
|
780
|
+
// 'visible', so a backgrounded tab/window (Electron we app.hide() on launch) never reconnects
|
|
781
|
+
// and never reloads — stranding the chain on a dead socket while BOOT_ID is unchanged.
|
|
782
|
+
//
|
|
783
|
+
// The fix is EVENT-DRIVEN, not polled. An earlier version set a flag for the client-patch
|
|
784
|
+
// HTTP heartbeat to notice — but a long-lived background setInterval is exactly what the
|
|
785
|
+
// browser throttles/freezes (verified: a page-load-time interval stops firing in a long-
|
|
786
|
+
// backgrounded tab, while the WS message channel stays instantly live). So we act the moment
|
|
787
|
+
// vite:ws:disconnect fires: kick off a short self-bound setTimeout poll (armed by the event,
|
|
788
|
+
// not a resident timer) that waits for /ping to answer, then reloads to re-handshake. We also
|
|
789
|
+
// keep the global flag for the heartbeat as a belt-and-suspenders second trigger.
|
|
790
|
+
const wsDead = (v: boolean) => { (window as { __aipeek_ws_dead__?: boolean }).__aipeek_ws_dead__ = v }
|
|
791
|
+
let healing = false
|
|
792
|
+
const healAfterDrop = async () => {
|
|
793
|
+
if (healing) return
|
|
794
|
+
healing = true
|
|
795
|
+
// Poll the HTTP heartbeat (independent of the dead WS). Once the server answers, reload.
|
|
796
|
+
// setTimeout (re-armed per tick) rather than setInterval: a fresh timer scheduled right
|
|
797
|
+
// after live activity resists the background freeze that kills resident intervals.
|
|
798
|
+
const tick = async () => {
|
|
799
|
+
try {
|
|
800
|
+
await fetch('/__aipeek/ping', { cache: 'no-store' })
|
|
801
|
+
location.reload() // server reachable again → re-handshake the WS via a fresh load
|
|
802
|
+
}
|
|
803
|
+
catch {
|
|
804
|
+
setTimeout(tick, 2000) // server still down — keep waiting, don't reload on failure
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
tick()
|
|
808
|
+
}
|
|
809
|
+
import.meta.hot.on('vite:ws:disconnect', () => { wsDead(true); healAfterDrop() })
|
|
786
810
|
import.meta.hot.on('vite:ws:connect', () => {
|
|
787
|
-
|
|
811
|
+
wsDead(false)
|
|
812
|
+
healing = false
|
|
788
813
|
hello() // socket came back without a page reload — re-register so roster doesn't go stale
|
|
789
814
|
})
|
|
790
815
|
|
package/src/core/perf.ts
CHANGED
|
@@ -77,8 +77,8 @@ export function detailPerformance(p: PerformanceData): string {
|
|
|
77
77
|
const windowSec = (p.windowMs / 1000).toFixed(1)
|
|
78
78
|
lines.push(`=== Performance (${windowSec}s window) ===`)
|
|
79
79
|
if (p.hiddenFrames > 0) {
|
|
80
|
-
lines.push(`⚠ Tab was backgrounded — ${p.hiddenFrames}
|
|
81
|
-
lines.push('For accurate profiling: keep tab FOREGROUND + run /profile/reset before reproducing')
|
|
80
|
+
lines.push(`⚠ Tab was backgrounded or unfocused — ${p.hiddenFrames} throttled frames skipped (rAF throttling lies about timing)`)
|
|
81
|
+
lines.push('For accurate profiling: keep tab FOREGROUND + FOCUSED + run /profile/reset before reproducing')
|
|
82
82
|
}
|
|
83
83
|
lines.push(`MobX attribution: ${p.mobxPatched ? 'ON' : 'OFF'}`)
|
|
84
84
|
if (!p.mobxPatched) {
|
|
@@ -175,12 +175,16 @@ export function diffPerformance(before: PerformanceData, after: PerformanceData)
|
|
|
175
175
|
const sawDrops = (beforeAll?.frames.long ?? 0) > 0 || (afterAll?.frames.long ?? 0) > 0
|
|
176
176
|
const framesExercised = bothSampledFrames && sawDrops
|
|
177
177
|
|
|
178
|
-
// Blind-window guard:
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
// (
|
|
182
|
-
//
|
|
183
|
-
|
|
178
|
+
// Blind-window guard: the after-window must actually REPRODUCE the workload to be diffable.
|
|
179
|
+
// afterSelf===0 means zero components re-rendered. Two shapes both mean "workload not run":
|
|
180
|
+
// (a) hard-blind — frames.total===0 too (rAF frozen, tab fully suspended);
|
|
181
|
+
// (b) soft-blind — frames still ticked (rAF only throttled) but self-time is zero while the
|
|
182
|
+
// before-window had real self-time. Frames alone then describe an idle tab's compositor,
|
|
183
|
+
// not the render workload — a -5% frame delta off a backgrounded tab is NOT a code win.
|
|
184
|
+
// A genuine fix shows frames recovering AND self-time dropping in agreement; "frames still
|
|
185
|
+
// dropping but self-time vanished entirely" is the contradiction that betrays an idle after-tab.
|
|
186
|
+
// The pure-paint case (beforeSelf===0===afterSelf, frames>0) is NOT blind — it goes to frames.
|
|
187
|
+
const afterBlind = afterSelf === 0 && (beforeSelf > 0 || (afterAll?.frames.total ?? 0) === 0)
|
|
184
188
|
let verdict: string
|
|
185
189
|
if (afterBlind) {
|
|
186
190
|
verdict = 'NO DATA'
|
|
@@ -197,7 +201,10 @@ export function diffPerformance(before: PerformanceData, after: PerformanceData)
|
|
|
197
201
|
|
|
198
202
|
out.push(`=== Perf diff: ${verdict} ===`)
|
|
199
203
|
if (afterBlind) {
|
|
200
|
-
|
|
204
|
+
const why = (afterAll?.frames.total ?? 0) === 0
|
|
205
|
+
? 'recorded no frames and no re-renders'
|
|
206
|
+
: `recorded frames but zero re-renders (was ${Math.round(beforeSelf)}ms self-time)`
|
|
207
|
+
out.push(`after-window ${why} — tab backgrounded or workload not reproduced. Re-run with the tab focused and the interaction repeated.`)
|
|
201
208
|
out.push('')
|
|
202
209
|
return out.join('\n').trimEnd()
|
|
203
210
|
}
|