aipeek 0.2.8 → 0.2.9
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-SDUTK75Y.cjs → chunk-7ALIH3JX.cjs} +25 -24
- package/dist/{chunk-4BPXH2SW.js → chunk-7NJSWR7E.js} +13 -12
- 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 +14 -8
- package/src/client/client.ts +18 -5
- package/src/core/perf.ts +27 -16
- package/src/core/types.ts +2 -2
|
@@ -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) {
|
|
@@ -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 = (_nullishCoalesce(_optionalChain([afterAll, 'optionalAccess', _9 => _9.frames, 'access', _10 => _10.total]), () => ( 0))) === 0 && afterSelf === 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,11 @@ function diffPerformance(before, after) {
|
|
|
205
201
|
else verdict = "UNCHANGED";
|
|
206
202
|
}
|
|
207
203
|
out.push(`=== Perf diff: ${verdict} ===`);
|
|
204
|
+
if (afterBlind) {
|
|
205
|
+
out.push("after-window recorded no frames and no self-time \u2014 tab backgrounded or idle. Re-run with the tab focused and the interaction reproduced.");
|
|
206
|
+
out.push("");
|
|
207
|
+
return out.join("\n").trimEnd();
|
|
208
|
+
}
|
|
208
209
|
out.push(`Dropped frames: ${beforeDrop.toFixed(1)}% \u2192 ${afterDrop.toFixed(1)}% (${dropDelta >= 0 ? "+" : ""}${dropDelta.toFixed(1)}%)`);
|
|
209
210
|
if (!framesExercised && Math.round(selfDelta) !== 0) {
|
|
210
211
|
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 +214,15 @@ function diffPerformance(before, after) {
|
|
|
213
214
|
out.push(`Longtasks: ${before.longtasks.count} \u2192 ${after.longtasks.count}`);
|
|
214
215
|
}
|
|
215
216
|
out.push("");
|
|
216
|
-
const compDelta = keyedTotalDelta(_nullishCoalesce(_optionalChain([beforeAll, 'optionalAccess',
|
|
217
|
+
const compDelta = keyedTotalDelta(_nullishCoalesce(_optionalChain([beforeAll, 'optionalAccess', _11 => _11.components]), () => ( {})), _nullishCoalesce(_optionalChain([afterAll, 'optionalAccess', _12 => _12.components]), () => ( {})));
|
|
217
218
|
if (compDelta.length > 0) {
|
|
218
219
|
out.push("Components (\u0394 total self-time):");
|
|
219
220
|
for (const d of compDelta) out.push(" " + formatDelta(d));
|
|
220
221
|
out.push("");
|
|
221
222
|
}
|
|
222
|
-
const lineDelta = keyedTotalDelta(_nullishCoalesce(_optionalChain([beforeAll, 'optionalAccess',
|
|
223
|
+
const lineDelta = keyedTotalDelta(_nullishCoalesce(_optionalChain([beforeAll, 'optionalAccess', _13 => _13.lines]), () => ( {})), _nullishCoalesce(_optionalChain([afterAll, 'optionalAccess', _14 => _14.lines]), () => ( {})));
|
|
223
224
|
if (lineDelta.length > 0) {
|
|
224
|
-
out.push("Hot lines (\u0394 total
|
|
225
|
+
out.push("Hot lines (\u0394 total inclusive time):");
|
|
225
226
|
for (const d of lineDelta) out.push(" " + formatDelta(d));
|
|
226
227
|
}
|
|
227
228
|
return out.join("\n").trimEnd();
|
|
@@ -230,8 +231,8 @@ function keyedTotalDelta(a, b) {
|
|
|
230
231
|
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
231
232
|
const rows = [];
|
|
232
233
|
for (const key of keys) {
|
|
233
|
-
const beforeT = _nullishCoalesce(_optionalChain([a, 'access',
|
|
234
|
-
const afterT = _nullishCoalesce(_optionalChain([b, 'access',
|
|
234
|
+
const beforeT = _nullishCoalesce(_optionalChain([a, 'access', _15 => _15[key], 'optionalAccess', _16 => _16.total]), () => ( 0));
|
|
235
|
+
const afterT = _nullishCoalesce(_optionalChain([b, 'access', _17 => _17[key], 'optionalAccess', _18 => _18.total]), () => ( 0));
|
|
235
236
|
const delta = afterT - beforeT;
|
|
236
237
|
if (Math.round(delta) === 0) continue;
|
|
237
238
|
rows.push({ key, before: beforeT, after: afterT, delta, isNew: !a[key], gone: !b[key] });
|
|
@@ -348,7 +349,7 @@ function formatActions(entries, anchorTs) {
|
|
|
348
349
|
}
|
|
349
350
|
function appendAction(log, tab, entry, max) {
|
|
350
351
|
const last = log[log.length - 1];
|
|
351
|
-
if (entry.type === "input" && _optionalChain([last, 'optionalAccess',
|
|
352
|
+
if (entry.type === "input" && _optionalChain([last, 'optionalAccess', _19 => _19.type]) === "input" && last.tab === tab && last.target === entry.target) {
|
|
352
353
|
log[log.length - 1] = { ...entry, tab };
|
|
353
354
|
return;
|
|
354
355
|
}
|
|
@@ -359,7 +360,7 @@ function appendAction(log, tab, entry, max) {
|
|
|
359
360
|
function uiChange(prev, cur) {
|
|
360
361
|
if (cur.modal === void 0)
|
|
361
362
|
return "";
|
|
362
|
-
const prevModal = _nullishCoalesce(_optionalChain([prev, 'optionalAccess',
|
|
363
|
+
const prevModal = _nullishCoalesce(_optionalChain([prev, 'optionalAccess', _20 => _20.modal]), () => ( "none"));
|
|
363
364
|
if (cur.modal !== prevModal) {
|
|
364
365
|
if (cur.modal === "none")
|
|
365
366
|
return "\u5F39\u7A97\u5173\u95ED";
|
|
@@ -909,7 +910,7 @@ function emit(state) {
|
|
|
909
910
|
for (const key of SECTIONS) {
|
|
910
911
|
if (state[key]) {
|
|
911
912
|
const countKey = COUNTED_SECTIONS[key];
|
|
912
|
-
const count = countKey ? _nullishCoalesce(_optionalChain([state, 'access',
|
|
913
|
+
const count = countKey ? _nullishCoalesce(_optionalChain([state, 'access', _21 => _21.counts, 'optionalAccess', _22 => _22[countKey]]), () => ( 0)) : 0;
|
|
913
914
|
const attr = count ? ` count="${count}"` : "";
|
|
914
915
|
sections.push(`<${key}${attr}>
|
|
915
916
|
${state[key]}
|
|
@@ -1264,9 +1265,9 @@ function aipeekPlugin() {
|
|
|
1264
1265
|
const prev = tabs.get(data.tab);
|
|
1265
1266
|
tabs.set(data.tab, {
|
|
1266
1267
|
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',
|
|
1268
|
+
url: _nullishCoalesce(_nullishCoalesce(data.url, () => ( _optionalChain([prev, 'optionalAccess', _23 => _23.url]))), () => ( "")),
|
|
1269
|
+
title: _nullishCoalesce(_nullishCoalesce(data.title, () => ( _optionalChain([prev, 'optionalAccess', _24 => _24.title]))), () => ( "")),
|
|
1270
|
+
visible: _nullishCoalesce(_nullishCoalesce(data.visible, () => ( _optionalChain([prev, 'optionalAccess', _25 => _25.visible]))), () => ( false)),
|
|
1270
1271
|
lastSeen: Date.now()
|
|
1271
1272
|
});
|
|
1272
1273
|
}
|
|
@@ -1275,7 +1276,7 @@ function aipeekPlugin() {
|
|
|
1275
1276
|
const actionLog = [];
|
|
1276
1277
|
const BOOT_ID = Date.now().toString(36);
|
|
1277
1278
|
const mergeActions = (tab, actions) => {
|
|
1278
|
-
if (!tab || !_optionalChain([actions, 'optionalAccess',
|
|
1279
|
+
if (!tab || !_optionalChain([actions, 'optionalAccess', _26 => _26.length]))
|
|
1279
1280
|
return;
|
|
1280
1281
|
for (const entry of actions) {
|
|
1281
1282
|
if (!actionLog.some((e) => e.tab === tab && e.ts === entry.ts))
|
|
@@ -1520,7 +1521,7 @@ function aipeekPlugin() {
|
|
|
1520
1521
|
const r = await fetch(target, { method: req.method, body });
|
|
1521
1522
|
send(res, r.status, await r.text());
|
|
1522
1523
|
} catch (e) {
|
|
1523
|
-
const code = _optionalChain([e, 'access',
|
|
1524
|
+
const code = _optionalChain([e, 'access', _27 => _27.cause, 'optionalAccess', _28 => _28.code]);
|
|
1524
1525
|
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
1526
|
send(res, 502, `cannot reach aipeek peer at ${host}: ${why}`);
|
|
1526
1527
|
}
|
|
@@ -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) {
|
|
@@ -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 = (afterAll?.frames.total ?? 0) === 0 && afterSelf === 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,11 @@ function diffPerformance(before, after) {
|
|
|
201
197
|
else verdict = "UNCHANGED";
|
|
202
198
|
}
|
|
203
199
|
out.push(`=== Perf diff: ${verdict} ===`);
|
|
200
|
+
if (afterBlind) {
|
|
201
|
+
out.push("after-window recorded no frames and no self-time \u2014 tab backgrounded or idle. Re-run with the tab focused and the interaction reproduced.");
|
|
202
|
+
out.push("");
|
|
203
|
+
return out.join("\n").trimEnd();
|
|
204
|
+
}
|
|
204
205
|
out.push(`Dropped frames: ${beforeDrop.toFixed(1)}% \u2192 ${afterDrop.toFixed(1)}% (${dropDelta >= 0 ? "+" : ""}${dropDelta.toFixed(1)}%)`);
|
|
205
206
|
if (!framesExercised && Math.round(selfDelta) !== 0) {
|
|
206
207
|
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 +218,7 @@ function diffPerformance(before, after) {
|
|
|
217
218
|
}
|
|
218
219
|
const lineDelta = keyedTotalDelta(beforeAll?.lines ?? {}, afterAll?.lines ?? {});
|
|
219
220
|
if (lineDelta.length > 0) {
|
|
220
|
-
out.push("Hot lines (\u0394 total
|
|
221
|
+
out.push("Hot lines (\u0394 total inclusive time):");
|
|
221
222
|
for (const d of lineDelta) out.push(" " + formatDelta(d));
|
|
222
223
|
}
|
|
223
224
|
return out.join("\n").trimEnd();
|
package/dist/index.cjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
var
|
|
8
|
+
var _chunk7ALIH3JXcjs = require('./chunk-7ALIH3JX.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 = _chunk7ALIH3JXcjs.aipeekPlugin; exports.check = _chunk7ALIH3JXcjs.check; exports.diffState = _chunk7ALIH3JXcjs.diffState; exports.emitCheck = _chunk7ALIH3JXcjs.emitCheck; exports.emitDiff = _chunk7ALIH3JXcjs.emitDiff; exports.emitSummary = _chunk7ALIH3JXcjs.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 _chunk7ALIH3JXcjs = require('./chunk-7ALIH3JX.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 = _chunk7ALIH3JXcjs.END_TAG; exports.START_TAG = _chunk7ALIH3JXcjs.START_TAG; exports.aipeekPlugin = _chunk7ALIH3JXcjs.aipeekPlugin; exports.injectClaudeMd = _chunk7ALIH3JXcjs.injectClaudeMd; exports.renderClaudeMd = _chunk7ALIH3JXcjs.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.
|
|
@@ -268,7 +267,14 @@ const originalFetch = window.fetch
|
|
|
268
267
|
sessionStorage.setItem(SEEN_KEY, id) // first sighting — remember this server
|
|
269
268
|
return
|
|
270
269
|
}
|
|
271
|
-
|
|
270
|
+
// Two reload triggers, same fix (reload → re-handshake), both debounced by RELOADING_KEY:
|
|
271
|
+
// 1. BOOT_ID changed → server restarted (the original case).
|
|
272
|
+
// 2. BOOT_ID same but the HMR WebSocket is dead → the socket dropped (Electron window
|
|
273
|
+
// backgrounded/suspended) and Vite's reconnect is gated on visibility, so it never
|
|
274
|
+
// comes back on its own. The action chain rides that socket, so a dead WS = "can't
|
|
275
|
+
// see" even though the server is fine. client.ts sets this flag on vite:ws:disconnect.
|
|
276
|
+
const wsDead = (window as { __aipeek_ws_dead__?: boolean }).__aipeek_ws_dead__ === true
|
|
277
|
+
if (seen !== id || wsDead) {
|
|
272
278
|
sessionStorage.setItem(SEEN_KEY, id)
|
|
273
279
|
if (!sessionStorage.getItem(RELOADING_KEY)) {
|
|
274
280
|
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,19 @@ if (import.meta.hot) {
|
|
|
775
775
|
hello()
|
|
776
776
|
addEventListener('visibilitychange', hello)
|
|
777
777
|
|
|
778
|
+
// WS-death beacon for the HTTP heartbeat in client-patch. The whole action chain rides this
|
|
779
|
+
// HMR socket; when it drops, Vite's own reconnect (client.mjs waitForSuccessfulPing) is
|
|
780
|
+
// GATED on document.visibilityState === 'visible' — so a backgrounded Electron window (we
|
|
781
|
+
// app.hide() on launch, and backgroundThrottling throttles it) never reconnects and never
|
|
782
|
+
// reloads, stranding the chain on a dead socket while BOOT_ID is unchanged. client-patch
|
|
783
|
+
// can't see import.meta.hot (it's a non-module inline script), so we surface liveness as a
|
|
784
|
+
// global flag it polls: WS dead + ping OK + same BOOT_ID → it forces the reload Vite won't.
|
|
785
|
+
import.meta.hot.on('vite:ws:disconnect', () => { (window as { __aipeek_ws_dead__?: boolean }).__aipeek_ws_dead__ = true })
|
|
786
|
+
import.meta.hot.on('vite:ws:connect', () => {
|
|
787
|
+
;(window as { __aipeek_ws_dead__?: boolean }).__aipeek_ws_dead__ = false
|
|
788
|
+
hello() // socket came back without a page reload — re-register so roster doesn't go stale
|
|
789
|
+
})
|
|
790
|
+
|
|
778
791
|
import.meta.hot.on('aipeek:collect', (msg: { requireVisible?: boolean, tab?: string }) => {
|
|
779
792
|
if (skip(msg))
|
|
780
793
|
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
|
|
|
@@ -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,17 @@ 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: a backgrounded after-tab records NEITHER frames (rAF frozen) NOR
|
|
179
|
+
// self-time (nothing re-renders, so Reaction.track never fires). Both signals are absent, so
|
|
180
|
+
// selfDelta = 0 - beforeSelf is a large negative → a FALSE "IMPROVED" off an empty window.
|
|
181
|
+
// (The fallback comment above assumed self-time survives backgrounding; live data disproved it
|
|
182
|
+
// — a hidden tab renders nothing.) No data is not a win: refuse to render any direction.
|
|
183
|
+
const afterBlind = (afterAll?.frames.total ?? 0) === 0 && afterSelf === 0
|
|
180
184
|
let verdict: string
|
|
181
|
-
if (
|
|
185
|
+
if (afterBlind) {
|
|
186
|
+
verdict = 'NO DATA'
|
|
187
|
+
} else if (framesExercised) {
|
|
182
188
|
if (dropDelta < -FRAME_NOISE_PCT) verdict = 'IMPROVED'
|
|
183
189
|
else if (dropDelta > FRAME_NOISE_PCT) verdict = 'REGRESSED'
|
|
184
190
|
else verdict = 'UNCHANGED'
|
|
@@ -190,6 +196,11 @@ export function diffPerformance(before: PerformanceData, after: PerformanceData)
|
|
|
190
196
|
}
|
|
191
197
|
|
|
192
198
|
out.push(`=== Perf diff: ${verdict} ===`)
|
|
199
|
+
if (afterBlind) {
|
|
200
|
+
out.push('after-window recorded no frames and no self-time — tab backgrounded or idle. Re-run with the tab focused and the interaction reproduced.')
|
|
201
|
+
out.push('')
|
|
202
|
+
return out.join('\n').trimEnd()
|
|
203
|
+
}
|
|
193
204
|
out.push(`Dropped frames: ${beforeDrop.toFixed(1)}% → ${afterDrop.toFixed(1)}% (${dropDelta >= 0 ? '+' : ''}${dropDelta.toFixed(1)}%)`)
|
|
194
205
|
if (!framesExercised && Math.round(selfDelta) !== 0) {
|
|
195
206
|
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 +221,7 @@ export function diffPerformance(before: PerformanceData, after: PerformanceData)
|
|
|
210
221
|
// Per-line total-ms deltas (only when line instrumentation was on).
|
|
211
222
|
const lineDelta = keyedTotalDelta(beforeAll?.lines ?? {}, afterAll?.lines ?? {})
|
|
212
223
|
if (lineDelta.length > 0) {
|
|
213
|
-
out.push('Hot lines (Δ total
|
|
224
|
+
out.push('Hot lines (Δ total inclusive time):')
|
|
214
225
|
for (const d of lineDelta) out.push(' ' + formatDelta(d))
|
|
215
226
|
}
|
|
216
227
|
|
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。
|