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.
@@ -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, p95 ${Math.round(c.p95)}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 self-time):");
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 (framesExercised) {
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', _9 => _9.components]), () => ( {})), _nullishCoalesce(_optionalChain([afterAll, 'optionalAccess', _10 => _10.components]), () => ( {})));
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', _11 => _11.lines]), () => ( {})), _nullishCoalesce(_optionalChain([afterAll, 'optionalAccess', _12 => _12.lines]), () => ( {})));
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 self-time):");
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', _13 => _13[key], 'optionalAccess', _14 => _14.total]), () => ( 0));
234
- const afterT = _nullishCoalesce(_optionalChain([b, 'access', _15 => _15[key], 'optionalAccess', _16 => _16.total]), () => ( 0));
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', _17 => _17.type]) === "input" && last.tab === tab && last.target === entry.target) {
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', _18 => _18.modal]), () => ( "none"));
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', _19 => _19.counts, 'optionalAccess', _20 => _20[countKey]]), () => ( 0)) : 0;
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', _21 => _21.url]))), () => ( "")),
1268
- title: _nullishCoalesce(_nullishCoalesce(data.title, () => ( _optionalChain([prev, 'optionalAccess', _22 => _22.title]))), () => ( "")),
1269
- visible: _nullishCoalesce(_nullishCoalesce(data.visible, () => ( _optionalChain([prev, 'optionalAccess', _23 => _23.visible]))), () => ( false)),
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', _24 => _24.length]))
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', _25 => _25.cause, 'optionalAccess', _26 => _26.code]);
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, p95 ${Math.round(c.p95)}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 self-time):");
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 (framesExercised) {
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 self-time):");
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 _chunkSDUTK75Ycjs = require('./chunk-SDUTK75Y.cjs');
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 = _chunkSDUTK75Ycjs.aipeekPlugin; exports.check = _chunkSDUTK75Ycjs.check; exports.diffState = _chunkSDUTK75Ycjs.diffState; exports.emitCheck = _chunkSDUTK75Ycjs.emitCheck; exports.emitDiff = _chunkSDUTK75Ycjs.emitDiff; exports.emitSummary = _chunkSDUTK75Ycjs.emitSummary;
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
@@ -57,7 +57,6 @@ interface PerfStat {
57
57
  total: number;
58
58
  n: number;
59
59
  max: number;
60
- samples: number[];
61
60
  }
62
61
  interface PerfBucketData {
63
62
  name: string;
package/dist/index.d.ts CHANGED
@@ -57,7 +57,6 @@ interface PerfStat {
57
57
  total: number;
58
58
  n: number;
59
59
  max: number;
60
- samples: number[];
61
60
  }
62
61
  interface PerfBucketData {
63
62
  name: string;
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  emitCheck,
6
6
  emitDiff,
7
7
  emitSummary
8
- } from "./chunk-4BPXH2SW.js";
8
+ } from "./chunk-7NJSWR7E.js";
9
9
  export {
10
10
  aipeekPlugin,
11
11
  check,
package/dist/plugin.cjs CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
 
6
6
 
7
- var _chunkSDUTK75Ycjs = require('./chunk-SDUTK75Y.cjs');
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 = _chunkSDUTK75Ycjs.END_TAG; exports.START_TAG = _chunkSDUTK75Ycjs.START_TAG; exports.aipeekPlugin = _chunkSDUTK75Ycjs.aipeekPlugin; exports.injectClaudeMd = _chunkSDUTK75Ycjs.injectClaudeMd; exports.renderClaudeMd = _chunkSDUTK75Ycjs.renderClaudeMd;
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
@@ -4,7 +4,7 @@ import {
4
4
  aipeekPlugin,
5
5
  injectClaudeMd,
6
6
  renderClaudeMd
7
- } from "./chunk-4BPXH2SW.js";
7
+ } from "./chunk-7NJSWR7E.js";
8
8
  export {
9
9
  END_TAG,
10
10
  START_TAG,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aipeek",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Gives AI a peek into your running browser app — UI tree, console, network, errors, state",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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, samples: 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 + capped
77
- // samples. samples capped so a long session doesn't grow unbounded (p95 still representative).
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, samples: [] }
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
- if (seen !== id) { // BOOT_ID changed server restarted re-handshake
270
+ // Two reload triggers, same fix (reloadre-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')
@@ -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, samples: 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, samples: 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, samples: [...stat.samples] }
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, samples: 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, samples: [...stat.samples] }
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
- // 95th percentile sort samples and pick the 95% position
13
- export function p95(samples: number[]): number {
14
- if (samples.length === 0) return 0
15
- const sorted = [...samples].sort((a, b) => a - b)
16
- const idx = Math.floor(0.95 * sorted.length)
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, p95 ${Math.round(c.p95)}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 self-time) — babel line-level instrumentation.
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 self-time):')
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 (framesExercised) {
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 self-time):')
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
- // 一段重复计时的累积统计 —— litcode __rec 的 Stat 同构(samples 封顶供 p95)。
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。