@nimbus-sh/platform 0.8.1 → 0.13.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 (67) hide show
  1. package/dist/composition.d.ts +3 -2
  2. package/dist/composition.d.ts.map +1 -1
  3. package/dist/composition.js +34 -19
  4. package/dist/crc32.d.ts +5 -5
  5. package/dist/crc32.d.ts.map +1 -1
  6. package/dist/crc32.js +8 -12
  7. package/dist/diag-counters.d.ts +18 -10
  8. package/dist/diag-counters.d.ts.map +1 -1
  9. package/dist/diag-counters.js +7 -20
  10. package/dist/heap-estimate.d.ts +5 -20
  11. package/dist/heap-estimate.d.ts.map +1 -1
  12. package/dist/heap-estimate.js +6 -33
  13. package/dist/heavy-alloc-coord.d.ts +131 -11
  14. package/dist/heavy-alloc-coord.d.ts.map +1 -1
  15. package/dist/heavy-alloc-coord.js +241 -22
  16. package/dist/hex.d.ts +20 -0
  17. package/dist/hex.d.ts.map +1 -0
  18. package/dist/hex.js +29 -0
  19. package/dist/install-phase.d.ts +1 -15
  20. package/dist/install-phase.d.ts.map +1 -1
  21. package/dist/lexical-path.d.ts +11 -0
  22. package/dist/lexical-path.d.ts.map +1 -0
  23. package/dist/lexical-path.js +19 -0
  24. package/dist/limits.d.ts +4 -0
  25. package/dist/limits.d.ts.map +1 -1
  26. package/dist/limits.js +25 -0
  27. package/dist/module-source.d.ts.map +1 -1
  28. package/dist/module-source.js +2 -1
  29. package/dist/oom-classify.d.ts +5 -1
  30. package/dist/oom-classify.d.ts.map +1 -1
  31. package/dist/oom-classify.js +7 -1
  32. package/dist/realm-builtin.d.ts +2 -0
  33. package/dist/realm-builtin.d.ts.map +1 -0
  34. package/dist/realm-builtin.js +11 -0
  35. package/dist/utf8.d.ts +13 -1
  36. package/dist/utf8.d.ts.map +1 -1
  37. package/dist/utf8.js +67 -1
  38. package/dist/w7-frame.d.ts +47 -13
  39. package/dist/w7-frame.d.ts.map +1 -1
  40. package/dist/w7-frame.js +42 -24
  41. package/dist/wave-writer.d.ts +11 -6
  42. package/dist/wave-writer.d.ts.map +1 -1
  43. package/dist/wave-writer.js +28 -29
  44. package/dist/weighted-credit-pool.d.ts +43 -0
  45. package/dist/weighted-credit-pool.d.ts.map +1 -1
  46. package/dist/weighted-credit-pool.js +250 -46
  47. package/dist/worker-code.d.ts +48 -0
  48. package/dist/worker-code.d.ts.map +1 -0
  49. package/dist/worker-code.js +10 -0
  50. package/package.json +1 -1
  51. package/src/composition.ts +41 -21
  52. package/src/crc32.ts +9 -12
  53. package/src/diag-counters.ts +23 -22
  54. package/src/heap-estimate.ts +11 -40
  55. package/src/heavy-alloc-coord.ts +275 -20
  56. package/src/hex.ts +29 -0
  57. package/src/install-phase.ts +1 -15
  58. package/src/lexical-path.ts +17 -0
  59. package/src/limits.ts +29 -0
  60. package/src/module-source.ts +3 -1
  61. package/src/oom-classify.ts +9 -1
  62. package/src/realm-builtin.ts +22 -0
  63. package/src/utf8.ts +69 -1
  64. package/src/w7-frame.ts +74 -33
  65. package/src/wave-writer.ts +28 -27
  66. package/src/weighted-credit-pool.ts +256 -51
  67. package/src/worker-code.ts +38 -0
@@ -33,6 +33,7 @@
33
33
  // (parsing package-lock.json). See _shared/install-phase.ts for full
34
34
  // semantics documentation.
35
35
  import type { InstallPhase } from './install-phase.js';
36
+ import { readSupervisorAllocationAttribution, type AllocationAttribution, type SupervisorAllocationLease } from './heavy-alloc-coord.js';
36
37
 
