deepline 0.3.128 → 0.3.129
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/bundling-sources/sdk/src/client.ts +34 -9
- package/dist/bundling-sources/sdk/src/http.ts +22 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-data-plane/r2.ts +22 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +265 -2
- package/dist/bundling-sources/shared_libs/play-runtime/bounded-batch-function.ts +682 -0
- package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +21 -5
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +5390 -1732
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +107 -1
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-execution-relay.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +229 -7
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +25 -4
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-capacity.ts +71 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-drain.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-topology.ts +4 -1
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +694 -28
- package/dist/bundling-sources/shared_libs/play-runtime/egress-bandwidth-probe.ts +167 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execute-server-timing.ts +103 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-receipt-output-reference.ts +34 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-response-size.ts +27 -0
- package/dist/bundling-sources/shared_libs/play-runtime/gateway-postgres-admission.ts +66 -1
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +425 -71
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +210 -32
- package/dist/bundling-sources/shared_libs/play-runtime/governor/in-memory-rate-state-backend.ts +27 -13
- package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +19 -6
- package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +61 -2
- package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/contracts.ts +33 -1
- package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/job.ts +42 -1
- package/dist/bundling-sources/shared_libs/play-runtime/launch-source-policy.ts +11 -11
- package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +49 -16
- package/dist/bundling-sources/shared_libs/play-runtime/map-memory-limits.ts +1653 -98
- package/dist/bundling-sources/shared_libs/play-runtime/memory-ledger.ts +807 -0
- package/dist/bundling-sources/shared_libs/play-runtime/memory-pressure-signal.ts +138 -0
- package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +85 -3
- package/dist/bundling-sources/shared_libs/play-runtime/persistence-latch.ts +60 -2
- package/dist/bundling-sources/shared_libs/play-runtime/play-run-recovery-policy.ts +46 -1
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/contracts.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/job.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/ports.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/postgres-adapter.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-semantics.ts +70 -27
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +56 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/adaptive-provider-capacity-controller.ts +192 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/epoch-aimd-rate-controller.ts +194 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/fixture-capacity-simulator.ts +220 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/gradient2-concurrency-controller.ts +214 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-capacity-policy.ts +43 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-concurrency-policy.ts +30 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-rate-policy.ts +95 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-throttle-classifier.ts +40 -0
- package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +1535 -11
- package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +49 -2
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +6 -1
- package/dist/bundling-sources/shared_libs/play-runtime/run-log-buffer.ts +55 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/egress-probe.ts +221 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +339 -29
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-deadline.ts +28 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +192 -165
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +90 -5
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +281 -6
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +27 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +19 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal.ts +108 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runtime-sandbox-reconciliation.ts +40 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +11 -5
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +30 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api-compression.ts +276 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +723 -115
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-capacity-policy.ts +98 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +3 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-environment.ts +41 -4
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-flow.ts +448 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-map-row-feed.ts +215 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-operation-contract.ts +11 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-store-adapter.ts +161 -14
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-value.ts +184 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-writer.ts +607 -121
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sandbox-placement-policy.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-row-writer.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/contract.ts +15 -2
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/runner-backend-adapter.ts +71 -14
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-play-deploy-task.ts +2 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +288 -119
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +8 -6
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-worker-required-env.ts +28 -3
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/canonical-integration-operation-store.ts +179 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-heavy-request-admission.ts +80 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-progress-registry.ts +31 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-store-contract.ts +104 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-reference.ts +47 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-retirement.ts +193 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-storage.ts +420 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +41 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +134 -3
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +1657 -95
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +885 -73
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-completion-coalescer.ts +452 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +601 -42
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-terminal-transition.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +371 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/worker-resource-metrics.ts +86 -7
- package/dist/bundling-sources/shared_libs/play-runtime/secret-redaction.ts +153 -22
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/receipt-cohort.ts +188 -38
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +28 -13
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +44 -1
- package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +56 -4
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +2 -0
- package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +16 -10
- package/dist/bundling-sources/shared_libs/plays/dataset.ts +4 -1
- package/dist/cli/index.js +250 -38
- package/dist/cli/index.mjs +250 -38
- package/dist/{compiler-manifest-DfSnbCTW.d.mts → compiler-manifest-BIqRyj5m.d.mts} +1 -1
- package/dist/{compiler-manifest-DfSnbCTW.d.ts → compiler-manifest-BIqRyj5m.d.ts} +1 -1
- package/dist/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +231 -35
- package/dist/index.mjs +231 -35
- package/dist/install-integrity.json +31 -2
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/plays/bundle-play-file.mjs +65 -1
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explicit accounting of the bytes a Play Runtime process is retaining.
|
|
3
|
+
*
|
|
4
|
+
* Admission used to guess the per-row footprint by dividing observed heap
|
|
5
|
+
* growth by the rows in flight. That quotient is not a measurement: it read
|
|
6
|
+
* 0.9 MB/row while provider bodies were still on the wire and 48 MB/row while
|
|
7
|
+
* a drained map held uncollected garbage, and both readings were acted on. A
|
|
8
|
+
* ledger instead records what the runtime actually holds, at the site that
|
|
9
|
+
* holds it, with the serialized length that site already knows.
|
|
10
|
+
*
|
|
11
|
+
* The ledger is pure and dependency-free: it never probes the process, never
|
|
12
|
+
* schedules work, and never throws on anything but a caller passing a byte
|
|
13
|
+
* count that is not a byte count. The heap probe calibrates it; it does not
|
|
14
|
+
* replace the probe.
|
|
15
|
+
*
|
|
16
|
+
* Calibration is per category, not global. A ledger byte means a different
|
|
17
|
+
* number of live bytes depending on what holds it: `transportInFlight` is a JS
|
|
18
|
+
* string of exactly those bytes (a provider response OR a receipt-completion
|
|
19
|
+
* request serialized for the gateway), `providerBody` is the graph that a
|
|
20
|
+
* response string parsed into. One fitted factor across both is a weighted average of two
|
|
21
|
+
* regimes applied to whichever one is currently in front of it.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export const RUNTIME_MEMORY_LEDGER_CATEGORIES = [
|
|
25
|
+
/** A decoded provider response body, retained until its row is durable. */
|
|
26
|
+
'providerBody',
|
|
27
|
+
/** A deep-cloned tool input retained while the call waits for provider admission. */
|
|
28
|
+
'toolInputSnapshot',
|
|
29
|
+
/** Cell provenance and status retained for map rows that have not durably settled. */
|
|
30
|
+
'mapCellMetadata',
|
|
31
|
+
/** Receipt completion inputs buffered by the runtime receipt writer. */
|
|
32
|
+
'receiptCompletionBuffer',
|
|
33
|
+
/** Completed map rows retained in memory for the caller's result. */
|
|
34
|
+
'materializedRows',
|
|
35
|
+
/** A response body or completion request held as text while in transport. */
|
|
36
|
+
'transportInFlight',
|
|
37
|
+
/** Durable-store read-backs of outputs the completion response omitted. */
|
|
38
|
+
'hydrationReadBack',
|
|
39
|
+
/** Buffered live-run events awaiting egress. */
|
|
40
|
+
'eventBuffer',
|
|
41
|
+
] as const;
|
|
42
|
+
|
|
43
|
+
export type RuntimeMemoryLedgerCategory =
|
|
44
|
+
(typeof RUNTIME_MEMORY_LEDGER_CATEGORIES)[number];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Settled per-owner peaks kept for the percentile that sizes a row reserve.
|
|
48
|
+
*
|
|
49
|
+
* The sample is bounded, so past this many settled owners the ledger must
|
|
50
|
+
* choose which peaks to keep. It keeps a uniform sample of every owner the map
|
|
51
|
+
* has settled (see `settleOwner`), never the most recent ones: a map's rows do
|
|
52
|
+
* not settle in the order they were admitted, so "most recent" is a sample of
|
|
53
|
+
* whichever cohort happens to be slowest, not of the map.
|
|
54
|
+
*/
|
|
55
|
+
export const RUNTIME_MEMORY_LEDGER_SETTLED_RESERVOIR_SIZE = 256;
|
|
56
|
+
/**
|
|
57
|
+
* Ledger total below which a calibration sample means nothing: at a few
|
|
58
|
+
* hundred kilobytes the runtime's own baseline noise dominates the quotient.
|
|
59
|
+
*/
|
|
60
|
+
export const RUNTIME_MEMORY_LEDGER_CALIBRATION_FLOOR_BYTES = 8 * 1024 * 1024;
|
|
61
|
+
/**
|
|
62
|
+
* JSON bytes become 2-3x as V8 objects, so a runtime with no ledger at all
|
|
63
|
+
* assumes the middle of that range rather than parity. Inside the ledger every
|
|
64
|
+
* category carries its own prior (see
|
|
65
|
+
* `RUNTIME_MEMORY_LEDGER_CATEGORY_EXPANSION`); this constant is what lanes
|
|
66
|
+
* without a ledger snapshot fall back to.
|
|
67
|
+
*/
|
|
68
|
+
export const RUNTIME_MEMORY_LEDGER_INITIAL_FACTOR = 2.5;
|
|
69
|
+
/** A factor below 1 would claim V8 stores a payload smaller than its bytes. */
|
|
70
|
+
export const RUNTIME_MEMORY_LEDGER_MIN_FACTOR = 1;
|
|
71
|
+
/** Above 6 the reading is unaccounted retention, not object overhead. */
|
|
72
|
+
export const RUNTIME_MEMORY_LEDGER_MAX_FACTOR = 6;
|
|
73
|
+
export const RUNTIME_MEMORY_LEDGER_CALIBRATION_ALPHA = 0.2;
|
|
74
|
+
/**
|
|
75
|
+
* A parsed JSON graph: small string keys, boxed numbers, per-object headers.
|
|
76
|
+
* Measured between 3x and 5x its serialized length in V8.
|
|
77
|
+
*/
|
|
78
|
+
export const RUNTIME_MEMORY_LEDGER_OBJECT_PRIOR_FACTOR = 3;
|
|
79
|
+
/**
|
|
80
|
+
* A payload still held as text. A JS string of JSON is one byte per character
|
|
81
|
+
* while V8 keeps it one-byte, so its live size is its serialized size plus a
|
|
82
|
+
* header; it is not a 3x object graph and must never be priced as one.
|
|
83
|
+
*/
|
|
84
|
+
export const RUNTIME_MEMORY_LEDGER_RAW_PRIOR_FACTOR = 1.1;
|
|
85
|
+
/** A two-byte string of a one-byte payload is the worst honest text reading. */
|
|
86
|
+
export const RUNTIME_MEMORY_LEDGER_RAW_MAX_FACTOR = 2.5;
|
|
87
|
+
/**
|
|
88
|
+
* A category must own at least this share of the current prediction before a
|
|
89
|
+
* sample is allowed to move its factor. One heap reading is one equation in
|
|
90
|
+
* six unknowns: attributing it to a category that contributed 1% of the
|
|
91
|
+
* prediction is fitting noise, and it is how the transport regime ended up
|
|
92
|
+
* wearing the parsed-body regime's multiplier.
|
|
93
|
+
*/
|
|
94
|
+
export const RUNTIME_MEMORY_LEDGER_CATEGORY_SHARE_FLOOR = 0.05;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Per-category live-bytes-per-ledger-byte priors and the band a fitted factor
|
|
98
|
+
* may occupy.
|
|
99
|
+
*
|
|
100
|
+
* One factor cannot describe this set. `transportInFlight` is a JS string of
|
|
101
|
+
* exactly the bytes it registered (whether response or completion request);
|
|
102
|
+
* `providerBody` is the object graph that
|
|
103
|
+
* string parsed into. Fitting a single number across both reads whichever
|
|
104
|
+
* regime happened to dominate the probes and then applies it to the other: on
|
|
105
|
+
* the jai 1,000-row fixture the fit landed at 4.6-4.8 and predicted a 1,019 MB
|
|
106
|
+
* peak against a 376 MB collected observation, so map admission believed the
|
|
107
|
+
* heap was full at ~35% of the target and issued thousands of holds.
|
|
108
|
+
*/
|
|
109
|
+
export const RUNTIME_MEMORY_LEDGER_CATEGORY_EXPANSION: Record<
|
|
110
|
+
RuntimeMemoryLedgerCategory,
|
|
111
|
+
{ prior: number; min: number; max: number }
|
|
112
|
+
> = {
|
|
113
|
+
// The decoded graph. This is the regime the single factor was fitted on.
|
|
114
|
+
providerBody: {
|
|
115
|
+
prior: RUNTIME_MEMORY_LEDGER_OBJECT_PRIOR_FACTOR,
|
|
116
|
+
min: RUNTIME_MEMORY_LEDGER_MIN_FACTOR,
|
|
117
|
+
max: RUNTIME_MEMORY_LEDGER_MAX_FACTOR,
|
|
118
|
+
},
|
|
119
|
+
// `callToolExecutionAPI` deep-clones caller input before provider admission
|
|
120
|
+
// so retries observe stable bytes. That graph stays live while a paced call
|
|
121
|
+
// waits in the shared provider queue, which is the same object regime as a
|
|
122
|
+
// decoded provider body.
|
|
123
|
+
toolInputSnapshot: {
|
|
124
|
+
prior: RUNTIME_MEMORY_LEDGER_OBJECT_PRIOR_FACTOR,
|
|
125
|
+
min: RUNTIME_MEMORY_LEDGER_MIN_FACTOR,
|
|
126
|
+
max: RUNTIME_MEMORY_LEDGER_MAX_FACTOR,
|
|
127
|
+
},
|
|
128
|
+
// Active map cell metadata is a nested object graph (status, provenance,
|
|
129
|
+
// producer attempts). It is released as soon as its terminal row is
|
|
130
|
+
// durably persisted, rather than accumulating for the whole map.
|
|
131
|
+
mapCellMetadata: {
|
|
132
|
+
prior: RUNTIME_MEMORY_LEDGER_OBJECT_PRIOR_FACTOR,
|
|
133
|
+
min: RUNTIME_MEMORY_LEDGER_MIN_FACTOR,
|
|
134
|
+
max: RUNTIME_MEMORY_LEDGER_MAX_FACTOR,
|
|
135
|
+
},
|
|
136
|
+
// Rows the caller will read: decoded cell values, same regime as a body.
|
|
137
|
+
materializedRows: {
|
|
138
|
+
prior: RUNTIME_MEMORY_LEDGER_OBJECT_PRIOR_FACTOR,
|
|
139
|
+
min: RUNTIME_MEMORY_LEDGER_MIN_FACTOR,
|
|
140
|
+
max: RUNTIME_MEMORY_LEDGER_MAX_FACTOR,
|
|
141
|
+
},
|
|
142
|
+
// Live-run events buffered as objects until egress serializes them.
|
|
143
|
+
eventBuffer: {
|
|
144
|
+
prior: RUNTIME_MEMORY_LEDGER_OBJECT_PRIOR_FACTOR,
|
|
145
|
+
min: RUNTIME_MEMORY_LEDGER_MIN_FACTOR,
|
|
146
|
+
max: RUNTIME_MEMORY_LEDGER_MAX_FACTOR,
|
|
147
|
+
},
|
|
148
|
+
// The buffered receipt's output is, for a mapped tool call, the same graph
|
|
149
|
+
// already charged at object rate under `providerBody`; charging it again at
|
|
150
|
+
// object rate double-counts one retained object. What the buffer adds is the
|
|
151
|
+
// receipt envelope around it, so it starts at the text prior. The band stays
|
|
152
|
+
// open to 6 because a receipt the runtime built itself is a real graph.
|
|
153
|
+
receiptCompletionBuffer: {
|
|
154
|
+
prior: RUNTIME_MEMORY_LEDGER_RAW_PRIOR_FACTOR,
|
|
155
|
+
min: RUNTIME_MEMORY_LEDGER_MIN_FACTOR,
|
|
156
|
+
max: RUNTIME_MEMORY_LEDGER_MAX_FACTOR,
|
|
157
|
+
},
|
|
158
|
+
// A hydration turn reserves the wire size it is about to read and holds the
|
|
159
|
+
// response text across the gateway round trip, then expands it. It opens at
|
|
160
|
+
// the text prior and the fit carries it up if the expansion is what a turn
|
|
161
|
+
// actually holds.
|
|
162
|
+
hydrationReadBack: {
|
|
163
|
+
prior: RUNTIME_MEMORY_LEDGER_RAW_PRIOR_FACTOR,
|
|
164
|
+
min: RUNTIME_MEMORY_LEDGER_MIN_FACTOR,
|
|
165
|
+
max: RUNTIME_MEMORY_LEDGER_MAX_FACTOR,
|
|
166
|
+
},
|
|
167
|
+
// A response or receipt-completion request JSON string, and nothing but a
|
|
168
|
+
// string. It cannot honestly read above 2.5.
|
|
169
|
+
transportInFlight: {
|
|
170
|
+
prior: RUNTIME_MEMORY_LEDGER_RAW_PRIOR_FACTOR,
|
|
171
|
+
min: RUNTIME_MEMORY_LEDGER_MIN_FACTOR,
|
|
172
|
+
max: RUNTIME_MEMORY_LEDGER_RAW_MAX_FACTOR,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export type RuntimeMemoryLedgerHandle = {
|
|
177
|
+
/** Resize this entry in place; a gauge (a buffer, a growing cache). */
|
|
178
|
+
update(bytes: number): void;
|
|
179
|
+
/** Drop the entry. Idempotent: a double release is not a double subtract. */
|
|
180
|
+
release(): void;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export type RuntimeMemoryLedgerSnapshot = {
|
|
184
|
+
bytesByCategory: Record<RuntimeMemoryLedgerCategory, number>;
|
|
185
|
+
/** Per-category high-water marks. These are independent maxima. */
|
|
186
|
+
peakBytesByCategory: Record<RuntimeMemoryLedgerCategory, number>;
|
|
187
|
+
/** Registered bytes the runtime still holds. */
|
|
188
|
+
liveBytes: number;
|
|
189
|
+
livePeakBytes: number;
|
|
190
|
+
/** Registered, unreleased entries. */
|
|
191
|
+
liveEntries: number;
|
|
192
|
+
/** Owners registered and not yet settled. */
|
|
193
|
+
ownersActive: number;
|
|
194
|
+
/** Owners settled over the ledger's lifetime. */
|
|
195
|
+
ownersSettled: number;
|
|
196
|
+
/** Settled owner peaks currently in the reservoir. */
|
|
197
|
+
ownerPeakSamples: number;
|
|
198
|
+
ownerPeakBytesP50: number | null;
|
|
199
|
+
ownerPeakBytesP90: number | null;
|
|
200
|
+
/**
|
|
201
|
+
* Per-category p90s from the same uniform settled-owner reservoir as the
|
|
202
|
+
* aggregate owner peak. This identifies the representation that priced a
|
|
203
|
+
* row reserve after the live ledger has drained.
|
|
204
|
+
*/
|
|
205
|
+
ownerPeakBytesByCategoryP90: Record<
|
|
206
|
+
RuntimeMemoryLedgerCategory,
|
|
207
|
+
number | null
|
|
208
|
+
>;
|
|
209
|
+
/**
|
|
210
|
+
* The mix-weighted live bytes per ledger byte this ledger currently implies:
|
|
211
|
+
* `predictedLiveBytes / liveBytes`. It is a readout of the prediction, not
|
|
212
|
+
* the thing that is fitted — `categoryFactors` is. With nothing registered
|
|
213
|
+
* it reports the `providerBody` factor, the one that prices a row.
|
|
214
|
+
*/
|
|
215
|
+
calibrationFactor: number;
|
|
216
|
+
calibrationSamples: number;
|
|
217
|
+
/** Live bytes per ledger byte, fitted per category. */
|
|
218
|
+
categoryFactors: Record<RuntimeMemoryLedgerCategory, number>;
|
|
219
|
+
/** Samples that were allowed to move each category's factor. */
|
|
220
|
+
categoryCalibrationSamples: Record<RuntimeMemoryLedgerCategory, number>;
|
|
221
|
+
/** `bytesByCategory` x `categoryFactors`, the prediction's own breakdown. */
|
|
222
|
+
predictedBytesByCategory: Record<RuntimeMemoryLedgerCategory, number>;
|
|
223
|
+
/** Category composition at the instant the global predicted peak occurred. */
|
|
224
|
+
predictedPeakBytesByCategory: Record<RuntimeMemoryLedgerCategory, number>;
|
|
225
|
+
/** Sum over categories of bytes x factor: the live footprint predicted. */
|
|
226
|
+
predictedLiveBytes: number;
|
|
227
|
+
/** High-water mark of `predictedLiveBytes` over this ledger's life. */
|
|
228
|
+
predictedLivePeakBytes: number;
|
|
229
|
+
/** heapUsed + external at the last calibration, or null before the first. */
|
|
230
|
+
observedLiveBytes: number | null;
|
|
231
|
+
/** Largest collected observation so far: the peak a prediction is judged against. */
|
|
232
|
+
observedCollectedPeakBytes: number | null;
|
|
233
|
+
/**
|
|
234
|
+
* The prediction the runtime was holding at the instant `observedCollected
|
|
235
|
+
* PeakBytes` was read, before that reading was allowed to move any factor.
|
|
236
|
+
*
|
|
237
|
+
* This is the only honest partner for the observed peak. `livePeakBytes x
|
|
238
|
+
* calibrationFactor` multiplies a ledger peak by a factor fitted later,
|
|
239
|
+
* against an observation taken at a third moment; the three need not
|
|
240
|
+
* coincide and on the jai fixture they did not, which is most of a 2.5x
|
|
241
|
+
* "divergence" that was never a prediction error. Excludes the baseline, the
|
|
242
|
+
* same as `predictedLiveBytes`.
|
|
243
|
+
*/
|
|
244
|
+
predictedAtObservedPeakBytes: number | null;
|
|
245
|
+
/** Ledger bytes registered at that same instant, for the same reason. */
|
|
246
|
+
ledgerBytesAtObservedPeak: number | null;
|
|
247
|
+
/**
|
|
248
|
+
* The full p90 row reservations for rows already admitted at that same
|
|
249
|
+
* instant. These rows can be executing or waiting on a receipt before they
|
|
250
|
+
* have registered a body, so they are real retained-risk not ledger bytes.
|
|
251
|
+
*/
|
|
252
|
+
admittedRowReserveBytesAtObservedPeak: number | null;
|
|
253
|
+
baselineBytes: number | null;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export type RuntimeMemoryLedger = {
|
|
257
|
+
register(input: {
|
|
258
|
+
category: RuntimeMemoryLedgerCategory;
|
|
259
|
+
ownerId?: string | null;
|
|
260
|
+
bytes: number;
|
|
261
|
+
}): RuntimeMemoryLedgerHandle;
|
|
262
|
+
/**
|
|
263
|
+
* Record an owner's peak into the settled reservoir and drop every entry it
|
|
264
|
+
* still holds. Called when a row's record is durable: from that point the
|
|
265
|
+
* runtime is entitled to hold none of the row's bytes, and anything it does
|
|
266
|
+
* hold is a leak the audit test is meant to catch.
|
|
267
|
+
*/
|
|
268
|
+
settleOwner(ownerId: string): number | null;
|
|
269
|
+
/** Bytes currently registered to an owner, or null if it is not tracked. */
|
|
270
|
+
ownerBytes(ownerId: string): number | null;
|
|
271
|
+
/**
|
|
272
|
+
* Fit the ledger to the process. `observedLiveBytes` is heapUsed + external
|
|
273
|
+
* from the same probe that paces admission; `baselineBytes` is the reading
|
|
274
|
+
* taken before the first row was admitted.
|
|
275
|
+
*/
|
|
276
|
+
calibrate(input: {
|
|
277
|
+
observedLiveBytes: number;
|
|
278
|
+
baselineBytes: number | null;
|
|
279
|
+
/**
|
|
280
|
+
* Full live-byte reservations for rows already admitted but not yet
|
|
281
|
+
* necessarily represented in the ledger. This is an input from map
|
|
282
|
+
* admission, not a new ledger category, because it is a conservative
|
|
283
|
+
* promise about in-flight work rather than a retained object.
|
|
284
|
+
*/
|
|
285
|
+
admittedRowReserveBytes?: number;
|
|
286
|
+
}): void;
|
|
287
|
+
snapshot(): RuntimeMemoryLedgerSnapshot;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
type LedgerEntry = {
|
|
291
|
+
category: RuntimeMemoryLedgerCategory;
|
|
292
|
+
ownerId: string | null;
|
|
293
|
+
bytes: number;
|
|
294
|
+
released: boolean;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
type OwnerState = {
|
|
298
|
+
bytes: number;
|
|
299
|
+
peakBytes: number;
|
|
300
|
+
bytesByCategory: Record<RuntimeMemoryLedgerCategory, number>;
|
|
301
|
+
peakBytesByCategory: Record<RuntimeMemoryLedgerCategory, number>;
|
|
302
|
+
entries: Set<LedgerEntry>;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
type SettledOwnerPeak = {
|
|
306
|
+
bytes: number;
|
|
307
|
+
bytesByCategory: Record<RuntimeMemoryLedgerCategory, number>;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
export function createRuntimeMemoryLedger(options?: {
|
|
311
|
+
settledReservoirSize?: number;
|
|
312
|
+
calibrationFloorBytes?: number;
|
|
313
|
+
calibrationAlpha?: number;
|
|
314
|
+
/** Overrides every category prior; see the note where it is read. */
|
|
315
|
+
initialFactor?: number;
|
|
316
|
+
/** Overrides every category's lower band. */
|
|
317
|
+
minFactor?: number;
|
|
318
|
+
/** Overrides every category's upper band. */
|
|
319
|
+
maxFactor?: number;
|
|
320
|
+
/** Minimum share of the prediction a category needs to learn from a probe. */
|
|
321
|
+
categoryShareFloor?: number;
|
|
322
|
+
/**
|
|
323
|
+
* Uniform [0, 1) source for the settled-peak reservoir. Injected only so a
|
|
324
|
+
* test can state an exact eviction sequence; the runtime uses `Math.random`.
|
|
325
|
+
*/
|
|
326
|
+
random?: () => number;
|
|
327
|
+
}): RuntimeMemoryLedger {
|
|
328
|
+
const random = typeof options?.random === 'function' ? options.random : null;
|
|
329
|
+
const uniform = (): number => {
|
|
330
|
+
const value = random ? random() : Math.random();
|
|
331
|
+
return Number.isFinite(value)
|
|
332
|
+
? Math.min(0.999999999, Math.max(0, value))
|
|
333
|
+
: 0;
|
|
334
|
+
};
|
|
335
|
+
const reservoirSize = positiveInteger(
|
|
336
|
+
options?.settledReservoirSize,
|
|
337
|
+
RUNTIME_MEMORY_LEDGER_SETTLED_RESERVOIR_SIZE,
|
|
338
|
+
);
|
|
339
|
+
const calibrationFloorBytes = positiveInteger(
|
|
340
|
+
options?.calibrationFloorBytes,
|
|
341
|
+
RUNTIME_MEMORY_LEDGER_CALIBRATION_FLOOR_BYTES,
|
|
342
|
+
);
|
|
343
|
+
const alpha = finiteInRange(
|
|
344
|
+
options?.calibrationAlpha,
|
|
345
|
+
RUNTIME_MEMORY_LEDGER_CALIBRATION_ALPHA,
|
|
346
|
+
0,
|
|
347
|
+
1,
|
|
348
|
+
);
|
|
349
|
+
const minFactor = positiveFinite(
|
|
350
|
+
options?.minFactor,
|
|
351
|
+
RUNTIME_MEMORY_LEDGER_MIN_FACTOR,
|
|
352
|
+
);
|
|
353
|
+
const maxFactor = Math.max(
|
|
354
|
+
minFactor,
|
|
355
|
+
positiveFinite(options?.maxFactor, RUNTIME_MEMORY_LEDGER_MAX_FACTOR),
|
|
356
|
+
);
|
|
357
|
+
const categoryShareFloor = finiteInRange(
|
|
358
|
+
options?.categoryShareFloor,
|
|
359
|
+
RUNTIME_MEMORY_LEDGER_CATEGORY_SHARE_FLOOR,
|
|
360
|
+
0,
|
|
361
|
+
1,
|
|
362
|
+
);
|
|
363
|
+
const bytesByCategory = emptyCategoryTotals();
|
|
364
|
+
const peakBytesByCategory = emptyCategoryTotals();
|
|
365
|
+
const owners = new Map<string, OwnerState>();
|
|
366
|
+
const settledPeaks: SettledOwnerPeak[] = [];
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Keep a uniform sample of settled row peaks.
|
|
370
|
+
*
|
|
371
|
+
* Vitter's Algorithm R: the n-th settled owner takes a slot with probability
|
|
372
|
+
* reservoirSize/n, so the sample stays uniform over every row the map has
|
|
373
|
+
* settled.
|
|
374
|
+
*
|
|
375
|
+
* A ring buffer here kept the last `reservoirSize` peaks instead, and rows do
|
|
376
|
+
* not settle in the order they were admitted. Jai's 1,000-row fixture opens
|
|
377
|
+
* all of its rows at once and its three cohorts settle at wildly different
|
|
378
|
+
* speeds — person search averaged ~129 s per call against ~20 s for the agent
|
|
379
|
+
* cohort — so the last 256 rows to settle were all person rows, whose bodies
|
|
380
|
+
* are ~920 B, while the ~1 MB agent rows had all aged out. The p90 that
|
|
381
|
+
* prices admission read 920 B, and the same map at 100 rows (reservoir never
|
|
382
|
+
* full) read 992 KB from the same shape. A recency-biased sample is not a
|
|
383
|
+
* sample of the map.
|
|
384
|
+
*/
|
|
385
|
+
/**
|
|
386
|
+
* Percentiles over `settledPeaks`, computed on demand.
|
|
387
|
+
*
|
|
388
|
+
* `snapshot()` reports nine of them - p50, p90, and a p90 per category - and
|
|
389
|
+
* each one used to copy the reservoir and sort it. Map row admission
|
|
390
|
+
* reconciles once per settled row and snapshots twice per reconcile, so a
|
|
391
|
+
* million-row map paid roughly eighteen million sorts of a 256-element
|
|
392
|
+
* array, which was the whole of its throughput regression. The reservoir
|
|
393
|
+
* changes only in `recordSettledPeak`, so the answer is stable between
|
|
394
|
+
* writes and is cached until one lands.
|
|
395
|
+
*/
|
|
396
|
+
let settledPeakPercentiles: {
|
|
397
|
+
p50: number | null;
|
|
398
|
+
p90: number | null;
|
|
399
|
+
byCategory: Record<RuntimeMemoryLedgerCategory, number | null>;
|
|
400
|
+
} | null = null;
|
|
401
|
+
|
|
402
|
+
function settledPeakPercentileView(): {
|
|
403
|
+
ownerPeakBytesP50: number | null;
|
|
404
|
+
ownerPeakBytesP90: number | null;
|
|
405
|
+
ownerPeakBytesByCategoryP90: Record<
|
|
406
|
+
RuntimeMemoryLedgerCategory,
|
|
407
|
+
number | null
|
|
408
|
+
>;
|
|
409
|
+
} {
|
|
410
|
+
if (!settledPeakPercentiles) {
|
|
411
|
+
const bytes = settledPeaks.map((peak) => peak.bytes);
|
|
412
|
+
settledPeakPercentiles = {
|
|
413
|
+
p50: percentile(bytes, 0.5),
|
|
414
|
+
p90: percentile(bytes, 0.9),
|
|
415
|
+
byCategory: Object.fromEntries(
|
|
416
|
+
RUNTIME_MEMORY_LEDGER_CATEGORIES.map((category) => [
|
|
417
|
+
category,
|
|
418
|
+
percentile(
|
|
419
|
+
settledPeaks.map((peak) => peak.bytesByCategory[category]),
|
|
420
|
+
0.9,
|
|
421
|
+
),
|
|
422
|
+
]),
|
|
423
|
+
) as Record<RuntimeMemoryLedgerCategory, number | null>,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
ownerPeakBytesP50: settledPeakPercentiles.p50,
|
|
428
|
+
ownerPeakBytesP90: settledPeakPercentiles.p90,
|
|
429
|
+
// Copied so a caller cannot mutate the cache it shares with the ledger.
|
|
430
|
+
ownerPeakBytesByCategoryP90: { ...settledPeakPercentiles.byCategory },
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function recordSettledPeak(settledPeak: SettledOwnerPeak): void {
|
|
435
|
+
settledPeakPercentiles = null;
|
|
436
|
+
if (settledPeaks.length < reservoirSize) {
|
|
437
|
+
settledPeaks.push(settledPeak);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const slot = Math.floor(uniform() * ownersSettled);
|
|
441
|
+
if (slot < reservoirSize) settledPeaks[slot] = settledPeak;
|
|
442
|
+
}
|
|
443
|
+
let liveBytes = 0;
|
|
444
|
+
let livePeakBytes = 0;
|
|
445
|
+
let liveEntries = 0;
|
|
446
|
+
let ownersSettled = 0;
|
|
447
|
+
// An explicit `initialFactor` overrides every category prior: a caller that
|
|
448
|
+
// states one is stating the whole model, and silently keeping six different
|
|
449
|
+
// priors underneath it would make the option a lie.
|
|
450
|
+
const initialFactorOverride =
|
|
451
|
+
options?.initialFactor === undefined ||
|
|
452
|
+
!Number.isFinite(options.initialFactor) ||
|
|
453
|
+
options.initialFactor <= 0
|
|
454
|
+
? null
|
|
455
|
+
: options.initialFactor;
|
|
456
|
+
const categoryBand = (
|
|
457
|
+
category: RuntimeMemoryLedgerCategory,
|
|
458
|
+
): { min: number; max: number } => {
|
|
459
|
+
const band = RUNTIME_MEMORY_LEDGER_CATEGORY_EXPANSION[category];
|
|
460
|
+
const min = options?.minFactor === undefined ? band.min : minFactor;
|
|
461
|
+
const max = Math.max(
|
|
462
|
+
min,
|
|
463
|
+
options?.maxFactor === undefined ? band.max : maxFactor,
|
|
464
|
+
);
|
|
465
|
+
return { min, max };
|
|
466
|
+
};
|
|
467
|
+
const categoryFactors = {} as Record<RuntimeMemoryLedgerCategory, number>;
|
|
468
|
+
const categoryCalibrationSamples = emptyCategoryTotals();
|
|
469
|
+
for (const category of RUNTIME_MEMORY_LEDGER_CATEGORIES) {
|
|
470
|
+
const band = categoryBand(category);
|
|
471
|
+
categoryFactors[category] = clamp(
|
|
472
|
+
initialFactorOverride ??
|
|
473
|
+
RUNTIME_MEMORY_LEDGER_CATEGORY_EXPANSION[category].prior,
|
|
474
|
+
band.min,
|
|
475
|
+
band.max,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
let predictedLivePeakBytes = 0;
|
|
479
|
+
const predictedPeakBytesByCategory = emptyCategoryTotals();
|
|
480
|
+
let calibrationSamples = 0;
|
|
481
|
+
let observedLiveBytes: number | null = null;
|
|
482
|
+
let observedCollectedPeakBytes: number | null = null;
|
|
483
|
+
let predictedAtObservedPeakBytes: number | null = null;
|
|
484
|
+
let ledgerBytesAtObservedPeak: number | null = null;
|
|
485
|
+
let admittedRowReserveBytesAtObservedPeak: number | null = null;
|
|
486
|
+
let baselineBytes: number | null = null;
|
|
487
|
+
|
|
488
|
+
/** Sum over categories of registered bytes times that category's factor. */
|
|
489
|
+
const predictedLiveBytesNow = (): number => {
|
|
490
|
+
let total = 0;
|
|
491
|
+
for (const category of RUNTIME_MEMORY_LEDGER_CATEGORIES) {
|
|
492
|
+
total += bytesByCategory[category] * categoryFactors[category];
|
|
493
|
+
}
|
|
494
|
+
return total;
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const recordPredictedPeak = (): void => {
|
|
498
|
+
const predicted = predictedLiveBytesNow();
|
|
499
|
+
if (predicted < predictedLivePeakBytes) return;
|
|
500
|
+
predictedLivePeakBytes = predicted;
|
|
501
|
+
for (const category of RUNTIME_MEMORY_LEDGER_CATEGORIES) {
|
|
502
|
+
predictedPeakBytesByCategory[category] = Math.round(
|
|
503
|
+
bytesByCategory[category] * categoryFactors[category],
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const applyDelta = (entry: LedgerEntry, deltaBytes: number): void => {
|
|
509
|
+
if (deltaBytes === 0) return;
|
|
510
|
+
bytesByCategory[entry.category] = Math.max(
|
|
511
|
+
0,
|
|
512
|
+
bytesByCategory[entry.category] + deltaBytes,
|
|
513
|
+
);
|
|
514
|
+
peakBytesByCategory[entry.category] = Math.max(
|
|
515
|
+
peakBytesByCategory[entry.category],
|
|
516
|
+
bytesByCategory[entry.category],
|
|
517
|
+
);
|
|
518
|
+
liveBytes = Math.max(0, liveBytes + deltaBytes);
|
|
519
|
+
livePeakBytes = Math.max(livePeakBytes, liveBytes);
|
|
520
|
+
recordPredictedPeak();
|
|
521
|
+
if (entry.ownerId === null) return;
|
|
522
|
+
const owner = owners.get(entry.ownerId);
|
|
523
|
+
if (!owner) return;
|
|
524
|
+
owner.bytes = Math.max(0, owner.bytes + deltaBytes);
|
|
525
|
+
owner.peakBytes = Math.max(owner.peakBytes, owner.bytes);
|
|
526
|
+
owner.bytesByCategory[entry.category] = Math.max(
|
|
527
|
+
0,
|
|
528
|
+
owner.bytesByCategory[entry.category] + deltaBytes,
|
|
529
|
+
);
|
|
530
|
+
owner.peakBytesByCategory[entry.category] = Math.max(
|
|
531
|
+
owner.peakBytesByCategory[entry.category],
|
|
532
|
+
owner.bytesByCategory[entry.category],
|
|
533
|
+
);
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const releaseEntry = (entry: LedgerEntry): void => {
|
|
537
|
+
if (entry.released) return;
|
|
538
|
+
entry.released = true;
|
|
539
|
+
liveEntries = Math.max(0, liveEntries - 1);
|
|
540
|
+
applyDelta(entry, -entry.bytes);
|
|
541
|
+
entry.bytes = 0;
|
|
542
|
+
if (entry.ownerId !== null) {
|
|
543
|
+
owners.get(entry.ownerId)?.entries.delete(entry);
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
register({ category, ownerId, bytes }) {
|
|
549
|
+
const normalizedBytes = requireByteCount(bytes);
|
|
550
|
+
if (!RUNTIME_MEMORY_LEDGER_CATEGORIES.includes(category)) {
|
|
551
|
+
throw new RangeError(
|
|
552
|
+
`Runtime memory ledger received an unknown category "${String(category)}".`,
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
const normalizedOwnerId =
|
|
556
|
+
typeof ownerId === 'string' && ownerId.length > 0 ? ownerId : null;
|
|
557
|
+
const entry: LedgerEntry = {
|
|
558
|
+
category,
|
|
559
|
+
ownerId: normalizedOwnerId,
|
|
560
|
+
bytes: 0,
|
|
561
|
+
released: false,
|
|
562
|
+
};
|
|
563
|
+
liveEntries += 1;
|
|
564
|
+
if (normalizedOwnerId !== null) {
|
|
565
|
+
let owner = owners.get(normalizedOwnerId);
|
|
566
|
+
if (!owner) {
|
|
567
|
+
owner = {
|
|
568
|
+
bytes: 0,
|
|
569
|
+
peakBytes: 0,
|
|
570
|
+
bytesByCategory: emptyCategoryTotals(),
|
|
571
|
+
peakBytesByCategory: emptyCategoryTotals(),
|
|
572
|
+
entries: new Set(),
|
|
573
|
+
};
|
|
574
|
+
owners.set(normalizedOwnerId, owner);
|
|
575
|
+
}
|
|
576
|
+
owner.entries.add(entry);
|
|
577
|
+
}
|
|
578
|
+
entry.bytes = normalizedBytes;
|
|
579
|
+
applyDelta(entry, normalizedBytes);
|
|
580
|
+
return {
|
|
581
|
+
update(nextBytes) {
|
|
582
|
+
if (entry.released) return;
|
|
583
|
+
const normalizedNext = requireByteCount(nextBytes);
|
|
584
|
+
const delta = normalizedNext - entry.bytes;
|
|
585
|
+
entry.bytes = normalizedNext;
|
|
586
|
+
applyDelta(entry, delta);
|
|
587
|
+
},
|
|
588
|
+
release() {
|
|
589
|
+
releaseEntry(entry);
|
|
590
|
+
},
|
|
591
|
+
};
|
|
592
|
+
},
|
|
593
|
+
|
|
594
|
+
settleOwner(ownerId) {
|
|
595
|
+
const owner = owners.get(ownerId);
|
|
596
|
+
if (!owner) {
|
|
597
|
+
// A row settles here whether or not it ever registered tracked bytes.
|
|
598
|
+
// An owner is created lazily on the first tracked allocation, so a
|
|
599
|
+
// stage that performs only local work — no provider bodies — has none.
|
|
600
|
+
// Recording nothing leaves that stage with zero footprint samples, and
|
|
601
|
+
// an unmeasured reserve falls back to the conservative default. A map
|
|
602
|
+
// that provably holds nothing per row is then priced as if it held
|
|
603
|
+
// megabytes, and its admission concurrency is throttled for memory it
|
|
604
|
+
// never uses. "This row held nothing" is a measurement, not missing
|
|
605
|
+
// data, so record it as a zero-byte peak.
|
|
606
|
+
ownersSettled += 1;
|
|
607
|
+
recordSettledPeak({ bytes: 0, bytesByCategory: emptyCategoryTotals() });
|
|
608
|
+
// The return value still reports "no owner was tracked", which is what
|
|
609
|
+
// callers branch on; only the sample is new.
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
owners.delete(ownerId);
|
|
613
|
+
for (const entry of [...owner.entries]) {
|
|
614
|
+
releaseEntry(entry);
|
|
615
|
+
}
|
|
616
|
+
ownersSettled += 1;
|
|
617
|
+
recordSettledPeak({
|
|
618
|
+
bytes: owner.peakBytes,
|
|
619
|
+
bytesByCategory: { ...owner.peakBytesByCategory },
|
|
620
|
+
});
|
|
621
|
+
return owner.peakBytes;
|
|
622
|
+
},
|
|
623
|
+
|
|
624
|
+
ownerBytes(ownerId) {
|
|
625
|
+
return owners.get(ownerId)?.bytes ?? null;
|
|
626
|
+
},
|
|
627
|
+
|
|
628
|
+
calibrate({
|
|
629
|
+
observedLiveBytes: observed,
|
|
630
|
+
baselineBytes: baseline,
|
|
631
|
+
admittedRowReserveBytes: admittedRowReserve = 0,
|
|
632
|
+
}) {
|
|
633
|
+
// The prediction admission was acting on at this probe, judged before
|
|
634
|
+
// this probe is allowed to change it. Learning first and then scoring
|
|
635
|
+
// would grade the fit against itself.
|
|
636
|
+
const predictedBefore = predictedLiveBytesNow();
|
|
637
|
+
const reservedBytes = requireByteCount(admittedRowReserve);
|
|
638
|
+
if (Number.isFinite(observed) && observed >= 0) {
|
|
639
|
+
observedLiveBytes = observed;
|
|
640
|
+
if (observed > (observedCollectedPeakBytes ?? -1)) {
|
|
641
|
+
observedCollectedPeakBytes = observed;
|
|
642
|
+
// An admission reserve bounds what an active row may materialize;
|
|
643
|
+
// it is not itself a retained object. Keep it beside this probe so
|
|
644
|
+
// readouts can explain an unaccounted gap, but judge the ledger's
|
|
645
|
+
// prediction only on bytes its owners actually registered.
|
|
646
|
+
predictedAtObservedPeakBytes = Math.round(predictedBefore);
|
|
647
|
+
ledgerBytesAtObservedPeak = liveBytes;
|
|
648
|
+
admittedRowReserveBytesAtObservedPeak = reservedBytes;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (baseline != null && Number.isFinite(baseline) && baseline >= 0) {
|
|
652
|
+
baselineBytes = baseline;
|
|
653
|
+
}
|
|
654
|
+
if (observedLiveBytes === null || baselineBytes === null) return;
|
|
655
|
+
if (liveBytes < calibrationFloorBytes) return;
|
|
656
|
+
// A row reserve is a conservative admission promise, not evidence that
|
|
657
|
+
// a category is retaining fewer bytes. Fitting the ledger after
|
|
658
|
+
// subtracting it taught providerBody to ignore an actually observed
|
|
659
|
+
// body; recording the reserve above gives diagnostics that distinction
|
|
660
|
+
// without smuggling it into the retained-byte model.
|
|
661
|
+
const attributable = observedLiveBytes - baselineBytes;
|
|
662
|
+
if (!(attributable > 0)) return;
|
|
663
|
+
if (!(predictedBefore > 0)) return;
|
|
664
|
+
calibrationSamples += 1;
|
|
665
|
+
// One heap reading is one equation in as many unknowns as there are
|
|
666
|
+
// categories holding bytes, so it is not solved — it is descended.
|
|
667
|
+
// Each category is nudged toward the reading in proportion to how much
|
|
668
|
+
// of the prediction it was responsible for, which is exactly the
|
|
669
|
+
// gradient of the squared error in log-factor space. A probe taken while
|
|
670
|
+
// response text dominates therefore moves `transportInFlight` and barely
|
|
671
|
+
// touches `providerBody`, and a probe taken while parsed bodies dominate
|
|
672
|
+
// does the reverse. Under a mix that never varies the factors keep their
|
|
673
|
+
// priors' ratio, which is the correct answer to a question the data
|
|
674
|
+
// cannot distinguish.
|
|
675
|
+
const ratio = attributable / predictedBefore;
|
|
676
|
+
for (const category of RUNTIME_MEMORY_LEDGER_CATEGORIES) {
|
|
677
|
+
const bytes = bytesByCategory[category];
|
|
678
|
+
if (bytes <= 0) continue;
|
|
679
|
+
const share = (bytes * categoryFactors[category]) / predictedBefore;
|
|
680
|
+
if (share < categoryShareFloor) continue;
|
|
681
|
+
categoryCalibrationSamples[category] += 1;
|
|
682
|
+
const band = categoryBand(category);
|
|
683
|
+
categoryFactors[category] = clamp(
|
|
684
|
+
categoryFactors[category] * (1 + alpha * share * (ratio - 1)),
|
|
685
|
+
band.min,
|
|
686
|
+
band.max,
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
recordPredictedPeak();
|
|
690
|
+
},
|
|
691
|
+
|
|
692
|
+
snapshot() {
|
|
693
|
+
const predictedBytesByCategory = emptyCategoryTotals();
|
|
694
|
+
for (const category of RUNTIME_MEMORY_LEDGER_CATEGORIES) {
|
|
695
|
+
predictedBytesByCategory[category] = Math.round(
|
|
696
|
+
bytesByCategory[category] * categoryFactors[category],
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
const predictedLiveBytes = predictedLiveBytesNow();
|
|
700
|
+
return {
|
|
701
|
+
bytesByCategory: { ...bytesByCategory },
|
|
702
|
+
peakBytesByCategory: { ...peakBytesByCategory },
|
|
703
|
+
liveBytes,
|
|
704
|
+
livePeakBytes,
|
|
705
|
+
liveEntries,
|
|
706
|
+
ownersActive: owners.size,
|
|
707
|
+
ownersSettled,
|
|
708
|
+
ownerPeakSamples: settledPeaks.length,
|
|
709
|
+
...settledPeakPercentileView(),
|
|
710
|
+
calibrationFactor:
|
|
711
|
+
liveBytes > 0
|
|
712
|
+
? predictedLiveBytes / liveBytes
|
|
713
|
+
: categoryFactors.providerBody,
|
|
714
|
+
calibrationSamples,
|
|
715
|
+
categoryFactors: { ...categoryFactors },
|
|
716
|
+
categoryCalibrationSamples: { ...categoryCalibrationSamples },
|
|
717
|
+
predictedBytesByCategory,
|
|
718
|
+
predictedPeakBytesByCategory: { ...predictedPeakBytesByCategory },
|
|
719
|
+
predictedLiveBytes: Math.round(predictedLiveBytes),
|
|
720
|
+
predictedLivePeakBytes: Math.round(predictedLivePeakBytes),
|
|
721
|
+
observedLiveBytes,
|
|
722
|
+
observedCollectedPeakBytes,
|
|
723
|
+
predictedAtObservedPeakBytes,
|
|
724
|
+
ledgerBytesAtObservedPeak,
|
|
725
|
+
admittedRowReserveBytesAtObservedPeak,
|
|
726
|
+
baselineBytes,
|
|
727
|
+
};
|
|
728
|
+
},
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const ledgerTextEncoder =
|
|
733
|
+
typeof TextEncoder === 'undefined' ? null : new TextEncoder();
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Serialized size of a value, in the unit every ledger entry is stated in.
|
|
737
|
+
*
|
|
738
|
+
* It lives here rather than beside map admission because every site that
|
|
739
|
+
* registers bytes needs it, including receipt hydration, which must not pull
|
|
740
|
+
* `node:v8` or `node:perf_hooks` into its module graph to ask how big a
|
|
741
|
+
* payload is.
|
|
742
|
+
*/
|
|
743
|
+
export function runtimeJsonByteLength(value: unknown): number {
|
|
744
|
+
const serialized = JSON.stringify(value);
|
|
745
|
+
if (serialized === undefined) return 0;
|
|
746
|
+
return ledgerTextEncoder?.encode(serialized).byteLength ?? serialized.length;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** Nearest-rank percentile. A reservoir of settled peaks is never large. */
|
|
750
|
+
export function percentile(
|
|
751
|
+
values: readonly number[],
|
|
752
|
+
fraction: number,
|
|
753
|
+
): number | null {
|
|
754
|
+
if (values.length === 0) return null;
|
|
755
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
756
|
+
const rank = Math.ceil(fraction * sorted.length);
|
|
757
|
+
const index = Math.min(sorted.length - 1, Math.max(0, rank - 1));
|
|
758
|
+
return sorted[index] ?? null;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function emptyCategoryTotals(): Record<RuntimeMemoryLedgerCategory, number> {
|
|
762
|
+
const totals = {} as Record<RuntimeMemoryLedgerCategory, number>;
|
|
763
|
+
for (const category of RUNTIME_MEMORY_LEDGER_CATEGORIES) {
|
|
764
|
+
totals[category] = 0;
|
|
765
|
+
}
|
|
766
|
+
return totals;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* A byte count that is not a finite, non-negative number is a defect at the
|
|
771
|
+
* call site, not a value to be coerced: silently rounding it to zero is how a
|
|
772
|
+
* ledger stops accounting for the bytes that kill the sandbox.
|
|
773
|
+
*/
|
|
774
|
+
function requireByteCount(bytes: number): number {
|
|
775
|
+
if (typeof bytes !== 'number' || !Number.isFinite(bytes) || bytes < 0) {
|
|
776
|
+
throw new RangeError(
|
|
777
|
+
`Runtime memory ledger requires a finite, non-negative byte count; received ${String(bytes)}.`,
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
return Math.ceil(bytes);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function clamp(value: number, min: number, max: number): number {
|
|
784
|
+
return Math.min(max, Math.max(min, value));
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function positiveInteger(value: number | undefined, fallback: number): number {
|
|
788
|
+
if (value === undefined || !Number.isFinite(value)) return fallback;
|
|
789
|
+
return Math.max(1, Math.floor(value));
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function positiveFinite(value: number | undefined, fallback: number): number {
|
|
793
|
+
if (value === undefined || !Number.isFinite(value) || value <= 0) {
|
|
794
|
+
return fallback;
|
|
795
|
+
}
|
|
796
|
+
return value;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function finiteInRange(
|
|
800
|
+
value: number | undefined,
|
|
801
|
+
fallback: number,
|
|
802
|
+
min: number,
|
|
803
|
+
max: number,
|
|
804
|
+
): number {
|
|
805
|
+
if (value === undefined || !Number.isFinite(value)) return fallback;
|
|
806
|
+
return clamp(value, min, max);
|
|
807
|
+
}
|