@nimbus-sh/core 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_shared/cache-stats.d.ts +23 -1
- package/dist/_shared/cache-stats.d.ts.map +1 -1
- package/dist/_shared/cache-stats.js +13 -1
- package/dist/_shared/exports-resolver.d.ts +1 -1
- package/dist/_shared/exports-resolver.js +1 -1
- package/dist/_shared/retry.js +1 -1
- package/dist/constants.d.ts +5 -11
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +16 -56
- package/dist/runtime/assets-loader.js +1 -1
- package/dist/runtime/virtual-socket-kernel.generated.d.ts +1 -1
- package/dist/runtime/virtual-socket-kernel.generated.js +1 -1
- package/dist/vfs/seed-project.d.ts.map +1 -1
- package/dist/vfs/seed-project.js +1 -1
- package/dist/vfs/sqlite-vfs.d.ts +2 -30
- package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
- package/dist/vfs/sqlite-vfs.js +7 -6
- package/package.json +3 -2
- package/src/_shared/cache-stats.ts +27 -1
- package/src/_shared/exports-resolver.ts +1 -1
- package/src/_shared/retry.ts +1 -1
- package/src/constants.ts +30 -60
- package/src/runtime/assets-loader.ts +1 -1
- package/src/runtime/virtual-socket-kernel.generated.ts +1 -1
- package/src/vfs/seed-project.ts +3 -3
- package/src/vfs/sqlite-vfs.ts +24 -41
- package/dist/_shared/install-phase.d.ts +0 -47
- package/dist/_shared/install-phase.d.ts.map +0 -1
- package/dist/_shared/install-phase.js +0 -1
- package/dist/_shared/rpc-dispose.d.ts +0 -4
- package/dist/_shared/rpc-dispose.d.ts.map +0 -1
- package/dist/_shared/rpc-dispose.js +0 -30
- package/dist/_shared/w7-frame.d.ts +0 -79
- package/dist/_shared/w7-frame.d.ts.map +0 -1
- package/dist/_shared/w7-frame.js +0 -840
- package/dist/_shared/weighted-credit-pool.d.ts +0 -61
- package/dist/_shared/weighted-credit-pool.d.ts.map +0 -1
- package/dist/_shared/weighted-credit-pool.js +0 -196
- package/dist/observability/diag-counters.d.ts +0 -236
- package/dist/observability/diag-counters.d.ts.map +0 -1
- package/dist/observability/diag-counters.js +0 -195
- package/dist/observability/heap-estimate.d.ts +0 -225
- package/dist/observability/heap-estimate.d.ts.map +0 -1
- package/dist/observability/heap-estimate.js +0 -185
- package/dist/observability/heavy-alloc-coord.d.ts +0 -93
- package/dist/observability/heavy-alloc-coord.d.ts.map +0 -1
- package/dist/observability/heavy-alloc-coord.js +0 -181
- package/dist/observability/oom-classify.d.ts +0 -138
- package/dist/observability/oom-classify.d.ts.map +0 -1
- package/dist/observability/oom-classify.js +0 -268
- package/dist/observability/oom-discriminator.d.ts +0 -176
- package/dist/observability/oom-discriminator.d.ts.map +0 -1
- package/dist/observability/oom-discriminator.js +0 -272
- package/src/_shared/install-phase.ts +0 -56
- package/src/_shared/rpc-dispose.ts +0 -31
- package/src/_shared/w7-frame.ts +0 -1013
- package/src/_shared/weighted-credit-pool.ts +0 -244
- package/src/observability/diag-counters.ts +0 -343
- package/src/observability/heap-estimate.ts +0 -331
- package/src/observability/heavy-alloc-coord.ts +0 -241
- package/src/observability/oom-classify.ts +0 -260
- package/src/observability/oom-discriminator.ts +0 -405
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
export interface CreditLease {
|
|
2
|
-
readonly bytes: number;
|
|
3
|
-
release(): void;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export interface ResizableCreditLease extends CreditLease {
|
|
7
|
-
shrinkTo(bytes: number): void;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface WeightedCreditPoolStats {
|
|
11
|
-
readonly current: number;
|
|
12
|
-
readonly peak: number;
|
|
13
|
-
readonly queued: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface WeightedCreditPoolOptions {
|
|
17
|
-
/**
|
|
18
|
-
* Requests at or below this size draw from `reserve` instead of the shared
|
|
19
|
-
* capacity. Omit to keep a single lane.
|
|
20
|
-
*/
|
|
21
|
-
readonly smallRequestBytes?: number;
|
|
22
|
-
/**
|
|
23
|
-
* Bytes set aside for small requests. Held apart from `capacity` so a
|
|
24
|
-
* full-capacity owner stays grantable and small requests stay servable
|
|
25
|
-
* while it works.
|
|
26
|
-
*/
|
|
27
|
-
readonly reserve?: number;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface CreditWaiter {
|
|
31
|
-
readonly bytes: number;
|
|
32
|
-
readonly resolve: (lease: ResizableCreditLease) => void;
|
|
33
|
-
readonly reject: (error: Error) => void;
|
|
34
|
-
readonly signal?: AbortSignal;
|
|
35
|
-
readonly onAbort?: () => void;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** One FIFO queue over one byte capacity. */
|
|
39
|
-
class CreditLane {
|
|
40
|
-
private current = 0;
|
|
41
|
-
private peak = 0;
|
|
42
|
-
private readonly waiters: CreditWaiter[] = [];
|
|
43
|
-
|
|
44
|
-
constructor(readonly capacity: number) {}
|
|
45
|
-
|
|
46
|
-
get stats(): WeightedCreditPoolStats {
|
|
47
|
-
return { current: this.current, peak: this.peak, queued: this.waiters.length };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
tryAcquire(bytes: number): ResizableCreditLease | null {
|
|
51
|
-
if (this.waiters.length > 0 || this.current + bytes > this.capacity) return null;
|
|
52
|
-
return this.grant(bytes);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
acquire(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease> {
|
|
56
|
-
if (signal?.aborted) return Promise.reject(abortError(signal));
|
|
57
|
-
const immediate = this.tryAcquire(bytes);
|
|
58
|
-
if (immediate) return Promise.resolve(immediate);
|
|
59
|
-
|
|
60
|
-
return new Promise<ResizableCreditLease>((resolve, reject) => {
|
|
61
|
-
const waiter: CreditWaiter = {
|
|
62
|
-
bytes,
|
|
63
|
-
resolve,
|
|
64
|
-
reject,
|
|
65
|
-
signal,
|
|
66
|
-
onAbort: signal
|
|
67
|
-
? () => {
|
|
68
|
-
const index = this.waiters.indexOf(waiter);
|
|
69
|
-
if (index < 0) return;
|
|
70
|
-
this.waiters.splice(index, 1);
|
|
71
|
-
signal.removeEventListener('abort', waiter.onAbort!);
|
|
72
|
-
reject(abortError(signal));
|
|
73
|
-
this.drain();
|
|
74
|
-
}
|
|
75
|
-
: undefined,
|
|
76
|
-
};
|
|
77
|
-
this.waiters.push(waiter);
|
|
78
|
-
signal?.addEventListener('abort', waiter.onAbort!, { once: true });
|
|
79
|
-
if (signal?.aborted) {
|
|
80
|
-
waiter.onAbort!();
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
this.drain();
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
private grant(bytes: number): ResizableCreditLease {
|
|
88
|
-
this.current += bytes;
|
|
89
|
-
this.peak = Math.max(this.peak, this.current);
|
|
90
|
-
let leasedBytes = bytes;
|
|
91
|
-
let released = false;
|
|
92
|
-
return {
|
|
93
|
-
get bytes() {
|
|
94
|
-
return leasedBytes;
|
|
95
|
-
},
|
|
96
|
-
shrinkTo: (nextBytes: number) => {
|
|
97
|
-
if (released) {
|
|
98
|
-
throw new Error('cannot shrink a released weighted credit lease');
|
|
99
|
-
}
|
|
100
|
-
if (
|
|
101
|
-
!Number.isSafeInteger(nextBytes)
|
|
102
|
-
|| nextBytes <= 0
|
|
103
|
-
|| nextBytes > leasedBytes
|
|
104
|
-
) {
|
|
105
|
-
throw new RangeError(
|
|
106
|
-
`weighted credit lease can only shrink to a positive safe integer no larger than ${leasedBytes}: ${nextBytes}`,
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
this.current -= leasedBytes - nextBytes;
|
|
110
|
-
leasedBytes = nextBytes;
|
|
111
|
-
this.drain();
|
|
112
|
-
},
|
|
113
|
-
release: () => {
|
|
114
|
-
if (released) return;
|
|
115
|
-
released = true;
|
|
116
|
-
this.current -= leasedBytes;
|
|
117
|
-
if (this.current < 0) {
|
|
118
|
-
throw new Error('weighted credit accounting underflow');
|
|
119
|
-
}
|
|
120
|
-
this.drain();
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
private drain(): void {
|
|
126
|
-
while (this.waiters.length > 0) {
|
|
127
|
-
const waiter = this.waiters[0];
|
|
128
|
-
if (waiter.signal?.aborted) {
|
|
129
|
-
this.waiters.shift();
|
|
130
|
-
waiter.signal.removeEventListener('abort', waiter.onAbort!);
|
|
131
|
-
waiter.reject(abortError(waiter.signal));
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
if (this.current + waiter.bytes > this.capacity) return;
|
|
135
|
-
this.waiters.shift();
|
|
136
|
-
waiter.signal?.removeEventListener('abort', waiter.onAbort!);
|
|
137
|
-
waiter.resolve(this.grant(waiter.bytes));
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* FIFO byte-credit pool shared by concurrent allocation owners.
|
|
144
|
-
* Capacity is measured in retained bytes, not operation count.
|
|
145
|
-
*
|
|
146
|
-
* One FIFO queue makes every request wait behind the one ahead of it even
|
|
147
|
-
* when the pool has room to spare — `tryAcquire` refuses outright while
|
|
148
|
-
* anyone is queued — so a single multi-megabyte owner parks every request
|
|
149
|
-
* behind it for as long as it holds. Between owners of comparable size that
|
|
150
|
-
* is the point. For a chunk-sized filesystem read it means a wait with
|
|
151
|
-
* nothing to do with its own cost.
|
|
152
|
-
*
|
|
153
|
-
* So a request at or below `smallRequestBytes` takes shared capacity when
|
|
154
|
-
* that can be granted outright, and otherwise falls back to a separate
|
|
155
|
-
* `reserve` where it queues only among its own kind. It never joins the
|
|
156
|
-
* shared queue, so it is never parked behind a larger owner.
|
|
157
|
-
*
|
|
158
|
-
* Neither side can starve the other. Shared capacity stops admitting small
|
|
159
|
-
* requests the moment anything is queued for it, so a large waiter is never
|
|
160
|
-
* overtaken; and the reserve is held apart from `capacity`, so a
|
|
161
|
-
* full-capacity owner stays grantable while small requests stay servable.
|
|
162
|
-
* Peak occupancy only reaches `capacity + reserve` while the shared lane is
|
|
163
|
-
* contended.
|
|
164
|
-
*/
|
|
165
|
-
export class WeightedCreditPool {
|
|
166
|
-
private readonly general: CreditLane;
|
|
167
|
-
private readonly small: CreditLane | null;
|
|
168
|
-
private readonly smallRequestBytes: number;
|
|
169
|
-
|
|
170
|
-
constructor(readonly capacity: number, options: WeightedCreditPoolOptions = {}) {
|
|
171
|
-
if (!Number.isSafeInteger(capacity) || capacity <= 0) {
|
|
172
|
-
throw new RangeError(`weighted credit capacity must be a positive safe integer: ${capacity}`);
|
|
173
|
-
}
|
|
174
|
-
const smallRequestBytes = options.smallRequestBytes ?? 0;
|
|
175
|
-
const reserve = options.reserve ?? 0;
|
|
176
|
-
if (!Number.isSafeInteger(smallRequestBytes) || smallRequestBytes < 0
|
|
177
|
-
|| !Number.isSafeInteger(reserve) || reserve < 0) {
|
|
178
|
-
throw new RangeError('weighted credit reserve and small-request size must be non-negative safe integers');
|
|
179
|
-
}
|
|
180
|
-
if (smallRequestBytes > reserve) {
|
|
181
|
-
throw new RangeError(
|
|
182
|
-
`a small request (${smallRequestBytes}) must fit in the reserve (${reserve}) or it could never be granted`,
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
this.general = new CreditLane(capacity);
|
|
186
|
-
this.smallRequestBytes = smallRequestBytes;
|
|
187
|
-
this.small = smallRequestBytes > 0 ? new CreditLane(reserve) : null;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
get stats(): WeightedCreditPoolStats {
|
|
191
|
-
const general = this.general.stats;
|
|
192
|
-
if (!this.small) return general;
|
|
193
|
-
const small = this.small.stats;
|
|
194
|
-
return {
|
|
195
|
-
current: general.current + small.current,
|
|
196
|
-
peak: general.peak + small.peak,
|
|
197
|
-
queued: general.queued + small.queued,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
private isSmall(bytes: number): boolean {
|
|
202
|
-
return this.small !== null && bytes <= this.smallRequestBytes;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
tryAcquire(bytes: number): ResizableCreditLease | null {
|
|
206
|
-
this.validateRequest(bytes);
|
|
207
|
-
const shared = this.general.tryAcquire(bytes);
|
|
208
|
-
if (shared || !this.isSmall(bytes)) return shared;
|
|
209
|
-
return this.small!.tryAcquire(bytes);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
acquire(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease> {
|
|
213
|
-
try {
|
|
214
|
-
this.validateRequest(bytes);
|
|
215
|
-
} catch (error) {
|
|
216
|
-
return Promise.reject(error as Error);
|
|
217
|
-
}
|
|
218
|
-
if (this.isSmall(bytes)) {
|
|
219
|
-
// Shared capacity while it is free — a small request should get the
|
|
220
|
-
// same concurrency as any other when nothing is contending. It never
|
|
221
|
-
// joins the shared queue, so it cannot be parked behind a large owner.
|
|
222
|
-
const shared = this.general.tryAcquire(bytes);
|
|
223
|
-
if (shared) return Promise.resolve(shared);
|
|
224
|
-
return this.small!.acquire(bytes, signal);
|
|
225
|
-
}
|
|
226
|
-
return this.general.acquire(bytes, signal);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
private validateRequest(bytes: number): void {
|
|
230
|
-
if (!Number.isSafeInteger(bytes) || bytes <= 0 || bytes > this.capacity) {
|
|
231
|
-
throw new RangeError(
|
|
232
|
-
`weighted credit request must be a positive safe integer no larger than ${this.capacity}: ${bytes}`,
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function abortError(signal: AbortSignal): DOMException {
|
|
239
|
-
const reason = signal.reason;
|
|
240
|
-
return new DOMException(
|
|
241
|
-
reason instanceof Error ? reason.message : String(reason ?? 'The operation was aborted'),
|
|
242
|
-
'AbortError',
|
|
243
|
-
);
|
|
244
|
-
}
|
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* diag-counters.ts — application-level memory + phase observability.
|
|
3
|
-
*
|
|
4
|
-
* Why: workerd's `process.memoryUsage()` returns 0 for all fields inside
|
|
5
|
-
* a Durable Object class context (only dynamic-worker isolates under
|
|
6
|
-
* nodejs_compat get the real implementation). Without a working heap
|
|
7
|
-
* probe we have no way to confirm OOM hypotheses or verify a fix.
|
|
8
|
-
*
|
|
9
|
-
* Replacement: deterministic counters bumped at known allocation sites.
|
|
10
|
-
* Cumulative bytes decoded, in-flight stub counts, and phase markers
|
|
11
|
-
* give us the same operational signal as a heap probe — and they're
|
|
12
|
-
* exact, not estimated.
|
|
13
|
-
*
|
|
14
|
-
* Singleton-per-isolate. Lives at module scope so any code path in the
|
|
15
|
-
* supervisor bundle can write to it (installer and RPC handlers) and
|
|
16
|
-
* the request handler in nimbus-session.ts:/api/_diag/memory can read
|
|
17
|
-
* it. Survives across requests within the same isolate; resets on DO
|
|
18
|
-
* reboot — itself a useful signal (counters at 0 immediately after the
|
|
19
|
-
* banner reprinted = the killed isolate took its state with it).
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Phase tags surfaced via /api/_diag/memory. Strings are ASCII so
|
|
24
|
-
* humans can grep for them in logs.
|
|
25
|
-
*
|
|
26
|
-
* installPhase tracks the high-level npm-install state machine:
|
|
27
|
-
* idle → resolve → hoist → diff → fetch → write → link-bins → bundle → done
|
|
28
|
-
*
|
|
29
|
-
*/
|
|
30
|
-
// CLN-1 (2026-05-11): InstallPhase moved to _shared/install-phase.ts so
|
|
31
|
-
// it can't drift between this module and npm/installer.ts again. The
|
|
32
|
-
// merged union includes both 'idle' (resting state) and 'lock-check'
|
|
33
|
-
// (parsing package-lock.json). See _shared/install-phase.ts for full
|
|
34
|
-
// semantics documentation.
|
|
35
|
-
import type { InstallPhase } from '../_shared/install-phase.js';
|
|
36
|
-
|
|
37
|
-
export interface DiagCounters {
|
|
38
|
-
/** Top-level install phase. */
|
|
39
|
-
installPhase: InstallPhase;
|
|
40
|
-
/**
|
|
41
|
-
* Sum of payload bytes claimed by RPCs currently in flight on the
|
|
42
|
-
* SUPERVISOR boundary [Phase 2 A'.2].
|
|
43
|
-
*
|
|
44
|
-
* Bumped at RPC entry by `rpcPayloadStart(bytes)` in the DO-side
|
|
45
|
-
* byte-returning handlers in src/session/rpc.ts; debited at exit by
|
|
46
|
-
* `rpcPayloadEnd(bytes)`. Goes back to zero after every RPC settles.
|
|
47
|
-
*
|
|
48
|
-
* The C'.1 heap estimator surfaces this as
|
|
49
|
-
* `breakdown.streamingBuffersBytes` so any RPC path that buffers
|
|
50
|
-
* MORE than the W7 streaming guarantee shows up as supervisor heap
|
|
51
|
-
* pressure rather than disappearing into the bundle baseline.
|
|
52
|
-
*
|
|
53
|
-
* Streamed payloads (writeBatchStream over W7 frames) report -1
|
|
54
|
-
* up-front; we substitute 0 here because the bytes flow with
|
|
55
|
-
* backpressure and the supervisor-resident buffer is bounded by
|
|
56
|
-
* the W7 chunk size (a few KiB), not by the total payload.
|
|
57
|
-
*/
|
|
58
|
-
inFlightRpcPayloadBytes: number;
|
|
59
|
-
/**
|
|
60
|
-
* Bytes reserved by prefetch-bundle builds currently in flight.
|
|
61
|
-
*
|
|
62
|
-
* `buildPrefetchBundle` accumulates raw VFS file contents into an in-heap
|
|
63
|
-
* bundle object, and used to do so with nothing observing it: the estimator
|
|
64
|
-
* read 9.4 MiB while those bytes were resetting the DO. Each build now takes
|
|
65
|
-
* an `acquireSupervisorAllocation` lease for the budget it may use and bumps
|
|
66
|
-
* this alongside it, so the reservation is ATTRIBUTED to the bundle path
|
|
67
|
-
* rather than landing in `unattributedReservationBytes`.
|
|
68
|
-
*
|
|
69
|
-
* Surfaced as `breakdown.prefetchBundleBytes`. Back to zero between builds.
|
|
70
|
-
*/
|
|
71
|
-
prefetchBundleBytes: number;
|
|
72
|
-
/**
|
|
73
|
-
* Bytes retained by the FacetManager's prefetch-bundle LRU right now.
|
|
74
|
-
*
|
|
75
|
-
* Unlike the counter above these bytes persist ACROSS execs — each entry
|
|
76
|
-
* holds the raw bundle plus its serialized source, manifest and metadata.
|
|
77
|
-
* The LRU used to be bounded by entry count alone, which bounded nothing:
|
|
78
|
-
* 16 pi-sized entries exceed the supervisor ceiling several times over. It
|
|
79
|
-
* is now bounded by PREFETCH_CACHE_MAX_BYTES and reports its live total
|
|
80
|
-
* here, so the retained cost is a measurement rather than a cap.
|
|
81
|
-
*
|
|
82
|
-
* Surfaced as `breakdown.prefetchCacheBytes`. A gauge, not a delta.
|
|
83
|
-
*/
|
|
84
|
-
prefetchCacheBytes: number;
|
|
85
|
-
/** Install-facet counters. Populated by npm-installer after a
|
|
86
|
-
* successful batch-facet dispatch returns. Confirms the install ran
|
|
87
|
-
* in the facet (tarballsCompleted > 0) and surfaces the
|
|
88
|
-
* facet-internal byte count. */
|
|
89
|
-
installFacet: {
|
|
90
|
-
/** Number of tarballs the install-batch-facet successfully streamed. */
|
|
91
|
-
tarballsCompleted: number;
|
|
92
|
-
/** Cumulative tarball body bytes the facet decoded (gunzip input). */
|
|
93
|
-
cumulativeBytesDecoded: number;
|
|
94
|
-
/** Peak in-flight tarball pipelines inside the facet at any one moment. */
|
|
95
|
-
peakInFlight: number;
|
|
96
|
-
};
|
|
97
|
-
/** Pre-bundle facet counters. Tracks the fire-and-forget pre-bundle
|
|
98
|
-
* phase. Aggregates across all dispatches in this DO's lifetime. */
|
|
99
|
-
preBundleFacet: {
|
|
100
|
-
/** Specifiers attempted (queue depth at start of phase). */
|
|
101
|
-
attempted: number;
|
|
102
|
-
/** Bundles that completed successfully (esmCode produced). */
|
|
103
|
-
bundlesCompleted: number;
|
|
104
|
-
/** Bundles that failed (errorText set on PrebundleResult). */
|
|
105
|
-
errors: number;
|
|
106
|
-
/** Bundles skipped before facet dispatch (e.g. slice cap exceeded). */
|
|
107
|
-
skipped: number;
|
|
108
|
-
/** First-call wasm-fetch payload size. 0 until first successful dispatch
|
|
109
|
-
* records it; stays at first-seen value (RPC returns the same Module
|
|
110
|
-
* every time, but the cumulativeBytes the FACET reports may grow on
|
|
111
|
-
* retries). */
|
|
112
|
-
wasmBootBytes: number;
|
|
113
|
-
/** Per-module errors from the MOST RECENT pre-bundle batch, keyed by
|
|
114
|
-
* specifier (e.g. "lucide-react", "framer-motion"). REPLACED on each
|
|
115
|
-
* recordPreBundleSummary() call so the map is bounded by the batch
|
|
116
|
-
* size (≤ pending.length, typically <20).
|
|
117
|
-
*
|
|
118
|
-
* Example: { "lucide-react": "Worker exceeded memory limit." }
|
|
119
|
-
*
|
|
120
|
-
* Empty when the most recent batch had zero errors. */
|
|
121
|
-
errorsByModule: Record<string, string>;
|
|
122
|
-
};
|
|
123
|
-
/** Pipelined R2 race outcomes. All counts are cumulative since
|
|
124
|
-
* DO-isolate start. Per-tier cache statistics live in cache-stats. */
|
|
125
|
-
r2: {
|
|
126
|
-
/** Pipelined-RPC race wins for tarballs (R2 came back first; network
|
|
127
|
-
* was cancelled). */
|
|
128
|
-
pipelinedTarballRaceWins: number;
|
|
129
|
-
/** Pipelined-RPC race losses for tarballs (R2 came back too slow or
|
|
130
|
-
* empty; network response used). */
|
|
131
|
-
pipelinedTarballRaceLosses: number;
|
|
132
|
-
/** Pipelined-RPC race wins for packuments. */
|
|
133
|
-
pipelinedPackumentRaceWins: number;
|
|
134
|
-
/** Pipelined-RPC race losses for packuments. */
|
|
135
|
-
pipelinedPackumentRaceLosses: number;
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const _counters: DiagCounters = {
|
|
140
|
-
installPhase: 'idle',
|
|
141
|
-
inFlightRpcPayloadBytes: 0,
|
|
142
|
-
prefetchBundleBytes: 0,
|
|
143
|
-
prefetchCacheBytes: 0,
|
|
144
|
-
installFacet: {
|
|
145
|
-
tarballsCompleted: 0,
|
|
146
|
-
cumulativeBytesDecoded: 0,
|
|
147
|
-
peakInFlight: 0,
|
|
148
|
-
},
|
|
149
|
-
preBundleFacet: {
|
|
150
|
-
attempted: 0,
|
|
151
|
-
bundlesCompleted: 0,
|
|
152
|
-
errors: 0,
|
|
153
|
-
skipped: 0,
|
|
154
|
-
wasmBootBytes: 0,
|
|
155
|
-
errorsByModule: {},
|
|
156
|
-
},
|
|
157
|
-
r2: {
|
|
158
|
-
pipelinedTarballRaceWins: 0,
|
|
159
|
-
pipelinedTarballRaceLosses: 0,
|
|
160
|
-
pipelinedPackumentRaceWins: 0,
|
|
161
|
-
pipelinedPackumentRaceLosses: 0,
|
|
162
|
-
},
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
/** Read a snapshot — caller-side mutations don't affect the singleton. */
|
|
166
|
-
export function readDiagCounters(): DiagCounters {
|
|
167
|
-
return { ..._counters };
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** Set the install phase. */
|
|
171
|
-
export function setInstallPhase(p: InstallPhase): void {
|
|
172
|
-
_counters.installPhase = p;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Track an in-flight supervisor RPC payload [Phase 2 A'.2].
|
|
177
|
-
*
|
|
178
|
-
* Call at RPC entry, BEFORE awaiting any work that depends on
|
|
179
|
-
* `payload`. Pair with `rpcPayloadEnd(bytes)` in the matching `finally`
|
|
180
|
-
* so the counter goes back to zero on both success and failure.
|
|
181
|
-
*
|
|
182
|
-
* `bytes` is the supervisor-resident byte cost of the RPC's argument
|
|
183
|
-
* (or return value, whichever is bigger). For structured-clone RPCs
|
|
184
|
-
* this is the size of the cloned payload in bytes.
|
|
185
|
-
*
|
|
186
|
-
* Streamed payloads (ReadableStream-over-RPC) flow with backpressure
|
|
187
|
-
* and the supervisor-resident bound is the chunk size, not the total
|
|
188
|
-
* payload. Pass the chunk-size estimate (typically ≤ 1 MiB) — never
|
|
189
|
-
* the unknown total. -1 is silently coerced to 0 to keep the counter
|
|
190
|
-
* non-negative; callers that don't know the size should call this
|
|
191
|
-
* with 0 explicitly rather than relying on coercion.
|
|
192
|
-
*/
|
|
193
|
-
export function rpcPayloadStart(bytes: number): void {
|
|
194
|
-
const n = Number(bytes);
|
|
195
|
-
if (!Number.isFinite(n) || n <= 0) return;
|
|
196
|
-
_counters.inFlightRpcPayloadBytes += n;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Release an in-flight supervisor RPC payload [Phase 2 A'.2].
|
|
201
|
-
* Pass the same byte count given to the matching `rpcPayloadStart`.
|
|
202
|
-
*
|
|
203
|
-
* Floors at 0 to absorb arithmetic drift (e.g. if a payload-byte
|
|
204
|
-
* counter rounding gave a slightly different number on entry vs.
|
|
205
|
-
* exit). Drift in routine paths should be zero — a non-zero floor
|
|
206
|
-
* hit is a bug worth investigating.
|
|
207
|
-
*/
|
|
208
|
-
export function rpcPayloadEnd(bytes: number): void {
|
|
209
|
-
const n = Number(bytes);
|
|
210
|
-
if (!Number.isFinite(n) || n <= 0) return;
|
|
211
|
-
_counters.inFlightRpcPayloadBytes -= n;
|
|
212
|
-
if (_counters.inFlightRpcPayloadBytes < 0) {
|
|
213
|
-
_counters.inFlightRpcPayloadBytes = 0;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Track an in-flight prefetch-bundle build. Call with the byte reservation the
|
|
219
|
-
* build took from the supervisor allocation budget, and pair with
|
|
220
|
-
* `prefetchBundleEnd` in the matching `finally` so it returns to zero whether
|
|
221
|
-
* the build succeeded or threw.
|
|
222
|
-
*/
|
|
223
|
-
export function prefetchBundleStart(bytes: number): void {
|
|
224
|
-
const n = Number(bytes);
|
|
225
|
-
if (!Number.isFinite(n) || n <= 0) return;
|
|
226
|
-
_counters.prefetchBundleBytes += n;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/** Release an in-flight prefetch-bundle reservation. Floors at 0. */
|
|
230
|
-
export function prefetchBundleEnd(bytes: number): void {
|
|
231
|
-
const n = Number(bytes);
|
|
232
|
-
if (!Number.isFinite(n) || n <= 0) return;
|
|
233
|
-
_counters.prefetchBundleBytes -= n;
|
|
234
|
-
if (_counters.prefetchBundleBytes < 0) _counters.prefetchBundleBytes = 0;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Publish the prefetch-bundle LRU's live retained total. A gauge: the cache
|
|
239
|
-
* owns the number and reports it whenever it admits or evicts an entry.
|
|
240
|
-
*/
|
|
241
|
-
export function setPrefetchCacheBytes(bytes: number): void {
|
|
242
|
-
const n = Number(bytes);
|
|
243
|
-
_counters.prefetchCacheBytes = Number.isFinite(n) && n > 0 ? n : 0;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/** Fold facet-returned counters into the supervisor's diag state.
|
|
247
|
-
* Called by npm-installer after the batch-facet returns; aggregates
|
|
248
|
-
* rather than replaces so multiple install runs in the same DO
|
|
249
|
-
* lifetime accumulate in cumulativeBytesDecoded. */
|
|
250
|
-
export function recordInstallFacetCounters(c: {
|
|
251
|
-
tarballsCompleted: number;
|
|
252
|
-
cumulativeBytesDecoded: number;
|
|
253
|
-
peakInFlight: number;
|
|
254
|
-
}): void {
|
|
255
|
-
_counters.installFacet.tarballsCompleted += c.tarballsCompleted;
|
|
256
|
-
_counters.installFacet.cumulativeBytesDecoded += c.cumulativeBytesDecoded;
|
|
257
|
-
if (c.peakInFlight > _counters.installFacet.peakInFlight) {
|
|
258
|
-
_counters.installFacet.peakInFlight = c.peakInFlight;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/** Record pre-bundle phase summary. Aggregates lifetime totals across
|
|
263
|
-
* all phases run in this DO's lifetime, but REPLACES the
|
|
264
|
-
* errorsByModule map every call so it reflects only the most recent
|
|
265
|
-
* batch. Losing prior-batch errors is fine — they're already aggregated
|
|
266
|
-
* into `errors` (count); the map is for "which modules failed this
|
|
267
|
-
* time." */
|
|
268
|
-
export function recordPreBundleSummary(s: {
|
|
269
|
-
attempted: number;
|
|
270
|
-
bundlesCompleted: number;
|
|
271
|
-
errors: number;
|
|
272
|
-
skipped: number;
|
|
273
|
-
wasmBootBytes?: number;
|
|
274
|
-
errorsByModule?: Record<string, string>;
|
|
275
|
-
}): void {
|
|
276
|
-
_counters.preBundleFacet.attempted += s.attempted;
|
|
277
|
-
_counters.preBundleFacet.bundlesCompleted += s.bundlesCompleted;
|
|
278
|
-
_counters.preBundleFacet.errors += s.errors;
|
|
279
|
-
_counters.preBundleFacet.skipped += s.skipped;
|
|
280
|
-
if (s.wasmBootBytes && _counters.preBundleFacet.wasmBootBytes === 0) {
|
|
281
|
-
_counters.preBundleFacet.wasmBootBytes = s.wasmBootBytes;
|
|
282
|
-
}
|
|
283
|
-
// Replace (not merge) so we capture only the most recent batch.
|
|
284
|
-
// Bounded by batch size; truncate each value to 200 chars.
|
|
285
|
-
if (s.errorsByModule) {
|
|
286
|
-
const trimmed: Record<string, string> = {};
|
|
287
|
-
for (const [name, msg] of Object.entries(s.errorsByModule)) {
|
|
288
|
-
trimmed[name] = String(msg).slice(0, 200);
|
|
289
|
-
}
|
|
290
|
-
_counters.preBundleFacet.errorsByModule = trimmed;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/** Bump pipelined-RPC race outcome counters. The facet returns these
|
|
295
|
-
* in its result counters; the supervisor folds them in alongside the
|
|
296
|
-
* existing installFacet counters. */
|
|
297
|
-
export function recordR2RaceCounters(c: {
|
|
298
|
-
pipelinedTarballRaceWins: number;
|
|
299
|
-
pipelinedTarballRaceLosses: number;
|
|
300
|
-
pipelinedPackumentRaceWins: number;
|
|
301
|
-
pipelinedPackumentRaceLosses: number;
|
|
302
|
-
}): void {
|
|
303
|
-
_counters.r2.pipelinedTarballRaceWins += c.pipelinedTarballRaceWins;
|
|
304
|
-
_counters.r2.pipelinedTarballRaceLosses += c.pipelinedTarballRaceLosses;
|
|
305
|
-
_counters.r2.pipelinedPackumentRaceWins += c.pipelinedPackumentRaceWins;
|
|
306
|
-
_counters.r2.pipelinedPackumentRaceLosses += c.pipelinedPackumentRaceLosses;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* cache-obs-2: fold facet-collected per-tier cache events into the
|
|
311
|
-
* DO-side cache-stats singleton. Called from installer.ts after a
|
|
312
|
-
* batch-facet / resolve-facet returns — mirrors recordR2RaceCounters
|
|
313
|
-
* (the facet collects metrics in its result and the supervisor folds
|
|
314
|
-
* them into the DO isolate).
|
|
315
|
-
*
|
|
316
|
-
* Each event has shape:
|
|
317
|
-
* { kind: 'hit', tier: 'L2'|'L3'|'L4', cacheKind: 'tarball'|'packument'|'asset', bytes: number }
|
|
318
|
-
* { kind: 'miss', tier: ..., cacheKind: ... }
|
|
319
|
-
*
|
|
320
|
-
* The recordHit/recordMiss surface is defined in
|
|
321
|
-
* src/_shared/cache-stats.ts.
|
|
322
|
-
*/
|
|
323
|
-
import {
|
|
324
|
-
recordHit as _cacheRecordHit,
|
|
325
|
-
recordMiss as _cacheRecordMiss,
|
|
326
|
-
type CacheTier,
|
|
327
|
-
type CacheKind,
|
|
328
|
-
} from '../_shared/cache-stats.js';
|
|
329
|
-
|
|
330
|
-
export type CacheStatEvent =
|
|
331
|
-
| { kind: 'hit'; tier: CacheTier; cacheKind: CacheKind; bytes: number }
|
|
332
|
-
| { kind: 'miss'; tier: CacheTier; cacheKind: CacheKind };
|
|
333
|
-
|
|
334
|
-
export function recordCacheStatEvents(events: readonly CacheStatEvent[] | undefined): void {
|
|
335
|
-
if (!events || events.length === 0) return;
|
|
336
|
-
for (const e of events) {
|
|
337
|
-
if (e.kind === 'hit') {
|
|
338
|
-
_cacheRecordHit(e.tier, e.cacheKind, e.bytes);
|
|
339
|
-
} else {
|
|
340
|
-
_cacheRecordMiss(e.tier, e.cacheKind);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|