@nimbus-sh/platform 0.5.1 → 0.7.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/composition.d.ts +2 -1
- package/dist/composition.d.ts.map +1 -1
- package/dist/composition.js +33 -1
- package/dist/crc32.d.ts +19 -0
- package/dist/crc32.d.ts.map +1 -0
- package/dist/crc32.js +67 -0
- package/dist/diag-counters.d.ts +0 -8
- package/dist/diag-counters.d.ts.map +1 -1
- package/dist/diag-counters.js +0 -6
- package/dist/heap-estimate.d.ts +1 -7
- package/dist/heap-estimate.d.ts.map +1 -1
- package/dist/heap-estimate.js +1 -3
- package/dist/heavy-alloc-coord.d.ts +0 -13
- package/dist/heavy-alloc-coord.d.ts.map +1 -1
- package/dist/heavy-alloc-coord.js +2 -19
- package/dist/limits.d.ts +4 -1
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +29 -1
- package/dist/oom-classify.d.ts +1 -1
- package/dist/oom-classify.d.ts.map +1 -1
- package/dist/oom-classify.js +8 -6
- package/dist/tracing.d.ts +65 -0
- package/dist/tracing.d.ts.map +1 -0
- package/dist/tracing.js +107 -0
- package/dist/w7-frame.d.ts.map +1 -1
- package/dist/w7-frame.js +12 -35
- package/dist/weighted-credit-pool.d.ts +0 -13
- package/dist/weighted-credit-pool.d.ts.map +1 -1
- package/dist/weighted-credit-pool.js +6 -54
- package/package.json +4 -4
- package/src/composition.ts +34 -1
- package/src/crc32.ts +71 -0
- package/src/diag-counters.ts +0 -13
- package/src/heap-estimate.ts +1 -9
- package/src/heavy-alloc-coord.ts +1 -26
- package/src/limits.ts +32 -1
- package/src/oom-classify.ts +8 -6
- package/src/tracing.ts +129 -0
- package/src/w7-frame.ts +12 -38
- package/src/weighted-credit-pool.ts +6 -61
package/src/heavy-alloc-coord.ts
CHANGED
|
@@ -24,8 +24,6 @@ export interface SupervisorAllocationBudgetStats {
|
|
|
24
24
|
readonly current: number;
|
|
25
25
|
readonly peak: number;
|
|
26
26
|
readonly queued: number;
|
|
27
|
-
/** Credit held for an owner's whole lifetime; see `acquireResident`. */
|
|
28
|
-
readonly resident: number;
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
interface AllocationBudgetLifecycle {
|
|
@@ -84,24 +82,12 @@ export class SupervisorAllocationBudget {
|
|
|
84
82
|
return this._acquire(bytes, signal, false);
|
|
85
83
|
}
|
|
86
84
|
|
|
87
|
-
/**
|
|
88
|
-
* Reserve bytes an owner holds for its whole lifetime, not for one
|
|
89
|
-
* operation. Recorded as a floor: a later claim larger than what remains
|
|
90
|
-
* around it is refused with both numbers rather than parked forever.
|
|
91
|
-
*/
|
|
92
|
-
acquireResidentBytes(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease> {
|
|
93
|
-
return this._acquire(bytes, signal, true, true);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
85
|
private async _acquire(
|
|
97
86
|
bytes: number,
|
|
98
87
|
signal: AbortSignal | undefined,
|
|
99
88
|
drivesLifecycle: boolean,
|
|
100
|
-
resident = false,
|
|
101
89
|
): Promise<ResizableCreditLease> {
|
|
102
|
-
const credit =
|
|
103
|
-
? await this.credits.acquireResident(bytes, signal)
|
|
104
|
-
: await this.credits.acquire(bytes, signal);
|
|
90
|
+
const credit = await this.credits.acquire(bytes, signal);
|
|
105
91
|
if (drivesLifecycle) {
|
|
106
92
|
this.lifecycleHolders++;
|
|
107
93
|
if (!this.active) {
|
|
@@ -238,17 +224,6 @@ export function acquireSupervisorReadAllocation(
|
|
|
238
224
|
return supervisorAllocationBudget.acquireWithoutLifecycle(bytes, signal);
|
|
239
225
|
}
|
|
240
226
|
|
|
241
|
-
/**
|
|
242
|
-
* Reserve bytes for the whole life of the owner that takes them — an image
|
|
243
|
-
* a pool keeps until it is disposed. See `acquireResidentBytes`.
|
|
244
|
-
*/
|
|
245
|
-
export function acquireResidentSupervisorAllocation(
|
|
246
|
-
bytes: number,
|
|
247
|
-
signal?: AbortSignal,
|
|
248
|
-
): Promise<ResizableCreditLease> {
|
|
249
|
-
return supervisorAllocationBudget.acquireResidentBytes(bytes, signal);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
227
|
/**
|
|
253
228
|
* Reserve the full budget for an allocation whose retained size is not known
|
|
254
229
|
* accurately enough to overlap safely with other heavy work.
|
package/src/limits.ts
CHANGED
|
@@ -52,6 +52,12 @@ export const SQL_MAX_BOUND_PARAMETERS = 100;
|
|
|
52
52
|
*/
|
|
53
53
|
export const DO_STORAGE_LIMIT_BYTES = 10_000_000_000;
|
|
54
54
|
|
|
55
|
+
// N17: how long a launch that reads synchronously (WASI) waits for the paths
|
|
56
|
+
// it names to hydrate out of a lazy import before it fails with EIO naming
|
|
57
|
+
// the first that is not local. Bounded so a fetch that never resolves cannot
|
|
58
|
+
// hold a launch forever.
|
|
59
|
+
export const HYDRATION_DEADLINE_MS = 30_000;
|
|
60
|
+
|
|
55
61
|
/**
|
|
56
62
|
* Maximum SQL statement text per exec. Documented "100 KB"; read as binary
|
|
57
63
|
* KiB because the value is SQLite's compile-time SQLITE_MAX_SQL_LENGTH
|
|
@@ -119,7 +125,7 @@ export const RESIDENT_KEEPALIVE_DETACHED_MS = 60_000;
|
|
|
119
125
|
|
|
120
126
|
/**
|
|
121
127
|
* Bytes one hibernatable WebSocket attachment may serialize to. Verified in
|
|
122
|
-
* workerd source at the pinned version (v1.
|
|
128
|
+
* workerd source at the pinned version (v1.20260926.1,
|
|
123
129
|
* src/workerd/api/web-socket.h: `MAX_ATTACHMENT_SIZE = 1024 * 16`). The
|
|
124
130
|
* bound is on the SERIALIZED bytes — workerd re-serializes on every
|
|
125
131
|
* `serializeAttachment` call to check it — not on UTF-8 JSON text, so a
|
|
@@ -136,6 +142,25 @@ export const WS_ATTACHMENT_LIMIT_BYTES = 16_384;
|
|
|
136
142
|
// metadata and matches the proven on-demand facet transfer envelope.
|
|
137
143
|
export const MAX_RPC_SAFE_PAYLOAD_BYTES = 28 * 1024 * 1024;
|
|
138
144
|
|
|
145
|
+
// ── A one-shot's module map [#13] ───────────────────────────────────────
|
|
146
|
+
//
|
|
147
|
+
// The largest one-shot module map (its closure's raw bytes) the session DO
|
|
148
|
+
// builds and hands to the Worker Loader. What limits it is the DO, not the
|
|
149
|
+
// loader (whose documented limits are concurrency only): the map is built,
|
|
150
|
+
// serialized and passed on from the DO's 128 MB isolate.
|
|
151
|
+
//
|
|
152
|
+
// Measured on a throwaway (2026-09-26; session DO max memoryUsageBytes from
|
|
153
|
+
// GraphQL durableObjectsPeriodicGroups, one launch per minute; a package of
|
|
154
|
+
// 256 KiB distinct modules required whole, verify/release/do-headroom.md),
|
|
155
|
+
// with the map serialized one side module at a time:
|
|
156
|
+
// 4 MiB 64 MB, 8 MiB 81 MB, 12 MiB 97 MB, 16 MiB 110 MB, 20 MiB 126 MB,
|
|
157
|
+
// 24 MiB 145 MB, 28 MiB 164 MB, over an idle 40-55 MB.
|
|
158
|
+
// That is about 4.5 MB of DO per MiB of map. 18 MiB, about 118 MB, is the
|
|
159
|
+
// largest size that leaves about 10 MB of the 128 MB for the rest of the
|
|
160
|
+
// session's work. (Before the single-copy build it was 6-7 MB per MiB: 24 MiB
|
|
161
|
+
// reset the DO.)
|
|
162
|
+
export const ONE_SHOT_MODULE_MAP_MAX_BYTES = 18 * 1024 * 1024;
|
|
163
|
+
|
|
139
164
|
// ── Supervisor heap budget [C'.1] ───────────────────────────────────────
|
|
140
165
|
//
|
|
141
166
|
// Three distinct memory regimes, not one shared pool:
|
|
@@ -165,6 +190,12 @@ export const MAX_RPC_SAFE_PAYLOAD_BYTES = 28 * 1024 * 1024;
|
|
|
165
190
|
// percentOfCeiling accordingly.
|
|
166
191
|
export const SUPERVISOR_HEAP_CEILING_BYTES = 64 * 1024 * 1024;
|
|
167
192
|
|
|
193
|
+
// A process facet's own writes its store could not hold (N18: the session's
|
|
194
|
+
// storage is full) are kept in its heap instead, so a write-then-read still
|
|
195
|
+
// works: at most this many bytes, least recently used out first. An eighth
|
|
196
|
+
// of a facet's measured ~208-256 MiB, the rest left to the program.
|
|
197
|
+
export const FACET_OWN_WRITE_MEMORY_BYTES = 32 * 1024 * 1024;
|
|
198
|
+
|
|
168
199
|
// Shared allowance for transient allocations in the supervisor DO. With the
|
|
169
200
|
// VFS LRU shrunk to 8 MiB during an active reservation, 40 MiB of admitted
|
|
170
201
|
// payload plus the 9 MiB bundle baseline stays below the 64 MiB soft ceiling
|
package/src/oom-classify.ts
CHANGED
|
@@ -127,11 +127,12 @@ export function classifyMessage(msg: string): OomCause {
|
|
|
127
127
|
// Subrequest cap (Cloudflare platform)
|
|
128
128
|
if (m.includes('too many subrequests')) return 'subrequest_cap';
|
|
129
129
|
|
|
130
|
-
// Worker
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
130
|
+
// Dynamic Worker concurrency limit (Cloudflare platform): a Durable Object
|
|
131
|
+
// may have a fixed number of distinct Dynamic Workers with in-flight
|
|
132
|
+
// requests at once (fabric budgets.ts, DO_DYNAMIC_WORKER_LIMIT). Production
|
|
133
|
+
// words it "Dynamic worker concurrency limit exceeded: each request may
|
|
134
|
+
// have up to 10 concurrent dynamic worker invocations. …".
|
|
135
|
+
if (m.includes('dynamic worker concurrency limit exceeded')) return 'dynamic_worker_cap';
|
|
135
136
|
|
|
136
137
|
// Memory exhaustion. workerd trace outcome `exceededMemory`; in-band it
|
|
137
138
|
// arrives as a `broken.exceededMemory` actor break, a "…exceeded [its]
|
|
@@ -224,7 +225,7 @@ export function isDoOverloaded(input: unknown): boolean {
|
|
|
224
225
|
|
|
225
226
|
/**
|
|
226
227
|
* How one failed Durable Object call relates to a retry. The taxonomy
|
|
227
|
-
* Proteus hand-wrote in `cf-backend/src/lib/do-rpc.ts
|
|
228
|
+
* Proteus originally hand-wrote in `cf-backend/src/lib/do-rpc.ts` because the
|
|
228
229
|
* Agents SDK does not export its own, plus the `overloaded` class both
|
|
229
230
|
* consumers need: Cloudflare documents that errors carry `.retryable` and
|
|
230
231
|
* `.overloaded`, that a retryable error should be retried with backoff on a
|
|
@@ -329,6 +330,7 @@ export function describeError(input: unknown): string {
|
|
|
329
330
|
const suffix = cause !== 'unknown' ? ` [${cause}]`
|
|
330
331
|
: isTransientDoReset(input) ? ' [transient-do-reset]'
|
|
331
332
|
: isDoOverloaded(input) ? ' [do-overloaded]'
|
|
333
|
+
: classifyDoCall(input) === 'retryable_flag' ? ' [retryable]'
|
|
332
334
|
: '';
|
|
333
335
|
return `${name}${message}${suffix}`;
|
|
334
336
|
}
|
package/src/tracing.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tracing.ts — Workers custom spans, for modules that cannot import
|
|
3
|
+
* `cloudflare:workers`, as best-effort telemetry that never changes an
|
|
4
|
+
* outcome.
|
|
5
|
+
*
|
|
6
|
+
* The span API is `tracing` from `cloudflare:workers`
|
|
7
|
+
* (https://developers.cloudflare.com/workers/observability/traces/custom-spans/):
|
|
8
|
+
* `enterSpan(name, callback)` runs the callback in a span that is the active
|
|
9
|
+
* parent of every span and platform call made inside it — automatic RPC
|
|
10
|
+
* spans included — and ends it when the callback returns or its promise
|
|
11
|
+
* settles. A span records nothing unless the invocation was head-sampled
|
|
12
|
+
* (`observability.traces.head_sampling_rate`).
|
|
13
|
+
*
|
|
14
|
+
* `setAttributes` and `recordException` shipped on 2026-09-25
|
|
15
|
+
* (https://developers.cloudflare.com/changelog/post/2026-09-25-custom-span-apis/);
|
|
16
|
+
* a runtime from before then has `isTraced` and `setAttribute` only. So the
|
|
17
|
+
* code being traced never touches the runtime's span. It gets a
|
|
18
|
+
* {@link SpanRecorder}, which records only while the span is traced, uses
|
|
19
|
+
* whichever methods the runtime has, and swallows anything a span method
|
|
20
|
+
* throws.
|
|
21
|
+
*
|
|
22
|
+
* Core and fabric stay importable outside workerd, so they reach the API
|
|
23
|
+
* through this leaf: the Worker's composition root hands it over once, at
|
|
24
|
+
* module scope, with {@link adoptTracing}, exactly as it does `ctx.exports`
|
|
25
|
+
* (./composition.ts). With nothing adopted — unit tests, an embedder that
|
|
26
|
+
* never adopts — {@link traced} runs its callback with {@link untraced}.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** Attribute values a span holds; `undefined` entries are ignored. */
|
|
30
|
+
export type SpanAttributes = Record<string, string | number | boolean | undefined>;
|
|
31
|
+
|
|
32
|
+
/** The runtime's `Span`, as any Workers runtime with `tracing` may have it. */
|
|
33
|
+
export interface RuntimeSpan {
|
|
34
|
+
readonly isTraced: boolean;
|
|
35
|
+
setAttribute?(key: string, value?: string | number | boolean): unknown;
|
|
36
|
+
setAttributes?(attributes: SpanAttributes): unknown;
|
|
37
|
+
recordException?(
|
|
38
|
+
exception: string | { code?: string | number; name?: string; message?: string; stack?: string },
|
|
39
|
+
): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The part of `cloudflare:workers`'s `tracing` Nimbus uses. */
|
|
43
|
+
export interface Tracer {
|
|
44
|
+
enterSpan<T>(name: string, callback: (span: RuntimeSpan) => T): T;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** What traced code records on its span. Never throws; records nothing unless the span is traced. */
|
|
48
|
+
export interface SpanRecorder {
|
|
49
|
+
set(attributes: SpanAttributes): void;
|
|
50
|
+
/**
|
|
51
|
+
* Record `error` as an exception event. `code`, when given, replaces the
|
|
52
|
+
* error's own; `context` is prefixed to its message.
|
|
53
|
+
*/
|
|
54
|
+
exception(error: unknown, code?: string, context?: string): void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The recorder of a span that is not traced, or of no span at all. */
|
|
58
|
+
export const untraced: SpanRecorder = Object.freeze({ set() {}, exception() {} });
|
|
59
|
+
|
|
60
|
+
let tracer: Tracer | null = null;
|
|
61
|
+
|
|
62
|
+
/** Hand this isolate's `tracing` over. The first adoption stands, like `adoptCtxExports`. */
|
|
63
|
+
export function adoptTracing(value: Tracer): void {
|
|
64
|
+
tracer ??= value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function recorderFor(span: RuntimeSpan): SpanRecorder {
|
|
68
|
+
try {
|
|
69
|
+
if (!span.isTraced) return untraced;
|
|
70
|
+
} catch {
|
|
71
|
+
return untraced;
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
set(attributes) {
|
|
75
|
+
try {
|
|
76
|
+
if (typeof span.setAttributes === 'function') {
|
|
77
|
+
span.setAttributes(attributes);
|
|
78
|
+
} else if (typeof span.setAttribute === 'function') {
|
|
79
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
80
|
+
if (value !== undefined) span.setAttribute(key, value);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch { /* telemetry is best-effort */ }
|
|
84
|
+
},
|
|
85
|
+
exception(error, code, context = '') {
|
|
86
|
+
try {
|
|
87
|
+
if (typeof span.recordException !== 'function') return;
|
|
88
|
+
if (error instanceof Error) {
|
|
89
|
+
const own = 'code' in error ? error.code : undefined;
|
|
90
|
+
span.recordException({
|
|
91
|
+
code: code ?? (typeof own === 'string' || typeof own === 'number' ? own : undefined),
|
|
92
|
+
name: error.name,
|
|
93
|
+
message: context + error.message,
|
|
94
|
+
stack: error.stack,
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
span.recordException(code === undefined && !context
|
|
98
|
+
? String(error)
|
|
99
|
+
: { code, message: context + String(error) });
|
|
100
|
+
}
|
|
101
|
+
} catch { /* telemetry is best-effort */ }
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Run `fn` in a span named `name` carrying `attributes`, and record on it the
|
|
108
|
+
* exception `fn` throws or its promise rejects with. What `fn` returns or
|
|
109
|
+
* throws reaches the caller unchanged, whatever the span does.
|
|
110
|
+
*/
|
|
111
|
+
export function traced<T>(name: string, attributes: SpanAttributes, fn: (span: SpanRecorder) => T): T {
|
|
112
|
+
if (!tracer) return fn(untraced);
|
|
113
|
+
return tracer.enterSpan(name, (runtimeSpan) => {
|
|
114
|
+
const span = recorderFor(runtimeSpan);
|
|
115
|
+
span.set(attributes);
|
|
116
|
+
let result: T;
|
|
117
|
+
try {
|
|
118
|
+
result = fn(span);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
span.exception(error);
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
if (!(result instanceof Promise)) return result;
|
|
124
|
+
return result.catch((error: unknown) => {
|
|
125
|
+
span.exception(error);
|
|
126
|
+
throw error;
|
|
127
|
+
}) as T;
|
|
128
|
+
});
|
|
129
|
+
}
|
package/src/w7-frame.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* The format is internal: every producer and consumer deploys together.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { crc32 } from './crc32.js';
|
|
6
7
|
import { CHUNK_SIZE } from './limits.js';
|
|
7
8
|
|
|
8
9
|
// The batch payload types live WITH the wire format that encodes them: every
|
|
@@ -246,7 +247,7 @@ export async function decodeWriteBatchStream(
|
|
|
246
247
|
}
|
|
247
248
|
const beginPayload = await buffer.readExact(beginEnvelope.length, 'batch-begin payload');
|
|
248
249
|
const begin = parseBatchBegin(beginPayload);
|
|
249
|
-
const initialCheck = updateRecordCheck(
|
|
250
|
+
const initialCheck = updateRecordCheck(0, beginEnvelope.header, beginPayload);
|
|
250
251
|
handedOff = true;
|
|
251
252
|
return {
|
|
252
253
|
batchId: begin.id,
|
|
@@ -343,7 +344,7 @@ async function* decodeRecords(
|
|
|
343
344
|
throwIfAborted(options.signal);
|
|
344
345
|
const data = await buffer.readExact(dataLength, 'file-chunk data');
|
|
345
346
|
batchCheck = updateRecordCheck(batchCheck, envelope.header, headerPayload, data);
|
|
346
|
-
active.check =
|
|
347
|
+
active.check = crc32(data, active.check);
|
|
347
348
|
active.nextChunkId++;
|
|
348
349
|
active.receivedBytes += dataLength;
|
|
349
350
|
summary.recordCount++;
|
|
@@ -406,7 +407,7 @@ async function* decodeRecords(
|
|
|
406
407
|
inode,
|
|
407
408
|
nextChunkId: 0,
|
|
408
409
|
receivedBytes: 0,
|
|
409
|
-
check:
|
|
410
|
+
check: 0,
|
|
410
411
|
};
|
|
411
412
|
yield { type: 'file-begin', streamContentId: metadata.contentId, inode };
|
|
412
413
|
break;
|
|
@@ -414,7 +415,7 @@ async function* decodeRecords(
|
|
|
414
415
|
case RecordTag.FileEnd: {
|
|
415
416
|
if (!active) throw new Error('w7-frame: file-end without active file');
|
|
416
417
|
const metadata = parseFileEnd(payload);
|
|
417
|
-
const actualCheck =
|
|
418
|
+
const actualCheck = active.check;
|
|
418
419
|
if (metadata.contentId !== active.metadata.contentId) {
|
|
419
420
|
throw new Error(`w7-frame: file-end content id mismatch for ${active.inode.path}`);
|
|
420
421
|
}
|
|
@@ -443,7 +444,7 @@ async function* decodeRecords(
|
|
|
443
444
|
case RecordTag.BatchEnd: {
|
|
444
445
|
if (active) throw new Error(`w7-frame: batch-end while file ${active.inode.path} is active`);
|
|
445
446
|
const actual = parseBatchEnd(payload);
|
|
446
|
-
const expected = { ...summary, check:
|
|
447
|
+
const expected = { ...summary, check: batchCheck };
|
|
447
448
|
if (!sameSummary(actual, expected)) {
|
|
448
449
|
throw new Error(
|
|
449
450
|
`w7-frame: batch-end summary mismatch; expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
|
|
@@ -479,7 +480,7 @@ function* encodeRecords(
|
|
|
479
480
|
files: EncoderFile[],
|
|
480
481
|
): Generator<Uint8Array[]> {
|
|
481
482
|
const state: EncoderState = {
|
|
482
|
-
batchCheck:
|
|
483
|
+
batchCheck: 0,
|
|
483
484
|
summary: {
|
|
484
485
|
recordCount: 0,
|
|
485
486
|
pathCount: 0,
|
|
@@ -514,7 +515,7 @@ function* encodeRecords(
|
|
|
514
515
|
size: file.inode.size,
|
|
515
516
|
chunkCount: file.inode.chunkCount,
|
|
516
517
|
}, state);
|
|
517
|
-
let fileCheck =
|
|
518
|
+
let fileCheck = 0;
|
|
518
519
|
for (const chunk of file.chunks) {
|
|
519
520
|
const data = chunk.data;
|
|
520
521
|
const contentBytes = new TextEncoder().encode(file.contentId);
|
|
@@ -528,19 +529,19 @@ function* encodeRecords(
|
|
|
528
529
|
state.summary.recordCount++;
|
|
529
530
|
state.summary.chunkCount++;
|
|
530
531
|
state.summary.byteCount += data.byteLength;
|
|
531
|
-
fileCheck =
|
|
532
|
+
fileCheck = crc32(data, fileCheck);
|
|
532
533
|
yield [concatBytes(header, prefix), data];
|
|
533
534
|
}
|
|
534
535
|
yield encodeMetadataRecord(RecordTag.FileEnd, {
|
|
535
536
|
contentId: file.contentId,
|
|
536
537
|
size: file.inode.size,
|
|
537
538
|
chunkCount: file.inode.chunkCount,
|
|
538
|
-
check:
|
|
539
|
+
check: fileCheck,
|
|
539
540
|
}, state);
|
|
540
541
|
}
|
|
541
542
|
const end: W7BatchSummary = {
|
|
542
543
|
...state.summary,
|
|
543
|
-
check:
|
|
544
|
+
check: state.batchCheck,
|
|
544
545
|
};
|
|
545
546
|
yield encodeMetadataRecord(RecordTag.BatchEnd, end);
|
|
546
547
|
}
|
|
@@ -916,37 +917,10 @@ function readU32LE(bytes: Uint8Array, offset: number): number {
|
|
|
916
917
|
|
|
917
918
|
function updateRecordCheck(seed: number, ...parts: Uint8Array[]): number {
|
|
918
919
|
let check = seed;
|
|
919
|
-
for (const part of parts) check =
|
|
920
|
+
for (const part of parts) check = crc32(part, check);
|
|
920
921
|
return check;
|
|
921
922
|
}
|
|
922
923
|
|
|
923
|
-
const CRC_SEED = 0xffff_ffff;
|
|
924
|
-
let crcTable: Uint32Array | null = null;
|
|
925
|
-
|
|
926
|
-
function crc32Update(check: number, bytes: Uint8Array): number {
|
|
927
|
-
crcTable ??= createCrcTable();
|
|
928
|
-
const table = crcTable;
|
|
929
|
-
let value = check;
|
|
930
|
-
for (const byte of bytes) value = table[(value ^ byte) & 0xff] ^ (value >>> 8);
|
|
931
|
-
return value >>> 0;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
function crc32Finish(check: number): number {
|
|
935
|
-
return (check ^ 0xffff_ffff) >>> 0;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
function createCrcTable(): Uint32Array {
|
|
939
|
-
const table = new Uint32Array(256);
|
|
940
|
-
for (let index = 0; index < table.length; index++) {
|
|
941
|
-
let value = index;
|
|
942
|
-
for (let bit = 0; bit < 8; bit++) {
|
|
943
|
-
value = (value & 1) !== 0 ? 0xedb8_8320 ^ (value >>> 1) : value >>> 1;
|
|
944
|
-
}
|
|
945
|
-
table[index] = value >>> 0;
|
|
946
|
-
}
|
|
947
|
-
return table;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
924
|
function noopRetention(bytes: number): W7ChunkRetention {
|
|
951
925
|
return { bytes, release() {} };
|
|
952
926
|
}
|
|
@@ -11,11 +11,6 @@ export interface WeightedCreditPoolStats {
|
|
|
11
11
|
readonly current: number;
|
|
12
12
|
readonly peak: number;
|
|
13
13
|
readonly queued: number;
|
|
14
|
-
/**
|
|
15
|
-
* Credit held by owners that keep it for their whole lifetime rather than
|
|
16
|
-
* for one operation — the floor no waiter can ever expect to get back.
|
|
17
|
-
*/
|
|
18
|
-
readonly resident: number;
|
|
19
14
|
}
|
|
20
15
|
|
|
21
16
|
export interface WeightedCreditPoolOptions {
|
|
@@ -34,8 +29,6 @@ export interface WeightedCreditPoolOptions {
|
|
|
34
29
|
|
|
35
30
|
interface CreditWaiter {
|
|
36
31
|
readonly bytes: number;
|
|
37
|
-
/** Whether the granted lease counts toward the resident floor. */
|
|
38
|
-
readonly resident: boolean;
|
|
39
32
|
readonly resolve: (lease: ResizableCreditLease) => void;
|
|
40
33
|
readonly reject: (error: Error) => void;
|
|
41
34
|
readonly signal?: AbortSignal;
|
|
@@ -46,55 +39,27 @@ interface CreditWaiter {
|
|
|
46
39
|
class CreditLane {
|
|
47
40
|
private current = 0;
|
|
48
41
|
private peak = 0;
|
|
49
|
-
private resident = 0;
|
|
50
42
|
private readonly waiters: CreditWaiter[] = [];
|
|
51
43
|
|
|
52
44
|
constructor(readonly capacity: number) {}
|
|
53
45
|
|
|
54
46
|
get stats(): WeightedCreditPoolStats {
|
|
55
|
-
return {
|
|
56
|
-
current: this.current, peak: this.peak, queued: this.waiters.length, resident: this.resident,
|
|
57
|
-
};
|
|
47
|
+
return { current: this.current, peak: this.peak, queued: this.waiters.length };
|
|
58
48
|
}
|
|
59
49
|
|
|
60
50
|
tryAcquire(bytes: number): ResizableCreditLease | null {
|
|
61
51
|
if (this.waiters.length > 0 || this.current + bytes > this.capacity) return null;
|
|
62
|
-
return this.grant(bytes
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* The largest claim this lane can ever grant: capacity less what resident
|
|
67
|
-
* owners hold for their whole lifetime. A claim above it cannot be
|
|
68
|
-
* satisfied by waiting, because nothing it is waiting for will be
|
|
69
|
-
* released.
|
|
70
|
-
*/
|
|
71
|
-
get grantableCeiling(): number {
|
|
72
|
-
return this.capacity - this.resident;
|
|
52
|
+
return this.grant(bytes);
|
|
73
53
|
}
|
|
74
54
|
|
|
75
|
-
acquire(bytes: number, signal?: AbortSignal
|
|
55
|
+
acquire(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease> {
|
|
76
56
|
if (signal?.aborted) return Promise.reject(abortError(signal));
|
|
77
|
-
|
|
78
|
-
// in the FIFO forever. A parked claim is indistinguishable from a caller
|
|
79
|
-
// that stopped: no error, no timeout, and — because the queue refuses
|
|
80
|
-
// everyone behind it — no further progress anywhere in the isolate. That
|
|
81
|
-
// is what a real-vite launch looked like for four rounds of debugging.
|
|
82
|
-
if (bytes > this.grantableCeiling) {
|
|
83
|
-
return Promise.reject(new RangeError(
|
|
84
|
-
`weighted credit claim of ${bytes} bytes can never be granted: capacity is ${this.capacity} `
|
|
85
|
-
+ `and ${this.resident} bytes are held by resident owners, leaving ${this.grantableCeiling}`,
|
|
86
|
-
));
|
|
87
|
-
}
|
|
88
|
-
const immediate = resident ? null : this.tryAcquire(bytes);
|
|
57
|
+
const immediate = this.tryAcquire(bytes);
|
|
89
58
|
if (immediate) return Promise.resolve(immediate);
|
|
90
|
-
if (resident && this.waiters.length === 0 && this.current + bytes <= this.capacity) {
|
|
91
|
-
return Promise.resolve(this.grant(bytes, true));
|
|
92
|
-
}
|
|
93
59
|
|
|
94
60
|
return new Promise<ResizableCreditLease>((resolve, reject) => {
|
|
95
61
|
const waiter: CreditWaiter = {
|
|
96
62
|
bytes,
|
|
97
|
-
resident,
|
|
98
63
|
resolve,
|
|
99
64
|
reject,
|
|
100
65
|
signal,
|
|
@@ -119,9 +84,8 @@ class CreditLane {
|
|
|
119
84
|
});
|
|
120
85
|
}
|
|
121
86
|
|
|
122
|
-
private grant(bytes: number
|
|
87
|
+
private grant(bytes: number): ResizableCreditLease {
|
|
123
88
|
this.current += bytes;
|
|
124
|
-
if (resident) this.resident += bytes;
|
|
125
89
|
this.peak = Math.max(this.peak, this.current);
|
|
126
90
|
let leasedBytes = bytes;
|
|
127
91
|
let released = false;
|
|
@@ -143,7 +107,6 @@ class CreditLane {
|
|
|
143
107
|
);
|
|
144
108
|
}
|
|
145
109
|
this.current -= leasedBytes - nextBytes;
|
|
146
|
-
if (resident) this.resident -= leasedBytes - nextBytes;
|
|
147
110
|
leasedBytes = nextBytes;
|
|
148
111
|
this.drain();
|
|
149
112
|
},
|
|
@@ -151,7 +114,6 @@ class CreditLane {
|
|
|
151
114
|
if (released) return;
|
|
152
115
|
released = true;
|
|
153
116
|
this.current -= leasedBytes;
|
|
154
|
-
if (resident) this.resident -= leasedBytes;
|
|
155
117
|
if (this.current < 0) {
|
|
156
118
|
throw new Error('weighted credit accounting underflow');
|
|
157
119
|
}
|
|
@@ -172,7 +134,7 @@ class CreditLane {
|
|
|
172
134
|
if (this.current + waiter.bytes > this.capacity) return;
|
|
173
135
|
this.waiters.shift();
|
|
174
136
|
waiter.signal?.removeEventListener('abort', waiter.onAbort!);
|
|
175
|
-
waiter.resolve(this.grant(waiter.bytes
|
|
137
|
+
waiter.resolve(this.grant(waiter.bytes));
|
|
176
138
|
}
|
|
177
139
|
}
|
|
178
140
|
}
|
|
@@ -233,7 +195,6 @@ export class WeightedCreditPool {
|
|
|
233
195
|
current: general.current + small.current,
|
|
234
196
|
peak: general.peak + small.peak,
|
|
235
197
|
queued: general.queued + small.queued,
|
|
236
|
-
resident: general.resident + small.resident,
|
|
237
198
|
};
|
|
238
199
|
}
|
|
239
200
|
|
|
@@ -265,22 +226,6 @@ export class WeightedCreditPool {
|
|
|
265
226
|
return this.general.acquire(bytes, signal);
|
|
266
227
|
}
|
|
267
228
|
|
|
268
|
-
/**
|
|
269
|
-
* Take credit that will be held for the owner's whole lifetime rather than
|
|
270
|
-
* for one operation — the esbuild pool's wasm image is the case.
|
|
271
|
-
*
|
|
272
|
-
* Recorded as a floor so a later claim that could never fit around it is
|
|
273
|
-
* refused with both numbers instead of parking in the FIFO forever.
|
|
274
|
-
*/
|
|
275
|
-
acquireResident(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease> {
|
|
276
|
-
try {
|
|
277
|
-
this.validateRequest(bytes);
|
|
278
|
-
} catch (error) {
|
|
279
|
-
return Promise.reject(error as Error);
|
|
280
|
-
}
|
|
281
|
-
return this.general.acquire(bytes, signal, true);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
229
|
private validateRequest(bytes: number): void {
|
|
285
230
|
if (!Number.isSafeInteger(bytes) || bytes <= 0 || bytes > this.capacity) {
|
|
286
231
|
throw new RangeError(
|