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.
@@ -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} hidden frames skipped (rAF throttling lies about timing)`);
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, 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 = afterSelf === 0 && (beforeSelf > 0 || (afterAll?.frames.total ?? 0) === 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,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 self-time):");
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} hidden frames skipped (rAF throttling lies about timing)`);
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, 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 = afterSelf === 0 && (beforeSelf > 0 || (_nullishCoalesce(_optionalChain([afterAll, 'optionalAccess', _9 => _9.frames, 'access', _10 => _10.total]), () => ( 0))) === 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,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', _9 => _9.components]), () => ( {})), _nullishCoalesce(_optionalChain([afterAll, 'optionalAccess', _10 => _10.components]), () => ( {})));
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', _11 => _11.lines]), () => ( {})), _nullishCoalesce(_optionalChain([afterAll, 'optionalAccess', _12 => _12.lines]), () => ( {})));
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 self-time):");
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', _13 => _13[key], 'optionalAccess', _14 => _14.total]), () => ( 0));
234
- const afterT = _nullishCoalesce(_optionalChain([b, 'access', _15 => _15[key], 'optionalAccess', _16 => _16.total]), () => ( 0));
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', _17 => _17.type]) === "input" && last.tab === tab && last.target === entry.target) {
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', _18 => _18.modal]), () => ( "none"));
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', _19 => _19.counts, 'optionalAccess', _20 => _20[countKey]]), () => ( 0)) : 0;
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', _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)),
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', _24 => _24.length]))
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', _25 => _25.cause, 'optionalAccess', _26 => _26.code]);
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 _chunkSDUTK75Ycjs = require('./chunk-SDUTK75Y.cjs');
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 = _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 = _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
@@ -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-FBY5P5KH.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 _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 = _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 = _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
@@ -4,7 +4,7 @@ import {
4
4
  aipeekPlugin,
5
5
  injectClaudeMd,
6
6
  renderClaudeMd
7
- } from "./chunk-4BPXH2SW.js";
7
+ } from "./chunk-FBY5P5KH.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.10",
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.
@@ -130,16 +129,19 @@ function hitFrame(dt: number) {
130
129
  }
131
130
  }
132
131
 
133
- // (b) Frame loop — zero-coupling, holds for any app. document.hidden guard: backgrounded rAF
134
- // throttles to ~1fps, so each hidden frame looks like a 1000ms "dropped" frame and lies.
135
- // Don't count dt while hidden; tally hiddenFrames so the report can flag it.
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
- if (seen !== id) { // BOOT_ID changed server restarted re-handshake
273
+ // Two reload triggers, same fix (reloadre-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')
@@ -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,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
- // 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
 
@@ -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} hidden frames skipped (rAF throttling lies about timing)`)
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, 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,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 (framesExercised) {
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 self-time):')
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
- // 一段重复计时的累积统计 —— 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。