37
38
  export interface DiagCounters {
38
39
  /** Top-level install phase. */
@@ -62,13 +63,22 @@ export interface DiagCounters {
62
63
  * `buildPrefetchBundle` accumulates raw VFS file contents into an in-heap
63
64
  * bundle object, and used to do so with nothing observing it: the estimator
64
65
  * 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`.
66
+ * an `acquireSupervisorAllocation` lease for the budget it may use, counted
67
+ * here while it is held (attributeAllocation), so the reservation is
68
+ * ATTRIBUTED to the bundle path rather than landing in
69
+ * `unattributedReservationBytes`.
68
70
  *
69
71
  * Surfaced as `breakdown.prefetchBundleBytes`. Back to zero between builds.
70
72
  */
71
73
  prefetchBundleBytes: number;
74
+ /**
75
+ * Bytes the slices of pre-bundles in flight hold: each build's lease
76
+ * (browser-prebundle.ts), counted while it is held (attributeAllocation),
77
+ * after it shrinks to the slice it walked.
78
+ *
79
+ * Surfaced as `breakdown.preBundleSliceBytes`. Back to zero between builds.
80
+ */
81
+ preBundleSliceBytes: number;
72
82
  /**
73
83
  * Bytes retained by the FacetManager's prefetch-bundle LRU right now.
74
84
  *
@@ -145,10 +155,9 @@ export interface DiagCounters {
145
155
  };
146
156
  }
147
157
 
148
- const _counters: DiagCounters = {
158
+ const _counters: Omit<DiagCounters, 'prefetchBundleBytes' | 'preBundleSliceBytes'> = {
149
159
  installPhase: 'idle',
150
160
  inFlightRpcPayloadBytes: 0,
151
- prefetchBundleBytes: 0,
152
161
  prefetchCacheBytes: 0,
153
162
  supervisorAnsweredBytes: 0,
154
163
  installFacet: {
@@ -174,7 +183,7 @@ const _counters: DiagCounters = {
174
183
 
175
184
  /** Read a snapshot — caller-side mutations don't affect the singleton. */
176
185
  export function readDiagCounters(): DiagCounters {
177
- return { ..._counters };
186
+ return { ..._counters, ...readSupervisorAllocationAttribution() };
178
187
  }
179
188
 
180
189
  /** Count `bytes` the session answered a process's supervisor call with. */
@@ -229,24 +238,16 @@ export function rpcPayloadEnd(bytes: number): void {
229
238
  }
230
239
  }
231
240
 
241
+ /** The counters that name the live allocation leases of one owner. */
242
+ export type AllocationOwner = AllocationAttribution;
243
+
232
244
  /**
233
- * Track an in-flight prefetch-bundle build. Call with the byte reservation the
234
- * build took from the supervisor allocation budget, and pair with
235
- * `prefetchBundleEnd` in the matching `finally` so it returns to zero whether
236
- * the build succeeded or threw.
245
+ * Attribution belongs to the budget's actual lease. The public gauges are
246
+ * derived from its holders, never maintained in a second counter registry.
237
247
  */
238
- export function prefetchBundleStart(bytes: number): void {
239
- const n = Number(bytes);
240
- if (!Number.isFinite(n) || n <= 0) return;
241
- _counters.prefetchBundleBytes += n;
242
- }
243
-
244
- /** Release an in-flight prefetch-bundle reservation. Floors at 0. */
245
- export function prefetchBundleEnd(bytes: number): void {
246
- const n = Number(bytes);
247
- if (!Number.isFinite(n) || n <= 0) return;
248
- _counters.prefetchBundleBytes -= n;
249
- if (_counters.prefetchBundleBytes < 0) _counters.prefetchBundleBytes = 0;
248
+ export function attributeAllocation(owner: AllocationOwner, lease: SupervisorAllocationLease): SupervisorAllocationLease {
249
+ lease.attribute(owner);
250
+ return lease;
250
251
  }
251
252
 
252
253
  /**
@@ -58,11 +58,7 @@
58
58
  * recovery_event ring, separate from this module.
59
59
  */
60
60
 
61
- import {
62
- PRE_BUNDLE_CONCURRENCY,
63
- PRE_BUNDLE_SLICE_CAP_BYTES,
64
- SUPERVISOR_HEAP_CEILING_BYTES,
65
- } from './limits.js';
61
+ import { SUPERVISOR_HEAP_CEILING_BYTES } from './limits.js';
66
62
  import type { DiagCounters } from './diag-counters.js';
67
63
  import {
68
64
  readSupervisorAllocationBudget,
@@ -169,9 +165,11 @@ export interface HeapBreakdown {
169
165
  vfsLruBytes: number;
170
166
  /** SqliteVFS current logical retained write payload bytes. */
171
167
  vfsInFlightBytes: number;
172
- /** Pre-bundle slice bytes resident in supervisor heap (peak across
173
- * in-flight pool slots × SLICE_CAP_BYTES). Drops to 0 once
174
- * A'.2/A'.3 stream the slice through ReadableStream-over-RPC. */
168
+ /**
169
+ * Bytes the slices of pre-bundles in flight hold (diag-counters
170
+ * `preBundleSliceBytes`): each build's lease, from its worst case down to
171
+ * the slice it walked, released as the build ends, however it ends.
172
+ */
175
173
  preBundleSliceBytes: number;
176
174
  /**
177
175
  * In-flight supervisor RPC payload bytes (Phase 2 A'.2).
@@ -243,32 +241,6 @@ export interface VfsHeapInputs {
243
241
  */
244
242
  const SUPERVISOR_BASELINE_BYTES = 9 * 1024 * 1024;
245
243
 
246
- /**
247
- * Estimate pre-bundle slice bytes resident in the supervisor heap.
248
- *
249
- * Pre A'.2: the supervisor builds the slice in heap (up to
250
- * SLICE_CAP_BYTES = 28 MiB per concurrent slot) before passing to the
251
- * pre-bundle facet. With PRE_BUNDLE_CONCURRENCY = 1 the cap is 28 MiB.
252
- *
253
- * Diag counters track pre-bundle attempts/completions but not
254
- * in-flight slice bytes specifically. Until A'.2 streams the slice,
255
- * we treat the slice as PRESENT in supervisor heap iff the pre-bundle
256
- * phase is active AND a slot is in flight (no completion yet for the
257
- * current attempted count).
258
- *
259
- * After A'.2 lands the slice flows via ReadableStream-over-RPC and
260
- * supervisor never holds the bytes — this contribution drops to 0.
261
- */
262
- export function estimatePreBundleSliceBytes(c: DiagCounters): number {
263
- // Conservative: if the most recent batch attempted >0 specs and the
264
- // batch hasn't fully completed, assume one slice's worth of bytes
265
- // are in flight.
266
- const f = c.preBundleFacet;
267
- const inFlight = f.attempted - f.bundlesCompleted - f.errors - f.skipped;
268
- if (inFlight <= 0) return 0;
269
- return Math.min(inFlight, PRE_BUNDLE_CONCURRENCY) * PRE_BUNDLE_SLICE_CAP_BYTES;
270
- }
271
-
272
244
  /**
273
245
  * Build a heap estimate from runtime counters + VFS inputs.
274
246
  *
@@ -280,20 +252,19 @@ export function estimateSupervisorHeap(
280
252
  vfs: VfsHeapInputs,
281
253
  ): HeapEstimate {
282
254
  const allocationBudget = readSupervisorAllocationBudget();
283
- const preBundleSliceBytes = estimatePreBundleSliceBytes(c);
284
- // The prefetch build holds a lease, so its bytes are already inside
285
- // allocationBudget.current — naming it here moves it out of the
286
- // unattributed remainder rather than adding to the total twice.
255
+ // The prefetch build and the pre-bundle hold leases, so their bytes are
256
+ // already inside allocationBudget.current — naming them here moves them out
257
+ // of the unattributed remainder rather than adding to the total twice.
287
258
  const transientAttributedBytes =
288
259
  vfs.inFlightWriteBytes +
289
- preBundleSliceBytes +
260
+ c.preBundleSliceBytes +
290
261
  c.inFlightRpcPayloadBytes +
291
262
  c.prefetchBundleBytes;
292
263
  const breakdown: HeapBreakdown = {
293
264
  supervisorBaselineBytes: SUPERVISOR_BASELINE_BYTES,
294
265
  vfsLruBytes: vfs.cacheHotBytes,
295
266
  vfsInFlightBytes: vfs.inFlightWriteBytes,
296
- preBundleSliceBytes,
267
+ preBundleSliceBytes: c.preBundleSliceBytes,
297
268
  streamingBuffersBytes: c.inFlightRpcPayloadBytes,
298
269
  prefetchBundleBytes: c.prefetchBundleBytes,
299
270
  prefetchCacheBytes: c.prefetchCacheBytes,
@@ -11,6 +11,7 @@
11
11
  import {
12
12
  CHUNK_SIZE,
13
13
  SUPERVISOR_IN_FLIGHT_ALLOCATION_BUDGET_BYTES,
14
+ SUPERVISOR_LOAD_WAIT_MS,
14
15
  SUPERVISOR_READ_RESERVE_BYTES,
15
16
  } from './limits.js';
16
17
  import {
@@ -24,6 +25,58 @@ export interface SupervisorAllocationBudgetStats {
24
25
  readonly current: number;
25
26
  readonly peak: number;
26
27
  readonly queued: number;
28
+ readonly holders: SupervisorAllocationHolder[];
29
+ readonly waiting: SupervisorAllocationHolder[];
30
+ }
31
+
32
+ export type AllocationAttribution = 'prefetchBundleBytes' | 'preBundleSliceBytes';
33
+ export interface SupervisorAllocationOwner {
34
+ owner: string;
35
+ pid?: number;
36
+ session?: string;
37
+ attribution?: AllocationAttribution;
38
+ }
39
+ export interface SupervisorAllocationHolder {
40
+ owner: string;
41
+ bytes: number;
42
+ ageMs: number;
43
+ pid?: number;
44
+ session?: string;
45
+ }
46
+ export interface SupervisorAllocationLease extends ResizableCreditLease {
47
+ attribute(attribution: AllocationAttribution): void;
48
+ }
49
+ interface AllocationHolder {
50
+ owner: SupervisorAllocationOwner;
51
+ credit: ResizableCreditLease;
52
+ since: number;
53
+ }
54
+ interface AllocationWaiter {
55
+ owner: SupervisorAllocationOwner;
56
+ bytes: number;
57
+ since: number;
58
+ }
59
+ const DEFAULT_ALLOCATION_OWNER: SupervisorAllocationOwner = { owner: 'supervisor allocation' };
60
+ const MAX_ALLOCATION_OWNER_ROWS = 16;
61
+
62
+ /**
63
+ * A facet's load the budget did not admit within its bound
64
+ * (SUPERVISOR_LOAD_WAIT_MS): nothing of it was read, and the facet is not
65
+ * started. Resource temporarily unavailable, as the ledger's refusal of a
66
+ * wait no release can satisfy is.
67
+ */
68
+ export class SupervisorLoadWaitError extends Error {
69
+ readonly code = 'EAGAIN';
70
+ readonly errno = -11;
71
+ constructor(readonly load: string, readonly bytes: number, readonly waitedMs: number, readonly held: SupervisorAllocationBudgetStats) {
72
+ super(
73
+ `Resource temporarily unavailable: ${load} (${bytes.toLocaleString('en-US')} bytes) waited ${waitedMs} ms `
74
+ + `for this isolate's transient allocation budget and was not admitted (${held.current.toLocaleString('en-US')} `
75
+ + `of ${held.capacity.toLocaleString('en-US')} bytes held, ${held.queued} owners queued); `
76
+ + `holders=${JSON.stringify(held.holders)} waiting=${JSON.stringify(held.waiting)}`,
77
+ );
78
+ this.name = 'SupervisorLoadWaitError';
79
+ }
27
80
  }
