querysub 0.462.0 → 0.463.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.
Files changed (95) hide show
  1. package/package.json +1 -1
  2. package/src/-d-trust/NetworkTrust2.ts +1 -1
  3. package/src/0-path-value-core/LockWatcher2.ts +2 -5
  4. package/src/0-path-value-core/PathRouter.ts +2 -3
  5. package/src/0-path-value-core/PathRouterConstants.ts +4 -0
  6. package/src/0-path-value-core/PathValueCommitter.ts +0 -1
  7. package/src/0-path-value-core/PathValueController.ts +1 -1
  8. package/src/0-path-value-core/PathWatcher.ts +14 -5
  9. package/src/0-path-value-core/ValidStateComputer.ts +234 -86
  10. package/src/0-path-value-core/pathValueCore.ts +57 -78
  11. package/src/1-path-client/RemoteWatcher.ts +2 -1
  12. package/src/1-path-client/pathValueClientWatcher.ts +28 -2
  13. package/src/2-proxy/PathValueProxyWatcher.ts +29 -24
  14. package/src/2-proxy/TransactionDelayer.ts +44 -22
  15. package/src/2-proxy/archiveMoveHarness.ts +2 -2
  16. package/src/3-path-functions/PathFunctionRunner.ts +30 -22
  17. package/src/3-path-functions/PathFunctionRunnerMain.ts +1 -3
  18. package/src/4-deploy/deployFunctions.ts +1 -1
  19. package/src/4-deploy/deployMain.ts +1 -1
  20. package/src/4-deploy/edgeClientWatcher.tsx +1 -1
  21. package/src/4-deploy/edgeNodes.ts +1 -1
  22. package/src/4-dom/qreactTest.tsx +1 -1
  23. package/src/4-querysub/Querysub.ts +8 -9
  24. package/src/4-querysub/QuerysubController.ts +19 -1
  25. package/src/4-querysub/permissions.ts +1 -1
  26. package/src/4-querysub/predictionQueue.tsx +1 -1
  27. package/src/4-querysub/querysubPrediction.ts +25 -12
  28. package/src/5-diagnostics/GenericFormat.tsx +1 -1
  29. package/src/5-diagnostics/qreactDebug.tsx +2 -2
  30. package/src/archiveapps/archiveGCEntry.tsx +1 -1
  31. package/src/archiveapps/archiveJoinEntry.ts +3 -3
  32. package/src/config.ts +5 -1
  33. package/src/config2.ts +9 -7
  34. package/src/deployManager/components/CommitModal.tsx +1 -1
  35. package/src/deployManager/components/DeployProgressView.tsx +1 -1
  36. package/src/deployManager/components/MachineDetailPage.tsx +1 -1
  37. package/src/deployManager/components/MachinesListPage.tsx +1 -1
  38. package/src/deployManager/components/ServiceDetailPage.tsx +1 -1
  39. package/src/deployManager/components/ServicesListPage.tsx +1 -1
  40. package/src/deployManager/components/Tools.tsx +1 -1
  41. package/src/deployManager/machineApplyMainCode.ts +1 -1
  42. package/src/deployManager/machineController.ts +1 -1
  43. package/src/deployManager/machineSchema.ts +1 -1
  44. package/src/deployManager/setupMachineMain.ts +1 -1
  45. package/src/diagnostics/FunctionCallInfoState.ts +6 -3
  46. package/src/diagnostics/MachineThreadInfo.tsx +1 -1
  47. package/src/diagnostics/NodeViewer.tsx +2 -1
  48. package/src/diagnostics/StatWarning.tsx +56 -0
  49. package/src/diagnostics/StatsHeader.tsx +248 -0
  50. package/src/diagnostics/StatsOverrides.ts +50 -0
  51. package/src/diagnostics/SyncTestPage.tsx +3 -3
  52. package/src/diagnostics/TimeDebug.tsx +1 -1
  53. package/src/diagnostics/debugger/mcp-server.ts +1 -1
  54. package/src/diagnostics/grossStats/GrossStatsPage.tsx +1 -1
  55. package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +1 -1
  56. package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogsEntry.ts +1 -1
  57. package/src/diagnostics/logs/TimeRangeSelector.tsx +1 -1
  58. package/src/diagnostics/logs/errorNotifications2/ErrorNotificationPage.tsx +1 -1
  59. package/src/diagnostics/logs/errorNotifications2/ErrorWarning.tsx +0 -1
  60. package/src/diagnostics/logs/errorNotifications2/errorNotifications.ts +1 -1
  61. package/src/diagnostics/logs/errorNotifications2/errorWatchEntry.ts +1 -1
  62. package/src/diagnostics/logs/errorNotifications2/errorWatcher.ts +1 -1
  63. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleEntryEditor.tsx +1 -1
  64. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleEntryReadMode.tsx +1 -1
  65. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCyclePage.tsx +1 -1
  66. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleRenderer.tsx +1 -1
  67. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleSearch.tsx +1 -1
  68. package/src/diagnostics/managementPages.tsx +4 -9
  69. package/src/diagnostics/misc-pages/ArchiveViewer.tsx +1 -1
  70. package/src/diagnostics/misc-pages/ArchiveViewerTree.tsx +1 -1
  71. package/src/diagnostics/misc-pages/DNSPage.tsx +1 -1
  72. package/src/diagnostics/pathAuditer.ts +6 -6
  73. package/src/diagnostics/statsDefinitions.tsx +253 -0
  74. package/src/functional/throttleRender.ts +1 -1
  75. package/src/library-components/AspectSizedComponent.tsx +1 -1
  76. package/src/library-components/Button.tsx +1 -1
  77. package/src/library-components/ButtonSelector.tsx +1 -1
  78. package/src/library-components/DropdownCustom.tsx +1 -1
  79. package/src/library-components/DropdownSelector.tsx +1 -1
  80. package/src/library-components/Histogram.tsx +1 -1
  81. package/src/library-components/InlinePopup.tsx +1 -1
  82. package/src/library-components/LazyComponent.tsx +5 -1
  83. package/src/library-components/StickyBottomScroll.tsx +1 -1
  84. package/src/library-components/TypedConfigEditor.tsx +1 -1
  85. package/src/library-components/URLParam.ts +5 -9
  86. package/src/library-components/drag.ts +1 -1
  87. package/src/misc/formatJSX.tsx +1 -1
  88. package/src/user-implementation/userData.ts +1 -1
  89. package/test2.ts +1 -1
  90. package/testEntry2.ts +1 -1
  91. package/valid.md +205 -0
  92. package/src/deployManager/LaunchTrackingHeader.tsx +0 -65
  93. package/src/diagnostics/FunctionCallInfo.tsx +0 -141
  94. package/src/diagnostics/PathDistributionInfo.tsx +0 -110
  95. package/src/diagnostics/ValuePathWarning.tsx +0 -68
