rerender-lens 0.3.0 → 0.4.0
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/CHANGELOG.md +41 -1
- package/README.md +14 -5
- package/dist/{devtools-DmhWiWcN.d.cts → devtools-BkCct3cJ.d.cts} +10 -2
- package/dist/{devtools-BHGUUo-p.d.ts → devtools-CZebzpF6.d.ts} +10 -2
- package/dist/index.cjs +199 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +199 -71
- package/dist/index.js.map +1 -1
- package/dist/playwright.cjs +5 -2
- package/dist/playwright.cjs.map +1 -1
- package/dist/playwright.d.cts +5 -3
- package/dist/playwright.d.ts +5 -3
- package/dist/playwright.js +5 -3
- package/dist/playwright.js.map +1 -1
- package/dist/rerender-lens.iife.js +768 -307
- package/dist/setup.cjs +199 -71
- package/dist/setup.cjs.map +1 -1
- package/dist/setup.js +199 -71
- package/dist/setup.js.map +1 -1
- package/dist/vitest-setup.cjs +117 -48
- package/dist/vitest-setup.cjs.map +1 -1
- package/dist/vitest-setup.js +117 -48
- package/dist/vitest-setup.js.map +1 -1
- package/dist/vitest.cjs +117 -48
- package/dist/vitest.cjs.map +1 -1
- package/dist/vitest.js +117 -48
- package/dist/vitest.js.map +1 -1
- package/package.json +2 -2
- package/panel/panel.css +0 -34
- package/panel/panel.js +400 -438
package/panel/panel.js
CHANGED
|
@@ -2,12 +2,6 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
(() => {
|
|
4
4
|
// extension/src/panel.ts
|
|
5
|
-
var OPTIONAL_COLUMNS = [
|
|
6
|
-
{ key: "places", label: "Places" },
|
|
7
|
-
{ key: "lastSeen", label: "Last seen" }
|
|
8
|
-
];
|
|
9
|
-
var VIRTUAL_THRESHOLD = 200;
|
|
10
|
-
var GRID_ROW_H = 26;
|
|
11
5
|
var PROTOCOL = 2;
|
|
12
6
|
var KIND_LABEL = {
|
|
13
7
|
"deep-equal": "equal by value",
|
|
@@ -22,6 +16,14 @@
|
|
|
22
16
|
var MAX_REPORTS = 2e3;
|
|
23
17
|
var MAX_PER_NODE = 200;
|
|
24
18
|
var MAX_COMMITS = 500;
|
|
19
|
+
var MAX_PER_COMMIT = 500;
|
|
20
|
+
var THROTTLE_MIN_REPORTS = 200;
|
|
21
|
+
var BEST_FIX_INTERVAL = 500;
|
|
22
|
+
var LEFT_RENDER_INTERVAL = 250;
|
|
23
|
+
var SYNC_RETRY_MIN = 500;
|
|
24
|
+
var SYNC_RETRY_MAX = 5e3;
|
|
25
|
+
var SYNC_RETRIES = 20;
|
|
26
|
+
var NAVIGATION_SETTLE = 1200;
|
|
25
27
|
var ROW_H = 22;
|
|
26
28
|
var ITEM_H = 20;
|
|
27
29
|
var OVERSCAN = 8;
|
|
@@ -386,12 +388,22 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
386
388
|
return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
387
389
|
}
|
|
388
390
|
var isAncestorReport = (anc, r) => anc.path.length < r.path.length && r.path[anc.path.length] === anc.component && anc.path.every((p, i) => r.path[i] === p);
|
|
389
|
-
function
|
|
391
|
+
function indexByComponent(reports) {
|
|
392
|
+
const index = /* @__PURE__ */ new Map();
|
|
393
|
+
for (const r of reports) {
|
|
394
|
+
const list = index.get(r.component);
|
|
395
|
+
if (list) list.push(r);
|
|
396
|
+
else index.set(r.component, [r]);
|
|
397
|
+
}
|
|
398
|
+
return index;
|
|
399
|
+
}
|
|
400
|
+
function rootCauseOf(r, commitReports, index = indexByComponent(commitReports)) {
|
|
390
401
|
let cur = r;
|
|
391
402
|
const seen = /* @__PURE__ */ new Set([r]);
|
|
392
403
|
while (cur.trigger === "parent" && cur.parent) {
|
|
393
404
|
const parent = cur.parent;
|
|
394
|
-
const
|
|
405
|
+
const candidates = index.get(parent.name);
|
|
406
|
+
const p = candidates && candidates.find((x) => isAncestorReport(x, cur));
|
|
395
407
|
if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
|
|
396
408
|
seen.add(p);
|
|
397
409
|
cur = p;
|
|
@@ -400,15 +412,18 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
400
412
|
}
|
|
401
413
|
function analyzeCommit(reports) {
|
|
402
414
|
const roots = /* @__PURE__ */ new Map();
|
|
415
|
+
const rootByReport = /* @__PURE__ */ new Map();
|
|
416
|
+
const index = indexByComponent(reports);
|
|
403
417
|
let avoidable = 0;
|
|
404
418
|
let wasted = 0;
|
|
405
419
|
for (const r of reports) {
|
|
406
420
|
if (!r.avoidable) continue;
|
|
407
421
|
avoidable++;
|
|
408
422
|
if (typeof r.selfDuration === "number") wasted += r.selfDuration;
|
|
409
|
-
const root = rootCauseOf(r, reports);
|
|
423
|
+
const root = rootCauseOf(r, reports, index);
|
|
410
424
|
const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
|
|
411
425
|
const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
|
|
426
|
+
rootByReport.set(r, name);
|
|
412
427
|
let agg = roots.get(name);
|
|
413
428
|
if (!agg) {
|
|
414
429
|
agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
|
|
@@ -425,6 +440,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
425
440
|
avoidable,
|
|
426
441
|
wasted,
|
|
427
442
|
roots: [...roots.values()].sort((a, b) => b.count - a.count),
|
|
443
|
+
rootByReport,
|
|
428
444
|
contexts: contextAttribution(reports),
|
|
429
445
|
fixes: rankFixes(reports),
|
|
430
446
|
reports
|
|
@@ -471,22 +487,18 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
471
487
|
}
|
|
472
488
|
return root;
|
|
473
489
|
}
|
|
474
|
-
function rootCauseSummary(name, commits) {
|
|
490
|
+
function rootCauseSummary(name, commits, analyze = (_, reports) => analyzeCommit(reports)) {
|
|
475
491
|
const out = { name, trigger: "parent", commits: [], total: 0, components: /* @__PURE__ */ new Map(), fixes: [] };
|
|
476
492
|
const affected = [];
|
|
477
493
|
for (const [key, reports] of commits) {
|
|
478
|
-
const analysis =
|
|
494
|
+
const analysis = analyze(key, reports);
|
|
479
495
|
const root = analysis.roots.find((x) => x.name === name);
|
|
480
496
|
if (!root) continue;
|
|
481
497
|
out.trigger = root.trigger;
|
|
482
498
|
out.commits.push({ key, analysis, count: root.count, components: root.components });
|
|
483
499
|
out.total += root.count;
|
|
484
500
|
for (const [c, n] of root.components) out.components.set(c, (out.components.get(c) || 0) + n);
|
|
485
|
-
for (const r of reports)
|
|
486
|
-
if (!r.avoidable) continue;
|
|
487
|
-
const rc = rootCauseOf(r, reports);
|
|
488
|
-
if ((rc ? rc.name : r.parent && r.parent.name) === name) affected.push(r);
|
|
489
|
-
}
|
|
501
|
+
for (const r of reports) if (r.avoidable && analysis.rootByReport.get(r) === name) affected.push(r);
|
|
490
502
|
}
|
|
491
503
|
out.commits.reverse();
|
|
492
504
|
out.fixes = rankFixes(affected);
|
|
@@ -544,65 +556,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
544
556
|
};
|
|
545
557
|
}
|
|
546
558
|
var PRIORITY_LABEL = { immediate: "discrete input", "user-blocking": "continuous input", normal: "transition / async", low: "low", idle: "idle" };
|
|
547
|
-
var b64url = (bytes) => {
|
|
548
|
-
let s = "";
|
|
549
|
-
for (const b of bytes) s += String.fromCharCode(b);
|
|
550
|
-
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
551
|
-
};
|
|
552
|
-
var unb64url = (s) => Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
|
|
553
|
-
async function transformBytes(bytes, transform) {
|
|
554
|
-
const source = new ReadableStream({
|
|
555
|
-
start(controller) {
|
|
556
|
-
controller.enqueue(bytes);
|
|
557
|
-
controller.close();
|
|
558
|
-
}
|
|
559
|
-
});
|
|
560
|
-
const reader = source.pipeThrough(transform).getReader();
|
|
561
|
-
const chunks = [];
|
|
562
|
-
let total = 0;
|
|
563
|
-
for (; ; ) {
|
|
564
|
-
const { value, done } = await reader.read();
|
|
565
|
-
if (done) break;
|
|
566
|
-
chunks.push(value);
|
|
567
|
-
total += value.length;
|
|
568
|
-
}
|
|
569
|
-
const out = new Uint8Array(total);
|
|
570
|
-
let offset = 0;
|
|
571
|
-
for (const c of chunks) {
|
|
572
|
-
out.set(c, offset);
|
|
573
|
-
offset += c.length;
|
|
574
|
-
}
|
|
575
|
-
return out;
|
|
576
|
-
}
|
|
577
|
-
async function encodeShare(report) {
|
|
578
|
-
const json = JSON.stringify(report);
|
|
579
|
-
const bytes = new TextEncoder().encode(json);
|
|
580
|
-
if (typeof CompressionStream === "function") {
|
|
581
|
-
try {
|
|
582
|
-
return "d." + b64url(await transformBytes(bytes, new CompressionStream("deflate-raw")));
|
|
583
|
-
} catch {
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
return "j." + b64url(bytes);
|
|
587
|
-
}
|
|
588
|
-
async function decodeShare(value) {
|
|
589
|
-
const [kind, data] = value.split(".", 2);
|
|
590
|
-
if (!data) return null;
|
|
591
|
-
let bytes = unb64url(data);
|
|
592
|
-
if (kind === "d") {
|
|
593
|
-
if (typeof DecompressionStream !== "function") return null;
|
|
594
|
-
try {
|
|
595
|
-
bytes = await transformBytes(bytes, new DecompressionStream("deflate-raw"));
|
|
596
|
-
} catch {
|
|
597
|
-
return null;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
try {
|
|
601
|
-
return normalizeReport(JSON.parse(new TextDecoder().decode(bytes)));
|
|
602
|
-
} catch {
|
|
603
|
-
return null;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
559
|
function sourceContext(text, line, around = 3) {
|
|
607
560
|
const lines = text.split("\n");
|
|
608
561
|
const from = Math.max(1, line - around);
|
|
@@ -725,10 +678,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
725
678
|
const copy = actions.copy;
|
|
726
679
|
bar.append(el("button", { onclick: () => copy(reportToMarkdown(r)) }, "\u2398 Copy as Markdown"));
|
|
727
680
|
}
|
|
728
|
-
if (actions.share) {
|
|
729
|
-
const share = actions.share;
|
|
730
|
-
bar.append(el("button", { title: "Copy a link that opens this report in the panel", onclick: () => share(r) }, "\u{1F517} Copy link"));
|
|
731
|
-
}
|
|
732
681
|
if (bar.children.length) head.append(bar);
|
|
733
682
|
const loc = r.source;
|
|
734
683
|
if (loc && loc.lineNumber && actions.readSource) {
|
|
@@ -922,16 +871,13 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
922
871
|
flashOn: false,
|
|
923
872
|
settingsOpen: false,
|
|
924
873
|
origin: null,
|
|
925
|
-
legacyCommit: 0,
|
|
926
874
|
tabLabel: transport.tabLabel ?? null,
|
|
927
875
|
compact: false,
|
|
928
876
|
sessions: [],
|
|
929
877
|
recording: null,
|
|
930
878
|
selectedSession: null,
|
|
931
879
|
compareWith: null,
|
|
932
|
-
byInstance: false
|
|
933
|
-
columns: [],
|
|
934
|
-
notes: {}
|
|
880
|
+
byInstance: false
|
|
935
881
|
};
|
|
936
882
|
let persistTimer = null;
|
|
937
883
|
let queue = [];
|
|
@@ -1091,6 +1037,33 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1091
1037
|
const measure = () => setCompact(root.clientWidth > 0 && root.clientWidth < 720);
|
|
1092
1038
|
if (typeof ResizeObserver === "function") new ResizeObserver(measure).observe(root);
|
|
1093
1039
|
else window.addEventListener("resize", measure);
|
|
1040
|
+
const totals = { avoidable: 0, wasted: 0, perComponent: /* @__PURE__ */ new Map() };
|
|
1041
|
+
let bestFix;
|
|
1042
|
+
let bestFixAt = 0;
|
|
1043
|
+
let bestFixGen = -1;
|
|
1044
|
+
let bestFixTimer = null;
|
|
1045
|
+
const throttled = () => state.reports.length >= THROTTLE_MIN_REPORTS;
|
|
1046
|
+
function refreshBestFix() {
|
|
1047
|
+
if (!totals.avoidable) {
|
|
1048
|
+
bestFix = void 0;
|
|
1049
|
+
bestFixGen = dataGen;
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
if (bestFixGen === dataGen) return;
|
|
1053
|
+
const now = Date.now();
|
|
1054
|
+
if (throttled() && now - bestFixAt < BEST_FIX_INTERVAL) {
|
|
1055
|
+
if (!bestFixTimer) {
|
|
1056
|
+
bestFixTimer = setTimeout(() => {
|
|
1057
|
+
bestFixTimer = null;
|
|
1058
|
+
renderSummary();
|
|
1059
|
+
}, BEST_FIX_INTERVAL - (now - bestFixAt));
|
|
1060
|
+
}
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
bestFixAt = now;
|
|
1064
|
+
bestFixGen = dataGen;
|
|
1065
|
+
bestFix = rankFixes(state.reports)[0];
|
|
1066
|
+
}
|
|
1094
1067
|
function renderSummary() {
|
|
1095
1068
|
summary.textContent = "";
|
|
1096
1069
|
const total = state.reports.length;
|
|
@@ -1099,17 +1072,11 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1099
1072
|
return;
|
|
1100
1073
|
}
|
|
1101
1074
|
summary.hidden = false;
|
|
1102
|
-
|
|
1103
|
-
let
|
|
1104
|
-
const perComponent
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
avoidable++;
|
|
1108
|
-
if (typeof r.selfDuration === "number") wasted += r.selfDuration;
|
|
1109
|
-
if (!isMuted(r.component)) perComponent.set(r.component, (perComponent.get(r.component) || 0) + 1);
|
|
1110
|
-
}
|
|
1111
|
-
const top = [...perComponent].sort((a, b) => b[1] - a[1])[0];
|
|
1112
|
-
const fix = avoidable ? rankFixes(state.reports.filter((r) => !isMuted(r.component)))[0] : void 0;
|
|
1075
|
+
const { avoidable, wasted } = totals;
|
|
1076
|
+
let top;
|
|
1077
|
+
for (const entry of totals.perComponent) if (!top || entry[1] > top[1]) top = entry;
|
|
1078
|
+
refreshBestFix();
|
|
1079
|
+
const fix = bestFix;
|
|
1113
1080
|
const stat = (value, label, cls = "") => el("span", { class: "stat " + cls }, [el("b", { text: value }), el("span", { class: "label", text: label })]);
|
|
1114
1081
|
summary.append(stat(String(total), plural(total, "render").replace(/^\d+ /, "")), stat(String(avoidable), "avoidable", avoidable ? "bad" : "good"));
|
|
1115
1082
|
if (wasted) summary.append(stat(fmtMs(wasted), "wasted", "bad"));
|
|
@@ -1177,8 +1144,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1177
1144
|
collapsed: [...state.collapsed],
|
|
1178
1145
|
treeWidth: state.treeWidth,
|
|
1179
1146
|
flashOn: state.flashOn,
|
|
1180
|
-
byInstance: state.byInstance
|
|
1181
|
-
columns: state.columns
|
|
1147
|
+
byInstance: state.byInstance
|
|
1182
1148
|
};
|
|
1183
1149
|
transport.storage.set("panel", saved);
|
|
1184
1150
|
}, 150);
|
|
@@ -1230,7 +1196,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1230
1196
|
instancesBtn.classList.toggle("active", state.byInstance);
|
|
1231
1197
|
rebuildTree();
|
|
1232
1198
|
}
|
|
1233
|
-
if (Array.isArray(saved.columns)) state.columns = saved.columns.filter((c) => OPTIONAL_COLUMNS.some((o) => o.key === c));
|
|
1234
1199
|
if (saved.tab === "history" || saved.tab === "fix") state.tab = saved.tab;
|
|
1235
1200
|
if (saved.view && viewButtons.has(saved.view)) state.view = saved.view;
|
|
1236
1201
|
for (const n of state.nodesByKey.values()) n.expanded = !state.collapsed.has(n.key);
|
|
@@ -1266,7 +1231,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1266
1231
|
for (const r of state.reports) {
|
|
1267
1232
|
const node = nodeOfReport(r);
|
|
1268
1233
|
node.reports.push(r);
|
|
1269
|
-
if (node.reports.length > MAX_PER_NODE) node.reports.shift();
|
|
1270
1234
|
node.total++;
|
|
1271
1235
|
if (r.avoidable) {
|
|
1272
1236
|
node.avoidable++;
|
|
@@ -1274,49 +1238,93 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1274
1238
|
}
|
|
1275
1239
|
node.lastReport = r;
|
|
1276
1240
|
}
|
|
1241
|
+
for (const node of state.nodesByKey.values()) trimNode(node);
|
|
1277
1242
|
renderLeft();
|
|
1278
1243
|
renderDetails();
|
|
1279
1244
|
}
|
|
1280
|
-
function
|
|
1281
|
-
|
|
1282
|
-
|
|
1245
|
+
function trimNode(node) {
|
|
1246
|
+
const excess = node.reports.length - MAX_PER_NODE;
|
|
1247
|
+
if (excess > 0) node.reports.splice(0, excess);
|
|
1248
|
+
}
|
|
1249
|
+
const commitAnalyses = /* @__PURE__ */ new Map();
|
|
1250
|
+
function analysisFor(key, reports) {
|
|
1251
|
+
const cached = commitAnalyses.get(key);
|
|
1252
|
+
if (cached && cached.len === reports.length) return cached.analysis;
|
|
1253
|
+
const analysis = analyzeCommit(reports);
|
|
1254
|
+
commitAnalyses.set(key, { len: reports.length, analysis });
|
|
1255
|
+
return analysis;
|
|
1256
|
+
}
|
|
1257
|
+
const commitMember = /* @__PURE__ */ new WeakSet();
|
|
1258
|
+
function forgetCommit(key) {
|
|
1259
|
+
state.commits.delete(key);
|
|
1260
|
+
commitAnalyses.delete(key);
|
|
1283
1261
|
}
|
|
1284
1262
|
function ingest(report) {
|
|
1285
1263
|
if (!report.receivedAt) report.receivedAt = Date.now();
|
|
1286
1264
|
state.reports.push(report);
|
|
1287
|
-
if (state.reports.length > MAX_REPORTS) state.reports.shift();
|
|
1288
1265
|
const node = nodeOfReport(report);
|
|
1289
1266
|
node.reports.push(report);
|
|
1290
|
-
if (node.reports.length > MAX_PER_NODE) node.reports.shift();
|
|
1291
1267
|
node.total++;
|
|
1292
1268
|
if (report.avoidable) {
|
|
1293
1269
|
node.avoidable++;
|
|
1294
|
-
|
|
1270
|
+
totals.avoidable++;
|
|
1271
|
+
totals.perComponent.set(report.component, (totals.perComponent.get(report.component) || 0) + 1);
|
|
1272
|
+
if (typeof report.selfDuration === "number") {
|
|
1273
|
+
node.wasted += report.selfDuration;
|
|
1274
|
+
totals.wasted += report.selfDuration;
|
|
1275
|
+
}
|
|
1295
1276
|
}
|
|
1296
1277
|
node.lastReport = report;
|
|
1297
1278
|
node.flash = true;
|
|
1298
1279
|
node.flashAt = Date.now();
|
|
1299
|
-
const ck =
|
|
1280
|
+
const ck = report.commitId;
|
|
1300
1281
|
let list = state.commits.get(ck);
|
|
1301
1282
|
if (!list) {
|
|
1302
1283
|
list = [];
|
|
1303
1284
|
state.commits.set(ck, list);
|
|
1304
1285
|
state.commitOrder.push(ck);
|
|
1305
|
-
if (state.commitOrder.length > MAX_COMMITS)
|
|
1286
|
+
if (state.commitOrder.length > MAX_COMMITS) forgetCommit(state.commitOrder.shift());
|
|
1287
|
+
}
|
|
1288
|
+
if (list.length < MAX_PER_COMMIT) {
|
|
1289
|
+
list.push(report);
|
|
1290
|
+
commitMember.add(report);
|
|
1306
1291
|
}
|
|
1307
|
-
list.push(report);
|
|
1308
1292
|
return node;
|
|
1309
1293
|
}
|
|
1294
|
+
function evict() {
|
|
1295
|
+
const excess = state.reports.length - MAX_REPORTS;
|
|
1296
|
+
if (excess <= 0) return;
|
|
1297
|
+
for (const r of state.reports.splice(0, excess)) {
|
|
1298
|
+
if (r.avoidable) {
|
|
1299
|
+
totals.avoidable--;
|
|
1300
|
+
const n = (totals.perComponent.get(r.component) || 0) - 1;
|
|
1301
|
+
if (n > 0) totals.perComponent.set(r.component, n);
|
|
1302
|
+
else totals.perComponent.delete(r.component);
|
|
1303
|
+
if (typeof r.selfDuration === "number") totals.wasted -= r.selfDuration;
|
|
1304
|
+
}
|
|
1305
|
+
if (!commitMember.has(r)) continue;
|
|
1306
|
+
const list = state.commits.get(r.commitId);
|
|
1307
|
+
if (!list || list[0] !== r) continue;
|
|
1308
|
+
list.shift();
|
|
1309
|
+
if (!list.length) {
|
|
1310
|
+
forgetCommit(r.commitId);
|
|
1311
|
+
const i = state.commitOrder.indexOf(r.commitId);
|
|
1312
|
+
if (i >= 0) state.commitOrder.splice(i, 1);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
if (totals.wasted < 0) totals.wasted = 0;
|
|
1316
|
+
}
|
|
1310
1317
|
function flush() {
|
|
1311
1318
|
flushScheduled = false;
|
|
1312
1319
|
if (!queue.length) return;
|
|
1313
1320
|
const batch = queue;
|
|
1314
1321
|
queue = [];
|
|
1315
|
-
state.legacyCommit++;
|
|
1316
1322
|
let touchedSelected = false;
|
|
1317
1323
|
let avoidableCount = 0;
|
|
1324
|
+
const touched = /* @__PURE__ */ new Set();
|
|
1318
1325
|
for (const r of batch) {
|
|
1319
1326
|
const node = ingest(r);
|
|
1327
|
+
touched.add(node);
|
|
1320
1328
|
if (state.recording && state.recording.reports.length < MAX_REPORTS) state.recording.reports.push(r);
|
|
1321
1329
|
if (r.avoidable) avoidableCount++;
|
|
1322
1330
|
if (state.selectedKey === node.key) {
|
|
@@ -1324,11 +1332,45 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1324
1332
|
if (state.tab === "latest") state.selectedReport = r;
|
|
1325
1333
|
}
|
|
1326
1334
|
}
|
|
1327
|
-
|
|
1335
|
+
for (const node of touched) trimNode(node);
|
|
1336
|
+
evict();
|
|
1337
|
+
dataGen++;
|
|
1328
1338
|
renderStream(batch);
|
|
1329
1339
|
renderSummary();
|
|
1330
|
-
|
|
1331
|
-
if (state.
|
|
1340
|
+
const heavy = state.view === "commits" || state.view === "fixes" || state.tab === "root" || !!(state.recording && state.tab === "session");
|
|
1341
|
+
if (state.view === "tree") {
|
|
1342
|
+
renderTree();
|
|
1343
|
+
if (touchedSelected && !heavy) renderDetails();
|
|
1344
|
+
}
|
|
1345
|
+
scheduleHeavy(heavy || touchedSelected && state.view !== "tree");
|
|
1346
|
+
if (state.polling && avoidableCount) transport.badge?.(totals.avoidable);
|
|
1347
|
+
}
|
|
1348
|
+
let heavyAt = 0;
|
|
1349
|
+
let heavyTimer = null;
|
|
1350
|
+
let heavyDetailsPending = false;
|
|
1351
|
+
function renderHeavy(details2) {
|
|
1352
|
+
heavyAt = Date.now();
|
|
1353
|
+
if (state.view !== "tree") renderLeft();
|
|
1354
|
+
if (details2) renderDetails();
|
|
1355
|
+
}
|
|
1356
|
+
function scheduleHeavy(details2) {
|
|
1357
|
+
const wait = throttled() ? LEFT_RENDER_INTERVAL - (Date.now() - heavyAt) : 0;
|
|
1358
|
+
if (wait <= 0) {
|
|
1359
|
+
if (heavyTimer) clearTimeout(heavyTimer);
|
|
1360
|
+
heavyTimer = null;
|
|
1361
|
+
heavyDetailsPending = false;
|
|
1362
|
+
renderHeavy(details2);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
heavyDetailsPending = heavyDetailsPending || details2;
|
|
1366
|
+
if (!heavyTimer) {
|
|
1367
|
+
heavyTimer = setTimeout(() => {
|
|
1368
|
+
heavyTimer = null;
|
|
1369
|
+
const d = heavyDetailsPending;
|
|
1370
|
+
heavyDetailsPending = false;
|
|
1371
|
+
renderHeavy(d);
|
|
1372
|
+
}, wait);
|
|
1373
|
+
}
|
|
1332
1374
|
}
|
|
1333
1375
|
function enqueue(report) {
|
|
1334
1376
|
queue.push(report);
|
|
@@ -1343,6 +1385,13 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1343
1385
|
state.reports = [];
|
|
1344
1386
|
state.commits.clear();
|
|
1345
1387
|
state.commitOrder = [];
|
|
1388
|
+
commitAnalyses.clear();
|
|
1389
|
+
totals.avoidable = 0;
|
|
1390
|
+
totals.wasted = 0;
|
|
1391
|
+
totals.perComponent.clear();
|
|
1392
|
+
bestFix = void 0;
|
|
1393
|
+
bestFixAt = 0;
|
|
1394
|
+
dataGen++;
|
|
1346
1395
|
state.selectedKey = null;
|
|
1347
1396
|
state.selectedReport = null;
|
|
1348
1397
|
state.selectedCommit = null;
|
|
@@ -1355,18 +1404,28 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1355
1404
|
renderStream();
|
|
1356
1405
|
renderSummary();
|
|
1357
1406
|
}
|
|
1358
|
-
|
|
1359
|
-
function
|
|
1360
|
-
if (
|
|
1407
|
+
let filterCache = { filter: "", value: null, regex: null, text: "" };
|
|
1408
|
+
function parsedFilter() {
|
|
1409
|
+
if (filterCache.filter === state.filter) return filterCache;
|
|
1361
1410
|
const f = state.filter.trim();
|
|
1362
|
-
const
|
|
1411
|
+
const value = f.startsWith("~") ? f.slice(1).toLowerCase() : null;
|
|
1412
|
+
let regex = null;
|
|
1413
|
+
const m = value === null ? /^\/(.+)\/([a-z]*)$/.exec(f) : null;
|
|
1363
1414
|
if (m && m[1] !== void 0) {
|
|
1364
1415
|
try {
|
|
1365
|
-
|
|
1416
|
+
regex = new RegExp(m[1], (m[2] || "").replace(/[gy]/g, ""));
|
|
1366
1417
|
} catch {
|
|
1367
1418
|
}
|
|
1368
1419
|
}
|
|
1369
|
-
|
|
1420
|
+
filterCache = { filter: state.filter, value, regex, text: f.toLowerCase() };
|
|
1421
|
+
return filterCache;
|
|
1422
|
+
}
|
|
1423
|
+
const valueQuery = () => parsedFilter().value;
|
|
1424
|
+
function matchesFilter(name) {
|
|
1425
|
+
const f = parsedFilter();
|
|
1426
|
+
if (!f.filter || f.value !== null) return true;
|
|
1427
|
+
if (f.regex) return f.regex.test(name);
|
|
1428
|
+
return name.toLowerCase().includes(f.text);
|
|
1370
1429
|
}
|
|
1371
1430
|
const valueCache = /* @__PURE__ */ new WeakMap();
|
|
1372
1431
|
function matchesValues(r) {
|
|
@@ -1390,10 +1449,19 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1390
1449
|
for (const c of node.children.values()) if (visible(c)) return true;
|
|
1391
1450
|
return false;
|
|
1392
1451
|
}
|
|
1393
|
-
const isMuted = (component) => !!state.notes[component]?.muted;
|
|
1394
1452
|
const passes = (r) => (!state.avoidableOnly || r.avoidable) && matchesFilter(r.component) && matchesValues(r);
|
|
1395
|
-
|
|
1396
|
-
|
|
1453
|
+
let dataGen = 0;
|
|
1454
|
+
let filtered = { key: "", reports: [], fixes: null };
|
|
1455
|
+
function filteredReports() {
|
|
1456
|
+
const key = `${dataGen}|${state.avoidableOnly}|${state.filter}`;
|
|
1457
|
+
if (filtered.key !== key) filtered = { key, reports: state.reports.filter(passes), fixes: null };
|
|
1458
|
+
return filtered.reports;
|
|
1459
|
+
}
|
|
1460
|
+
function filteredFixes() {
|
|
1461
|
+
const reports = filteredReports();
|
|
1462
|
+
if (!filtered.fixes) filtered.fixes = rankFixes(reports);
|
|
1463
|
+
return filtered.fixes;
|
|
1464
|
+
}
|
|
1397
1465
|
function setView(view) {
|
|
1398
1466
|
state.view = view;
|
|
1399
1467
|
for (const [id, b] of viewButtons) b.classList.toggle("active", id === view);
|
|
@@ -1472,7 +1540,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1472
1540
|
row.classList.toggle("selected", state.selectedKey === node.key);
|
|
1473
1541
|
row.setAttribute("aria-selected", state.selectedKey === node.key ? "true" : "false");
|
|
1474
1542
|
row.setAttribute("aria-level", String(depth + 1));
|
|
1475
|
-
row.classList.toggle("muted", isMuted(node.lastReport ? node.lastReport.component : node.name));
|
|
1476
1543
|
const indent = row.querySelector(".indent");
|
|
1477
1544
|
if (indent.childElementCount !== depth) {
|
|
1478
1545
|
indent.textContent = "";
|
|
@@ -1574,7 +1641,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1574
1641
|
for (const r of filteredReports()) {
|
|
1575
1642
|
let o = byName.get(r.component);
|
|
1576
1643
|
if (!o) {
|
|
1577
|
-
o = { component: r.component, total: 0, avoidable: 0, wasted: 0, paths: /* @__PURE__ */ new Set(),
|
|
1644
|
+
o = { component: r.component, total: 0, avoidable: 0, wasted: 0, paths: /* @__PURE__ */ new Set(), reports: [], fix: "" };
|
|
1578
1645
|
byName.set(r.component, o);
|
|
1579
1646
|
}
|
|
1580
1647
|
o.total++;
|
|
@@ -1583,12 +1650,10 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1583
1650
|
if (typeof r.selfDuration === "number") o.wasted += r.selfDuration;
|
|
1584
1651
|
}
|
|
1585
1652
|
o.paths.add(keyOf(r.path));
|
|
1586
|
-
o.lastSeen = Math.max(o.lastSeen, r.receivedAt);
|
|
1587
1653
|
o.reports.push(r);
|
|
1588
1654
|
}
|
|
1589
1655
|
const rows = [...byName.values()];
|
|
1590
1656
|
for (const o of rows) {
|
|
1591
|
-
o.places = o.paths.size;
|
|
1592
1657
|
const fixes = o.avoidable ? rankFixes(o.reports) : [];
|
|
1593
1658
|
o.fix = fixes.length ? fixes[0].label : "";
|
|
1594
1659
|
}
|
|
@@ -1601,62 +1666,25 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1601
1666
|
});
|
|
1602
1667
|
return rows;
|
|
1603
1668
|
}
|
|
1604
|
-
function
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
return el("td", { class: "num", text: String(o.places) });
|
|
1624
|
-
case "lastSeen":
|
|
1625
|
-
return el("td", { class: "num mono", text: o.lastSeen ? fmtTime(o.lastSeen) : "" });
|
|
1626
|
-
default:
|
|
1627
|
-
return el("td");
|
|
1628
|
-
}
|
|
1629
|
-
};
|
|
1630
|
-
function offenderRow(o, tag) {
|
|
1631
|
-
const cells = [
|
|
1632
|
-
el("td", { class: "c" }, [el("span", { class: "name", text: o.component }), o.muted ? el("span", { class: "badge", text: "muted" }) : null, o.paths.size > 1 && !state.columns.includes("places") ? el("span", { class: "meta", text: ` \xD7${o.paths.size} places` }) : null]),
|
|
1633
|
-
el("td", { class: "num" }, o.avoidable ? el("span", { class: "badge avoid", text: String(o.avoidable) }) : "0"),
|
|
1634
|
-
el("td", { class: "num", text: String(o.total) }),
|
|
1635
|
-
el("td", { class: "num", text: o.wasted ? fmtMs(o.wasted) : "" }),
|
|
1636
|
-
...state.columns.map((k) => offenderCell(o, k)),
|
|
1637
|
-
el("td", { class: "fix", text: o.fix })
|
|
1638
|
-
];
|
|
1639
|
-
const attrs = {
|
|
1640
|
-
class: (o.avoidable ? "has-avoid" : "") + (o.muted ? " muted" : ""),
|
|
1641
|
-
role: tag === "div" ? "listitem" : null,
|
|
1642
|
-
onclick: () => {
|
|
1643
|
-
const last = o.reports[o.reports.length - 1];
|
|
1644
|
-
state.tab = "fix";
|
|
1645
|
-
select(nodeOfReport(last), last);
|
|
1646
|
-
}
|
|
1647
|
-
};
|
|
1648
|
-
if (tag === "tr") return el("tr", attrs, cells);
|
|
1649
|
-
const row = el("div", { ...attrs, class: "vrow " + attrs.class });
|
|
1650
|
-
for (const c of cells) {
|
|
1651
|
-
const span = el("span", { class: c.className });
|
|
1652
|
-
span.append(...c.childNodes);
|
|
1653
|
-
row.append(span);
|
|
1654
|
-
}
|
|
1655
|
-
return row;
|
|
1656
|
-
}
|
|
1657
|
-
const offendersList = virtualList(table, GRID_ROW_H, (o) => offenderRow(o, "div"), { attach: false, headerHeight: () => table.querySelector(".vhead")?.offsetHeight || 0 });
|
|
1658
|
-
function offendersHeader() {
|
|
1659
|
-
return [sortableHeader("Component", "component"), sortableHeader("Avoidable", "avoidable", true), sortableHeader("Total", "total", true), sortableHeader("Wasted", "wasted", true), ...state.columns.map((k) => sortableHeader(OPTIONAL_COLUMNS.find((c) => c.key === k).label, k, true)), el("th", { text: "Top fix" })];
|
|
1669
|
+
function offenderRow(o) {
|
|
1670
|
+
return el(
|
|
1671
|
+
"tr",
|
|
1672
|
+
{
|
|
1673
|
+
class: o.avoidable ? "has-avoid" : "",
|
|
1674
|
+
onclick: () => {
|
|
1675
|
+
const last = o.reports[o.reports.length - 1];
|
|
1676
|
+
state.tab = "fix";
|
|
1677
|
+
select(nodeOfReport(last), last);
|
|
1678
|
+
}
|
|
1679
|
+
},
|
|
1680
|
+
[
|
|
1681
|
+
el("td", { class: "c" }, [el("span", { class: "name", text: o.component }), o.paths.size > 1 ? el("span", { class: "meta", text: ` \xD7${o.paths.size} places` }) : null]),
|
|
1682
|
+
el("td", { class: "num" }, o.avoidable ? el("span", { class: "badge avoid", text: String(o.avoidable) }) : "0"),
|
|
1683
|
+
el("td", { class: "num", text: String(o.total) }),
|
|
1684
|
+
el("td", { class: "num", text: o.wasted ? fmtMs(o.wasted) : "" }),
|
|
1685
|
+
el("td", { class: "fix", text: o.fix })
|
|
1686
|
+
]
|
|
1687
|
+
);
|
|
1660
1688
|
}
|
|
1661
1689
|
function renderOffenders() {
|
|
1662
1690
|
table.textContent = "";
|
|
@@ -1665,28 +1693,12 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1665
1693
|
table.append(el("div", { class: "empty", text: "No re-renders reported yet." }));
|
|
1666
1694
|
return;
|
|
1667
1695
|
}
|
|
1668
|
-
const gridCols = `minmax(140px, 2fr) 70px 60px 80px ${state.columns.map(() => "90px").join(" ")} minmax(120px, 2fr)`;
|
|
1669
|
-
if (rows.length > VIRTUAL_THRESHOLD) {
|
|
1670
|
-
const head = el("div", { class: "vhead", style: `grid-template-columns:${gridCols}` });
|
|
1671
|
-
for (const th of offendersHeader()) {
|
|
1672
|
-
const cell = el("span", { class: th.className, onclick: null });
|
|
1673
|
-
cell.append(...th.childNodes);
|
|
1674
|
-
cell.addEventListener("click", () => th.click());
|
|
1675
|
-
head.append(cell);
|
|
1676
|
-
}
|
|
1677
|
-
head.append(columnsMenu());
|
|
1678
|
-
table.append(head, offendersList.inner);
|
|
1679
|
-
offendersList.inner.style.setProperty("--grid-cols", gridCols);
|
|
1680
|
-
offendersList.setItems(rows);
|
|
1681
|
-
return;
|
|
1682
|
-
}
|
|
1683
1696
|
const t = el("table", { class: "grid" });
|
|
1684
|
-
|
|
1685
|
-
t.append(el("thead", null, headRow));
|
|
1697
|
+
t.append(el("thead", null, el("tr", null, [sortableHeader("Component", "component"), sortableHeader("Avoidable", "avoidable", true), sortableHeader("Total", "total", true), sortableHeader("Wasted", "wasted", true), el("th", { text: "Top fix" })])));
|
|
1686
1698
|
const body = el("tbody");
|
|
1687
|
-
for (const o of rows) body.append(offenderRow(o
|
|
1699
|
+
for (const o of rows) body.append(offenderRow(o));
|
|
1688
1700
|
t.append(body);
|
|
1689
|
-
table.append(
|
|
1701
|
+
table.append(t);
|
|
1690
1702
|
}
|
|
1691
1703
|
function commitSummaries() {
|
|
1692
1704
|
const out = [];
|
|
@@ -1694,7 +1706,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1694
1706
|
const key = state.commitOrder[i];
|
|
1695
1707
|
const reports = state.commits.get(key);
|
|
1696
1708
|
if (!reports || !reports.some(passes)) continue;
|
|
1697
|
-
out.push({ key, analysis:
|
|
1709
|
+
out.push({ key, analysis: analysisFor(key, reports) });
|
|
1698
1710
|
}
|
|
1699
1711
|
return out;
|
|
1700
1712
|
}
|
|
@@ -1721,7 +1733,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1721
1733
|
const root2 = analysis.roots[0];
|
|
1722
1734
|
list.append(
|
|
1723
1735
|
el("li", { class: (state.selectedCommit === key && state.tab === "commit" ? "selected " : "") + (analysis.avoidable ? "has-avoid" : ""), onclick: () => showCommit(key) }, [
|
|
1724
|
-
el("span", { class: "id", text:
|
|
1736
|
+
el("span", { class: "id", text: `#${key}` }),
|
|
1725
1737
|
el("span", { class: "t", text: fmtTime(analysis.receivedAt) }),
|
|
1726
1738
|
el("span", { class: "n", text: plural(analysis.total, "render") }),
|
|
1727
1739
|
analysis.avoidable ? el("span", { class: "badge avoid", text: `${analysis.avoidable} avoidable` }) : el("span", { class: "badge", text: "ok" }),
|
|
@@ -1736,13 +1748,13 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1736
1748
|
}
|
|
1737
1749
|
table.append(list);
|
|
1738
1750
|
}
|
|
1739
|
-
function fixItem(f
|
|
1751
|
+
function fixItem(f) {
|
|
1752
|
+
const selected = state.selectedFix === f.key && state.tab === "fixlist";
|
|
1740
1753
|
return el(
|
|
1741
|
-
|
|
1754
|
+
"li",
|
|
1742
1755
|
{
|
|
1743
|
-
class:
|
|
1744
|
-
|
|
1745
|
-
"aria-selected": state.selectedFix === f.key && state.tab === "fixlist" ? "true" : null,
|
|
1756
|
+
class: selected ? "selected" : "",
|
|
1757
|
+
"aria-selected": selected ? "true" : null,
|
|
1746
1758
|
onclick: () => {
|
|
1747
1759
|
state.selectedFix = f.key;
|
|
1748
1760
|
state.tab = "fixlist";
|
|
@@ -1757,27 +1769,19 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1757
1769
|
]
|
|
1758
1770
|
);
|
|
1759
1771
|
}
|
|
1760
|
-
const fixesList = virtualList(table, GRID_ROW_H, (f) => fixItem(f, "div"), { attach: false, headerHeight: () => table.querySelector(".section-title")?.offsetHeight || 0 });
|
|
1761
1772
|
function renderFixes() {
|
|
1762
1773
|
table.textContent = "";
|
|
1763
|
-
const reports =
|
|
1764
|
-
const fixes =
|
|
1774
|
+
const reports = filteredReports();
|
|
1775
|
+
const fixes = filteredFixes();
|
|
1765
1776
|
const contexts = contextAttribution(reports);
|
|
1766
|
-
const mutedCount = Object.values(state.notes).filter((n) => n.muted).length;
|
|
1767
1777
|
if (!fixes.length && !contexts.length) {
|
|
1768
|
-
table.append(el("div", { class: "empty", text:
|
|
1778
|
+
table.append(el("div", { class: "empty", text: "No avoidable re-renders, nothing to fix." }));
|
|
1769
1779
|
return;
|
|
1770
1780
|
}
|
|
1771
1781
|
if (fixes.length) {
|
|
1772
|
-
const
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
fixesList.setItems(fixes);
|
|
1776
|
-
} else {
|
|
1777
|
-
const list = el("ol", { class: "fixes", role: "list" });
|
|
1778
|
-
for (const f of fixes) list.append(fixItem(f, "li"));
|
|
1779
|
-
table.append(title, list);
|
|
1780
|
-
}
|
|
1782
|
+
const list = el("ol", { class: "fixes", role: "list" });
|
|
1783
|
+
for (const f of fixes) list.append(fixItem(f));
|
|
1784
|
+
table.append(el("div", { class: "section-title", text: "Ranked by avoidable re-renders removed" }), list);
|
|
1781
1785
|
}
|
|
1782
1786
|
if (contexts.length) {
|
|
1783
1787
|
const list = el("ul", { class: "contexts" });
|
|
@@ -1816,9 +1820,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1816
1820
|
openSource: transport.openResource ? (src) => transport.openResource(src.fileName, src.lineNumber, src.columnNumber) : null,
|
|
1817
1821
|
highlight: transport.highlight ? (id) => transport.highlight(id) : null,
|
|
1818
1822
|
copy: copyText,
|
|
1819
|
-
share: transport.panelUrl ? (r) => {
|
|
1820
|
-
void encodeShare(r).then((code) => copyText(`${transport.panelUrl}?report=${code}`));
|
|
1821
|
-
} : null,
|
|
1822
1823
|
readSource: transport.readSource ? (url) => transport.readSource(url) : null
|
|
1823
1824
|
});
|
|
1824
1825
|
const tabButton = (id, label, onclick) => el("button", { class: state.tab === id ? "active" : "", onclick }, label);
|
|
@@ -1827,15 +1828,9 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1827
1828
|
details.append(el("div", { class: "empty", text: "Select a component to see why it re-rendered." }));
|
|
1828
1829
|
return;
|
|
1829
1830
|
}
|
|
1830
|
-
const componentName = node.lastReport ? node.lastReport.component : node.name;
|
|
1831
|
-
const note = state.notes[componentName] || {};
|
|
1832
|
-
const noteBtn = el("button", { class: "ib small" + (note.note ? " active" : ""), title: note.note ? note.note : "Add a note for this component", "aria-label": "Note", onclick: () => toggleNoteEditor() }, [el("span", { class: "glyph", text: "\u270E" })]);
|
|
1833
|
-
const muteBtn = el("button", { class: "ib small" + (note.muted ? " active" : ""), title: note.muted ? "Muted: hidden from Fixes and the summary. Click to unmute." : "Mute: hide this component from Fixes and the summary", "aria-label": note.muted ? "Unmute" : "Mute", onclick: () => setNote(componentName, { muted: !note.muted }) }, [el("span", { class: "glyph", text: note.muted ? "\u{1F515}" : "\u{1F514}" })]);
|
|
1834
1831
|
const header = el("div", { class: "details-header" }, [
|
|
1835
1832
|
el("span", { class: "title" }, [el("span", { class: "bracket", text: "<" }), el("span", { class: "name", text: node.name }), el("span", { class: "bracket", text: ">" })]),
|
|
1836
1833
|
el("span", { class: "meta", text: `${plural(node.total, "re-render")}, ${node.avoidable} avoidable${node.wasted ? ", " + fmtMs(node.wasted) + " wasted" : ""}` }),
|
|
1837
|
-
noteBtn,
|
|
1838
|
-
muteBtn,
|
|
1839
1834
|
el("span", { class: "tabs" }, [
|
|
1840
1835
|
tabButton("latest", "Report", () => {
|
|
1841
1836
|
state.tab = "latest";
|
|
@@ -1858,21 +1853,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1858
1853
|
details.append(header);
|
|
1859
1854
|
const body = el("div", { class: "details-body" });
|
|
1860
1855
|
details.append(body);
|
|
1861
|
-
if (note.note || noteEditorOpen === componentName) {
|
|
1862
|
-
const input = el("input", { type: "text", class: "note-input", placeholder: 'Note for this component (e.g. "known, ticket #123")', value: note.note || "" });
|
|
1863
|
-
input.addEventListener("change", () => {
|
|
1864
|
-
setNote(componentName, { note: input.value.trim() || void 0 });
|
|
1865
|
-
noteEditorOpen = null;
|
|
1866
|
-
});
|
|
1867
|
-
input.addEventListener("keydown", (e) => {
|
|
1868
|
-
if (e.key === "Escape") {
|
|
1869
|
-
noteEditorOpen = null;
|
|
1870
|
-
renderDetails();
|
|
1871
|
-
}
|
|
1872
|
-
});
|
|
1873
|
-
body.append(el("div", { class: "note-box" + (note.muted ? " muted" : "") }, [el("span", { class: "glyph", text: "\u270E" }), input, note.muted ? el("span", { class: "badge", text: "muted" }) : null]));
|
|
1874
|
-
if (noteEditorOpen === componentName) setTimeout(() => input.focus(), 0);
|
|
1875
|
-
}
|
|
1876
1856
|
if (state.tab === "history") {
|
|
1877
1857
|
const list = el("ul", { class: "history" });
|
|
1878
1858
|
for (const r2 of [...node.reports].reverse()) {
|
|
@@ -1927,10 +1907,10 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1927
1907
|
details.append(el("div", { class: "empty", text: "This commit is no longer buffered." }));
|
|
1928
1908
|
return;
|
|
1929
1909
|
}
|
|
1930
|
-
const a =
|
|
1910
|
+
const a = analysisFor(key, reports);
|
|
1931
1911
|
details.append(
|
|
1932
1912
|
el("div", { class: "details-header" }, [
|
|
1933
|
-
el("span", { class: "title", text:
|
|
1913
|
+
el("span", { class: "title", text: `Commit #${key}` }),
|
|
1934
1914
|
el("span", { class: "meta", text: `${plural(a.total, "render")}, ${a.avoidable} avoidable${a.wasted ? ", " + fmtMs(a.wasted) + " wasted" : ""} \xB7 ${fmtTime(a.receivedAt)}` })
|
|
1935
1915
|
])
|
|
1936
1916
|
);
|
|
@@ -1988,7 +1968,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1988
1968
|
return list;
|
|
1989
1969
|
}
|
|
1990
1970
|
function renderFixDetails() {
|
|
1991
|
-
const fix =
|
|
1971
|
+
const fix = filteredFixes().find((f) => f.key === state.selectedFix);
|
|
1992
1972
|
if (!fix) {
|
|
1993
1973
|
details.append(el("div", { class: "empty", text: "Select a fix." }));
|
|
1994
1974
|
return;
|
|
@@ -2001,7 +1981,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2001
1981
|
}
|
|
2002
1982
|
function renderRootDetails() {
|
|
2003
1983
|
const name = state.selectedRoot;
|
|
2004
|
-
const s = rootCauseSummary(name, state.commits);
|
|
1984
|
+
const s = rootCauseSummary(name, state.commits, analysisFor);
|
|
2005
1985
|
details.append(
|
|
2006
1986
|
el("div", { class: "details-header" }, [
|
|
2007
1987
|
el("span", { class: "title" }, ["Root cause ", el("span", { class: "name", text: `<${name}>` })]),
|
|
@@ -2016,7 +1996,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2016
1996
|
for (const c of s.commits) {
|
|
2017
1997
|
list.append(
|
|
2018
1998
|
el("li", { onclick: () => showCommit(c.key) }, [
|
|
2019
|
-
el("span", { class: "id", text:
|
|
1999
|
+
el("span", { class: "id", text: `#${c.key}` }),
|
|
2020
2000
|
el("span", { class: "t", text: fmtTime(c.analysis.receivedAt) }),
|
|
2021
2001
|
el("span", { class: "badge avoid", text: `${c.count} avoidable` }),
|
|
2022
2002
|
el("span", { class: "root", text: componentList(c.components) })
|
|
@@ -2029,37 +2009,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2029
2009
|
body.append(fixView(s.fixes, { copy: copyText }));
|
|
2030
2010
|
}
|
|
2031
2011
|
}
|
|
2032
|
-
let noteEditorOpen = null;
|
|
2033
|
-
function toggleNoteEditor() {
|
|
2034
|
-
const node = state.selectedKey ? state.nodesByKey.get(state.selectedKey) : null;
|
|
2035
|
-
const name = node ? node.lastReport ? node.lastReport.component : node.name : null;
|
|
2036
|
-
noteEditorOpen = noteEditorOpen === name ? null : name;
|
|
2037
|
-
renderDetails();
|
|
2038
|
-
}
|
|
2039
|
-
function setNote(component, patch) {
|
|
2040
|
-
const next = { ...state.notes[component] || {}, ...patch };
|
|
2041
|
-
if (!next.note && !next.muted) delete state.notes[component];
|
|
2042
|
-
else state.notes[component] = next;
|
|
2043
|
-
if (transport.storage) transport.storage.set("notes", state.notes);
|
|
2044
|
-
renderSummary();
|
|
2045
|
-
renderLeft();
|
|
2046
|
-
renderDetails();
|
|
2047
|
-
}
|
|
2048
|
-
function restoreNotes(raw) {
|
|
2049
|
-
if (!isRecord(raw)) return;
|
|
2050
|
-
const notes = {};
|
|
2051
|
-
for (const [k, v] of Object.entries(raw)) {
|
|
2052
|
-
if (!isRecord(v)) continue;
|
|
2053
|
-
const n = {};
|
|
2054
|
-
if (typeof v.note === "string" && v.note) n.note = v.note;
|
|
2055
|
-
if (v.muted === true) n.muted = true;
|
|
2056
|
-
if (n.note || n.muted) notes[k] = n;
|
|
2057
|
-
}
|
|
2058
|
-
state.notes = notes;
|
|
2059
|
-
renderSummary();
|
|
2060
|
-
renderLeft();
|
|
2061
|
-
renderDetails();
|
|
2062
|
-
}
|
|
2063
2012
|
const sessionSummary = (s) => summarizeSession(s, s.reports);
|
|
2064
2013
|
function persistSessions() {
|
|
2065
2014
|
if (!transport.storage) return;
|
|
@@ -2278,10 +2227,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2278
2227
|
};
|
|
2279
2228
|
const text = JSON.stringify(data, null, 2);
|
|
2280
2229
|
const name = `rerender-lens-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`;
|
|
2281
|
-
if (transport.download) {
|
|
2282
|
-
transport.download(name, text);
|
|
2283
|
-
return;
|
|
2284
|
-
}
|
|
2285
2230
|
try {
|
|
2286
2231
|
const blob = new Blob([text], { type: "application/json" });
|
|
2287
2232
|
const url = URL.createObjectURL(blob);
|
|
@@ -2333,6 +2278,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2333
2278
|
cls = "connected";
|
|
2334
2279
|
title = state.relay ? "live via content script" : "polling the page";
|
|
2335
2280
|
if (lib.overhead) title += ` \xB7 library overhead ${lib.overhead.totalMs.toFixed(1)} ms over ${plural(lib.commits ?? 0, "commit")}, worst ${lib.overhead.maxCommitMs.toFixed(1)} ms`;
|
|
2281
|
+
if (lib.truncated) title += ` \xB7 ${plural(lib.truncated, "report")} skipped by the per-commit cap`;
|
|
2336
2282
|
} else if (state.relay || state.polling) {
|
|
2337
2283
|
text = "no library in page";
|
|
2338
2284
|
cls = "partial";
|
|
@@ -2347,15 +2293,12 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2347
2293
|
tabChip.textContent = state.tabLabel || "";
|
|
2348
2294
|
banner.textContent = "";
|
|
2349
2295
|
const warnings = [];
|
|
2350
|
-
if (lib && typeof lib.protocol === "number" && lib.protocol
|
|
2351
|
-
warnings.push(
|
|
2352
|
-
lib.protocol < PROTOCOL ? `The page runs rerender-lens ${lib.library || ""} (protocol ${lib.protocol}); this panel expects protocol ${PROTOCOL}. Update the rerender-lens package for commit grouping, source links and settings.` : `The page runs a newer rerender-lens (protocol ${lib.protocol}) than this panel (${PROTOCOL}). Update the extension.`
|
|
2353
|
-
);
|
|
2354
|
-
}
|
|
2296
|
+
if (lib && typeof lib.protocol === "number" && lib.protocol > PROTOCOL) warnings.push(`The page runs a newer rerender-lens (protocol ${lib.protocol}) than this panel (${PROTOCOL}). Update the extension.`);
|
|
2355
2297
|
if (lib && lib.production) warnings.push("Production React build detected: component names may be minified and hooks are unlabeled. Use a development build.");
|
|
2356
2298
|
if (lib && lib.injected && lib.source === "page")
|
|
2357
2299
|
warnings.push(`The page runs its own rerender-lens ${lib.library || ""}; the copy injected by the extension stepped aside. Turn injection off for this origin in Settings to avoid loading the library twice.`);
|
|
2358
2300
|
if (lib && lib.enabled === false) warnings.push("rerender-lens is present but disabled in this page.");
|
|
2301
|
+
if (lib && lib.truncated) warnings.push(`${plural(lib.truncated, "report")} skipped: a commit re-rendered more tracked components than the per-commit cap (200) or took over its time budget. Narrow "include" in Settings, or fix the top offenders first.`);
|
|
2359
2302
|
banner.hidden = warnings.length === 0;
|
|
2360
2303
|
for (const w of warnings) banner.append(el("div", { text: w }));
|
|
2361
2304
|
}
|
|
@@ -2460,10 +2403,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2460
2403
|
sec.append(el("div", { class: "meta", text: "Connect to a page running rerender-lens to change its options." }));
|
|
2461
2404
|
return;
|
|
2462
2405
|
}
|
|
2463
|
-
if (lib.protocol < PROTOCOL) {
|
|
2464
|
-
sec.append(el("div", { class: "meta", text: "The page library is too old to be configured from here." }));
|
|
2465
|
-
return;
|
|
2466
|
-
}
|
|
2467
2406
|
const current = Object.assign({}, lib.options || {});
|
|
2468
2407
|
const applyOptions = async (patch) => {
|
|
2469
2408
|
Object.assign(current, patch);
|
|
@@ -2522,7 +2461,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2522
2461
|
renderStatus();
|
|
2523
2462
|
break;
|
|
2524
2463
|
case "hello":
|
|
2525
|
-
|
|
2464
|
+
if (isRecord(message.payload)) setLibrary(message.payload);
|
|
2526
2465
|
break;
|
|
2527
2466
|
case "clear":
|
|
2528
2467
|
case "navigated":
|
|
@@ -2538,6 +2477,9 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2538
2477
|
if (r) enqueue(r);
|
|
2539
2478
|
break;
|
|
2540
2479
|
}
|
|
2480
|
+
case "batch":
|
|
2481
|
+
if (Array.isArray(message.items)) for (const item of message.items) handle(item);
|
|
2482
|
+
break;
|
|
2541
2483
|
}
|
|
2542
2484
|
}
|
|
2543
2485
|
const panelApi = {
|
|
@@ -2552,8 +2494,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2552
2494
|
setView,
|
|
2553
2495
|
openSettings: () => toggleSettings(true),
|
|
2554
2496
|
startRecording,
|
|
2555
|
-
stopRecording
|
|
2556
|
-
setNote
|
|
2497
|
+
stopRecording
|
|
2557
2498
|
};
|
|
2558
2499
|
if (options.theme === "dark") document.documentElement.classList.add("theme-dark");
|
|
2559
2500
|
state.origin = transport.origin || null;
|
|
@@ -2568,12 +2509,60 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2568
2509
|
});
|
|
2569
2510
|
Promise.resolve(transport.storage.get("sessions")).then(restoreSessions, () => {
|
|
2570
2511
|
});
|
|
2571
|
-
Promise.resolve(transport.storage.get("notes")).then(restoreNotes, () => {
|
|
2572
|
-
});
|
|
2573
2512
|
}
|
|
2574
2513
|
transport.subscribe(handle);
|
|
2575
2514
|
return panelApi;
|
|
2576
2515
|
}
|
|
2516
|
+
var systemTheme = () => typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
2517
|
+
var fetchSource = (url) => fetch(url).then((res) => res.ok ? res.text() : null).catch(() => null);
|
|
2518
|
+
function localStorageAdapter(prefix) {
|
|
2519
|
+
const mem = (key) => `${prefix}:${key}`;
|
|
2520
|
+
return {
|
|
2521
|
+
get: (key) => {
|
|
2522
|
+
try {
|
|
2523
|
+
const raw = localStorage.getItem(mem(key));
|
|
2524
|
+
return raw ? JSON.parse(raw) : void 0;
|
|
2525
|
+
} catch {
|
|
2526
|
+
return void 0;
|
|
2527
|
+
}
|
|
2528
|
+
},
|
|
2529
|
+
set: (key, value) => {
|
|
2530
|
+
try {
|
|
2531
|
+
localStorage.setItem(mem(key), JSON.stringify(value));
|
|
2532
|
+
} catch {
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
};
|
|
2536
|
+
}
|
|
2537
|
+
function commandBridge(send, timeoutMs) {
|
|
2538
|
+
const pending = /* @__PURE__ */ new Map();
|
|
2539
|
+
const bridge = (cmd, arg) => new Promise((resolve, reject) => {
|
|
2540
|
+
const id = Math.random().toString(36).slice(2);
|
|
2541
|
+
const fail = () => {
|
|
2542
|
+
clearTimeout(timer);
|
|
2543
|
+
pending.delete(id);
|
|
2544
|
+
resolve(null);
|
|
2545
|
+
};
|
|
2546
|
+
const timer = setTimeout(fail, timeoutMs);
|
|
2547
|
+
pending.set(id, { resolve: (v) => v instanceof Error ? reject(v) : resolve(v), timer });
|
|
2548
|
+
try {
|
|
2549
|
+
void Promise.resolve(send({ __rerenderLensCmd: true, id, cmd, arg })).catch(fail);
|
|
2550
|
+
} catch {
|
|
2551
|
+
fail();
|
|
2552
|
+
}
|
|
2553
|
+
});
|
|
2554
|
+
const reply = (m) => {
|
|
2555
|
+
if (m.__rerenderLensReply !== true || typeof m.id !== "string") return false;
|
|
2556
|
+
const p = pending.get(m.id);
|
|
2557
|
+
if (p) {
|
|
2558
|
+
pending.delete(m.id);
|
|
2559
|
+
clearTimeout(p.timer);
|
|
2560
|
+
p.resolve(typeof m.error === "string" ? new Error(m.error) : m.result);
|
|
2561
|
+
}
|
|
2562
|
+
return true;
|
|
2563
|
+
};
|
|
2564
|
+
return { bridge, reply };
|
|
2565
|
+
}
|
|
2577
2566
|
function createRelayTransport(io) {
|
|
2578
2567
|
let listener = null;
|
|
2579
2568
|
let relayConnected = false;
|
|
@@ -2600,18 +2589,55 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2600
2589
|
try {
|
|
2601
2590
|
const info = await io.bridge("info");
|
|
2602
2591
|
if (info) {
|
|
2603
|
-
emit({ type: "hello", version: info.protocol
|
|
2592
|
+
emit({ type: "hello", version: info.protocol, payload: info });
|
|
2604
2593
|
return true;
|
|
2605
2594
|
}
|
|
2606
2595
|
} catch {
|
|
2607
2596
|
}
|
|
2608
2597
|
return false;
|
|
2609
2598
|
}
|
|
2599
|
+
let syncGen = 0;
|
|
2600
|
+
let syncTimer = null;
|
|
2601
|
+
let disposed = false;
|
|
2602
|
+
function cancelSync() {
|
|
2603
|
+
syncGen++;
|
|
2604
|
+
if (syncTimer) clearTimeout(syncTimer);
|
|
2605
|
+
syncTimer = null;
|
|
2606
|
+
}
|
|
2607
|
+
function syncWithRetry(onReady, initialDelay = 0) {
|
|
2608
|
+
cancelSync();
|
|
2609
|
+
const gen = syncGen;
|
|
2610
|
+
let delay = SYNC_RETRY_MIN;
|
|
2611
|
+
let tries = 0;
|
|
2612
|
+
const attempt = async () => {
|
|
2613
|
+
syncTimer = null;
|
|
2614
|
+
if (gen !== syncGen || disposed) return;
|
|
2615
|
+
const ok = await syncWithPage();
|
|
2616
|
+
if (gen !== syncGen || disposed) return;
|
|
2617
|
+
if (ok) {
|
|
2618
|
+
onReady();
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
if (++tries >= SYNC_RETRIES) return;
|
|
2622
|
+
syncTimer = setTimeout(() => void attempt(), delay);
|
|
2623
|
+
delay = Math.min(delay * 2, SYNC_RETRY_MAX);
|
|
2624
|
+
};
|
|
2625
|
+
if (initialDelay > 0) syncTimer = setTimeout(() => void attempt(), initialDelay);
|
|
2626
|
+
else void attempt();
|
|
2627
|
+
}
|
|
2628
|
+
let droppedSeen = false;
|
|
2629
|
+
function resetSince() {
|
|
2630
|
+
since = 0;
|
|
2631
|
+
droppedSeen = false;
|
|
2632
|
+
}
|
|
2610
2633
|
async function pollOnce() {
|
|
2611
2634
|
try {
|
|
2612
2635
|
const res = await io.bridge("pull", since);
|
|
2613
2636
|
if (!res) return;
|
|
2614
|
-
if (res.dropped)
|
|
2637
|
+
if (res.dropped && !droppedSeen) {
|
|
2638
|
+
droppedSeen = true;
|
|
2639
|
+
emit({ type: "clear" });
|
|
2640
|
+
}
|
|
2615
2641
|
for (const p of res.reports) emit({ type: "report", payload: p });
|
|
2616
2642
|
since = res.seq;
|
|
2617
2643
|
} catch {
|
|
@@ -2627,19 +2653,23 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2627
2653
|
emit({ type: "polling", on: false });
|
|
2628
2654
|
}
|
|
2629
2655
|
}
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
});
|
|
2634
|
-
else {
|
|
2635
|
-
const res = await io.bridge("pull", 0).catch(() => null);
|
|
2636
|
-
if (res) {
|
|
2637
|
-
for (const p of res.reports) emit({ type: "report", payload: p });
|
|
2638
|
-
since = res.seq;
|
|
2639
|
-
} else io.bridge("replay").catch(() => {
|
|
2656
|
+
function attachToPage(initialDelay = 0) {
|
|
2657
|
+
syncWithRetry(() => {
|
|
2658
|
+
if (relayConnected) io.bridge("replay").catch(() => {
|
|
2640
2659
|
});
|
|
2641
|
-
|
|
2642
|
-
|
|
2660
|
+
else {
|
|
2661
|
+
const gen = syncGen;
|
|
2662
|
+
void io.bridge("pull", 0).catch(() => null).then((res) => {
|
|
2663
|
+
if (gen !== syncGen || disposed) return;
|
|
2664
|
+
if (res) {
|
|
2665
|
+
for (const p of res.reports) emit({ type: "report", payload: p });
|
|
2666
|
+
since = res.seq;
|
|
2667
|
+
} else io.bridge("replay").catch(() => {
|
|
2668
|
+
});
|
|
2669
|
+
setPolling(true);
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2672
|
+
}, initialDelay);
|
|
2643
2673
|
}
|
|
2644
2674
|
function connect() {
|
|
2645
2675
|
if (panelPort) {
|
|
@@ -2660,16 +2690,20 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2660
2690
|
setPolling(false);
|
|
2661
2691
|
} else if (m.type === "disconnected") {
|
|
2662
2692
|
relayConnected = false;
|
|
2663
|
-
|
|
2693
|
+
syncWithRetry(() => setPolling(true));
|
|
2664
2694
|
}
|
|
2665
2695
|
emit(m);
|
|
2666
2696
|
});
|
|
2667
2697
|
port.onDisconnect.addListener(() => {
|
|
2668
2698
|
if (panelPort !== port) return;
|
|
2669
2699
|
panelPort = null;
|
|
2670
|
-
setTimeout(
|
|
2700
|
+
reconnectTimer = setTimeout(() => {
|
|
2701
|
+
reconnectTimer = null;
|
|
2702
|
+
if (!disposed) connect();
|
|
2703
|
+
}, 1e3);
|
|
2671
2704
|
});
|
|
2672
2705
|
}
|
|
2706
|
+
let reconnectTimer = null;
|
|
2673
2707
|
const transport = {
|
|
2674
2708
|
origin,
|
|
2675
2709
|
tabLabel: io.tabLabel ?? null,
|
|
@@ -2677,36 +2711,44 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2677
2711
|
listener = fn;
|
|
2678
2712
|
connect();
|
|
2679
2713
|
io.onNavigated(() => {
|
|
2680
|
-
|
|
2714
|
+
if (disposed) return;
|
|
2715
|
+
resetSince();
|
|
2716
|
+
setPolling(false);
|
|
2717
|
+
cancelSync();
|
|
2681
2718
|
fn({ type: "navigated" });
|
|
2682
2719
|
void resolveOrigin().then(() => {
|
|
2683
|
-
|
|
2720
|
+
if (!disposed) attachToPage(NAVIGATION_SETTLE);
|
|
2684
2721
|
});
|
|
2685
2722
|
});
|
|
2686
2723
|
io.onTabChange?.((label) => {
|
|
2724
|
+
if (disposed) return;
|
|
2687
2725
|
transport.tabLabel = label;
|
|
2688
2726
|
const next = io.tabId();
|
|
2689
2727
|
if (next === currentTab) {
|
|
2690
2728
|
fn({ type: "tab-label", payload: label });
|
|
2691
2729
|
return;
|
|
2692
2730
|
}
|
|
2693
|
-
|
|
2731
|
+
resetSince();
|
|
2694
2732
|
relayConnected = false;
|
|
2695
2733
|
setPolling(false);
|
|
2734
|
+
cancelSync();
|
|
2696
2735
|
currentTab = next;
|
|
2697
2736
|
void resolveOrigin().then(() => {
|
|
2737
|
+
if (disposed) return;
|
|
2698
2738
|
fn({ type: "tab", payload: label });
|
|
2699
2739
|
connect();
|
|
2700
|
-
|
|
2740
|
+
attachToPage();
|
|
2701
2741
|
});
|
|
2702
2742
|
});
|
|
2703
|
-
void resolveOrigin().then(() =>
|
|
2743
|
+
void resolveOrigin().then(() => {
|
|
2744
|
+
if (!disposed) attachToPage();
|
|
2745
|
+
});
|
|
2704
2746
|
},
|
|
2705
2747
|
replay() {
|
|
2706
2748
|
if (relayConnected) io.bridge("replay").catch(() => {
|
|
2707
2749
|
});
|
|
2708
2750
|
else {
|
|
2709
|
-
|
|
2751
|
+
resetSince();
|
|
2710
2752
|
emit({ type: "clear" });
|
|
2711
2753
|
void syncWithPage().then(() => pollOnce());
|
|
2712
2754
|
}
|
|
@@ -2714,7 +2756,22 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2714
2756
|
clear() {
|
|
2715
2757
|
io.bridge("clear").catch(() => {
|
|
2716
2758
|
});
|
|
2717
|
-
|
|
2759
|
+
resetSince();
|
|
2760
|
+
},
|
|
2761
|
+
dispose() {
|
|
2762
|
+
disposed = true;
|
|
2763
|
+
cancelSync();
|
|
2764
|
+
setPolling(false);
|
|
2765
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
2766
|
+
reconnectTimer = null;
|
|
2767
|
+
if (panelPort) {
|
|
2768
|
+
try {
|
|
2769
|
+
panelPort.disconnect();
|
|
2770
|
+
} catch {
|
|
2771
|
+
}
|
|
2772
|
+
panelPort = null;
|
|
2773
|
+
}
|
|
2774
|
+
listener = null;
|
|
2718
2775
|
},
|
|
2719
2776
|
configure: (options) => io.bridge("configure", options).then((r) => r ?? void 0),
|
|
2720
2777
|
highlight: (id) => io.bridge("highlight", id).catch(() => {
|
|
@@ -2737,7 +2794,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2737
2794
|
if (io.openResource) transport.openResource = io.openResource;
|
|
2738
2795
|
if (io.undock) transport.undock = io.undock;
|
|
2739
2796
|
if (io.readSource) transport.readSource = io.readSource;
|
|
2740
|
-
transport.panelUrl = chrome.runtime.getURL("panel.html");
|
|
2741
2797
|
return transport;
|
|
2742
2798
|
}
|
|
2743
2799
|
function devtoolsIO() {
|
|
@@ -2749,7 +2805,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2749
2805
|
})
|
|
2750
2806
|
);
|
|
2751
2807
|
const expressions = {
|
|
2752
|
-
info: () => "b.info
|
|
2808
|
+
info: () => "b.info()",
|
|
2753
2809
|
pull: (since) => `b.pull?b.pull(${Number(since) || 0}):null`,
|
|
2754
2810
|
replay: () => "b.replay()",
|
|
2755
2811
|
clear: () => "b.clear()",
|
|
@@ -2786,7 +2842,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2786
2842
|
try {
|
|
2787
2843
|
switch (cmd) {
|
|
2788
2844
|
case "info":
|
|
2789
|
-
return b.info
|
|
2845
|
+
return b.info();
|
|
2790
2846
|
case "pull":
|
|
2791
2847
|
return b.pull ? b.pull(arg) : null;
|
|
2792
2848
|
case "replay":
|
|
@@ -2893,19 +2949,17 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2893
2949
|
},
|
|
2894
2950
|
openResource: (url) => void chrome.tabs.create({ url }),
|
|
2895
2951
|
undock: (mode) => tabId === null ? Promise.reject(new Error("no tab")) : openOutside(mode, tabId),
|
|
2896
|
-
//
|
|
2897
|
-
readSource:
|
|
2952
|
+
// Host permission for the origin is required to fetch the module source (and present when the panel works at all).
|
|
2953
|
+
readSource: fetchSource
|
|
2898
2954
|
};
|
|
2899
2955
|
return io;
|
|
2900
2956
|
}
|
|
2901
2957
|
function bootExtension() {
|
|
2902
|
-
const
|
|
2903
|
-
const theme = chrome.devtools.panels.themeName === "dark" || prefersDark ? "dark" : "light";
|
|
2958
|
+
const theme = chrome.devtools.panels.themeName === "dark" ? "dark" : systemTheme();
|
|
2904
2959
|
createPanel(document.getElementById("root"), createRelayTransport(devtoolsIO()), { theme });
|
|
2905
2960
|
}
|
|
2906
2961
|
function bootStandalone(opts = {}) {
|
|
2907
|
-
|
|
2908
|
-
return createPanel(document.getElementById("root"), createRelayTransport(standaloneIO(opts)), { theme: prefersDark ? "dark" : "light" });
|
|
2962
|
+
return createPanel(document.getElementById("root"), createRelayTransport(standaloneIO(opts)), { theme: systemTheme() });
|
|
2909
2963
|
}
|
|
2910
2964
|
function sampleReports() {
|
|
2911
2965
|
const fn = (name) => FN_PREFIX + name;
|
|
@@ -3036,7 +3090,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3036
3090
|
function createBroadcastTransport(name) {
|
|
3037
3091
|
let listener = null;
|
|
3038
3092
|
let channel = null;
|
|
3039
|
-
const pending = /* @__PURE__ */ new Map();
|
|
3040
3093
|
const emit = (m) => {
|
|
3041
3094
|
if (listener) listener(m);
|
|
3042
3095
|
};
|
|
@@ -3045,36 +3098,15 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3045
3098
|
channel = new BroadcastChannel(name);
|
|
3046
3099
|
channel.onmessage = (event) => {
|
|
3047
3100
|
const data = event.data;
|
|
3048
|
-
if (!data) return;
|
|
3049
|
-
if (data.__rerenderLensReply === true && typeof data.id === "string") {
|
|
3050
|
-
const p = pending.get(data.id);
|
|
3051
|
-
if (!p) return;
|
|
3052
|
-
pending.delete(data.id);
|
|
3053
|
-
clearTimeout(p.timer);
|
|
3054
|
-
p.resolve(typeof data.error === "string" ? new Error(data.error) : data.result);
|
|
3055
|
-
return;
|
|
3056
|
-
}
|
|
3101
|
+
if (!data || reply(data)) return;
|
|
3057
3102
|
if (data.__rerenderLens === true && typeof data.type === "string") emit({ type: data.type, version: typeof data.version === "number" ? data.version : void 0, payload: data.payload });
|
|
3058
3103
|
};
|
|
3059
3104
|
return channel;
|
|
3060
3105
|
};
|
|
3061
|
-
const bridge = (
|
|
3062
|
-
const id = Math.random().toString(36).slice(2);
|
|
3063
|
-
const timer = setTimeout(() => {
|
|
3064
|
-
pending.delete(id);
|
|
3065
|
-
resolve(null);
|
|
3066
|
-
}, 1500);
|
|
3067
|
-
pending.set(id, {
|
|
3068
|
-
resolve: (v) => v instanceof Error ? reject(v) : resolve(v),
|
|
3069
|
-
timer
|
|
3070
|
-
});
|
|
3071
|
-
open().postMessage({ __rerenderLensCmd: true, id, cmd, arg });
|
|
3072
|
-
});
|
|
3073
|
-
const mem = (key) => `rerender-lens:${name}:${key}`;
|
|
3106
|
+
const { bridge, reply } = commandBridge((message) => open().postMessage(message), 1500);
|
|
3074
3107
|
const transport = {
|
|
3075
3108
|
origin: location.origin,
|
|
3076
3109
|
tabLabel: `channel "${name}"`,
|
|
3077
|
-
panelUrl: location.origin + location.pathname,
|
|
3078
3110
|
subscribe(fn) {
|
|
3079
3111
|
listener = fn;
|
|
3080
3112
|
open();
|
|
@@ -3084,7 +3116,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3084
3116
|
fn({ type: "disconnected" });
|
|
3085
3117
|
return;
|
|
3086
3118
|
}
|
|
3087
|
-
fn({ type: "hello", version: info.protocol
|
|
3119
|
+
fn({ type: "hello", version: info.protocol, payload: info });
|
|
3088
3120
|
const res = await bridge("pull", 0);
|
|
3089
3121
|
if (res) for (const p of res.reports) fn({ type: "report", payload: p });
|
|
3090
3122
|
});
|
|
@@ -3096,23 +3128,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3096
3128
|
}),
|
|
3097
3129
|
flashAvoidable: (on) => bridge("flash", !!on).catch(() => {
|
|
3098
3130
|
}),
|
|
3099
|
-
storage: {
|
|
3100
|
-
|
|
3101
|
-
try {
|
|
3102
|
-
const raw = localStorage.getItem(mem(key));
|
|
3103
|
-
return raw ? JSON.parse(raw) : void 0;
|
|
3104
|
-
} catch {
|
|
3105
|
-
return void 0;
|
|
3106
|
-
}
|
|
3107
|
-
},
|
|
3108
|
-
set: (key, value) => {
|
|
3109
|
-
try {
|
|
3110
|
-
localStorage.setItem(mem(key), JSON.stringify(value));
|
|
3111
|
-
} catch {
|
|
3112
|
-
}
|
|
3113
|
-
}
|
|
3114
|
-
},
|
|
3115
|
-
readSource: (url) => fetch(url).then((res) => res.ok ? res.text() : null).catch(() => null),
|
|
3131
|
+
storage: localStorageAdapter(`rerender-lens:${name}`),
|
|
3132
|
+
readSource: fetchSource,
|
|
3116
3133
|
copy: (text) => navigator.clipboard.writeText(text).catch(() => {
|
|
3117
3134
|
})
|
|
3118
3135
|
};
|
|
@@ -3123,24 +3140,11 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3123
3140
|
let listener = null;
|
|
3124
3141
|
let stream = null;
|
|
3125
3142
|
let appsOnline = null;
|
|
3126
|
-
const pending = /* @__PURE__ */ new Map();
|
|
3127
3143
|
const emit = (m) => {
|
|
3128
3144
|
if (listener) listener(m);
|
|
3129
3145
|
};
|
|
3130
3146
|
const post = (message) => fetch(`${base}/message`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(message) }).then(() => void 0);
|
|
3131
|
-
const bridge
|
|
3132
|
-
const id = Math.random().toString(36).slice(2);
|
|
3133
|
-
const timer = setTimeout(() => {
|
|
3134
|
-
pending.delete(id);
|
|
3135
|
-
resolve(null);
|
|
3136
|
-
}, 2e3);
|
|
3137
|
-
pending.set(id, { resolve: (v) => v instanceof Error ? reject(v) : resolve(v), timer });
|
|
3138
|
-
post({ __rerenderLensCmd: true, id, cmd, arg }).catch(() => {
|
|
3139
|
-
clearTimeout(timer);
|
|
3140
|
-
pending.delete(id);
|
|
3141
|
-
resolve(null);
|
|
3142
|
-
});
|
|
3143
|
-
});
|
|
3147
|
+
const { bridge, reply } = commandBridge(post, 2e3);
|
|
3144
3148
|
const handleData = (data) => {
|
|
3145
3149
|
let parsed;
|
|
3146
3150
|
try {
|
|
@@ -3149,14 +3153,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3149
3153
|
return;
|
|
3150
3154
|
}
|
|
3151
3155
|
for (const m of Array.isArray(parsed) ? parsed : [parsed]) {
|
|
3152
|
-
if (!isRecord(m)) continue;
|
|
3153
|
-
if (m.
|
|
3154
|
-
const p = pending.get(m.id);
|
|
3155
|
-
if (!p) continue;
|
|
3156
|
-
pending.delete(m.id);
|
|
3157
|
-
clearTimeout(p.timer);
|
|
3158
|
-
p.resolve(typeof m.error === "string" ? new Error(m.error) : m.result);
|
|
3159
|
-
} else if (m.__rerenderLens === true && m.type === "relay") {
|
|
3156
|
+
if (!isRecord(m) || reply(m)) continue;
|
|
3157
|
+
if (m.__rerenderLens === true && m.type === "relay") {
|
|
3160
3158
|
const apps = isRecord(m.payload) && typeof m.payload.apps === "number" ? m.payload.apps : 0;
|
|
3161
3159
|
const wasOnline = appsOnline;
|
|
3162
3160
|
appsOnline = apps;
|
|
@@ -3172,15 +3170,13 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3172
3170
|
return;
|
|
3173
3171
|
}
|
|
3174
3172
|
emit({ type: "connected" });
|
|
3175
|
-
emit({ type: "hello", version: info.protocol
|
|
3173
|
+
emit({ type: "hello", version: info.protocol, payload: info });
|
|
3176
3174
|
const res = await bridge("pull", 0);
|
|
3177
3175
|
if (res) for (const p of res.reports) emit({ type: "report", payload: p });
|
|
3178
3176
|
}
|
|
3179
|
-
const mem = (key) => `rerender-lens:relay:${base}:${key}`;
|
|
3180
3177
|
const transport = {
|
|
3181
3178
|
origin: base,
|
|
3182
3179
|
tabLabel: `relay ${base.replace(/^https?:\/\//, "")}`,
|
|
3183
|
-
panelUrl: location.origin + location.pathname,
|
|
3184
3180
|
subscribe(fn) {
|
|
3185
3181
|
listener = fn;
|
|
3186
3182
|
const open = () => {
|
|
@@ -3200,47 +3196,18 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3200
3196
|
}),
|
|
3201
3197
|
flashAvoidable: (on) => bridge("flash", !!on).catch(() => {
|
|
3202
3198
|
}),
|
|
3203
|
-
storage: {
|
|
3204
|
-
|
|
3205
|
-
try {
|
|
3206
|
-
const raw = localStorage.getItem(mem(key));
|
|
3207
|
-
return raw ? JSON.parse(raw) : void 0;
|
|
3208
|
-
} catch {
|
|
3209
|
-
return void 0;
|
|
3210
|
-
}
|
|
3211
|
-
},
|
|
3212
|
-
set: (key, value) => {
|
|
3213
|
-
try {
|
|
3214
|
-
localStorage.setItem(mem(key), JSON.stringify(value));
|
|
3215
|
-
} catch {
|
|
3216
|
-
}
|
|
3217
|
-
}
|
|
3218
|
-
},
|
|
3219
|
-
readSource: (url) => fetch(url).then((res) => res.ok ? res.text() : null).catch(() => null),
|
|
3199
|
+
storage: localStorageAdapter(`rerender-lens:relay:${base}`),
|
|
3200
|
+
readSource: fetchSource,
|
|
3220
3201
|
copy: (text) => navigator.clipboard.writeText(text).catch(() => {
|
|
3221
3202
|
})
|
|
3222
3203
|
};
|
|
3223
3204
|
return transport;
|
|
3224
3205
|
}
|
|
3225
3206
|
function bootRelay(relayUrl) {
|
|
3226
|
-
|
|
3227
|
-
return createPanel(document.getElementById("root"), createRelayClientTransport(relayUrl), { theme: prefersDark ? "dark" : "light" });
|
|
3207
|
+
return createPanel(document.getElementById("root"), createRelayClientTransport(relayUrl), { theme: systemTheme() });
|
|
3228
3208
|
}
|
|
3229
3209
|
function bootBroadcast(name) {
|
|
3230
|
-
|
|
3231
|
-
return createPanel(document.getElementById("root"), createBroadcastTransport(name), { theme: prefersDark ? "dark" : "light" });
|
|
3232
|
-
}
|
|
3233
|
-
async function bootShared(code) {
|
|
3234
|
-
const report = await decodeShare(code);
|
|
3235
|
-
const transport = { subscribe() {
|
|
3236
|
-
}, panelUrl: location.origin + location.pathname };
|
|
3237
|
-
const panel = createPanel(document.getElementById("root"), transport, { theme: typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" });
|
|
3238
|
-
if (!report) {
|
|
3239
|
-
panel.handle({ type: "hello", version: PROTOCOL, payload: { protocol: PROTOCOL, library: "shared link", react: [], enabled: false } });
|
|
3240
|
-
return;
|
|
3241
|
-
}
|
|
3242
|
-
panel.importData([report]);
|
|
3243
|
-
panel.select(report.component);
|
|
3210
|
+
return createPanel(document.getElementById("root"), createBroadcastTransport(name), { theme: systemTheme() });
|
|
3244
3211
|
}
|
|
3245
3212
|
function bootDemo() {
|
|
3246
3213
|
const params2 = new URLSearchParams(location.search);
|
|
@@ -3253,7 +3220,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3253
3220
|
origin: "http://localhost:5199",
|
|
3254
3221
|
subscribe(fn) {
|
|
3255
3222
|
fn({ type: "connected" });
|
|
3256
|
-
fn({ type: "hello", version: PROTOCOL, payload: {
|
|
3223
|
+
fn({ type: "hello", version: PROTOCOL, payload: { library: "demo", protocol: PROTOCOL, react: [{ version: "19.2.0", bundleType: 1 }], production: false, enabled: true, options: { trackAllMemoized: true, silent: true }, source: "page", injected: false } });
|
|
3257
3224
|
if (flood > 0) {
|
|
3258
3225
|
const t0 = performance.now();
|
|
3259
3226
|
for (const r of floodReports(flood)) fn({ type: "report", payload: r });
|
|
@@ -3283,11 +3250,9 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3283
3250
|
originStatus: () => Promise.resolve({ origin: "http://localhost:5199", builtIn: true, permitted: true, enabled: true, inject: false, deferHook: false }),
|
|
3284
3251
|
setOrigin: () => Promise.resolve(),
|
|
3285
3252
|
storage: { get: (k) => Promise.resolve(mem[k]), set: (k, v) => Promise.resolve(mem[k] = v) },
|
|
3286
|
-
panelUrl: location.origin + location.pathname,
|
|
3287
3253
|
readSource: () => Promise.resolve("import { memo } from 'react';\n\nexport const ProductList = memo(function ProductList(props) {\n const [selected, setSelected] = useState(null);\n return (\n <ul>\n {props.products.map((p) => (\n <ProductRow key={p.id} product={p} style={{ color: 'red' }} onSelect={(id) => props.onSelect(id)} />\n ))}\n </ul>\n );\n});\n")
|
|
3288
3254
|
};
|
|
3289
|
-
|
|
3290
|
-
createPanel(document.getElementById("root"), transport, { theme: dark ? "dark" : "light" });
|
|
3255
|
+
createPanel(document.getElementById("root"), transport, { theme: /theme=dark/.test(location.search) ? "dark" : systemTheme() });
|
|
3291
3256
|
}
|
|
3292
3257
|
var api = {
|
|
3293
3258
|
PROTOCOL,
|
|
@@ -3306,8 +3271,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3306
3271
|
createRelayTransport,
|
|
3307
3272
|
createBroadcastTransport,
|
|
3308
3273
|
createRelayClientTransport,
|
|
3309
|
-
encodeShare,
|
|
3310
|
-
decodeShare,
|
|
3311
3274
|
sourceContext,
|
|
3312
3275
|
analysis: { firstDifferentPath, diffLeaves, fixesFor, rankFixes, rootCauseOf, analyzeCommit, contextAttribution, cascadeTree, rootCauseSummary, summarizeSession, compareSessions }
|
|
3313
3276
|
};
|
|
@@ -3316,8 +3279,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3316
3279
|
var hasDevtools = hasChrome && !!chrome.devtools && !!chrome.devtools.inspectedWindow;
|
|
3317
3280
|
var params = typeof location !== "undefined" ? new URLSearchParams(location.search) : new URLSearchParams();
|
|
3318
3281
|
var pathname = typeof location !== "undefined" ? String(location.pathname) : "";
|
|
3319
|
-
if (params.has("
|
|
3320
|
-
else if (params.has("relay") && typeof EventSource === "function") bootRelay(params.get("relay") || location.origin);
|
|
3282
|
+
if (params.has("relay") && typeof EventSource === "function") bootRelay(params.get("relay") || location.origin);
|
|
3321
3283
|
else if (params.has("channel") && typeof BroadcastChannel === "function") bootBroadcast(params.get("channel") || "rerender-lens");
|
|
3322
3284
|
else if (hasDevtools && /panel\.html/.test(pathname) && !params.has("tabId")) bootExtension();
|
|
3323
3285
|
else if (hasChrome && (/sidepanel\.html/.test(pathname) || params.has("tabId"))) bootStandalone({ tabId: params.has("tabId") ? Number(params.get("tabId")) : null });
|