aipeek 0.2.8 → 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-4BPXH2SW.js → chunk-FBY5P5KH.js} +16 -14
- package/dist/{chunk-SDUTK75Y.cjs → chunk-NJ3KST7P.cjs} +28 -26
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +0 -1
- package/dist/index.d.ts +0 -1
- 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 +21 -12
- package/src/client/client.ts +43 -5
- package/src/core/perf.ts +36 -18
- package/src/core/types.ts +2 -2
|
@@ -96,20 +96,13 @@ var FRAME_NOISE_PCT = 3;
|
|
|
96
96
|
var SELF_NOISE_REL = 0.1;
|
|
97
97
|
var SELF_NOISE_MS = 5;
|
|
98
98
|
var avgOf = (samples) => samples.length > 0 ? samples.reduce((s, v) => s + v, 0) / samples.length : 0;
|
|
99
|
-
function p95(samples) {
|
|
100
|
-
if (samples.length === 0) return 0;
|
|
101
|
-
const sorted = [...samples].sort((a, b) => a - b);
|
|
102
|
-
const idx = Math.floor(0.95 * sorted.length);
|
|
103
|
-
return sorted[idx] ?? 0;
|
|
104
|
-
}
|
|
105
99
|
function summarizeStat(stat) {
|
|
106
100
|
const avg = stat.n > 0 ? stat.total / stat.n : 0;
|
|
107
101
|
return {
|
|
108
102
|
total: stat.total,
|
|
109
103
|
n: stat.n,
|
|
110
104
|
avg,
|
|
111
|
-
max: stat.max
|
|
112
|
-
p95: p95(stat.samples)
|
|
105
|
+
max: stat.max
|
|
113
106
|
};
|
|
114
107
|
}
|
|
115
108
|
function compactPerformance(p) {
|
|
@@ -134,8 +127,8 @@ function detailPerformance(p) {
|
|
|
134
127
|
const windowSec = (p.windowMs / 1e3).toFixed(1);
|
|
135
128
|
lines.push(`=== Performance (${windowSec}s window) ===`);
|
|
136
129
|
if (p.hiddenFrames > 0) {
|
|
137
|
-
lines.push(`\u26A0 Tab was backgrounded \u2014 ${p.hiddenFrames}
|
|
138
|
-
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");
|
|
139
132
|
}
|
|
140
133
|
lines.push(`MobX attribution: ${p.mobxPatched ? "ON" : "OFF"}`);
|
|
141
134
|
if (!p.mobxPatched) {
|
|
@@ -158,14 +151,14 @@ function detailPerformance(p) {
|
|
|
158
151
|
if (comps.length > 0 && p.mobxPatched) {
|
|
159
152
|
lines.push("Components (by total self-time):");
|
|
160
153
|
for (const c of comps) {
|
|
161
|
-
lines.push(` ${c.name.padEnd(30)} ${Math.round(c.total)}ms / ${c.n}\xD7 = ${c.avg.toFixed(1)}ms avg, max ${Math.round(c.max)}ms
|
|
154
|
+
lines.push(` ${c.name.padEnd(30)} ${Math.round(c.total)}ms / ${c.n}\xD7 = ${c.avg.toFixed(1)}ms avg, max ${Math.round(c.max)}ms`);
|
|
162
155
|
}
|
|
163
156
|
} else if (!p.mobxPatched) {
|
|
164
157
|
lines.push("(no component data \u2014 attribution OFF)");
|
|
165
158
|
}
|
|
166
159
|
const hotLines = Object.entries(bucket.lines ?? {}).map(([label, stat]) => ({ label, ...summarizeStat(stat) })).sort((a, b) => b.total - a.total).slice(0, 15);
|
|
167
160
|
if (hotLines.length > 0) {
|
|
168
|
-
lines.push("Hot lines (by total
|
|
161
|
+
lines.push("Hot lines (by total inclusive time \u2014 contains nested calls):");
|
|
169
162
|
for (const l of hotLines) {
|
|
170
163
|
lines.push(` ${l.label.padEnd(36)} ${Math.round(l.total)}ms / ${l.n}\xD7 = ${l.avg.toFixed(1)}ms avg, max ${Math.round(l.max)}ms`);
|
|
171
164
|
}
|
|
@@ -189,8 +182,11 @@ function diffPerformance(before, after) {
|
|
|
189
182
|
const bothSampledFrames = (beforeAll?.frames.total ?? 0) > 0 && (afterAll?.frames.total ?? 0) > 0;
|
|
190
183
|
const sawDrops = (beforeAll?.frames.long ?? 0) > 0 || (afterAll?.frames.long ?? 0) > 0;
|
|
191
184
|
const framesExercised = bothSampledFrames && sawDrops;
|
|
185
|
+
const afterBlind = afterSelf === 0 && (beforeSelf > 0 || (afterAll?.frames.total ?? 0) === 0);
|
|
192
186
|
let verdict;
|
|
193
|
-
if (
|
|
187
|
+
if (afterBlind) {
|
|
188
|
+
verdict = "NO DATA";
|
|
189
|
+
} else if (framesExercised) {
|
|
194
190
|
if (dropDelta < -FRAME_NOISE_PCT) verdict = "IMPROVED";
|
|
195
191
|
else if (dropDelta > FRAME_NOISE_PCT) verdict = "REGRESSED";
|
|
196
192
|
else verdict = "UNCHANGED";
|
|
@@ -201,6 +197,12 @@ function diffPerformance(before, after) {
|
|
|
201
197
|
else verdict = "UNCHANGED";
|
|
202
198
|
}
|
|
203
199
|
out.push(`=== Perf diff: ${verdict} ===`);
|
|
200
|
+
if (afterBlind) {
|
|
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.`);
|
|
203
|
+
out.push("");
|
|
204
|
+
return out.join("\n").trimEnd();
|
|
205
|
+
}
|
|
204
206
|
out.push(`Dropped frames: ${beforeDrop.toFixed(1)}% \u2192 ${afterDrop.toFixed(1)}% (${dropDelta >= 0 ? "+" : ""}${dropDelta.toFixed(1)}%)`);
|
|
205
207
|
if (!framesExercised && Math.round(selfDelta) !== 0) {
|
|
206
208
|
out.push(`Total self-time: ${Math.round(beforeSelf)}ms \u2192 ${Math.round(afterSelf)}ms (${selfDelta >= 0 ? "+" : ""}${Math.round(selfDelta)}ms) \u2014 frames flat, verdict by self-time`);
|
|
@@ -217,7 +219,7 @@ function diffPerformance(before, after) {
|
|
|
217
219
|
}
|
|
218
220
|
const lineDelta = keyedTotalDelta(beforeAll?.lines ?? {}, afterAll?.lines ?? {});
|
|
219
221
|
if (lineDelta.length > 0) {
|
|
220
|
-
out.push("Hot lines (\u0394 total
|
|
222
|
+
out.push("Hot lines (\u0394 total inclusive time):");
|
|
221
223
|
for (const d of lineDelta) out.push(" " + formatDelta(d));
|
|
222
224
|
}
|
|
223
225
|
return out.join("\n").trimEnd();
|
|
@@ -100,20 +100,13 @@ var FRAME_NOISE_PCT = 3;
|
|
|
100
100
|
var SELF_NOISE_REL = 0.1;
|
|
101
101
|
var SELF_NOISE_MS = 5;
|
|
102
102
|
var avgOf = (samples) => samples.length > 0 ? samples.reduce((s, v) => s + v, 0) / samples.length : 0;
|
|
103
|
-
function p95(samples) {
|
|
104
|
-
if (samples.length === 0) return 0;
|
|
105
|
-
const sorted = [...samples].sort((a, b) => a - b);
|
|
106
|
-
const idx = Math.floor(0.95 * sorted.length);
|
|
107
|
-
return _nullishCoalesce(sorted[idx], () => ( 0));
|
|
108
|
-
}
|
|
109
103
|
function summarizeStat(stat) {
|
|
110
104
|
const avg = stat.n > 0 ? stat.total / stat.n : 0;
|
|
111
105
|
return {
|
|
112
106
|
total: stat.total,
|
|
113
107
|
n: stat.n,
|
|
114
108
|
avg,
|
|
115
|
-
max: stat.max
|
|
116
|
-
p95: p95(stat.samples)
|
|
109
|
+
max: stat.max
|
|
117
110
|
};
|
|
118
111
|
}
|
|
119
112
|
function compactPerformance(p) {
|
|
@@ -138,8 +131,8 @@ function detailPerformance(p) {
|
|
|
138
131
|
const windowSec = (p.windowMs / 1e3).toFixed(1);
|
|
139
132
|
lines.push(`=== Performance (${windowSec}s window) ===`);
|
|
140
133
|
if (p.hiddenFrames > 0) {
|
|
141
|
-
lines.push(`\u26A0 Tab was backgrounded \u2014 ${p.hiddenFrames}
|
|
142
|
-
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");
|
|
143
136
|
}
|
|
144
137
|
lines.push(`MobX attribution: ${p.mobxPatched ? "ON" : "OFF"}`);
|
|
145
138
|
if (!p.mobxPatched) {
|
|
@@ -162,14 +155,14 @@ function detailPerformance(p) {
|
|
|
162
155
|
if (comps.length > 0 && p.mobxPatched) {
|
|
163
156
|
lines.push("Components (by total self-time):");
|
|
164
157
|
for (const c of comps) {
|
|
165
|
-
lines.push(` ${c.name.padEnd(30)} ${Math.round(c.total)}ms / ${c.n}\xD7 = ${c.avg.toFixed(1)}ms avg, max ${Math.round(c.max)}ms
|
|
158
|
+
lines.push(` ${c.name.padEnd(30)} ${Math.round(c.total)}ms / ${c.n}\xD7 = ${c.avg.toFixed(1)}ms avg, max ${Math.round(c.max)}ms`);
|
|
166
159
|
}
|
|
167
160
|
} else if (!p.mobxPatched) {
|
|
168
161
|
lines.push("(no component data \u2014 attribution OFF)");
|
|
169
162
|
}
|
|
170
163
|
const hotLines = Object.entries(_nullishCoalesce(bucket.lines, () => ( {}))).map(([label, stat]) => ({ label, ...summarizeStat(stat) })).sort((a, b) => b.total - a.total).slice(0, 15);
|
|
171
164
|
if (hotLines.length > 0) {
|
|
172
|
-
lines.push("Hot lines (by total
|
|
165
|
+
lines.push("Hot lines (by total inclusive time \u2014 contains nested calls):");
|
|
173
166
|
for (const l of hotLines) {
|
|
174
167
|
lines.push(` ${l.label.padEnd(36)} ${Math.round(l.total)}ms / ${l.n}\xD7 = ${l.avg.toFixed(1)}ms avg, max ${Math.round(l.max)}ms`);
|
|
175
168
|
}
|
|
@@ -193,8 +186,11 @@ function diffPerformance(before, after) {
|
|
|
193
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;
|
|
194
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;
|
|
195
188
|
const framesExercised = bothSampledFrames && sawDrops;
|
|
189
|
+
const afterBlind = afterSelf === 0 && (beforeSelf > 0 || (_nullishCoalesce(_optionalChain([afterAll, 'optionalAccess', _9 => _9.frames, 'access', _10 => _10.total]), () => ( 0))) === 0);
|
|
196
190
|
let verdict;
|
|
197
|
-
if (
|
|
191
|
+
if (afterBlind) {
|
|
192
|
+
verdict = "NO DATA";
|
|
193
|
+
} else if (framesExercised) {
|
|
198
194
|
if (dropDelta < -FRAME_NOISE_PCT) verdict = "IMPROVED";
|
|
199
195
|
else if (dropDelta > FRAME_NOISE_PCT) verdict = "REGRESSED";
|
|
200
196
|
else verdict = "UNCHANGED";
|
|
@@ -205,6 +201,12 @@ function diffPerformance(before, after) {
|
|
|
205
201
|
else verdict = "UNCHANGED";
|
|
206
202
|
}
|
|
207
203
|
out.push(`=== Perf diff: ${verdict} ===`);
|
|
204
|
+
if (afterBlind) {
|
|
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.`);
|
|
207
|
+
out.push("");
|
|
208
|
+
return out.join("\n").trimEnd();
|
|
209
|
+
}
|
|
208
210
|
out.push(`Dropped frames: ${beforeDrop.toFixed(1)}% \u2192 ${afterDrop.toFixed(1)}% (${dropDelta >= 0 ? "+" : ""}${dropDelta.toFixed(1)}%)`);
|
|
209
211
|
if (!framesExercised && Math.round(selfDelta) !== 0) {
|
|
210
212
|
out.push(`Total self-time: ${Math.round(beforeSelf)}ms \u2192 ${Math.round(afterSelf)}ms (${selfDelta >= 0 ? "+" : ""}${Math.round(selfDelta)}ms) \u2014 frames flat, verdict by self-time`);
|
|
@@ -213,15 +215,15 @@ function diffPerformance(before, after) {
|
|
|
213
215
|
out.push(`Longtasks: ${before.longtasks.count} \u2192 ${after.longtasks.count}`);
|
|
214
216
|
}
|
|
215
217
|
out.push("");
|
|
216
|
-
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]), () => ( {})));
|
|
217
219
|
if (compDelta.length > 0) {
|
|
218
220
|
out.push("Components (\u0394 total self-time):");
|
|
219
221
|
for (const d of compDelta) out.push(" " + formatDelta(d));
|
|
220
222
|
out.push("");
|
|
221
223
|
}
|
|
222
|
-
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]), () => ( {})));
|
|
223
225
|
if (lineDelta.length > 0) {
|
|
224
|
-
out.push("Hot lines (\u0394 total
|
|
226
|
+
out.push("Hot lines (\u0394 total inclusive time):");
|
|
225
227
|
for (const d of lineDelta) out.push(" " + formatDelta(d));
|
|
226
228
|
}
|
|
227
229
|
return out.join("\n").trimEnd();
|
|
@@ -230,8 +232,8 @@ function keyedTotalDelta(a, b) {
|
|
|
230
232
|
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
231
233
|
const rows = [];
|
|
232
234
|
for (const key of keys) {
|
|
233
|
-
const beforeT = _nullishCoalesce(_optionalChain([a, 'access',
|
|
234
|
-
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));
|
|
235
237
|
const delta = afterT - beforeT;
|
|
236
238
|
if (Math.round(delta) === 0) continue;
|
|
237
239
|
rows.push({ key, before: beforeT, after: afterT, delta, isNew: !a[key], gone: !b[key] });
|
|
@@ -348,7 +350,7 @@ function formatActions(entries, anchorTs) {
|
|
|
348
350
|
}
|
|
349
351
|
function appendAction(log, tab, entry, max) {
|
|
350
352
|
const last = log[log.length - 1];
|
|
351
|
-
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) {
|
|
352
354
|
log[log.length - 1] = { ...entry, tab };
|
|
353
355
|
return;
|
|
354
356
|
}
|
|
@@ -359,7 +361,7 @@ function appendAction(log, tab, entry, max) {
|
|
|
359
361
|
function uiChange(prev, cur) {
|
|
360
362
|
if (cur.modal === void 0)
|
|
361
363
|
return "";
|
|
362
|
-
const prevModal = _nullishCoalesce(_optionalChain([prev, 'optionalAccess',
|
|
364
|
+
const prevModal = _nullishCoalesce(_optionalChain([prev, 'optionalAccess', _22 => _22.modal]), () => ( "none"));
|
|
363
365
|
if (cur.modal !== prevModal) {
|
|
364
366
|
if (cur.modal === "none")
|
|
365
367
|
return "\u5F39\u7A97\u5173\u95ED";
|
|
@@ -909,7 +911,7 @@ function emit(state) {
|
|
|
909
911
|
for (const key of SECTIONS) {
|
|
910
912
|
if (state[key]) {
|
|
911
913
|
const countKey = COUNTED_SECTIONS[key];
|
|
912
|
-
const count = countKey ? _nullishCoalesce(_optionalChain([state, 'access',
|
|
914
|
+
const count = countKey ? _nullishCoalesce(_optionalChain([state, 'access', _23 => _23.counts, 'optionalAccess', _24 => _24[countKey]]), () => ( 0)) : 0;
|
|
913
915
|
const attr = count ? ` count="${count}"` : "";
|
|
914
916
|
sections.push(`<${key}${attr}>
|
|
915
917
|
${state[key]}
|
|
@@ -1264,9 +1266,9 @@ function aipeekPlugin() {
|
|
|
1264
1266
|
const prev = tabs.get(data.tab);
|
|
1265
1267
|
tabs.set(data.tab, {
|
|
1266
1268
|
id: data.tab,
|
|
1267
|
-
url: _nullishCoalesce(_nullishCoalesce(data.url, () => ( _optionalChain([prev, 'optionalAccess',
|
|
1268
|
-
title: _nullishCoalesce(_nullishCoalesce(data.title, () => ( _optionalChain([prev, 'optionalAccess',
|
|
1269
|
-
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)),
|
|
1270
1272
|
lastSeen: Date.now()
|
|
1271
1273
|
});
|
|
1272
1274
|
}
|
|
@@ -1275,7 +1277,7 @@ function aipeekPlugin() {
|
|
|
1275
1277
|
const actionLog = [];
|
|
1276
1278
|
const BOOT_ID = Date.now().toString(36);
|
|
1277
1279
|
const mergeActions = (tab, actions) => {
|
|
1278
|
-
if (!tab || !_optionalChain([actions, 'optionalAccess',
|
|
1280
|
+
if (!tab || !_optionalChain([actions, 'optionalAccess', _28 => _28.length]))
|
|
1279
1281
|
return;
|
|
1280
1282
|
for (const entry of actions) {
|
|
1281
1283
|
if (!actionLog.some((e) => e.tab === tab && e.ts === entry.ts))
|
|
@@ -1520,7 +1522,7 @@ function aipeekPlugin() {
|
|
|
1520
1522
|
const r = await fetch(target, { method: req.method, body });
|
|
1521
1523
|
send(res, r.status, await r.text());
|
|
1522
1524
|
} catch (e) {
|
|
1523
|
-
const code = _optionalChain([e, 'access',
|
|
1525
|
+
const code = _optionalChain([e, 'access', _29 => _29.cause, 'optionalAccess', _30 => _30.code]);
|
|
1524
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"))}).`;
|
|
1525
1527
|
send(res, 502, `cannot reach aipeek peer at ${host}: ${why}`);
|
|
1526
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.d.cts
CHANGED
package/dist/index.d.ts
CHANGED
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
|
@@ -10,7 +10,7 @@ type NetworkEntry = NetworkRequest
|
|
|
10
10
|
// Internal shape differs from core/types PerformanceData: buckets is a name→bucket map (not
|
|
11
11
|
// array), carries startedAt + active (the live view key). client.ts does the array/windowMs
|
|
12
12
|
// projection on collect.
|
|
13
|
-
interface PerfStatBuf { total: number, n: number, max: number
|
|
13
|
+
interface PerfStatBuf { total: number, n: number, max: number }
|
|
14
14
|
interface PerfBucketBuf {
|
|
15
15
|
components: Record<string, PerfStatBuf>
|
|
16
16
|
frames: { total: number, long: number, max: number, samples: number[] }
|
|
@@ -73,21 +73,20 @@ function targetBuckets(): string[] {
|
|
|
73
73
|
return perf.active === '__all__' ? ['__all__'] : [perf.active, '__all__']
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
// One timed hit (component render / line exec) → a stat map, accumulating total/n/max
|
|
77
|
-
//
|
|
76
|
+
// One timed hit (component render / line exec) → a stat map, accumulating total/n/max.
|
|
77
|
+
// No samples buffer: total/n/max are O(1) streaming all-time truths; p95 was the only sample
|
|
78
|
+
// consumer and is gone (near-window p95 vs all-time total = same-row lie). Dropping the per-hit
|
|
79
|
+
// push/shift also lightens the line-instrumentation hot path (less self-pollution).
|
|
78
80
|
function recordStat(map: Record<string, PerfStatBuf>, key: string, ms: number) {
|
|
79
81
|
let s = map[key]
|
|
80
82
|
if (!s) {
|
|
81
|
-
s = { total: 0, n: 0, max: 0
|
|
83
|
+
s = { total: 0, n: 0, max: 0 }
|
|
82
84
|
map[key] = s
|
|
83
85
|
}
|
|
84
86
|
s.total += ms
|
|
85
87
|
s.n += 1
|
|
86
88
|
if (ms > s.max)
|
|
87
89
|
s.max = ms
|
|
88
|
-
s.samples.push(ms)
|
|
89
|
-
if (s.samples.length > PERF_SAMPLE_CAP)
|
|
90
|
-
s.samples.shift()
|
|
91
90
|
}
|
|
92
91
|
|
|
93
92
|
// One component render's self-time → both its live view bucket and the __all__ aggregate.
|
|
@@ -130,16 +129,19 @@ function hitFrame(dt: number) {
|
|
|
130
129
|
}
|
|
131
130
|
}
|
|
132
131
|
|
|
133
|
-
// (b) Frame loop — zero-coupling, holds for any app.
|
|
134
|
-
// throttles to ~1fps, so each
|
|
135
|
-
//
|
|
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.
|
|
136
138
|
const WARN_THRESHOLD = 20 // dropped% — push warning when ≥20% frames jank
|
|
137
139
|
const WARN_COOLDOWN = 5000 // ms — don't spam, at most one warn per 5s
|
|
138
140
|
let lastFrame = performance.now()
|
|
139
141
|
let lastWarn = 0
|
|
140
142
|
function frameLoop() {
|
|
141
143
|
const now = performance.now()
|
|
142
|
-
if (document.hidden) {
|
|
144
|
+
if (document.hidden || !document.hasFocus()) {
|
|
143
145
|
perf.hiddenFrames += 1
|
|
144
146
|
lastFrame = now
|
|
145
147
|
requestAnimationFrame(frameLoop)
|
|
@@ -268,7 +270,14 @@ const originalFetch = window.fetch
|
|
|
268
270
|
sessionStorage.setItem(SEEN_KEY, id) // first sighting — remember this server
|
|
269
271
|
return
|
|
270
272
|
}
|
|
271
|
-
|
|
273
|
+
// Two reload triggers, same fix (reload → re-handshake), both debounced by RELOADING_KEY:
|
|
274
|
+
// 1. BOOT_ID changed → server restarted (the original case).
|
|
275
|
+
// 2. BOOT_ID same but the HMR WebSocket is dead → the socket dropped (Electron window
|
|
276
|
+
// backgrounded/suspended) and Vite's reconnect is gated on visibility, so it never
|
|
277
|
+
// comes back on its own. The action chain rides that socket, so a dead WS = "can't
|
|
278
|
+
// see" even though the server is fine. client.ts sets this flag on vite:ws:disconnect.
|
|
279
|
+
const wsDead = (window as { __aipeek_ws_dead__?: boolean }).__aipeek_ws_dead__ === true
|
|
280
|
+
if (seen !== id || wsDead) {
|
|
272
281
|
sessionStorage.setItem(SEEN_KEY, id)
|
|
273
282
|
if (!sessionStorage.getItem(RELOADING_KEY)) {
|
|
274
283
|
sessionStorage.setItem(RELOADING_KEY, '1')
|
package/src/client/client.ts
CHANGED
|
@@ -24,7 +24,7 @@ declare global {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// Internal perf buffer shape (from client-patch.ts) — differs from PerformanceData
|
|
27
|
-
interface PerfStatBuf { total: number, n: number, max: number
|
|
27
|
+
interface PerfStatBuf { total: number, n: number, max: number }
|
|
28
28
|
interface PerfBucketBuf {
|
|
29
29
|
components: Record<string, PerfStatBuf>
|
|
30
30
|
frames: { total: number, long: number, max: number, samples: number[] }
|
|
@@ -64,15 +64,15 @@ function collectPerformance(): PerformanceData | undefined {
|
|
|
64
64
|
const names = Object.keys(perfBuffer.buckets).sort((a, b) => (a === '__all__' ? -1 : b === '__all__' ? 1 : a.localeCompare(b)))
|
|
65
65
|
for (const name of names) {
|
|
66
66
|
const b = perfBuffer.buckets[name]
|
|
67
|
-
const comps: Record<string, { total: number, n: number, max: number
|
|
67
|
+
const comps: Record<string, { total: number, n: number, max: number }> = {}
|
|
68
68
|
const sorted = Object.entries(b.components).sort((a, b) => b[1].total - a[1].total).slice(0, 20)
|
|
69
69
|
for (const [cname, stat] of sorted) {
|
|
70
|
-
comps[cname] = { total: stat.total, n: stat.n, max: stat.max
|
|
70
|
+
comps[cname] = { total: stat.total, n: stat.n, max: stat.max }
|
|
71
71
|
}
|
|
72
|
-
const lines: Record<string, { total: number, n: number, max: number
|
|
72
|
+
const lines: Record<string, { total: number, n: number, max: number }> = {}
|
|
73
73
|
const sortedLines = Object.entries(b.lines ?? {}).sort((a, b) => b[1].total - a[1].total).slice(0, 30)
|
|
74
74
|
for (const [label, stat] of sortedLines) {
|
|
75
|
-
lines[label] = { total: stat.total, n: stat.n, max: stat.max
|
|
75
|
+
lines[label] = { total: stat.total, n: stat.n, max: stat.max }
|
|
76
76
|
}
|
|
77
77
|
buckets.push({
|
|
78
78
|
name,
|
|
@@ -775,6 +775,44 @@ if (import.meta.hot) {
|
|
|
775
775
|
hello()
|
|
776
776
|
addEventListener('visibilitychange', hello)
|
|
777
777
|
|
|
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() })
|
|
810
|
+
import.meta.hot.on('vite:ws:connect', () => {
|
|
811
|
+
wsDead(false)
|
|
812
|
+
healing = false
|
|
813
|
+
hello() // socket came back without a page reload — re-register so roster doesn't go stale
|
|
814
|
+
})
|
|
815
|
+
|
|
778
816
|
import.meta.hot.on('aipeek:collect', (msg: { requireVisible?: boolean, tab?: string }) => {
|
|
779
817
|
if (skip(msg))
|
|
780
818
|
return
|
package/src/core/perf.ts
CHANGED
|
@@ -9,15 +9,11 @@ const SELF_NOISE_MS = 5
|
|
|
9
9
|
const avgOf = (samples: number[]) =>
|
|
10
10
|
samples.length > 0 ? samples.reduce((s, v) => s + v, 0) / samples.length : 0
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return sorted[idx] ?? 0
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Summarize accumulated stat into display shape
|
|
12
|
+
// Summarize accumulated stat into display shape. total/n/max are O(1) streaming all-time
|
|
13
|
+
// truths; avg derives from them. No p95 here: it would need a per-key sample buffer, which is
|
|
14
|
+
// near-window while total/n/max are all-time — putting two different sample sets in one row
|
|
15
|
+
// (a row implies one distribution). max already gives the worst case, avg the center; that's
|
|
16
|
+
// enough to decide "is this worth optimizing" without a same-row lie.
|
|
21
17
|
export function summarizeStat(stat: PerfStat) {
|
|
22
18
|
const avg = stat.n > 0 ? stat.total / stat.n : 0
|
|
23
19
|
return {
|
|
@@ -25,7 +21,6 @@ export function summarizeStat(stat: PerfStat) {
|
|
|
25
21
|
n: stat.n,
|
|
26
22
|
avg,
|
|
27
23
|
max: stat.max,
|
|
28
|
-
p95: p95(stat.samples)
|
|
29
24
|
}
|
|
30
25
|
}
|
|
31
26
|
|
|
@@ -35,7 +30,6 @@ export function mergeStat(a: PerfStat, b: PerfStat): PerfStat {
|
|
|
35
30
|
total: a.total + b.total,
|
|
36
31
|
n: a.n + b.n,
|
|
37
32
|
max: Math.max(a.max, b.max),
|
|
38
|
-
samples: [...a.samples, ...b.samples].slice(-5000) // cap at 5000
|
|
39
33
|
}
|
|
40
34
|
}
|
|
41
35
|
|
|
@@ -83,8 +77,8 @@ export function detailPerformance(p: PerformanceData): string {
|
|
|
83
77
|
const windowSec = (p.windowMs / 1000).toFixed(1)
|
|
84
78
|
lines.push(`=== Performance (${windowSec}s window) ===`)
|
|
85
79
|
if (p.hiddenFrames > 0) {
|
|
86
|
-
lines.push(`⚠ Tab was backgrounded — ${p.hiddenFrames}
|
|
87
|
-
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')
|
|
88
82
|
}
|
|
89
83
|
lines.push(`MobX attribution: ${p.mobxPatched ? 'ON' : 'OFF'}`)
|
|
90
84
|
if (!p.mobxPatched) {
|
|
@@ -120,21 +114,24 @@ export function detailPerformance(p: PerformanceData): string {
|
|
|
120
114
|
if (comps.length > 0 && p.mobxPatched) {
|
|
121
115
|
lines.push('Components (by total self-time):')
|
|
122
116
|
for (const c of comps) {
|
|
123
|
-
lines.push(` ${c.name.padEnd(30)} ${Math.round(c.total)}ms / ${c.n}× = ${c.avg.toFixed(1)}ms avg, max ${Math.round(c.max)}ms
|
|
117
|
+
lines.push(` ${c.name.padEnd(30)} ${Math.round(c.total)}ms / ${c.n}× = ${c.avg.toFixed(1)}ms avg, max ${Math.round(c.max)}ms`)
|
|
124
118
|
}
|
|
125
119
|
} else if (!p.mobxPatched) {
|
|
126
120
|
lines.push('(no component data — attribution OFF)')
|
|
127
121
|
}
|
|
128
122
|
|
|
129
|
-
// Hot lines (top 15 by total
|
|
123
|
+
// Hot lines (top 15 by total time) — babel line-level instrumentation.
|
|
130
124
|
// Orthogonal to component attribution: points at the exact source line, not just the component.
|
|
125
|
+
// INCLUSIVE, not self-time: __line wraps each statement, so an outer statement's timing
|
|
126
|
+
// contains every nested __line below it (`const x = foo(bar())` counts foo+bar). Labeling
|
|
127
|
+
// this "self-time" would lie — you'd optimize a high-level line that just sums its callees.
|
|
131
128
|
const hotLines = Object.entries(bucket.lines ?? {})
|
|
132
129
|
.map(([label, stat]) => ({ label, ...summarizeStat(stat) }))
|
|
133
130
|
.sort((a, b) => b.total - a.total)
|
|
134
131
|
.slice(0, 15)
|
|
135
132
|
|
|
136
133
|
if (hotLines.length > 0) {
|
|
137
|
-
lines.push('Hot lines (by total
|
|
134
|
+
lines.push('Hot lines (by total inclusive time — contains nested calls):')
|
|
138
135
|
for (const l of hotLines) {
|
|
139
136
|
lines.push(` ${l.label.padEnd(36)} ${Math.round(l.total)}ms / ${l.n}× = ${l.avg.toFixed(1)}ms avg, max ${Math.round(l.max)}ms`)
|
|
140
137
|
}
|
|
@@ -177,8 +174,21 @@ export function diffPerformance(before: PerformanceData, after: PerformanceData)
|
|
|
177
174
|
const bothSampledFrames = (beforeAll?.frames.total ?? 0) > 0 && (afterAll?.frames.total ?? 0) > 0
|
|
178
175
|
const sawDrops = (beforeAll?.frames.long ?? 0) > 0 || (afterAll?.frames.long ?? 0) > 0
|
|
179
176
|
const framesExercised = bothSampledFrames && sawDrops
|
|
177
|
+
|
|
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)
|
|
180
188
|
let verdict: string
|
|
181
|
-
if (
|
|
189
|
+
if (afterBlind) {
|
|
190
|
+
verdict = 'NO DATA'
|
|
191
|
+
} else if (framesExercised) {
|
|
182
192
|
if (dropDelta < -FRAME_NOISE_PCT) verdict = 'IMPROVED'
|
|
183
193
|
else if (dropDelta > FRAME_NOISE_PCT) verdict = 'REGRESSED'
|
|
184
194
|
else verdict = 'UNCHANGED'
|
|
@@ -190,6 +200,14 @@ export function diffPerformance(before: PerformanceData, after: PerformanceData)
|
|
|
190
200
|
}
|
|
191
201
|
|
|
192
202
|
out.push(`=== Perf diff: ${verdict} ===`)
|
|
203
|
+
if (afterBlind) {
|
|
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.`)
|
|
208
|
+
out.push('')
|
|
209
|
+
return out.join('\n').trimEnd()
|
|
210
|
+
}
|
|
193
211
|
out.push(`Dropped frames: ${beforeDrop.toFixed(1)}% → ${afterDrop.toFixed(1)}% (${dropDelta >= 0 ? '+' : ''}${dropDelta.toFixed(1)}%)`)
|
|
194
212
|
if (!framesExercised && Math.round(selfDelta) !== 0) {
|
|
195
213
|
out.push(`Total self-time: ${Math.round(beforeSelf)}ms → ${Math.round(afterSelf)}ms (${selfDelta >= 0 ? '+' : ''}${Math.round(selfDelta)}ms) — frames flat, verdict by self-time`)
|
|
@@ -210,7 +228,7 @@ export function diffPerformance(before: PerformanceData, after: PerformanceData)
|
|
|
210
228
|
// Per-line total-ms deltas (only when line instrumentation was on).
|
|
211
229
|
const lineDelta = keyedTotalDelta(beforeAll?.lines ?? {}, afterAll?.lines ?? {})
|
|
212
230
|
if (lineDelta.length > 0) {
|
|
213
|
-
out.push('Hot lines (Δ total
|
|
231
|
+
out.push('Hot lines (Δ total inclusive time):')
|
|
214
232
|
for (const d of lineDelta) out.push(' ' + formatDelta(d))
|
|
215
233
|
}
|
|
216
234
|
|
package/src/core/types.ts
CHANGED
|
@@ -64,12 +64,12 @@ export interface RawState {
|
|
|
64
64
|
// 语义分桶 profiler 的投影。三类正交信号(组件 self-time / 帧 dt / longtask)按 view 分桶,
|
|
65
65
|
// 答「哪个组件在烧帧」。client-patch 采集进 ring buffer,client 投影成此结构,core/perf.ts 纯渲染。
|
|
66
66
|
|
|
67
|
-
//
|
|
67
|
+
// 一段重复计时的累积统计。total/n/max 全是 O(1) 流式全程真值,avg=total/n 派生。
|
|
68
|
+
// 不存 samples:它过去只为 p95,而 p95 是近窗近似、和全程 total 不同源(放同一行即撒谎),已删。
|
|
68
69
|
export interface PerfStat {
|
|
69
70
|
total: number
|
|
70
71
|
n: number
|
|
71
72
|
max: number
|
|
72
|
-
samples: number[]
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
// 一个语义桶(一个 view,或全局 __all__):组件 self-time map + 帧统计 + 行级 self-time map。
|