28
81
 
29
82
  interface AllocationBudgetLifecycle {
@@ -44,6 +97,8 @@ export class SupervisorAllocationBudget {
44
97
  * cache it is filling.
45
98
  */
46
99
  private lifecycleHolders = 0;
100
+ private readonly holders = new Set<AllocationHolder>();
101
+ private readonly waiting = new Set<AllocationWaiter>();
47
102
 
48
103
  constructor(
49
104
  readonly capacity: number,
@@ -54,19 +109,33 @@ export class SupervisorAllocationBudget {
54
109
  }
55
110
 
56
111
  get stats(): SupervisorAllocationBudgetStats {
112
+ const now = Date.now();
113
+ const describe = (owner: SupervisorAllocationOwner, bytes: number, since: number): SupervisorAllocationHolder => ({
114
+ owner: owner.owner.slice(0, 256), bytes, ageMs: Math.max(0, now - since),
115
+ ...(owner.pid === undefined ? {} : { pid: owner.pid }), ...(owner.session === undefined ? {} : { session: owner.session }),
116
+ });
57
117
  return {
58
118
  capacity: this.capacity,
59
119
  ...this.credits.stats,
120
+ holders: [...this.holders].sort((a, b) => b.credit.bytes - a.credit.bytes).slice(0, MAX_ALLOCATION_OWNER_ROWS)
121
+ .map(({ owner, credit, since }) => describe(owner, credit.bytes, since)),
122
+ waiting: [...this.waiting].slice(0, MAX_ALLOCATION_OWNER_ROWS).map(({ owner, bytes, since }) => describe(owner, bytes, since)),
60
123
  };
61
124
  }
62
125
 
126
+ get attributed(): Record<AllocationAttribution, number> {
127
+ const totals = { prefetchBundleBytes: 0, preBundleSliceBytes: 0 };
128
+ for (const { owner, credit } of this.holders) if (owner.attribution !== undefined) totals[owner.attribution] += credit.bytes;
129
+ return totals;
130
+ }
131
+
63
132
  /**
64
133
  * Reserve bytes and, while held, mark the budget active so observers can
65
134
  * free heap headroom. For owners whose payload is what the headroom is for:
66
135
  * installs, clones, pre-bundles, boot payloads, streamed writes.
67
136
  */
68
- acquire(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease> {
69
- return this._acquire(bytes, signal, true);
137
+ acquire(bytes: number, signal?: AbortSignal, owner = DEFAULT_ALLOCATION_OWNER): Promise<SupervisorAllocationLease> {
138
+ return this._acquire(bytes, signal, true, owner);
70
139
  }
71
140
 
72
141
  /**
@@ -78,25 +147,119 @@ export class SupervisorAllocationBudget {
78
147
  * never warming. The byte credit is still taken — back-pressure is what
79
148
  * stops a large read overlapping a large write.
80
149
  */
81
- acquireWithoutLifecycle(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease> {
82
- return this._acquire(bytes, signal, false);
150
+ acquireWithoutLifecycle(bytes: number, signal?: AbortSignal, owner = DEFAULT_ALLOCATION_OWNER): Promise<SupervisorAllocationLease> {
151
+ return this._acquire(bytes, signal, false, owner);
152
+ }
153
+
154
+ /**
155
+ * Reserve what a facet's load holds while the loader takes its module map:
156
+ * `bytes`, or for a load larger than the whole budget the whole budget, so
157
+ * that it holds it alone and every owner after it waits. Granted in the
158
+ * budget's one FIFO order, so no owner queued after a load overtakes it,
159
+ * however small, and refused with SupervisorLoadWaitError when not granted
160
+ * within `waitMs`. Like a copy it holds bytes the session already has, for
161
+ * as long as the load takes, so the disposable VFS cache is not shrunk.
162
+ */
163
+ async acquireLoad(load: string, bytes: number, options: { waitMs: number; signal?: AbortSignal; owner?: SupervisorAllocationOwner }): Promise<SupervisorAllocationLease> {
164
+ options.signal?.throwIfAborted();
165
+ const wanted = Math.min(bytes, this.capacity);
166
+ // Granted at once when nothing is queued and it fits, with no bound to arm.
167
+ const owner = options.owner ?? { owner: load };
168
+ const now = this.tryAcquire(wanted, false, owner);
169
+ if (now) return now;
170
+ const started = Date.now();
171
+ const bound = new AbortController();
172
+ // Cleared as the wait settles: a pending timer keeps the hosting object from hibernating.
173
+ const timer = setTimeout(() => bound.abort(), options.waitMs);
174
+ try {
175
+ return await this._acquire(wanted, options.signal ? AbortSignal.any([bound.signal, options.signal]) : bound.signal, false, owner);
176
+ } catch (error) {
177
+ if (bound.signal.aborted && options.signal?.aborted !== true) throw new SupervisorLoadWaitError(load, bytes, Date.now() - started, this.stats);
178
+ throw error;
179
+ } finally {
180
+ clearTimeout(timer);
181
+ }
182
+ }
183
+
184
+ /** Reclaimers of parked launches' retained credit, oldest registered first (reclaimable). */
185
+ private readonly reclaimers = new Set<() => void>();
186
+
187
+ /**
188
+ * Register `reclaim`, which gives a parked launch's retained credit back
189
+ * before it returns (its map dropped, its lease released, the launch sent
190
+ * back to prepare), for running work to call when nothing else frees what
191
+ * it needs (acquireRunning). Unregistered just before it is called; the
192
+ * returned function unregisters it.
193
+ */
194
+ reclaimable(reclaim: () => void): () => void {
195
+ this.reclaimers.add(reclaim);
196
+ // A running ask queued before this launch parked is owed it, once the registration has returned.
197
+ if (this.credits.aheadStarved) queueMicrotask(() => this.reclaimForRunning());
198
+ return () => {
199
+ this.reclaimers.delete(reclaim);
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Running work's credit, the lock order's one exception
205
+ * (acquireSupervisorLoad): granted at once where it fits, ahead of every
206
+ * queued ask; else, queued ahead of them all, it reclaims parked launches'
207
+ * retained credit, the oldest first, until it is granted; else it waits
208
+ * only for credit other running work holds.
209
+ */
210
+ async acquireRunning(bytes: number, signal?: AbortSignal, drivesLifecycle = true, owner = DEFAULT_ALLOCATION_OWNER): Promise<SupervisorAllocationLease> {
211
+ const wanted = Math.min(bytes, this.capacity);
212
+ const waiter = { owner: { ...owner }, bytes: wanted, since: Date.now() };
213
+ this.waiting.add(waiter);
214
+ try {
215
+ const lease = this.credits.acquireAhead(wanted, signal);
216
+ this.reclaimForRunning();
217
+ const credit = await lease;
218
+ this.waiting.delete(waiter);
219
+ return this.wrap(credit, drivesLifecycle, waiter.owner);
220
+ } finally { this.waiting.delete(waiter); }
221
+ }
222
+
223
+ /** Reclaim parked launches' credit, the oldest parked first, while a running ask waits: what a reclaim frees goes to running asks first. */
224
+ private reclaimForRunning(): void {
225
+ for (const reclaim of this.reclaimers) {
226
+ if (!this.credits.aheadStarved) return;
227
+ this.reclaimers.delete(reclaim);
228
+ reclaim();
229
+ }
83
230
  }
84
231
 
85
- /** Reserve bytes now or not at all, driving the lifecycle as acquire does. */
86
- tryAcquire(bytes: number): ResizableCreditLease | null {
232
+ /** acquireRunning's credit now, ahead of every queued ask, without reclaiming, or null. */
233
+ tryAcquireRunning(bytes: number, owner = DEFAULT_ALLOCATION_OWNER): SupervisorAllocationLease | null {
234
+ const credit = this.credits.tryAcquireAhead(Math.min(bytes, this.capacity));
235
+ return credit ? this.wrap(credit, true, owner) : null;
236
+ }
237
+
238
+ /** Reserve bytes now or not at all, driving the lifecycle as acquire does (or not, `drivesLifecycle` false). */
239
+ tryAcquire(bytes: number, drivesLifecycle = true, owner = DEFAULT_ALLOCATION_OWNER): SupervisorAllocationLease | null {
87
240
  const credit = this.credits.tryAcquire(bytes);
88
- return credit ? this.wrap(credit, true) : null;
241
+ return credit ? this.wrap(credit, drivesLifecycle, owner) : null;
89
242
  }
90
243
 
91
244
  private async _acquire(
92
245
  bytes: number,
93
246
  signal: AbortSignal | undefined,
94
247
  drivesLifecycle: boolean,
95
- ): Promise<ResizableCreditLease> {
96
- return this.wrap(await this.credits.acquire(bytes, signal), drivesLifecycle);
248
+ owner: SupervisorAllocationOwner,
249
+ ): Promise<SupervisorAllocationLease> {
250
+ const waiter = { owner: { ...owner }, bytes, since: Date.now() };
251
+ this.waiting.add(waiter);
252
+ try {
253
+ const credit = await this.credits.acquire(bytes, signal);
254
+ this.waiting.delete(waiter);
255
+ return this.wrap(credit, drivesLifecycle, waiter.owner);
256
+ }
257
+ finally { this.waiting.delete(waiter); }
97
258
  }
98
259
 
99
- private wrap(credit: ResizableCreditLease, drivesLifecycle: boolean): ResizableCreditLease {
260
+ private wrap(credit: ResizableCreditLease, drivesLifecycle: boolean, owner: SupervisorAllocationOwner): SupervisorAllocationLease {
261
+ const holder = { owner: { ...owner }, credit, since: Date.now() };
262
+ this.holders.add(holder);
100
263
  if (drivesLifecycle) {
101
264
  this.lifecycleHolders++;
102
265
  if (!this.active) {
@@ -113,9 +276,12 @@ export class SupervisorAllocationBudget {
113
276
  shrinkTo: (nextBytes: number) => {
114
277
  credit.shrinkTo(nextBytes);
115
278
  },
279
+ tryGrowTo: (nextBytes: number) => credit.tryGrowTo(nextBytes),
280
+ attribute: (attribution) => { if (!released) holder.owner = { ...holder.owner, attribution }; },
116
281
  release: () => {
117
282
  if (released) return;
118
283
  released = true;
284
+ this.holders.delete(holder);
119
285
  credit.release();
120
286
  if (!drivesLifecycle) return;
121
287
  this.lifecycleHolders--;
@@ -217,35 +383,120 @@ const supervisorAllocationBudget = new SupervisorAllocationBudget(
217
383
  export function acquireSupervisorAllocation(
218
384
  bytes: number,
219
385
  signal?: AbortSignal,
220
- ): Promise<ResizableCreditLease> {
221
- return supervisorAllocationBudget.acquire(bytes, signal);
386
+ owner?: SupervisorAllocationOwner,
387
+ ): Promise<SupervisorAllocationLease> {
388
+ return supervisorAllocationBudget.acquire(bytes, signal, owner);
222
389
  }
223
390
 
224
391
  /** Reserve bytes now if they can be granted without waiting, else null. */
225
- export function tryAcquireSupervisorAllocation(bytes: number): ResizableCreditLease | null {
226
- return supervisorAllocationBudget.tryAcquire(bytes);
392
+ export function tryAcquireSupervisorAllocation(bytes: number, owner?: SupervisorAllocationOwner): SupervisorAllocationLease | null {
393
+ return supervisorAllocationBudget.tryAcquire(bytes, true, owner);
227
394
  }
228
395
 
229
396
  /**
230
- * Reserve bytes for a filesystem READ. Takes the same byte credit as any
231
- * other owner but does not shrink the disposable VFS cache, which reads are
232
- * there to fill.
397
+ * Reserve bytes for a running process's filesystem READ (its read RPCs):
398
+ * running work (acquireRunning), which does not shrink the disposable VFS
399
+ * cache, which reads are there to fill.
233
400
  */
234
401
  export function acquireSupervisorReadAllocation(
235
402
  bytes: number,
236
403
  signal?: AbortSignal,
237
- ): Promise<ResizableCreditLease> {
238
- return supervisorAllocationBudget.acquireWithoutLifecycle(bytes, signal);
404
+ owner?: SupervisorAllocationOwner,
405
+ ): Promise<SupervisorAllocationLease> {
406
+ return supervisorAllocationBudget.acquireRunning(bytes, signal, false, owner ?? { owner: 'running filesystem read' });
407
+ }
408
+
409
+ /**
410
+ * Reserve bytes for a short-lived copy of what the session already holds
411
+ * (a launch's module map made from a staged state), now or not at all, or
412
+ * by waiting (`signal`): the byte credit bounds how many such copies are
413
+ * live at once, but the disposable VFS cache is not shrunk for one, as it
414
+ * is not for a read. Shrinking it for every launch of a program already
415
+ * built cost each one its cache, and the copy is no install.
416
+ */
417
+ export function tryAcquireSupervisorCopyAllocation(bytes: number, owner?: SupervisorAllocationOwner): SupervisorAllocationLease | null {
418
+ return supervisorAllocationBudget.tryAcquire(bytes, false, owner ?? { owner: 'supervisor copy' });
419
+ }
420
+
421
+ /** {@link tryAcquireSupervisorCopyAllocation}, waiting for room. */
422
+ export function acquireSupervisorCopyAllocation(bytes: number, signal?: AbortSignal, owner?: SupervisorAllocationOwner): Promise<SupervisorAllocationLease> {
423
+ return supervisorAllocationBudget.acquireWithoutLifecycle(bytes, signal, owner ?? { owner: 'supervisor copy' });
424
+ }
425
+
426
+ /**
427
+ * `bytes` of this isolate for a facet's load (the wasm images read for it;
428
+ * `name` says whose), admitted on the supervisor budget
429
+ * (SupervisorAllocationBudget.acquireLoad): FIFO, the whole budget for one
430
+ * larger than it, refused by name after SUPERVISOR_LOAD_WAIT_MS. `signal`
431
+ * ends the wait.
432
+ *
433
+ * Lock order: supervisor credit, then a Dynamic Worker. Nothing waits for
434
+ * credit (SupervisorAllocationBudget acquire or acquireLoad, or growing a
435
+ * lease) while it holds a worker's fetch hold (beginLoaderFetch,
436
+ * claimAdmission, readmitRefused). While it holds one, it may give credit
437
+ * back, shrink it, or take credit without waiting (tryAcquire). Running work
438
+ * is the one exception, and comes first. Credit a running process needs (its
439
+ * write waves and the session RPCs it makes, through acquireRunning)
440
+ * overtakes every queued ask. When nothing else frees it, it reclaims parked
441
+ * launches' retained maps, oldest parked first: each is dropped, its launch
442
+ * is sent back to prepare, and the map is rebuilt at that launch's turn.
443
+ * Otherwise it waits only for credit other running work holds. A facet's load
444
+ * is not running work.
445
+ */
446
+ export function acquireSupervisorLoad(name: string, bytes: number, signal?: AbortSignal, owner?: SupervisorAllocationOwner): Promise<SupervisorAllocationLease> {
447
+ return supervisorAllocationBudget.acquireLoad(name, bytes, { waitMs: SUPERVISOR_LOAD_WAIT_MS, signal, owner });
448
+ }
449
+
450
+ /** acquireSupervisorLoad's credit now, without waiting, or null: all a load may take while it holds a worker. */
451
+ export function tryAcquireSupervisorLoad(bytes: number, owner: SupervisorAllocationOwner = { owner: 'facet load' }): SupervisorAllocationLease | null {
452
+ return supervisorAllocationBudget.tryAcquire(Math.min(bytes, supervisorAllocationBudget.capacity), false, owner);
453
+ }
454
+
455
+ /**
456
+ * Running work's credit (SupervisorAllocationBudget.acquireRunning): what a
457
+ * running process needs to go on (its write waves, the session RPCs it
458
+ * makes), never held up by a launch that is not running.
459
+ */
460
+ export function acquireSupervisorRunningAllocation(bytes: number, signal?: AbortSignal, owner?: SupervisorAllocationOwner): Promise<SupervisorAllocationLease> {
461
+ return supervisorAllocationBudget.acquireRunning(bytes, signal, true, owner ?? { owner: 'running supervisor work' });
462
+ }
463
+
464
+ /** acquireSupervisorRunningAllocation's credit now, ahead of every queued ask, without reclaiming, or null. */
465
+ export function tryAcquireSupervisorRunningAllocation(bytes: number, owner?: SupervisorAllocationOwner): SupervisorAllocationLease | null {
466
+ return supervisorAllocationBudget.tryAcquireRunning(bytes, owner ?? { owner: 'running supervisor work' });
467
+ }
468
+
469
+ /** Register a parked launch's reclaimer on the isolate's budget (SupervisorAllocationBudget.reclaimable). */
470
+ export function reclaimableSupervisorAllocation(reclaim: () => void): () => void {
471
+ return supervisorAllocationBudget.reclaimable(reclaim);
472
+ }
473
+
474
+ /** Run `load` holding acquireSupervisorLoad's credit for `bytes`, given back as it settles; for a load no worker hold waits on. */
475
+ export async function withSupervisorLoadAllocation<T>(
476
+ name: string,
477
+ bytes: number,
478
+ signal: AbortSignal | undefined,
479
+ load: () => Promise<T>,
480
+ owner?: SupervisorAllocationOwner,
481
+ ): Promise<T> {
482
+ if (bytes === 0) return load();
483
+ const lease = await acquireSupervisorLoad(name, bytes, signal, owner);
484
+ try {
485
+ return await load();
486
+ } finally {
487
+ lease.release();
488
+ }
239
489
  }
240
490
 
241
491
  /**
242
492
  * Reserve the full budget for an allocation whose retained size is not known
243
493
  * accurately enough to overlap safely with other heavy work.
244
494
  */
245
- export async function acquireHeavyAlloc(signal?: AbortSignal): Promise<() => void> {
495
+ export async function acquireHeavyAlloc(signal?: AbortSignal, owner: SupervisorAllocationOwner = { owner: 'heavy supervisor work' }): Promise<() => void> {
246
496
  const lease = await acquireSupervisorAllocation(
247
497
  SUPERVISOR_IN_FLIGHT_ALLOCATION_BUDGET_BYTES,
248
498
  signal,
499
+ owner,
249
500
  );
250
501
  return lease.release;
251
502
  }
@@ -253,3 +504,7 @@ export async function acquireHeavyAlloc(signal?: AbortSignal): Promise<() => voi
253
504
  export function readSupervisorAllocationBudget(): SupervisorAllocationBudgetStats {
254
505
  return supervisorAllocationBudget.stats;
255
506
  }
507
+
508
+ export function readSupervisorAllocationAttribution(): Record<AllocationAttribution, number> {
509
+ return supervisorAllocationBudget.attributed;
510
+ }
package/src/hex.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * hex.ts — bytes as unseparated lowercase hex: the spelling of every content
3
+ * key, digest, object id and token Nimbus writes. It imports nothing, so a
4
+ * compiled guest body or a facet bundle takes it along; a hash is its
5
+ * caller's, as is any width, offset or truncation.
6
+ */
7
+
8
+ /**
9
+ * Each byte value's two hex digits: for a reader that spells a window of a
10
+ * larger buffer in place (git's object ids in a pack or a tree), where V8
11
+ * encodes a view natively slower than this table walks it.
12
+ */
13
+ export const HEX_BYTE: readonly string[] = Array.from({ length: 256 }, (_, byte) => byte.toString(16).padStart(2, '0'));
14
+ /** Uint8Array.prototype, with ES2026's toHex where the runtime has it: this TypeScript's library does not declare it. */
15
+ const prototype: Uint8Array & { toHex?: (this: Uint8Array) => string } = Uint8Array.prototype;
16
+ const nativeHex = prototype.toHex;
17
+
18
+ /**
19
+ * Lowercase hex of `bytes`. A listing encodes one key per file, so this is
20
+ * on the enumeration's hot path: the native encoder where the runtime has
21
+ * one, else a table (measured 0.03 and 0.17 µs per 32-byte key, against
22
+ * 0.82 for a toString/padStart loop).
23
+ */
24
+ export function bytesToHex(bytes: Uint8Array): string {
25
+ if (nativeHex !== undefined) return nativeHex.call(bytes);
26
+ let out = '';
27
+ for (let i = 0; i < bytes.length; i++) out += HEX_BYTE[bytes[i]!];
28
+ return out;
29
+ }
@@ -2,21 +2,7 @@
2
2
  * _shared/install-phase.ts — single source of truth for the npm-install
3
3
  * state-machine phase enum.
4
4
  *
5
- * History: CLN-1 (2026-05-11) — consolidated from two diverged copies.
6
- * - `npm/installer.ts:93-95` was the progress-event variant. Included
7
- * 'lock-check' (a real progress event emitted while parsing
8
- * package-lock.json) but lacked 'idle'.
9
- * - `observability/diag-counters.ts:33-35` was the counter-state variant.
10
- * Included 'idle' (the resting state when no install is in flight)
11
- * but lacked 'lock-check'. The diag-counter's `setInstallPhase()`
12
- * was actually called with 'idle' (installer.ts:184,201) — passing
13
- * a value not declared in installer.ts's local type. The type check
14
- * fell through to diag-counters' type because installer.ts imports
15
- * setInstallPhase from there; the local `InstallPhase` definition
16
- * in installer.ts was only used by `InstallProgress.phase`.
17
- *
18
- * Going forward both consumers import from this module; any new phase
19
- * value added here is automatically available everywhere.
5
+ * Every consumer imports it from here, so a phase added here is everyone's.
20
6
  *
21
7
  * Phase semantics (chronological order for a single install run):
22
8
  *
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The names of `path` once it is resolved lexically, after the names of
3
+ * `base` (taken as they are, not reduced again): empty names and `.`
4
+ * dropped, each `..` taking back the name before it, and a `..` at the root
5
+ * staying at the root, as POSIX resolves `/..`. The one root-clamped lexical
6
+ * reduction; whether the path was absolute or ended in `/` is its caller's to
7
+ * keep. Lexical only: a `..` after a symlink is not the link's parent, so a
8
+ * walk that follows links resolves it as it goes.
9
+ */
10
+ export function rootClampedSegments(path: string, base: readonly string[] = []): string[] {
11
+ const out = [...base];
12
+ for (const segment of path.split('/')) {
13
+ if (segment === '..') out.pop();
14
+ else if (segment !== '' && segment !== '.') out.push(segment);
15
+ }
16
+ return out;
17
+ }