package/valid.md ADDED
@@ -0,0 +1,205 @@
1
+ # Querysub Validity, Locks, and Function Reruns
2
+
3
+ This document covers the core system pieces that drive optimistic concurrency in querysub and the bugs we've found chasing rejection / rerun churn. It is written as a working reference, not a clean tutorial — the bugs section is the load-bearing part.
4
+
5
+ ---
6
+
7
+ ## 1. Core data shapes
8
+
9
+ ### PathValue and Time
10
+
11
+ Every write is a `PathValue` with a `path`, a `value`, a `time`, a `valid` flag, and (sometimes) `locks` and `transactionPaths`.
12
+
13
+ `Time` is `{ time, version, creatorId }`. The wall-clock `time` field is the primary ordering key; `version` disambiguates writes at the same wall-clock time on the same node; `creatorId` disambiguates across nodes. `compareTime` sorts by `(time, version, creatorId)` in that order.
14
+
15
+ Two writes can therefore share the same `time.time` and still be totally ordered as long as `(version, creatorId)` differ. This is the basis for the "function rerun at the same `runAtTime`" mechanism — each rerun gets a fresh `version` from `clientWatcher.getFreeWriteTime` which tracks a per-process `lastVersions: Map<time.time, version>`.
16
+
17
+ ### Locks and lockGroups
18
+
19
+ A `ReadLock` is `{ path, startTime, endTime, readIsTransparent }`. It claims:
20
+
21
+ > "I read the value at `path` at `startTime`, and I observed no contention in `(startTime, endTime)`."
22
+
23
+ Every write committed in the same `commitWrites` call shares the same `locks[]` array reference. `byLockGroup` groups by that array identity. The invariant asserted by `ValidStateComputer`:
24
+
25
+ - All locks in a group share the same `endTime`.
26
+ - All values in the group share a single validity decision.
27
+
28
+ A lockGroup is **the** unit of validity. Two values in the same lockGroup must either both be valid or both be rejected.
29
+
30
+ ### Authorities and routing
31
+
32
+ `PathRouter` maps each path to an authority node via a hash prefix. A given node may be the authority on some paths, a subscriber on others. Key calls:
33
+
34
+ - `isSelfAuthority(path)` — do we own this path?
35
+ - `isSelfParentAuthority(path)` — do we own everything under this path's parent hash prefix?
36
+
37
+ Critically: the Data path and the Results path for the same call commit go to **different authorities**. Their lockGroup invariant (same validity) is distributed across authorities, and disagreements between authorities have been the root cause of multiple bugs (see § 4.2).
38
+
39
+ ---
40
+
41
+ ## 2. The committing flow (`PathValueProxyWatcher`)
42
+
43
+ The proxy watcher runs a user function with synchronized reads/writes:
44
+
45
+ 1. `runWatcher` resets per-watcher state and sets:
46
+ - `watcher.nextWriteTime = options.runAtTime` (undefined for non-rerun proxies)
47
+ - If `nextWriteTime` is set, bumps it via `clientWatcher.getFreeWriteTime` to get a unique version for this commit
48
+ - `watcher.currentReadTime = nextWriteTime`, with `version -= 1_000_000` when set (see § 4.1 for why)
49
+ 2. The user function runs. Each read goes through `getCallback`:
50
+ - Calls `authorityStorage.getValueAtOrBeforeTime(path, currentReadTime)`
51
+ - Keys the read into `watcher.pendingAccesses` by `currentReadTime`
52
+ 3. If the function still has unsynced accesses, it re-evaluates (inner try loop). Each try resets `nextWriteTime` and gets a fresh version. The version used by the final committing run is the one allocated last.
53
+ 4. `commitWrites` builds locks from `pendingAccesses` (one lock per `(path, time)` read), with `endTime: readTime` (the bumped read time used during the function).
54
+ 5. The constraint at the end of `commitWrites` ensures `writeTime` is strictly after any lock `endTime`. In the runAtTime case this is automatic because `endTime = readTime` is `writeTime - 1_000_000` versions.
55
+
56
+ ### Where the read time / write time / lock endTime fit together
57
+
58
+ - `nextWriteTime`: the actual time we'll write at — has a unique version via `getFreeWriteTime`.
59
+ - `currentReadTime` (runAtTime case): `nextWriteTime` minus 1,000,000 versions. Used both as the read filter (so we never see our own prior versions) and as the lock `endTime` (so contention checks cover the right range).
60
+ - `currentReadTime` (non-runAtTime case): `undefined`. Reads return the latest value; locks fall back to `endTime = writeTime` (via the `if (!readTime) readTime = writeTime` branch in lock construction).
61
+
62
+ ---
63
+
64
+ ## 3. Validity computation (`ValidStateComputer`)
65
+
66
+ ### Entry point: `ingestValuesAndValidStates`
67
+
68
+ Called by `PathValueCommitter` (and others) when path values arrive. The flow:
69
+
70
+ 1. **Per-batch dedup by `(path, time)`**: collapse multiple values for the same `(path, time)` in one batch to the last-arriving one. (Pre-fix this was the "duplicate-with-stale-storage" bug — see § 4.3.)
71
+ 2. **Storage-vs-incoming dedup**: drop values whose `existing.valid` strictly equals incoming `valid`. Uses strict `===` so `undefined !== false`, which lets us deliberately re-ingest with `valid: undefined` to force a re-check. Bypassable via `initialTriggers.values`.
72
+ 3. **Split**: ours vs not-ours (`PathRouter.isSelfAuthority`).
73
+ 4. **LockWatcher2 subscription**: for our values' locks on non-self paths, register a watch via `watchValueLocks`. This holds for `MAX_CHANGE_AGE * 2` minimum — guarantees `destroyPath` doesn't strip the value during the lock's active window.
74
+ 5. **Initial-sync clobber**: for not-our paths that were just initially-synced, mark stale prior values as invalid.
75
+ 6. **Ingest** into `authorityStorage`.
76
+ 7. **Compute valid states** in a loop. Each iteration runs `computeValidStates(dependenciesChanged, now, prevValidStates)`. Changes feed back into `dependenciesChanged` via `lockWatcher2.getValuePathWatchers`. Deferred values (see § 4.5) accumulate into `deferredAll`.
77
+ 8. **Trigger watchers** via `pathWatcher.triggerValuesChanged`.
78
+ 9. **Schedule defer retry**: if any lockGroups deferred, `setTimeout` a re-call with the deferred values' paths in `initialTriggers.values` to bypass dedup.
79
+
80
+ ### Per-lockGroup decision: `computeValidStates`
81
+
82
+ For each lockGroup:
83
+
84
+ - Verify all locks share an `endTime`.
85
+ - Verify all values are on a self-authority path (we shouldn't be deciding validity for foreign-owned values).
86
+ - If `now - lockTime > MAX_CHANGE_AGE`, skip (validity is frozen by age).
87
+ - Else for each lock: `isLockValid(lock, now)` (tri-state: `true`/`false`/`"defer"`) and then `isLockContentionFree(lock)`. First definitive failure wins; defers yield to a later definitive failure but otherwise propagate up.
88
+ - If valid is `"defer"`, push values into the deferred array and don't touch storage or `pathValue.valid`.
89
+ - Else update `pathValue.valid` and push to changed if the decision differs from the previous state.
90
+
91
+ ### `isLockValid` semantics
92
+
93
+ ```
94
+ if (transparent) return true
95
+ if (value at lock.startTime exists in storage) return value.valid
96
+ if (!isSynced(lock.path)) return true // never received, can't decide
97
+ if (foreign authority and now - lock.startTime.time < DEFER_LOCK_WINDOW)
98
+ return "defer"
99
+ return false
100
+ ```
101
+
102
+ The defer branch is the optimistic-race accommodation: when the lock target is on a foreign authority and the missing value is recent, give it `DEFER_LOCK_WINDOW` (2.5s, from `pathValueCore.ts`) before rejecting. Reads that arrived later than the lock's `startTime` plus the window are decided immediately.
103
+
104
+ ### `isLockContentionFree`
105
+
106
+ Scans `getValuePlusHistory(lock.path)` for any **valid** value with `startTime < value.time < endTime`. Any hit means another write landed inside the read's claimed range — contention, reject.
107
+
108
+ The endTime being `currentReadTime` (which is bumped-version of nextWriteTime in the rerun case) means the range `(startTime, endTime)` covers our own intermediate write versions and concurrent writes from other creators at the same wall-clock time. That's the right semantic and the basis of OCC here.
109
+
110
+ ---
111
+
112
+ ## 4. Bugs and fixes
113
+
114
+ A list of things we have to do because of things that go wrong if we don't.
115
+
116
+ - **Read at `nextWriteTime.version - 1_000_000` on reruns** (not at `nextWriteTime` directly). Otherwise reruns at the same `runAtTime` read their own previous rejected version `.X[N-1]` as a predecessor, increment it, write `.X[N]`, which gets rejected (contention with `.X[N-1]`), and the function runner re-queues until the `MAX_QUEUE_COUNT` cap fires. The decrement only applies when `runAtTime` is set — natural forward proxies don't have own-prior versions to collide with.
117
+
118
+ - **Per-batch dedup by `(path, time)` at the top of `ingestValuesAndValidStates`**. Otherwise if a value flip-flops on its source authority and both the valid and the invalid update arrive in the same batch, the storage-vs-incoming dedup compares each against the pre-batch storage state independently — the correction (matching stale storage) gets dropped as a duplicate, and the wrong value survives and clobbers storage. End result: receivers stuck on the wrong validity forever.
119
+
120
+ - **Use strict `===` in the storage-vs-incoming dedup, not `!!` coercion**. Otherwise `valid: undefined` collapses to `false` and a deliberate "please re-evaluate" re-ingest with `undefined` looks like a duplicate of a rejected value and gets filtered. Strict equality lets us use `undefined` as a signal.
121
+
122
+ - **Defer `isLockValid` rejection for `DEFER_LOCK_WINDOW` after `lock.startTime`** when the missing value is on a foreign authority. Otherwise concurrent dependent writes get spuriously rejected when network ordering delivers the dependent before its predecessor — e.g. function runner commits `.726`, `.7266`, `.727` in parallel with `noWaitForCommit`, `.7266` arrives at the Data authority first, lock check fails because `.726` isn't there yet, `.7266` rejected. Cascade requeues. With the defer window, the value almost always arrives within 2.5s and `LockWatcher2` fires re-eval naturally.
123
+
124
+ - **Schedule a `setTimeout(DEFER_LOCK_WINDOW)` retry for any deferred values**. Otherwise if the foreign value never arrives, the deferred lockGroup never gets a decision and the value sits in undefined-valid limbo forever. The retry bypasses the storage dedup via `initialTriggers.values` so it actually re-runs `computeValidStates`; by then the lock age check decides "still missing → reject".
125
+
126
+ - **Lock `endTime` must be the actual `writeTime` (or a time strictly before any concurrent write at the same `time.time`), not the read time**. Otherwise contention detection at the lock-validation authority misses writes that landed between our read and our write. The fallback `if (!readTime) readTime = writeTime` in `commitWrites` already encodes this for the no-readTime case; the runAtTime case relies on the version-decrement trick (above) to keep `endTime = readTime < writeTime` correct.
127
+
128
+ - **Log validity transitions with their reason** (`lock-invalid on …`, `lock-contention on …`, `all locks valid and contention-free`). Otherwise cross-authority disagreements are undebuggable — the only way to figure out why two authorities reached different decisions about the same lockGroup is to diff their per-decision logs side by side. Without the reason field, the logs only tell you what the answer was, not why.
129
+
130
+ ### Open: cross-authority lockGroup disagreement
131
+
132
+ Two authorities holding parts of the same lockGroup (e.g. Data and Results for one commit) can reach different validity decisions and broadcast them independently. The Data authority sometimes commits a value as valid before its own contention scan finishes, briefly emits `valid: true`, then re-rejects. The Results authority uses the brief-valid window to validate dependent writes via its lock check, decides true, and broadcasts. The Data authority's correction races against the Results authority's wrong validation reaching downstream subscribers (notably the function runner's rerun gate, which only re-queues when Results is invalid).
133
+
134
+ The dedup fix and the defer window mitigate this but don't fix it. The real fix needs cross-authority consensus on lockGroup validity — currently every authority decides independently and the system relies on eventual convergence, which doesn't always converge correctly.
135
+
136
+ ---
137
+
138
+ ## 5. Function runner interactions (`PathFunctionRunner`)
139
+
140
+ ### Rerun gate
141
+
142
+ `findFunctionsToCall` watches `Calls[*]` and `Results[*]`. For each call, the gate is roughly:
143
+
144
+ ```ts
145
+ let result = atomicObjectRead(moduleData.Results[callId]);
146
+ if (result) continue; // already have a valid result, no rerun
147
+ queueCall(call);
148
+ ```
149
+
150
+ `atomicObjectRead` returns the latest *valid* value. If Results becomes invalid, the gate opens and re-queues. If it's then revalidated incorrectly (as in § 4.2), the gate closes permanently and the call never runs again.
151
+
152
+ ### Per-rerun commit
153
+
154
+ Each `proxyWatcher.commitFunction` call uses `runAtTime: callSpec.runAtTime`. `runAtTime` is fixed for the lifetime of the call (it's part of the call's identity). Reruns reuse the same `runAtTime`, get bumped `version` via `getFreeWriteTime`. The FINISHED log line shows `call.runAtTime` (always version 0) — it does **not** show the actual writeTime version, which is why "FINISHED ... .X[0]" appearing twice does not mean a version collision.
155
+
156
+ ### `MAX_QUEUE_COUNT` cap
157
+
158
+ Default 14 (dev) / 100 (prod). The cap catches infinite rerun loops — most commonly the self-read pattern in § 4.1. Hitting the cap produces an explicit error result rather than rerunning forever.
159
+
160
+ ### Skip on already-written
161
+
162
+ If a rerun executes but finds Results already has a value (because another authority already wrote it, or the cascade re-validated the prior run while the rerun was in flight), the function logs "Skipping function write" and doesn't commit.
163
+
164
+ ---
165
+
166
+ ## 6. Transaction delaying (`TransactionDelayer`)
167
+
168
+ Separate from the lock-level defer. Each committed write carries `transactionPaths` (the set of paths the same proxy committed at the same writeTime). On read, the proxy compares its observed values' transactionPaths against the times it actually saw — if it has only an older value for a path the transaction wrote at a newer time, it's missing a transaction part.
169
+
170
+ `waitIfReceivedIncompleteTransaction`:
171
+
172
+ 1. Computes the newest transaction time that's missing.
173
+ 2. Waits up to `MISSING_TRANSACTION_PART_TIMEOUT = 15s` for the missing parts.
174
+ 3. While waiting, the proxy holds the watcher's commit (via `triggerOnPromiseFinish`).
175
+ 4. On timeout, if still missing, logs an error and lets the commit proceed (the proxy will then likely produce a write that gets rejected — but that's user-visible and self-correcting via rerun).
176
+
177
+ This is the read-side analog of the lock defer: defer is for "we're an authority and the value we need for a lock check hasn't arrived"; transaction delay is for "we're a reader and one of our transaction siblings is older than the rest of the transaction".
178
+
179
+ ---
180
+
181
+ ## 7. Watching mechanics (`LockWatcher2`)
182
+
183
+ When we ingest a value whose lock targets a foreign path, we register a `pathWatcher.watchPath` for that path under `lockWatcher2`'s node id. The watch is kept alive until `garbageCollectOldWatchers` retires it (after `MAX_CHANGE_AGE * 2`).
184
+
185
+ `getValuePathWatchers(pathValue)` returns all our deferred / pending pathValues whose lock range contains `pathValue.time`. This is the mechanism by which a value arriving from a foreign authority triggers re-evaluation of local lockGroups — including the natural recovery for deferred locks (§ 4.5).
186
+
187
+ Critical guarantee: `destroyPath` (called when a path is unwatched) does NOT fire during a lock's active window, because `LockWatcher2` holds the watch. Earlier in this work we suspected `destroyPath` was wiping `isSyncedCache` and triggering wrong "missing + not-synced → valid by default" decisions in `isLockValid`; LockWatcher2's hold-time invariant rules that out for the window we care about.
188
+
189
+ ---
190
+
191
+ ## 8. Key invariants and where they're enforced
192
+
193
+ | Invariant | Enforced by |
194
+ |---|---|
195
+ | All locks in a lockGroup share the same `endTime` | `ValidStateComputer.computeValidStates` (asserts, errors) |
196
+ | All values in a lockGroup share a single validity decision | `computeValidStates` per-group loop |
197
+ | `writeTime > all lock endTimes` for one commit | `PathValueProxyWatcher.commitWrites` shift-forward branch (debugbreak if it fires) |
198
+ | Lock endTime = writeTime when readTime is undefined | `commitWrites` fallback at the lock construction |
199
+ | Each commit gets a unique `(time.time, version)` per node | `clientWatcher.lastVersions` map in `getFreeWriteTime` |
200
+ | Same `time.time` from different nodes distinguished by `creatorId` | `compareTime` ordering |
201
+ | LockWatcher2 holds remote-path watches for `MAX_CHANGE_AGE * 2` | `garbageCollectOldWatchers` threshold |
202
+ | Per-batch dedup keeps the last-arriving value for `(path, time)` | top of `ingestValuesAndValidStates` |
203
+ | Cross-batch dedup uses strict `===` on `valid` | `ingestValuesAndValidStates` filter |
204
+ | Deferred lockGroup decisions get re-checked within `DEFER_LOCK_WINDOW` | `setTimeout` at end of `ingestValuesAndValidStates` |
205
+ | **Cross-authority lockGroup consensus** | *not enforced* — open issue (§ 4.2) |
@@ -1,65 +0,0 @@
1
- module.allowclient = true;
2
-
3
- import { qreact } from "../4-dom/qreact";
4
- import { css } from "typesafecss";
5
- import { getBrowserUrlNode } from "../-f-node-discovery/NodeDiscovery";
6
- import { MachineServiceController } from "./machineSchema";
7
-
8
- const SINCE_DAYS = 2;
9
- const CRASH_HUE = 0;
10
- const OTHER_HUE = 210;
11
-
12
- export class LaunchTrackingHeader extends qreact.Component {
13
- render() {
14
- let summaries = MachineServiceController(getBrowserUrlNode()).getRecentLaunches(SINCE_DAYS);
15
- if (!summaries) return undefined;
16
- if (summaries.length === 0) return undefined;
17
-
18
- let totalCrashes = 0;
19
- let totalOther = 0;
20
- let perKey = new Map<string, { crashes: number; other: number }>();
21
- for (let s of summaries) {
22
- let isCrash = s.reason === "crashed";
23
- if (isCrash) totalCrashes++;
24
- else totalOther++;
25
- let entry = perKey.get(s.serviceKey);
26
- if (!entry) {
27
- entry = { crashes: 0, other: 0 };
28
- perKey.set(s.serviceKey, entry);
29
- }
30
- if (isCrash) entry.crashes++;
31
- else entry.other++;
32
- }
33
-
34
- let ranked: { serviceKey: string; crashes: number; other: number }[] = [];
35
- for (let [serviceKey, c] of perKey) {
36
- ranked.push({ serviceKey, crashes: c.crashes, other: c.other });
37
- }
38
- ranked.sort((a, b) => {
39
- if (b.crashes !== a.crashes) return b.crashes - a.crashes;
40
- return (b.crashes + b.other) - (a.crashes + a.other);
41
- });
42
- let top = ranked[0];
43
-
44
- let title = `Launches in last ${SINCE_DAYS} days: ${totalCrashes} crashed, ${totalOther} other`;
45
- for (let r of ranked) {
46
- title += `\n${r.serviceKey}: ${r.crashes} crashed, ${r.other} other`;
47
- }
48
-
49
- return <div title={title} className={css.hbox(6).colorhsl(0, 0, 20)}>
50
- <span>🚀</span>
51
- <span>
52
- <span className={css.colorhsl(CRASH_HUE, 70, 35).boldStyle}>{totalCrashes}</span>
53
- <span className={css.colorhsl(0, 0, 55)}>|</span>
54
- <span className={css.colorhsl(OTHER_HUE, 65, 35).boldStyle}>{totalOther}</span>
55
- </span>
56
- {top && top.crashes > 0 &&
57
- <span className={css.colorhsl(0, 0, 40)}>
58
- (<span className={css.colorhsl(CRASH_HUE, 70, 35)}>{top.crashes}</span>
59
- <span className={css.colorhsl(0, 0, 55)}>|</span>
60
- <span className={css.colorhsl(OTHER_HUE, 65, 35)}>{top.other}</span>)
61
- </span>
62
- }
63
- </div>;
64
- }
65
- }
@@ -1,141 +0,0 @@
1
- import { qreact } from "../4-dom/qreact";
2
- import { css } from "../4-dom/css";
3
- import { t } from "../2-proxy/schema2";
4
- import { callState, ensureSubscribed } from "./FunctionCallInfoState";
5
- import { formatNumber, formatPercent, formatTime } from "socket-function/src/formatting/format";
6
- import { measureBlock } from "socket-function/src/profiling/measure";
7
- import { StatsValue, getStatsTop } from "socket-function/src/profiling/stats";
8
- import { isCurrentUserSuperUser } from "../user-implementation/userData";
9
-
10
- module.hotreload = true;
11
-
12
- function formatTimeStats(stats: StatsValue): string {
13
- if (stats.count === 0) return formatTime(0);
14
-
15
- let avgTime = stats.sum / stats.count;
16
- let top = getStatsTop(stats);
17
-
18
- if (!top.topHeavy) {
19
- return formatTime(avgTime);
20
- }
21
-
22
- let bottomAvg = (stats.sum - top.value) / (stats.count - top.count) || 0;
23
- let topAvg = top.value / top.count;
24
- let topPercent = formatPercent(top.countFraction);
25
-
26
- return `${formatTime(bottomAvg)} (top ${topPercent} = ${formatTime(topAvg)})`;
27
- }
28
-
29
- export class FunctionCallInfo extends qreact.Component {
30
- showDetailedStats() {
31
- if (!isCurrentUserSuperUser()) return undefined;
32
- let state = callState();
33
-
34
- let avgInternalReruns = state.totalCalls > 0 && state.totalInternalReruns / state.totalCalls || 0;
35
- let avgFullReruns = state.totalCalls > 0 && state.totalFullReruns / state.totalCalls || 0;
36
- let percentMultipleInternalRuns = state.totalCalls > 0 && state.callsWithMultipleInternalRuns / state.totalCalls || 0;
37
- let percentCascadingRuns = state.totalCalls > 0 && state.callsWithCascadingRuns / state.totalCalls || 0;
38
- let avgCascadingReruns = state.callsWithCascadingRuns > 0 && state.totalInternalRerunsForCascading / state.callsWithCascadingRuns || 0;
39
- let percentMultipleFullRuns = state.totalCalls > 0 && state.callsWithMultipleFullRuns / state.totalCalls || 0;
40
-
41
- console.log("=== Function Call Statistics ===");
42
- console.log([
43
- `${formatNumber(state.totalCalls)} ⚡`,
44
- [
45
- `${formatNumber(avgInternalReruns)} ♻️`,
46
- percentMultipleInternalRuns > 0 && `(${formatPercent(percentMultipleInternalRuns)} 🔦)`,
47
- percentCascadingRuns > 0 && `(${formatPercent(percentCascadingRuns)} / ${formatNumber(avgCascadingReruns)} / MAX ${state.maxInternalReruns} 📈)`
48
- ].filter(v => v).join(" "),
49
- percentMultipleFullRuns > 0 && `${formatNumber(avgFullReruns)} (${formatPercent(percentMultipleFullRuns)}) ⚠️`,
50
- `${formatTimeStats(state.evalTimeStats)} 💫 / ${formatTimeStats(state.totalTimeStats)} 🕒`
51
- ].filter(v => v).join(" | "));
52
- console.log("");
53
- console.log("=== Per-Function Statistics ===");
54
-
55
- let perFunctionStats = state.perFunctionStats;
56
- if (perFunctionStats) {
57
- for (let functionId in perFunctionStats) {
58
- let stats = perFunctionStats[functionId];
59
- let avgInternal = stats.totalInternalReruns / stats.totalCalls;
60
- let avgFull = stats.totalFullReruns / stats.totalCalls;
61
- let pctInternal = stats.callsWithMultipleInternalRuns / stats.totalCalls;
62
- let pctCascading = stats.callsWithCascadingRuns / stats.totalCalls;
63
- let avgCascading = stats.callsWithCascadingRuns > 0 && stats.totalInternalRerunsForCascading / stats.callsWithCascadingRuns || 0;
64
- let pctFull = stats.callsWithMultipleFullRuns / stats.totalCalls;
65
-
66
- console.log([
67
- `${functionId}: ${formatNumber(stats.totalCalls)} ⚡`,
68
- [
69
- `${formatNumber(avgInternal)} ♻️`,
70
- pctInternal > 0 && `(${formatPercent(pctInternal)} 🔦)`,
71
- pctCascading > 0 && `(${formatPercent(pctCascading)} / ${formatNumber(avgCascading)} / MAX ${stats.maxInternalReruns} 📈)`
72
- ].filter(v => v).join(" "),
73
- pctFull > 0 && `${formatNumber(avgFull)} (${formatPercent(pctFull)}) ⚠️`,
74
- `${formatTimeStats(stats.evalTimeStats)} 💫 / ${formatTimeStats(stats.totalTimeStats)} 🕒`
75
- ].filter(v => v).join(" | "));
76
- }
77
- }
78
- }
79
-
80
- render() {
81
- ensureSubscribed();
82
- let state = callState();
83
-
84
- let avgInternalReruns = state.totalCalls > 0 && state.totalInternalReruns / state.totalCalls || 0;
85
- let avgFullReruns = state.totalCalls > 0 && state.totalFullReruns / state.totalCalls || 0;
86
- let percentMultipleInternalRuns = state.totalCalls > 0 && state.callsWithMultipleInternalRuns / state.totalCalls || 0;
87
- let percentCascadingRuns = state.totalCalls > 0 && state.callsWithCascadingRuns / state.totalCalls || 0;
88
- let avgCascadingReruns = state.callsWithCascadingRuns > 0 && state.totalInternalRerunsForCascading / state.callsWithCascadingRuns || 0;
89
- let percentMultipleFullRuns = state.totalCalls > 0 && state.callsWithMultipleFullRuns / state.totalCalls || 0;
90
-
91
- return <div className={css.button.vbox(4).pad2(4).alignItems("end")} onClick={() => this.showDetailedStats()}>
92
- <div className={css.hbox(10)}>
93
- <span title="Committed synced function calls">
94
- {formatNumber(state.totalCalls)}⚡
95
- </span>
96
- <div className={css.hbox(7)}>
97
- <span title="Number of function evaluations">
98
- {formatNumber(avgInternalReruns)}♻️
99
- </span>
100
- {percentMultipleInternalRuns > 0 && <span title="Cases where the first call did not have all the synced data">
101
- (🔦{formatPercent(percentMultipleInternalRuns)})
102
- </span>}
103
- {percentCascadingRuns > 0 && <span title="The initial sync did not provide all the data, requiring cascading syncing.">
104
- (📈{formatPercent(percentCascadingRuns)} / {formatNumber(avgCascadingReruns)} / MAX {state.maxInternalReruns})
105
- </span>}
106
- </div>
107
- {percentMultipleFullRuns > 0 && <span title="Rejected calls requiring full reruns. Unless you are changing data another user is actively changing, this is a bug!" className={css.hsl(60, 100, 80).pad2(2, 1).hslcolor(0, 0, 0)}>
108
- {formatNumber(avgFullReruns)} ({formatPercent(percentMultipleFullRuns)}) ⚠️
109
- </span>}
110
- </div>
111
- <div>
112
- <span title="Average time to evaluate the application function (excluding proxy overhead)">
113
- {formatTimeStats(state.evalTimeStats)}💫
114
- </span>
115
- {" / "}
116
- <span title="Average time to evaluate from start to finish, including rejections, etc. Top % shows the top % count, and if top % is shown the other amount has the top values removed.">
117
- {formatTimeStats(state.totalTimeStats)}🕒
118
- </span>
119
- </div>
120
- <div className={css.hbox(20)}>
121
- <div className={css.hbox(5)}>
122
- <span title="Remote watched paths (syncing from other nodes)">
123
- {formatNumber(state.remotePathCount)}🔥
124
- </span>
125
- {" / "}
126
- <span title="Local watched paths">
127
- {formatNumber(state.localPathCount)}🏠
128
- </span>
129
- {" / "}
130
- <span title="Total values stored in memory">
131
- {formatNumber(state.totalValueCount)}📦
132
- </span>
133
- {" / "}
134
- <span title="Watchers">
135
- {formatNumber(state.proxyWatcherCount)}🕵🏻‍♀️
136
- </span>
137
- </div>
138
- </div>
139
- </div>;
140
- }
141
- }
@@ -1,110 +0,0 @@
1
- import { qreact } from "../4-dom/qreact";
2
- import { css } from "../4-dom/css";
3
- import { sort } from "socket-function/src/misc";
4
- import { proxyWatcher } from "../2-proxy/PathValueProxyWatcher";
5
- import { pathWatcher } from "../0-path-value-core/PathWatcher";
6
- import { PathRouter } from "../0-path-value-core/PathRouter";
7
- import { QuerysubController, querysubNodeId } from "../4-querysub/QuerysubController";
8
- import { getSyncedController } from "../library-components/SyncedController";
9
- import { SocketFunction } from "socket-function/SocketFunction";
10
- import { isCurrentUserSuperUser } from "../user-implementation/userData";
11
- import { delay } from "socket-function/src/batching";
12
- import { remoteWatcher } from "../1-path-client/RemoteWatcher";
13
-
14
- module.hotreload = true;
15
-
16
- let querysubController = getSyncedController(QuerysubController);
17
-
18
- let pathNodeCache = new Map<string, string | "missing">();
19
- let pendingPaths = new Set<string>();
20
- let flushScheduled = false;
21
-
22
- function scheduleFlush() {
23
- if (flushScheduled) return;
24
- flushScheduled = true;
25
- let promise = Promise.resolve().then(async () => {
26
- flushScheduled = false;
27
- if (pendingPaths.size === 0) return;
28
- let paths = Array.from(pendingPaths);
29
- pendingPaths.clear();
30
- let nodeId = await querysubNodeId();
31
- await remoteWatcher.flushWatchers();
32
- if (!nodeId) return;
33
- let result = await QuerysubController.nodes[nodeId].debugGetPathNodeIds(paths);
34
- for (let path of paths) {
35
- pathNodeCache.set(path, result.get(path) ?? "missing");
36
- }
37
- });
38
- proxyWatcher.triggerOnPromiseFinish(promise, { waitReason: "pathDistribution" });
39
- }
40
-
41
- function getNodeForPath(path: string): string | "missing" | undefined {
42
- let cached = pathNodeCache.get(path);
43
- if (cached !== undefined) return cached;
44
- pendingPaths.add(path);
45
- scheduleFlush();
46
- return undefined;
47
- }
48
-
49
- export class PathDistributionInfo extends qreact.Component {
50
- renderBase() {
51
- if (!isCurrentUserSuperUser()) return undefined;
52
-
53
- let nodeSpecs = querysubController(SocketFunction.browserNodeId()).debugGetNodeSpecs();
54
-
55
- let paths = pathWatcher.getAllWatchedPaths().filter(p => !PathRouter.isLocalPath(p));
56
- let distribution = new Map<string, string[]>();
57
- let missingPaths: string[] = [];
58
- for (let path of paths) {
59
- let nodeId = getNodeForPath(path);
60
- if (nodeId === undefined) continue;
61
- if (nodeId === "missing") {
62
- missingPaths.push(path);
63
- continue;
64
- }
65
- let nodePaths = distribution.get(nodeId);
66
- if (!nodePaths) {
67
- nodePaths = [];
68
- distribution.set(nodeId, nodePaths);
69
- }
70
- nodePaths.push(path);
71
- }
72
-
73
- let entries = Array.from(distribution.entries());
74
- sort(entries, ([nodeId, p]) => nodeId);
75
- sort(entries, ([nodeId, p]) => (nodeSpecs?.get(nodeId)?.routeStart ?? 0));
76
-
77
- let titleParts = entries.map(([nodeId, p]) => {
78
- let spec = nodeSpecs?.get(nodeId);
79
- let range = spec ? ` (${spec.routeStart}-${spec.routeEnd})` : "";
80
- return `${nodeId}${range}: ${p.length}`;
81
- });
82
- if (missingPaths.length > 0) titleParts.push(`missing: ${missingPaths.length}`);
83
- let title = titleParts.join("\n");
84
-
85
- let parts: preact.ComponentChild[] = [];
86
- for (let i = 0; i < entries.length; i++) {
87
- let [nodeId, nodePaths] = entries[i];
88
- if (i > 0) parts.push(" | ");
89
- let spec = nodeSpecs?.get(nodeId);
90
- parts.push(<span className={css.button} onClick={() => console.log(`Paths for ${nodeId}:`, nodePaths, spec && { range: `${spec.routeStart}-${spec.routeEnd}` })}>{nodePaths.length}</span>);
91
- }
92
- if (missingPaths.length > 0) {
93
- if (parts.length > 0) parts.push(" | ");
94
- parts.push(<span className={css.button.boldStyle.hslcolor(0, 100, 60)} onClick={() => console.log("Missing paths:", missingPaths)}>{missingPaths.length}</span>);
95
- }
96
-
97
- return <div className={css.hbox(4)} title={title}>
98
- <span>🌐</span>
99
- {parts.length > 0 && parts || "..."}
100
- </div>;
101
- }
102
- render() {
103
- try {
104
- return this.renderBase();
105
- } catch (error) {
106
- console.error("Error in rendering PathDistributionInfo:", error);
107
- return undefined;
108
- }
109
- }
110
- }
@@ -1,68 +0,0 @@
1
- import { qreact } from "../4-dom/qreact";
2
- import { css } from "../4-dom/css";
3
- import { Querysub, QuerysubController, querysubNodeId } from "../4-querysub/QuerysubController";
4
- import { isCurrentUserSuperUser } from "../user-implementation/userData";
5
- import { t } from "../2-proxy/schema2";
6
-
7
- module.hotreload = true;
8
-
9
- export class ValuePathWarning extends qreact.Component {
10
- state = t.state({
11
- count: t.number
12
- });
13
- componentDidMount() {
14
- Querysub.onCommitFinished(async () => {
15
- let nodeId = await querysubNodeId();
16
- if (!nodeId) return;
17
- let count = await QuerysubController.nodes[nodeId].debugGetValuePathCount();
18
- Querysub.localCommit(() => this.state.count = count);
19
- });
20
- }
21
- renderBase() {
22
- if (!isCurrentUserSuperUser()) return undefined;
23
- let count = this.state.count;
24
- if (!count) return undefined;
25
-
26
- if (count <= 150) {
27
- return <div className={css.hbox(4)}>
28
- <span>📄</span>
29
- <span>{count}</span>
30
- </div>;
31
- }
32
-
33
- let bgClass = "";
34
- let flashing = false;
35
- if (count > 500) {
36
- bgClass = css.hsl(0, 80, 60);
37
- flashing = true;
38
- } else if (count > 300) {
39
- bgClass = css.hsl(0, 80, 60);
40
- } else {
41
- bgClass = css.hsl(50, 100, 40);
42
- }
43
-
44
- let animClassName = "ValuePathWarning-flash";
45
- return <div className={css.hbox(4).pad2(6, 2) + " " + bgClass + (flashing ? " " + animClassName : "")}>
46
- <span>📄</span>
47
- <span>{count}</span>
48
- <span>warning: high number of files, join is likely not running</span>
49
- {flashing && <style>{`
50
- @keyframes ${animClassName}-anim {
51
- 0%, 100% { background-color: hsl(0, 80%, 60%); }
52
- 50% { background-color: hsl(0, 80%, 30%); }
53
- }
54
- .${animClassName} {
55
- animation: ${animClassName}-anim 0.6s infinite;
56
- }
57
- `}</style>}
58
- </div>;
59
- }
60
- render() {
61
- try {
62
- return this.renderBase();
63
- } catch (error) {
64
- console.error("Error in rendering ValuePathWarning:", error);
65
- return undefined;
66
- }
67
- }
68
- }