@solidjs/signals 2.0.0-rc.8 → 2.0.0-rc.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dev-shared.js +1399 -285
- package/dist/dev.attribution.js +459 -307
- package/dist/dev.js +1864 -435
- package/dist/observe/affects.js +3 -1
- package/dist/observe/attribution.js +7 -1
- package/dist/observe/boundaries.js +209 -154
- package/dist/observe/core/action.js +18 -8
- package/dist/observe/core/async.js +220 -110
- package/dist/observe/core/attribution-costs.js +66 -0
- package/dist/observe/core/attribution-feedback.js +282 -0
- package/dist/observe/core/attribution-hooks.js +23 -1
- package/dist/observe/core/attribution-queries.js +28 -0
- package/dist/observe/core/attribution.js +262 -485
- package/dist/observe/core/constants.js +34 -1
- package/dist/observe/core/context.js +3 -3
- package/dist/observe/core/core.js +867 -367
- package/dist/observe/core/dev.js +78 -17
- package/dist/observe/core/effect.js +67 -51
- package/dist/observe/core/error-hooks.js +71 -0
- package/dist/observe/core/external.js +4 -4
- package/dist/observe/core/graph.js +37 -37
- package/dist/observe/core/heap.js +45 -45
- package/dist/observe/core/invariants.js +2 -0
- package/dist/observe/core/lanes.js +86 -49
- package/dist/observe/core/optimistic.js +242 -95
- package/dist/observe/core/owner.js +98 -84
- package/dist/observe/core/scheduler.js +543 -305
- package/dist/observe/core/verdict.js +247 -129
- package/dist/observe/index.js +7 -3
- package/dist/observe/map.js +126 -124
- package/dist/observe/signals.js +33 -17
- package/dist/observe/store/index.js +2 -0
- package/dist/observe/store/next/optimistic.js +141 -132
- package/dist/observe/store/next/projection.js +34 -21
- package/dist/observe/store/next/reconcile.js +58 -56
- package/dist/observe/store/next/store.js +260 -146
- package/dist/observe/store/store.js +5 -3
- package/dist/observe/store/utils.js +948 -135
- package/dist/prod/attribution.js +26 -17
- package/dist/prod/boundaries.js +128 -76
- package/dist/prod/core/action.js +16 -8
- package/dist/prod/core/async.js +251 -143
- package/dist/prod/core/constants.js +34 -1
- package/dist/prod/core/context.js +3 -3
- package/dist/prod/core/core.js +843 -347
- package/dist/prod/core/dev.js +17 -1
- package/dist/prod/core/effect.js +48 -34
- package/dist/prod/core/error-hooks.js +71 -0
- package/dist/prod/core/external.js +4 -4
- package/dist/prod/core/graph.js +37 -37
- package/dist/prod/core/heap.js +45 -45
- package/dist/prod/core/lanes.js +90 -53
- package/dist/prod/core/optimistic.js +247 -100
- package/dist/prod/core/owner.js +57 -45
- package/dist/prod/core/scheduler.js +543 -305
- package/dist/prod/core/verdict.js +245 -127
- package/dist/prod/index.js +7 -3
- package/dist/prod/map.js +112 -112
- package/dist/prod/signals.js +28 -12
- package/dist/prod/store/next/optimistic.js +135 -128
- package/dist/prod/store/next/projection.js +31 -20
- package/dist/prod/store/next/store.js +325 -252
- package/dist/prod/store/store.js +2 -2
- package/dist/prod/store/utils.js +946 -135
- package/dist/types/attribution.d.ts +7 -2
- package/dist/types/attribution.prod.d.ts +10 -1
- package/dist/types/boundaries.d.ts +10 -1
- package/dist/types/core/action.d.ts +12 -5
- package/dist/types/core/attribution-costs.d.ts +35 -0
- package/dist/types/core/attribution-feedback.d.ts +133 -0
- package/dist/types/core/attribution-hooks.d.ts +28 -3
- package/dist/types/core/attribution-queries.d.ts +10 -0
- package/dist/types/core/attribution.d.ts +51 -172
- package/dist/types/core/constants.d.ts +33 -0
- package/dist/types/core/core.d.ts +186 -5
- package/dist/types/core/dev.d.ts +129 -9
- package/dist/types/core/error-hooks.d.ts +71 -0
- package/dist/types/core/index.d.ts +3 -1
- package/dist/types/core/invariants.d.ts +4 -0
- package/dist/types/core/lanes.d.ts +31 -4
- package/dist/types/core/scheduler.d.ts +95 -2
- package/dist/types/core/types.d.ts +3 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/signals.d.ts +8 -0
- package/dist/types/store/index.d.ts +2 -1
- package/dist/types/store/next/optimistic.d.ts +1 -1
- package/dist/types/store/next/store.d.ts +6 -5
- package/dist/types/store/next/target.d.ts +1 -1
- package/dist/types/store/utils.d.ts +177 -6
- package/package.json +1 -1
package/dist/dev.attribution.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
bp as setAttributionHooks,
|
|
2
|
+
bK as setAttributionHooks,
|
|
4
3
|
b as NOT_PENDING,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
ax as reportDiagnostic,
|
|
5
|
+
ay as emitDiagnostic,
|
|
6
|
+
bL as isSuppressed,
|
|
7
|
+
bM as recordSubject,
|
|
8
|
+
bN as anyExcluded,
|
|
9
|
+
bO as isExcluded,
|
|
10
|
+
q as CONFIG_DERIVED_OVERRIDE,
|
|
11
|
+
bP as GRAPH_SIZE_WARN_AT,
|
|
12
|
+
bF as ownerPath,
|
|
13
|
+
bQ as subjectOf,
|
|
14
|
+
aT as $REFRESH
|
|
12
15
|
} from "./dev-shared.js";
|
|
13
16
|
|
|
14
17
|
/**
|
|
@@ -51,42 +54,21 @@ function emitRecord(type, record) {
|
|
|
51
54
|
function clearListeners() {
|
|
52
55
|
for (const type in recordListeners) recordListeners[type].clear();
|
|
53
56
|
}
|
|
57
|
+
/** @internal The engine's clock: `performance.now()` where it exists. */
|
|
54
58
|
const now = typeof performance !== "undefined" ? () => performance.now() : () => Date.now();
|
|
55
59
|
const frames = [];
|
|
56
|
-
const
|
|
57
|
-
|
|
60
|
+
const folds = [];
|
|
61
|
+
/** @internal */
|
|
62
|
+
function registerFold(hooks) {
|
|
63
|
+
folds.push(hooks);
|
|
64
|
+
}
|
|
65
|
+
/** @internal Root cause names of a cause chain — the writes/landings/refreshes the chain bottoms out in. */
|
|
58
66
|
function rootsOf(causes, out) {
|
|
59
67
|
for (const c of causes) {
|
|
60
68
|
if (c.kind === "derived" && c.causes && c.causes.length > 0) rootsOf(c.causes, out);
|
|
61
69
|
else out.add(c.name);
|
|
62
70
|
}
|
|
63
71
|
}
|
|
64
|
-
function recordCosts(event) {
|
|
65
|
-
let scope = scopeCosts.get(event.node);
|
|
66
|
-
if (scope === undefined) {
|
|
67
|
-
scope = {
|
|
68
|
-
name: event.nodeName,
|
|
69
|
-
kind: event.nodeKind,
|
|
70
|
-
runs: 0,
|
|
71
|
-
selfMs: 0,
|
|
72
|
-
wastedMs: 0,
|
|
73
|
-
overlayMs: 0
|
|
74
|
-
};
|
|
75
|
-
scopeCosts.set(event.node, scope);
|
|
76
|
-
}
|
|
77
|
-
scope.runs++;
|
|
78
|
-
scope.selfMs += event.selfMs;
|
|
79
|
-
if (event.phase !== "plain") scope.overlayMs += event.selfMs;
|
|
80
|
-
else if (!event.changed && !event.held) scope.wastedMs += event.selfMs;
|
|
81
|
-
const roots = new Set();
|
|
82
|
-
rootsOf(event.causes, roots);
|
|
83
|
-
for (const name of roots) {
|
|
84
|
-
let write = writeCosts.get(name);
|
|
85
|
-
if (write === undefined) writeCosts.set(name, (write = { name, runs: 0, downstreamMs: 0 }));
|
|
86
|
-
write.runs++;
|
|
87
|
-
write.downstreamMs += event.selfMs;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
72
|
function nodeName(node) {
|
|
91
73
|
return node._name ?? "anonymous";
|
|
92
74
|
}
|
|
@@ -179,6 +161,41 @@ function currentOrigin() {
|
|
|
179
161
|
if (frame !== undefined) return frame;
|
|
180
162
|
return currentInteraction ?? EXTERNAL_ORIGIN;
|
|
181
163
|
}
|
|
164
|
+
/** The root origin a cause list traces back to — the stamp of the nearest root write, derived links walked. */
|
|
165
|
+
function originIn(causes) {
|
|
166
|
+
for (const c of causes) {
|
|
167
|
+
const found =
|
|
168
|
+
c.kind === "derived" ? (c.causes !== undefined ? originIn(c.causes) : undefined) : c.origin;
|
|
169
|
+
if (found !== undefined && found.kind !== "external") return found;
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The `currentOrigin` hook: what a runtime recording its own fact right now
|
|
175
|
+
* (a server-function call) should stamp it with. Inside a recompute the
|
|
176
|
+
* fact belongs to the change that caused the run — a `createAsync` calling
|
|
177
|
+
* the server on a navigation's write is the navigation's, and through it the
|
|
178
|
+
* click's — walked down past create runs the way `trackFlightStart` does,
|
|
179
|
+
* since a node born inside a parent's run inherits the parent's causality.
|
|
180
|
+
* Outside one — or when the causes were themselves external (a memo a
|
|
181
|
+
* handler pulls, stale from a timer's write) — it is the write's answer
|
|
182
|
+
* (`currentOrigin`), minus the external sentinel: "none known" is
|
|
183
|
+
* `undefined` on a record, as `HoldEvent.origin` has it. The objects
|
|
184
|
+
* returned are the engine's own frames, so the caller's record joins
|
|
185
|
+
* `InteractionEvent.origin` / `NavigationEvent.origin` by identity.
|
|
186
|
+
*/
|
|
187
|
+
function ambientOrigin() {
|
|
188
|
+
for (let i = frames.length - 1; i >= 0; i--) {
|
|
189
|
+
const causes = frames[i].causes;
|
|
190
|
+
if (causes !== null) {
|
|
191
|
+
const cause = originIn(causes);
|
|
192
|
+
if (cause !== undefined) return cause;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const origin = currentOrigin();
|
|
197
|
+
return origin === EXTERNAL_ORIGIN ? undefined : origin;
|
|
198
|
+
}
|
|
182
199
|
const effectFrames = new WeakMap();
|
|
183
200
|
/** The interaction the innermost open frame runs under, else the ambient one. */
|
|
184
201
|
function enclosingInteraction() {
|
|
@@ -324,7 +341,7 @@ function stampWrite(node, kind, prev = NO_VALUES, value = NO_VALUES) {
|
|
|
324
341
|
const prior = node._devChange;
|
|
325
342
|
node._devChange = record;
|
|
326
343
|
noteNavigationWrite(prior, record);
|
|
327
|
-
noteInteractionWrite(record.origin);
|
|
344
|
+
noteInteractionWrite(record.origin, excludedNode(node));
|
|
328
345
|
if (kind === "write") trackEffectWrite(node, record, value);
|
|
329
346
|
// stampWrite is the single funnel for committed root invalidations (sync
|
|
330
347
|
// writes, refresh(), async landings), which makes it the one place the
|
|
@@ -361,10 +378,20 @@ function collectCauses(el) {
|
|
|
361
378
|
function markSeen(el) {
|
|
362
379
|
el._devSeenSeq = changeSeq;
|
|
363
380
|
}
|
|
364
|
-
/**
|
|
381
|
+
/**
|
|
382
|
+
* Snapshot the dep identities of the node's last pass (call before a run
|
|
383
|
+
* replaces them, or after it to read the fresh set). The validated prefix
|
|
384
|
+
* [`_deps`..`_depsTail`] is that pass's set; links past the tail are a
|
|
385
|
+
* previous pass's, kept linked while the frame they fed is still the
|
|
386
|
+
* committed one (A30 — a staged memo pass, or an effect pass whose run is
|
|
387
|
+
* still owed) and not part of the subscription diff.
|
|
388
|
+
*/
|
|
365
389
|
function captureDeps(el) {
|
|
366
390
|
const deps = [];
|
|
367
|
-
for (let l = el._deps; l !== null; l = l._nextDep)
|
|
391
|
+
for (let l = el._deps; l !== null; l = l._nextDep) {
|
|
392
|
+
deps.push(l._dep);
|
|
393
|
+
if (l === el._depsTail) break;
|
|
394
|
+
}
|
|
368
395
|
return deps;
|
|
369
396
|
}
|
|
370
397
|
/**
|
|
@@ -381,6 +408,7 @@ function checkDepWidth(el) {
|
|
|
381
408
|
for (let l = el._deps; l !== null; l = l._nextDep) {
|
|
382
409
|
count++;
|
|
383
410
|
if (names.length < 12) names.push(nodeName(l._dep));
|
|
411
|
+
if (l === el._depsTail) break; // the validated prefix, as captureDeps
|
|
384
412
|
}
|
|
385
413
|
const node = el;
|
|
386
414
|
if (count < limit || count < (node._devWideWarnedAt ?? 0) * 1.5) return;
|
|
@@ -558,7 +586,7 @@ function recordRerun(el, frame, timing, changed, phase, held) {
|
|
|
558
586
|
nodeRuns: (node._devRunCount = (node._devRunCount ?? 0) + 1),
|
|
559
587
|
nodeKind: el._type ? "effect" : "memo",
|
|
560
588
|
nodeName: nodeName(el),
|
|
561
|
-
|
|
589
|
+
nodeId: devId(el),
|
|
562
590
|
causes,
|
|
563
591
|
depCount: newDeps.length,
|
|
564
592
|
depsAdded,
|
|
@@ -577,10 +605,13 @@ function recordRerun(el, frame, timing, changed, phase, held) {
|
|
|
577
605
|
node._devRunInteraction = interaction;
|
|
578
606
|
node._devRunSeq = event.run;
|
|
579
607
|
node._devRunCauses = causes;
|
|
608
|
+
// The record is serializable and never carries the node; keep the node
|
|
609
|
+
// beside it for `OBSERVE.subjectOf` and the engine's own joins (effect
|
|
610
|
+
// cycles, relay tears, `why()`), for as long as anyone holds the record.
|
|
611
|
+
recordSubject(event, el);
|
|
580
612
|
history.push(event);
|
|
581
613
|
if (history.length > options.historyLimit) history.shift();
|
|
582
|
-
|
|
583
|
-
recordFeedbackRun(event);
|
|
614
|
+
for (const f of folds) f.rerun?.(el, event);
|
|
584
615
|
noteInteractionRun(interaction, timing.selfMs, false);
|
|
585
616
|
if (event.nodeKind === "effect") checkEffectCycle(el, causes);
|
|
586
617
|
checkRelayTear(el, causes, prevCauses);
|
|
@@ -1015,7 +1046,7 @@ function checkRelayTear(victim, causes, prevCauses) {
|
|
|
1015
1046
|
// new data (reconcile's job — and UNSTABLE_LIST_IDENTITY's, downstream). Once
|
|
1016
1047
|
// per store path.
|
|
1017
1048
|
const immutableReported = new Set();
|
|
1018
|
-
function checkImmutableUpdate(path, isArray, total, same, prevTotal) {
|
|
1049
|
+
function checkImmutableUpdate(path, isArray, total, same, prevTotal, owner) {
|
|
1019
1050
|
if (immutableReported.has(path) || total < 2) return;
|
|
1020
1051
|
// Push/filter/splice copies change the length by a little; a wholesale
|
|
1021
1052
|
// resize is a different operation even if some items survive.
|
|
@@ -1035,16 +1066,23 @@ function checkImmutableUpdate(path, isArray, total, same, prevTotal) {
|
|
|
1035
1066
|
`tracks ${isArray ? "items" : "leaves"}; a new container makes every reader of "${path}" ` +
|
|
1036
1067
|
`re-run for the ${changed === 1 ? "one that" : "few that"} moved. Instead, ${repair}. For ` +
|
|
1037
1068
|
`data arriving from outside (a fetch result), merge it with reconcile(data, key)(${path}).`;
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1069
|
+
// The subject is the store's own owner, not the writer's context: the
|
|
1070
|
+
// finding is about the store, and its writes legitimately arrive from
|
|
1071
|
+
// outside the graph (an event handler, an adapter). Falls back to the
|
|
1072
|
+
// ambient context (emitDiagnostic's default) when the store recorded none.
|
|
1073
|
+
const entry = emitDiagnostic(
|
|
1074
|
+
{
|
|
1075
|
+
code: "IMMUTABLE_UPDATE_IN_STORE",
|
|
1076
|
+
kind: "perf",
|
|
1077
|
+
severity: "warn",
|
|
1078
|
+
message,
|
|
1079
|
+
nodeName: path,
|
|
1080
|
+
data: { path, shape, total, unchanged: same, changed }
|
|
1081
|
+
},
|
|
1082
|
+
owner
|
|
1083
|
+
);
|
|
1084
|
+
// Paths are not unique across stores: an excluded owner's store (an
|
|
1085
|
+
// adapter's own "store.list") must not spend the app's once-per-path slot.
|
|
1048
1086
|
if (isSuppressed(entry)) return;
|
|
1049
1087
|
immutableReported.add(path);
|
|
1050
1088
|
reportDiagnostic(entry);
|
|
@@ -1160,9 +1198,7 @@ function trackFlightStart(el, flight) {
|
|
|
1160
1198
|
if (origin === at) flightOrigins.set(flight, at);
|
|
1161
1199
|
// Census: a flight still in the air when the node starts another was
|
|
1162
1200
|
// superseded — its answer will be discarded.
|
|
1163
|
-
const
|
|
1164
|
-
stats.flights++;
|
|
1165
|
-
if (liveFlights.has(el)) stats.abandoned++;
|
|
1201
|
+
for (const f of folds) f.flightStart?.(el, liveFlights.has(el));
|
|
1166
1202
|
// Nearest enclosing frame with causes: create runs carry null (a node born
|
|
1167
1203
|
// inside a parent's recompute inherits the parent's causality — the
|
|
1168
1204
|
// boundary-reveal case, and the lazy sibling whose first pull is gated
|
|
@@ -1200,10 +1236,7 @@ function finalizeFlight(el) {
|
|
|
1200
1236
|
liveFlights.delete(el);
|
|
1201
1237
|
const landedAt = now();
|
|
1202
1238
|
const ms = landedAt - flight.origin;
|
|
1203
|
-
const
|
|
1204
|
-
stats.landed++;
|
|
1205
|
-
stats.landedMs += ms;
|
|
1206
|
-
if (ms > stats.worstMs) stats.worstMs = ms;
|
|
1239
|
+
for (const f of folds) f.flightLanded?.(el, ms);
|
|
1207
1240
|
const record = el._devChange;
|
|
1208
1241
|
// Only a stamp this landing produced may carry the measurement — a stale
|
|
1209
1242
|
// async record from a previous landing must not be re-labeled.
|
|
@@ -1266,9 +1299,14 @@ function checkWaterfall(el, chain, ms) {
|
|
|
1266
1299
|
const holdStates = new WeakMap();
|
|
1267
1300
|
let activeHold = null;
|
|
1268
1301
|
let holdLog = [];
|
|
1269
|
-
/** Companions are optimistic nodes too; `_parentSource` marks them.
|
|
1302
|
+
/** Companions are optimistic nodes too; `_parentSource` marks them. So is a
|
|
1303
|
+
* memo carrying a DERIVED override (lanes stage, #3479) — a lane pass's
|
|
1304
|
+
* result, not a write anyone made: neither is an acknowledgement. */
|
|
1270
1305
|
function isCompanion(node) {
|
|
1271
|
-
return
|
|
1306
|
+
return (
|
|
1307
|
+
(!!node._x && node._x._parentSource !== undefined) ||
|
|
1308
|
+
(node._config & CONFIG_DERIVED_OVERRIDE) !== 0
|
|
1309
|
+
);
|
|
1272
1310
|
}
|
|
1273
1311
|
const HOLD_CENSUS_CAP = 10_000;
|
|
1274
1312
|
function acknowledge(state, kind, source, reader) {
|
|
@@ -1461,11 +1499,12 @@ function trackHoldSettled(t) {
|
|
|
1461
1499
|
emitRecord("hold", event);
|
|
1462
1500
|
settleNavigations(t, event);
|
|
1463
1501
|
settleInteractionsHeld(t, event);
|
|
1464
|
-
|
|
1502
|
+
for (const f of folds) f.hold?.(event);
|
|
1465
1503
|
if (isSilentHold(event)) checkSilentHold(event, subject);
|
|
1466
1504
|
else checkLongHold(event, subject);
|
|
1467
1505
|
}
|
|
1468
1506
|
/** `isLongHold` — the tail outlasted `longHolds.infoMs`. */
|
|
1507
|
+
/** @internal */
|
|
1469
1508
|
function isLongHold(event) {
|
|
1470
1509
|
const cfg = options.longHolds;
|
|
1471
1510
|
return cfg !== false && cfg !== undefined && event.tailMs >= cfg.infoMs;
|
|
@@ -1735,7 +1774,7 @@ function settleNavigation(state, outcome, hold) {
|
|
|
1735
1774
|
event.settledMs = now() - event.at;
|
|
1736
1775
|
event.outcome = outcome;
|
|
1737
1776
|
if (hold !== undefined) event.hold = hold;
|
|
1738
|
-
|
|
1777
|
+
for (const f of folds) f.navigation?.(event);
|
|
1739
1778
|
emitRecord("navigation", event);
|
|
1740
1779
|
// The interaction that performed it may have been waiting only on this.
|
|
1741
1780
|
const under = openInteractionOf(event.interaction);
|
|
@@ -1764,7 +1803,8 @@ function openInteraction(frame) {
|
|
|
1764
1803
|
open: true,
|
|
1765
1804
|
writeDrain: drainSeq,
|
|
1766
1805
|
heldIn: new Set(),
|
|
1767
|
-
held: false
|
|
1806
|
+
held: false,
|
|
1807
|
+
excludedWrites: 0
|
|
1768
1808
|
};
|
|
1769
1809
|
interactionStates.set(frame, state);
|
|
1770
1810
|
openInteractions.add(state);
|
|
@@ -1788,9 +1828,13 @@ function openInteractionOf(origin) {
|
|
|
1788
1828
|
return state !== undefined && openInteractions.has(state) ? state : undefined;
|
|
1789
1829
|
}
|
|
1790
1830
|
/** stampWrite: a root write stamped `origin`. */
|
|
1791
|
-
function noteInteractionWrite(origin) {
|
|
1831
|
+
function noteInteractionWrite(origin, excluded) {
|
|
1792
1832
|
const state = openInteractionOf(origin);
|
|
1793
1833
|
if (state === undefined) return;
|
|
1834
|
+
if (excluded) {
|
|
1835
|
+
state.excludedWrites++;
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1794
1838
|
state.event.writes++;
|
|
1795
1839
|
state.writeDrain = drainSeq;
|
|
1796
1840
|
}
|
|
@@ -1839,6 +1883,14 @@ function maybeSettleInteraction(state, end = now()) {
|
|
|
1839
1883
|
if (event.writes > 0 && drainSeq <= state.writeDrain) return;
|
|
1840
1884
|
for (const nav of event.navigations) if (nav.outcome === undefined) return;
|
|
1841
1885
|
openInteractions.delete(state);
|
|
1886
|
+
// Every write went to an excluded subject and nothing of the app's ran: the
|
|
1887
|
+
// click was on the observer's own UI (a devtools panel's button). Not a
|
|
1888
|
+
// fact about the app — forget it rather than report a dead interaction.
|
|
1889
|
+
if (event.writes === 0 && state.excludedWrites > 0 && event.runs === 0 && event.created === 0) {
|
|
1890
|
+
const i = interactionLog.indexOf(event);
|
|
1891
|
+
if (i !== -1) interactionLog.splice(i, 1);
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1842
1894
|
event.settledMs = end - event.at;
|
|
1843
1895
|
event.outcome = event.writes === 0 ? "idle" : state.held ? "held" : "committed";
|
|
1844
1896
|
emitRecord("interaction", event);
|
|
@@ -1852,209 +1904,10 @@ function navigationData(origin) {
|
|
|
1852
1904
|
if (origin.params !== undefined) data.params = origin.params;
|
|
1853
1905
|
return data;
|
|
1854
1906
|
}
|
|
1855
|
-
|
|
1856
|
-
const feedbackInteractions = new Map();
|
|
1857
|
-
const feedbackNavigations = new Map();
|
|
1858
|
-
const flightStats = new Map();
|
|
1859
|
-
const fallbackStats = new Map();
|
|
1860
|
-
const FALLBACK_FLASH_MS = 150;
|
|
1861
|
-
function flightBucket(el) {
|
|
1862
|
-
let row = flightStats.get(el);
|
|
1863
|
-
if (row === undefined) {
|
|
1864
|
-
row = { source: nodeName(el), flights: 0, landed: 0, abandoned: 0, landedMs: 0, worstMs: 0 };
|
|
1865
|
-
flightStats.set(el, row);
|
|
1866
|
-
}
|
|
1867
|
-
return row;
|
|
1868
|
-
}
|
|
1869
|
-
function trackFallback(boundary, tree, shown) {
|
|
1870
|
-
let bucket = fallbackStats.get(boundary);
|
|
1871
|
-
if (bucket === undefined) {
|
|
1872
|
-
bucket = {
|
|
1873
|
-
row: { boundary: "boundary", shows: 0, shownMs: 0, worstMs: 0, flashes: 0 },
|
|
1874
|
-
shownAt: null
|
|
1875
|
-
};
|
|
1876
|
-
fallbackStats.set(boundary, bucket);
|
|
1877
|
-
}
|
|
1878
|
-
// The first show can fire before the subtree exists; name on first sight.
|
|
1879
|
-
if (bucket.row.boundary === "boundary" && tree !== undefined) {
|
|
1880
|
-
const path = ownerPath(tree);
|
|
1881
|
-
if (path !== undefined) bucket.row.boundary = path.join(" › ");
|
|
1882
|
-
}
|
|
1883
|
-
if (shown) {
|
|
1884
|
-
if (bucket.shownAt === null) {
|
|
1885
|
-
bucket.shownAt = now();
|
|
1886
|
-
bucket.row.shows++;
|
|
1887
|
-
}
|
|
1888
|
-
return;
|
|
1889
|
-
}
|
|
1890
|
-
if (bucket.shownAt === null) return;
|
|
1891
|
-
const ms = now() - bucket.shownAt;
|
|
1892
|
-
bucket.shownAt = null;
|
|
1893
|
-
bucket.row.shownMs += ms;
|
|
1894
|
-
if (ms > bucket.row.worstMs) bucket.row.worstMs = ms;
|
|
1895
|
-
if (ms < FALLBACK_FLASH_MS) bucket.row.flashes++;
|
|
1896
|
-
}
|
|
1897
|
-
/** No affordance answered and nothing painted while held. */
|
|
1907
|
+
/** @internal No affordance answered and nothing painted while held. */
|
|
1898
1908
|
function isSilentHold(event) {
|
|
1899
1909
|
return event.paintedDuringHold === 0 && event.acknowledgements.length === 0;
|
|
1900
1910
|
}
|
|
1901
|
-
function interactionBucket(interaction) {
|
|
1902
|
-
const key = formatOrigin(interaction);
|
|
1903
|
-
let bucket = feedbackInteractions.get(key);
|
|
1904
|
-
if (bucket === undefined) {
|
|
1905
|
-
bucket = {
|
|
1906
|
-
row: {
|
|
1907
|
-
interaction: key,
|
|
1908
|
-
dispatches: 0,
|
|
1909
|
-
runs: 0,
|
|
1910
|
-
selfMs: 0,
|
|
1911
|
-
worstDispatchMs: 0,
|
|
1912
|
-
holds: 0,
|
|
1913
|
-
heldMs: 0,
|
|
1914
|
-
silentMs: 0,
|
|
1915
|
-
worstHoldMs: 0
|
|
1916
|
-
},
|
|
1917
|
-
dispatches: new Map()
|
|
1918
|
-
};
|
|
1919
|
-
feedbackInteractions.set(key, bucket);
|
|
1920
|
-
}
|
|
1921
|
-
const at = interaction.at ?? 0;
|
|
1922
|
-
if (!bucket.dispatches.has(at)) {
|
|
1923
|
-
bucket.dispatches.set(at, 0);
|
|
1924
|
-
bucket.row.dispatches++;
|
|
1925
|
-
}
|
|
1926
|
-
return bucket;
|
|
1927
|
-
}
|
|
1928
|
-
function recordFeedbackRun(event) {
|
|
1929
|
-
if (event.interaction === undefined) return;
|
|
1930
|
-
const bucket = interactionBucket(event.interaction);
|
|
1931
|
-
bucket.row.runs++;
|
|
1932
|
-
bucket.row.selfMs += event.selfMs;
|
|
1933
|
-
const at = event.interaction.at ?? 0;
|
|
1934
|
-
const dispatchMs = bucket.dispatches.get(at) + event.selfMs;
|
|
1935
|
-
bucket.dispatches.set(at, dispatchMs);
|
|
1936
|
-
if (dispatchMs > bucket.row.worstDispatchMs) bucket.row.worstDispatchMs = dispatchMs;
|
|
1937
|
-
}
|
|
1938
|
-
function recordFeedbackHold(event) {
|
|
1939
|
-
const sources = [...event.blockers].sort();
|
|
1940
|
-
const key = sources.join("\u0000");
|
|
1941
|
-
let bucket = feedbackSources.get(key);
|
|
1942
|
-
if (bucket === undefined) {
|
|
1943
|
-
bucket = {
|
|
1944
|
-
row: {
|
|
1945
|
-
sources,
|
|
1946
|
-
holds: 0,
|
|
1947
|
-
heldMs: 0,
|
|
1948
|
-
worstMs: 0,
|
|
1949
|
-
silent: 0,
|
|
1950
|
-
silentMs: 0,
|
|
1951
|
-
latestOnly: 0,
|
|
1952
|
-
long: 0,
|
|
1953
|
-
longMs: 0,
|
|
1954
|
-
acknowledgedBy: [],
|
|
1955
|
-
interactions: [],
|
|
1956
|
-
writes: [],
|
|
1957
|
-
actions: 0
|
|
1958
|
-
},
|
|
1959
|
-
acks: new Map(),
|
|
1960
|
-
interactions: new Map(),
|
|
1961
|
-
writes: new Set()
|
|
1962
|
-
};
|
|
1963
|
-
feedbackSources.set(key, bucket);
|
|
1964
|
-
}
|
|
1965
|
-
const row = bucket.row;
|
|
1966
|
-
const silent = isSilentHold(event);
|
|
1967
|
-
row.holds++;
|
|
1968
|
-
row.heldMs += event.holdMs;
|
|
1969
|
-
if (event.holdMs > row.worstMs) row.worstMs = event.holdMs;
|
|
1970
|
-
if (silent) {
|
|
1971
|
-
row.silent++;
|
|
1972
|
-
row.silentMs += event.holdMs;
|
|
1973
|
-
} else if (
|
|
1974
|
-
event.acknowledgements.length > 0 &&
|
|
1975
|
-
event.acknowledgements.every(a => a.kind === "latest")
|
|
1976
|
-
)
|
|
1977
|
-
row.latestOnly++;
|
|
1978
|
-
if (isLongHold(event)) {
|
|
1979
|
-
row.long++;
|
|
1980
|
-
row.longMs += event.tailMs;
|
|
1981
|
-
}
|
|
1982
|
-
if (event.action) row.actions++;
|
|
1983
|
-
for (const a of event.acknowledgements) {
|
|
1984
|
-
const by = `${a.kind}:${a.source}`;
|
|
1985
|
-
bucket.acks.set(by, (bucket.acks.get(by) ?? 0) + 1);
|
|
1986
|
-
}
|
|
1987
|
-
for (const w of event.heldWrites) bucket.writes.add(w.name);
|
|
1988
|
-
if (event.interaction !== undefined) {
|
|
1989
|
-
const key = formatOrigin(event.interaction);
|
|
1990
|
-
bucket.interactions.set(key, (bucket.interactions.get(key) ?? 0) + 1);
|
|
1991
|
-
const ib = interactionBucket(event.interaction);
|
|
1992
|
-
ib.row.holds++;
|
|
1993
|
-
ib.row.heldMs += event.holdMs;
|
|
1994
|
-
if (silent) ib.row.silentMs += event.holdMs;
|
|
1995
|
-
if (event.holdMs > ib.row.worstHoldMs) ib.row.worstHoldMs = event.holdMs;
|
|
1996
|
-
}
|
|
1997
|
-
}
|
|
1998
|
-
function recordFeedbackNavigation(event) {
|
|
1999
|
-
const name = event.name ?? event.to;
|
|
2000
|
-
if (name === undefined) return;
|
|
2001
|
-
let row = feedbackNavigations.get(name);
|
|
2002
|
-
if (row === undefined) {
|
|
2003
|
-
row = {
|
|
2004
|
-
name,
|
|
2005
|
-
navigations: 0,
|
|
2006
|
-
settledMs: 0,
|
|
2007
|
-
worstMs: 0,
|
|
2008
|
-
held: 0,
|
|
2009
|
-
heldMs: 0,
|
|
2010
|
-
silent: 0,
|
|
2011
|
-
superseded: 0,
|
|
2012
|
-
redirected: 0
|
|
2013
|
-
};
|
|
2014
|
-
feedbackNavigations.set(name, row);
|
|
2015
|
-
}
|
|
2016
|
-
row.navigations++;
|
|
2017
|
-
if (event.redirects !== undefined) row.redirected++;
|
|
2018
|
-
if (event.outcome === "superseded") {
|
|
2019
|
-
row.superseded++;
|
|
2020
|
-
return;
|
|
2021
|
-
}
|
|
2022
|
-
const ms = event.settledMs;
|
|
2023
|
-
row.settledMs += ms;
|
|
2024
|
-
if (ms > row.worstMs) row.worstMs = ms;
|
|
2025
|
-
if (event.outcome === "held") {
|
|
2026
|
-
row.held++;
|
|
2027
|
-
row.heldMs += event.hold?.holdMs ?? ms;
|
|
2028
|
-
if (event.hold !== undefined && isSilentHold(event.hold)) row.silent++;
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
function rankedCounts(counts, key) {
|
|
2032
|
-
return [...counts].sort((a, b) => b[1] - a[1]).map(([name, holds]) => ({ [key]: name, holds }));
|
|
2033
|
-
}
|
|
2034
|
-
function feedbackTables() {
|
|
2035
|
-
const navigations = [...feedbackNavigations.values()]
|
|
2036
|
-
.map(row => ({ ...row }))
|
|
2037
|
-
.sort((a, b) => b.heldMs - a.heldMs || b.settledMs - a.settledMs);
|
|
2038
|
-
const flights = [...flightStats.values()]
|
|
2039
|
-
.map(row => ({ ...row }))
|
|
2040
|
-
.sort((a, b) => b.abandoned - a.abandoned || b.flights - a.flights);
|
|
2041
|
-
const fallbacks = [...fallbackStats.values()]
|
|
2042
|
-
.map(bucket => ({ ...bucket.row }))
|
|
2043
|
-
.sort((a, b) => b.flashes - a.flashes || b.shownMs - a.shownMs);
|
|
2044
|
-
const sources = [...feedbackSources.values()]
|
|
2045
|
-
.map(bucket => ({
|
|
2046
|
-
...bucket.row,
|
|
2047
|
-
acknowledgedBy: rankedCounts(bucket.acks, "by"),
|
|
2048
|
-
interactions: rankedCounts(bucket.interactions, "interaction"),
|
|
2049
|
-
writes: [...bucket.writes]
|
|
2050
|
-
}))
|
|
2051
|
-
.sort((a, b) => b.silentMs - a.silentMs || b.heldMs - a.heldMs);
|
|
2052
|
-
// Ranked by the total time the user spent on it: held plus synchronous work.
|
|
2053
|
-
const interactions = [...feedbackInteractions.values()]
|
|
2054
|
-
.map(bucket => ({ ...bucket.row }))
|
|
2055
|
-
.sort((a, b) => b.heldMs + b.selfMs - (a.heldMs + a.selfMs));
|
|
2056
|
-
return { sources, interactions, navigations, flights, fallbacks };
|
|
2057
|
-
}
|
|
2058
1911
|
// The engine's implementation of the core's dev hook points. Installed by
|
|
2059
1912
|
// enable(), uninstalled by disable() — while uninstalled the core pays one
|
|
2060
1913
|
// null check per site and nothing else.
|
|
@@ -2213,28 +2066,26 @@ const engineHooks = {
|
|
|
2213
2066
|
transitionMerged(target, outgoing) {
|
|
2214
2067
|
trackHoldMerge(target, outgoing);
|
|
2215
2068
|
},
|
|
2216
|
-
storeReplaced(path, isArray, total, unchanged, prevTotal) {
|
|
2217
|
-
checkImmutableUpdate(path, isArray, total, unchanged, prevTotal);
|
|
2069
|
+
storeReplaced(path, isArray, total, unchanged, prevTotal, owner) {
|
|
2070
|
+
checkImmutableUpdate(path, isArray, total, unchanged, prevTotal, owner);
|
|
2218
2071
|
},
|
|
2219
2072
|
listChurn(el, removed, created, newLen, keyed) {
|
|
2220
2073
|
checkListIdentity(el, removed, created, newLen, keyed);
|
|
2221
2074
|
},
|
|
2222
2075
|
boundaryFallback(boundary, tree, shown) {
|
|
2223
|
-
|
|
2076
|
+
for (const f of folds) f.fallback?.(boundary, tree, shown);
|
|
2077
|
+
},
|
|
2078
|
+
currentOrigin() {
|
|
2079
|
+
return ambientOrigin();
|
|
2224
2080
|
}
|
|
2225
2081
|
};
|
|
2226
2082
|
const attribution = {
|
|
2227
2083
|
enable(opts) {
|
|
2228
2084
|
options = { ...defaultOptions, ...opts };
|
|
2229
2085
|
frames.length = 0;
|
|
2230
|
-
scopeCosts.clear();
|
|
2231
|
-
writeCosts.clear();
|
|
2232
2086
|
waterfallLog = [];
|
|
2233
2087
|
holdLog = [];
|
|
2234
2088
|
activeHold = null;
|
|
2235
|
-
feedbackSources.clear();
|
|
2236
|
-
feedbackInteractions.clear();
|
|
2237
|
-
feedbackNavigations.clear();
|
|
2238
2089
|
navigationLog = [];
|
|
2239
2090
|
openNavs.clear();
|
|
2240
2091
|
interactionLog = [];
|
|
@@ -2243,26 +2094,20 @@ const attribution = {
|
|
|
2243
2094
|
reportedCycles.clear();
|
|
2244
2095
|
relays.clear();
|
|
2245
2096
|
immutableReported.clear();
|
|
2246
|
-
flightStats.clear();
|
|
2247
|
-
fallbackStats.clear();
|
|
2248
2097
|
originFrames.length = 0;
|
|
2249
2098
|
interactionStack.length = 0;
|
|
2250
2099
|
currentInteraction = null;
|
|
2251
2100
|
hotCauses.clear();
|
|
2101
|
+
for (const f of folds) f.reset?.();
|
|
2252
2102
|
setAttributionHooks(engineHooks);
|
|
2253
2103
|
},
|
|
2254
2104
|
disable() {
|
|
2255
2105
|
clearListeners();
|
|
2256
2106
|
history = [];
|
|
2257
2107
|
frames.length = 0;
|
|
2258
|
-
scopeCosts.clear();
|
|
2259
|
-
writeCosts.clear();
|
|
2260
2108
|
waterfallLog = [];
|
|
2261
2109
|
holdLog = [];
|
|
2262
2110
|
activeHold = null;
|
|
2263
|
-
feedbackSources.clear();
|
|
2264
|
-
feedbackInteractions.clear();
|
|
2265
|
-
feedbackNavigations.clear();
|
|
2266
2111
|
navigationLog = [];
|
|
2267
2112
|
openNavs.clear();
|
|
2268
2113
|
interactionLog = [];
|
|
@@ -2271,12 +2116,11 @@ const attribution = {
|
|
|
2271
2116
|
reportedCycles.clear();
|
|
2272
2117
|
relays.clear();
|
|
2273
2118
|
immutableReported.clear();
|
|
2274
|
-
flightStats.clear();
|
|
2275
|
-
fallbackStats.clear();
|
|
2276
2119
|
originFrames.length = 0;
|
|
2277
2120
|
interactionStack.length = 0;
|
|
2278
2121
|
currentInteraction = null;
|
|
2279
2122
|
hotCauses.clear();
|
|
2123
|
+
for (const f of folds) f.reset?.();
|
|
2280
2124
|
setAttributionHooks(null);
|
|
2281
2125
|
},
|
|
2282
2126
|
subscribe(typeOrListener, listener) {
|
|
@@ -2289,22 +2133,6 @@ const attribution = {
|
|
|
2289
2133
|
history() {
|
|
2290
2134
|
return history;
|
|
2291
2135
|
},
|
|
2292
|
-
why(target) {
|
|
2293
|
-
const node = target?.[$REFRESH] ?? target;
|
|
2294
|
-
return history.filter(event => event.node === node);
|
|
2295
|
-
},
|
|
2296
|
-
subscriptions(target) {
|
|
2297
|
-
const node = target?.[$REFRESH] ?? target;
|
|
2298
|
-
const names = [];
|
|
2299
|
-
for (let l = node?._deps ?? null; l !== null; l = l._nextDep) names.push(nodeName(l._dep));
|
|
2300
|
-
return names;
|
|
2301
|
-
},
|
|
2302
|
-
costs() {
|
|
2303
|
-
return {
|
|
2304
|
-
scopes: [...scopeCosts.values()].sort((a, b) => b.selfMs - a.selfMs),
|
|
2305
|
-
writes: [...writeCosts.values()].sort((a, b) => b.downstreamMs - a.downstreamMs)
|
|
2306
|
-
};
|
|
2307
|
-
},
|
|
2308
2136
|
waterfalls() {
|
|
2309
2137
|
return waterfallLog;
|
|
2310
2138
|
},
|
|
@@ -2317,17 +2145,341 @@ const attribution = {
|
|
|
2317
2145
|
interactions() {
|
|
2318
2146
|
return interactionLog;
|
|
2319
2147
|
},
|
|
2320
|
-
feedback() {
|
|
2321
|
-
return feedbackTables();
|
|
2322
|
-
},
|
|
2323
2148
|
markFlight(flight, startedAt = now()) {
|
|
2324
2149
|
// Earliest wins: re-marking (a cache re-serving the same promise) must
|
|
2325
2150
|
// not move the origin later.
|
|
2326
2151
|
const existing = flightOrigins.get(flight);
|
|
2327
2152
|
if (existing === undefined || startedAt < existing) flightOrigins.set(flight, startedAt);
|
|
2328
|
-
}
|
|
2329
|
-
format: formatRerun,
|
|
2330
|
-
formatOrigin
|
|
2153
|
+
}
|
|
2331
2154
|
};
|
|
2332
2155
|
|
|
2333
|
-
|
|
2156
|
+
/**
|
|
2157
|
+
* `costs()` — the cost tables: scopes ranked by the self-time they burn,
|
|
2158
|
+
* root writes ranked by the downstream re-run time they cause. Folded from
|
|
2159
|
+
* every `RerunEvent` as the engine records it.
|
|
2160
|
+
*
|
|
2161
|
+
* Its own module on purpose: the tables register with the engine's fold seam
|
|
2162
|
+
* when this module is evaluated, so an observe-tier consumer that only
|
|
2163
|
+
* subscribes to records (an APM adapter) never ships the accumulation or the
|
|
2164
|
+
* tables; a consumer that imports `costs` gets both. Reset with the engine on
|
|
2165
|
+
* `enable()`/`disable()`.
|
|
2166
|
+
*/
|
|
2167
|
+
const scopeCosts = new Map();
|
|
2168
|
+
const writeCosts = new Map();
|
|
2169
|
+
function recordCosts(el, event) {
|
|
2170
|
+
let scope = scopeCosts.get(el);
|
|
2171
|
+
if (scope === undefined) {
|
|
2172
|
+
scope = {
|
|
2173
|
+
name: event.nodeName,
|
|
2174
|
+
kind: event.nodeKind,
|
|
2175
|
+
runs: 0,
|
|
2176
|
+
selfMs: 0,
|
|
2177
|
+
wastedMs: 0,
|
|
2178
|
+
overlayMs: 0
|
|
2179
|
+
};
|
|
2180
|
+
scopeCosts.set(el, scope);
|
|
2181
|
+
}
|
|
2182
|
+
scope.runs++;
|
|
2183
|
+
scope.selfMs += event.selfMs;
|
|
2184
|
+
if (event.phase !== "plain") scope.overlayMs += event.selfMs;
|
|
2185
|
+
else if (!event.changed && !event.held) scope.wastedMs += event.selfMs;
|
|
2186
|
+
const roots = new Set();
|
|
2187
|
+
rootsOf(event.causes, roots);
|
|
2188
|
+
for (const name of roots) {
|
|
2189
|
+
let write = writeCosts.get(name);
|
|
2190
|
+
if (write === undefined) writeCosts.set(name, (write = { name, runs: 0, downstreamMs: 0 }));
|
|
2191
|
+
write.runs++;
|
|
2192
|
+
write.downstreamMs += event.selfMs;
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
registerFold({
|
|
2196
|
+
rerun: recordCosts,
|
|
2197
|
+
reset() {
|
|
2198
|
+
scopeCosts.clear();
|
|
2199
|
+
writeCosts.clear();
|
|
2200
|
+
}
|
|
2201
|
+
});
|
|
2202
|
+
/**
|
|
2203
|
+
* Aggregated cost tables since `enable()`: `scopes` ranked by self-time
|
|
2204
|
+
* (with `wastedMs` = time spent on unchanged-value runs), `writes` ranked by
|
|
2205
|
+
* total downstream re-run time each root write caused.
|
|
2206
|
+
*/
|
|
2207
|
+
function costs() {
|
|
2208
|
+
return {
|
|
2209
|
+
scopes: [...scopeCosts.values()].sort((a, b) => b.selfMs - a.selfMs),
|
|
2210
|
+
writes: [...writeCosts.values()].sort((a, b) => b.downstreamMs - a.downstreamMs)
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
/**
|
|
2215
|
+
* `feedback()` — what the user waited on, as ranked tables. Folded from the
|
|
2216
|
+
* records the engine already keeps (holds, the interaction on each re-run,
|
|
2217
|
+
* navigations) and two censuses the engine reports as they happen (flights
|
|
2218
|
+
* started/landed/abandoned per async source; fallback shows per boundary).
|
|
2219
|
+
*
|
|
2220
|
+
* Its own module on purpose: the tables register with the engine's fold seam
|
|
2221
|
+
* when this module is evaluated, so an observe-tier consumer that only
|
|
2222
|
+
* subscribes to records never ships them. Reset with the engine on
|
|
2223
|
+
* `enable()`/`disable()`.
|
|
2224
|
+
*/
|
|
2225
|
+
const feedbackSources = new Map();
|
|
2226
|
+
const feedbackInteractions = new Map();
|
|
2227
|
+
const feedbackNavigations = new Map();
|
|
2228
|
+
const flightStats = new Map();
|
|
2229
|
+
const fallbackStats = new Map();
|
|
2230
|
+
const FALLBACK_FLASH_MS = 150;
|
|
2231
|
+
function flightBucket(el) {
|
|
2232
|
+
let row = flightStats.get(el);
|
|
2233
|
+
if (row === undefined) {
|
|
2234
|
+
row = { source: nodeName(el), flights: 0, landed: 0, abandoned: 0, landedMs: 0, worstMs: 0 };
|
|
2235
|
+
flightStats.set(el, row);
|
|
2236
|
+
}
|
|
2237
|
+
return row;
|
|
2238
|
+
}
|
|
2239
|
+
function trackFallback(boundary, tree, shown) {
|
|
2240
|
+
let bucket = fallbackStats.get(boundary);
|
|
2241
|
+
if (bucket === undefined) {
|
|
2242
|
+
bucket = {
|
|
2243
|
+
row: { boundary: "boundary", shows: 0, shownMs: 0, worstMs: 0, flashes: 0 },
|
|
2244
|
+
shownAt: null
|
|
2245
|
+
};
|
|
2246
|
+
fallbackStats.set(boundary, bucket);
|
|
2247
|
+
}
|
|
2248
|
+
// The first show can fire before the subtree exists; name on first sight.
|
|
2249
|
+
if (bucket.row.boundary === "boundary" && tree !== undefined) {
|
|
2250
|
+
const path = ownerPath(tree);
|
|
2251
|
+
if (path !== undefined) bucket.row.boundary = path.join(" › ");
|
|
2252
|
+
}
|
|
2253
|
+
if (shown) {
|
|
2254
|
+
if (bucket.shownAt === null) {
|
|
2255
|
+
bucket.shownAt = now();
|
|
2256
|
+
bucket.row.shows++;
|
|
2257
|
+
}
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
if (bucket.shownAt === null) return;
|
|
2261
|
+
const ms = now() - bucket.shownAt;
|
|
2262
|
+
bucket.shownAt = null;
|
|
2263
|
+
bucket.row.shownMs += ms;
|
|
2264
|
+
if (ms > bucket.row.worstMs) bucket.row.worstMs = ms;
|
|
2265
|
+
if (ms < FALLBACK_FLASH_MS) bucket.row.flashes++;
|
|
2266
|
+
}
|
|
2267
|
+
function interactionBucket(interaction) {
|
|
2268
|
+
const key = formatOrigin(interaction);
|
|
2269
|
+
let bucket = feedbackInteractions.get(key);
|
|
2270
|
+
if (bucket === undefined) {
|
|
2271
|
+
bucket = {
|
|
2272
|
+
row: {
|
|
2273
|
+
interaction: key,
|
|
2274
|
+
dispatches: 0,
|
|
2275
|
+
runs: 0,
|
|
2276
|
+
selfMs: 0,
|
|
2277
|
+
worstDispatchMs: 0,
|
|
2278
|
+
holds: 0,
|
|
2279
|
+
heldMs: 0,
|
|
2280
|
+
silentMs: 0,
|
|
2281
|
+
worstHoldMs: 0
|
|
2282
|
+
},
|
|
2283
|
+
dispatches: new Map()
|
|
2284
|
+
};
|
|
2285
|
+
feedbackInteractions.set(key, bucket);
|
|
2286
|
+
}
|
|
2287
|
+
const at = interaction.at ?? 0;
|
|
2288
|
+
if (!bucket.dispatches.has(at)) {
|
|
2289
|
+
bucket.dispatches.set(at, 0);
|
|
2290
|
+
bucket.row.dispatches++;
|
|
2291
|
+
}
|
|
2292
|
+
return bucket;
|
|
2293
|
+
}
|
|
2294
|
+
function recordFeedbackRun(event) {
|
|
2295
|
+
if (event.interaction === undefined) return;
|
|
2296
|
+
const bucket = interactionBucket(event.interaction);
|
|
2297
|
+
bucket.row.runs++;
|
|
2298
|
+
bucket.row.selfMs += event.selfMs;
|
|
2299
|
+
const at = event.interaction.at ?? 0;
|
|
2300
|
+
const dispatchMs = bucket.dispatches.get(at) + event.selfMs;
|
|
2301
|
+
bucket.dispatches.set(at, dispatchMs);
|
|
2302
|
+
if (dispatchMs > bucket.row.worstDispatchMs) bucket.row.worstDispatchMs = dispatchMs;
|
|
2303
|
+
}
|
|
2304
|
+
function recordFeedbackHold(event) {
|
|
2305
|
+
const sources = [...event.blockers].sort();
|
|
2306
|
+
const key = sources.join("\u0000");
|
|
2307
|
+
let bucket = feedbackSources.get(key);
|
|
2308
|
+
if (bucket === undefined) {
|
|
2309
|
+
bucket = {
|
|
2310
|
+
row: {
|
|
2311
|
+
sources,
|
|
2312
|
+
holds: 0,
|
|
2313
|
+
heldMs: 0,
|
|
2314
|
+
worstMs: 0,
|
|
2315
|
+
silent: 0,
|
|
2316
|
+
silentMs: 0,
|
|
2317
|
+
latestOnly: 0,
|
|
2318
|
+
long: 0,
|
|
2319
|
+
longMs: 0,
|
|
2320
|
+
acknowledgedBy: [],
|
|
2321
|
+
interactions: [],
|
|
2322
|
+
writes: [],
|
|
2323
|
+
actions: 0
|
|
2324
|
+
},
|
|
2325
|
+
acks: new Map(),
|
|
2326
|
+
interactions: new Map(),
|
|
2327
|
+
writes: new Set()
|
|
2328
|
+
};
|
|
2329
|
+
feedbackSources.set(key, bucket);
|
|
2330
|
+
}
|
|
2331
|
+
const row = bucket.row;
|
|
2332
|
+
const silent = isSilentHold(event);
|
|
2333
|
+
row.holds++;
|
|
2334
|
+
row.heldMs += event.holdMs;
|
|
2335
|
+
if (event.holdMs > row.worstMs) row.worstMs = event.holdMs;
|
|
2336
|
+
if (silent) {
|
|
2337
|
+
row.silent++;
|
|
2338
|
+
row.silentMs += event.holdMs;
|
|
2339
|
+
} else if (
|
|
2340
|
+
event.acknowledgements.length > 0 &&
|
|
2341
|
+
event.acknowledgements.every(a => a.kind === "latest")
|
|
2342
|
+
)
|
|
2343
|
+
row.latestOnly++;
|
|
2344
|
+
if (isLongHold(event)) {
|
|
2345
|
+
row.long++;
|
|
2346
|
+
row.longMs += event.tailMs;
|
|
2347
|
+
}
|
|
2348
|
+
if (event.action) row.actions++;
|
|
2349
|
+
for (const a of event.acknowledgements) {
|
|
2350
|
+
const by = `${a.kind}:${a.source}`;
|
|
2351
|
+
bucket.acks.set(by, (bucket.acks.get(by) ?? 0) + 1);
|
|
2352
|
+
}
|
|
2353
|
+
for (const w of event.heldWrites) bucket.writes.add(w.name);
|
|
2354
|
+
if (event.interaction !== undefined) {
|
|
2355
|
+
const key = formatOrigin(event.interaction);
|
|
2356
|
+
bucket.interactions.set(key, (bucket.interactions.get(key) ?? 0) + 1);
|
|
2357
|
+
const ib = interactionBucket(event.interaction);
|
|
2358
|
+
ib.row.holds++;
|
|
2359
|
+
ib.row.heldMs += event.holdMs;
|
|
2360
|
+
if (silent) ib.row.silentMs += event.holdMs;
|
|
2361
|
+
if (event.holdMs > ib.row.worstHoldMs) ib.row.worstHoldMs = event.holdMs;
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
function recordFeedbackNavigation(event) {
|
|
2365
|
+
const name = event.name ?? event.to;
|
|
2366
|
+
if (name === undefined) return;
|
|
2367
|
+
let row = feedbackNavigations.get(name);
|
|
2368
|
+
if (row === undefined) {
|
|
2369
|
+
row = {
|
|
2370
|
+
name,
|
|
2371
|
+
navigations: 0,
|
|
2372
|
+
settledMs: 0,
|
|
2373
|
+
worstMs: 0,
|
|
2374
|
+
held: 0,
|
|
2375
|
+
heldMs: 0,
|
|
2376
|
+
silent: 0,
|
|
2377
|
+
superseded: 0,
|
|
2378
|
+
redirected: 0
|
|
2379
|
+
};
|
|
2380
|
+
feedbackNavigations.set(name, row);
|
|
2381
|
+
}
|
|
2382
|
+
row.navigations++;
|
|
2383
|
+
if (event.redirects !== undefined) row.redirected++;
|
|
2384
|
+
if (event.outcome === "superseded") {
|
|
2385
|
+
row.superseded++;
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
2388
|
+
const ms = event.settledMs;
|
|
2389
|
+
row.settledMs += ms;
|
|
2390
|
+
if (ms > row.worstMs) row.worstMs = ms;
|
|
2391
|
+
if (event.outcome === "held") {
|
|
2392
|
+
row.held++;
|
|
2393
|
+
row.heldMs += event.hold?.holdMs ?? ms;
|
|
2394
|
+
if (event.hold !== undefined && isSilentHold(event.hold)) row.silent++;
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
function rankedCounts(counts, key) {
|
|
2398
|
+
return [...counts].sort((a, b) => b[1] - a[1]).map(([name, holds]) => ({ [key]: name, holds }));
|
|
2399
|
+
}
|
|
2400
|
+
registerFold({
|
|
2401
|
+
rerun: (_el, event) => recordFeedbackRun(event),
|
|
2402
|
+
hold: recordFeedbackHold,
|
|
2403
|
+
navigation: recordFeedbackNavigation,
|
|
2404
|
+
flightStart(el, abandoned) {
|
|
2405
|
+
const stats = flightBucket(el);
|
|
2406
|
+
stats.flights++;
|
|
2407
|
+
if (abandoned) stats.abandoned++;
|
|
2408
|
+
},
|
|
2409
|
+
flightLanded(el, ms) {
|
|
2410
|
+
const stats = flightBucket(el);
|
|
2411
|
+
stats.landed++;
|
|
2412
|
+
stats.landedMs += ms;
|
|
2413
|
+
if (ms > stats.worstMs) stats.worstMs = ms;
|
|
2414
|
+
},
|
|
2415
|
+
fallback: trackFallback,
|
|
2416
|
+
reset() {
|
|
2417
|
+
feedbackSources.clear();
|
|
2418
|
+
feedbackInteractions.clear();
|
|
2419
|
+
feedbackNavigations.clear();
|
|
2420
|
+
flightStats.clear();
|
|
2421
|
+
fallbackStats.clear();
|
|
2422
|
+
}
|
|
2423
|
+
});
|
|
2424
|
+
/**
|
|
2425
|
+
* What the user waited on, folded from holds and the interaction on each
|
|
2426
|
+
* re-run: `sources` ranks async sources by the silent time writes spent held
|
|
2427
|
+
* behind them (with which affordances answered, how often, and which
|
|
2428
|
+
* interactions were held); `interactions` ranks user events by the total time
|
|
2429
|
+
* they cost — re-run work caused (long-flush hazard) beside time held
|
|
2430
|
+
* (silent-hold hazard). Facts at every duration; SILENT_HOLD is the
|
|
2431
|
+
* thresholded verdict. Three more tables round out the picture: `navigations`
|
|
2432
|
+
* ranks routes by the time spent held navigating to them (folded from settled
|
|
2433
|
+
* navigations), `flights` counts each async source's flights and how many
|
|
2434
|
+
* were abandoned before landing (the re-ask storm), and `fallbacks` measures
|
|
2435
|
+
* how long each loading boundary showed its fallback and how often that was a
|
|
2436
|
+
* flash.
|
|
2437
|
+
*/
|
|
2438
|
+
function feedback() {
|
|
2439
|
+
const navigations = [...feedbackNavigations.values()]
|
|
2440
|
+
.map(row => ({ ...row }))
|
|
2441
|
+
.sort((a, b) => b.heldMs - a.heldMs || b.settledMs - a.settledMs);
|
|
2442
|
+
const flights = [...flightStats.values()]
|
|
2443
|
+
.map(row => ({ ...row }))
|
|
2444
|
+
.sort((a, b) => b.abandoned - a.abandoned || b.flights - a.flights);
|
|
2445
|
+
const fallbacks = [...fallbackStats.values()]
|
|
2446
|
+
.map(bucket => ({ ...bucket.row }))
|
|
2447
|
+
.sort((a, b) => b.flashes - a.flashes || b.shownMs - a.shownMs);
|
|
2448
|
+
const sources = [...feedbackSources.values()]
|
|
2449
|
+
.map(bucket => ({
|
|
2450
|
+
...bucket.row,
|
|
2451
|
+
acknowledgedBy: rankedCounts(bucket.acks, "by"),
|
|
2452
|
+
interactions: rankedCounts(bucket.interactions, "interaction"),
|
|
2453
|
+
writes: [...bucket.writes]
|
|
2454
|
+
}))
|
|
2455
|
+
.sort((a, b) => b.silentMs - a.silentMs || b.heldMs - a.heldMs);
|
|
2456
|
+
// Ranked by the total time the user spent on it: held plus synchronous work.
|
|
2457
|
+
const interactions = [...feedbackInteractions.values()]
|
|
2458
|
+
.map(bucket => ({ ...bucket.row }))
|
|
2459
|
+
.sort((a, b) => b.heldMs + b.selfMs - (a.heldMs + a.selfMs));
|
|
2460
|
+
return { sources, interactions, navigations, flights, fallbacks };
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
/**
|
|
2464
|
+
* Point queries over the engine's live state — the devtools/console view of
|
|
2465
|
+
* one scope. Their own module so a records-only consumer never ships them;
|
|
2466
|
+
* they read the engine's ring buffer and the graph, and register nothing.
|
|
2467
|
+
*/
|
|
2468
|
+
/** The node behind a memo/effect accessor, or the raw node passed through. */
|
|
2469
|
+
function nodeOf(target) {
|
|
2470
|
+
return target?.[$REFRESH] ?? target;
|
|
2471
|
+
}
|
|
2472
|
+
/** Re-run history for one node — pass a memo/effect accessor or raw node. */
|
|
2473
|
+
function why(target) {
|
|
2474
|
+
const node = nodeOf(target);
|
|
2475
|
+
return attribution.history().filter(event => subjectOf(event) === node);
|
|
2476
|
+
}
|
|
2477
|
+
/** Current dependency names of one scope — the devtools subscription view. */
|
|
2478
|
+
function subscriptions(target) {
|
|
2479
|
+
const node = nodeOf(target);
|
|
2480
|
+
const names = [];
|
|
2481
|
+
for (let l = node?._deps ?? null; l !== null; l = l._nextDep) names.push(nodeName(l._dep));
|
|
2482
|
+
return names;
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
export { attribution, costs, feedback, formatOrigin, formatRerun, subscriptions, why };
|