@nimbus-sh/platform 0.8.2 → 0.14.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 +3 -2
- package/dist/composition.d.ts.map +1 -1
- package/dist/composition.js +34 -19
- package/dist/crc32.d.ts +5 -5
- package/dist/crc32.d.ts.map +1 -1
- package/dist/crc32.js +8 -12
- package/dist/diag-counters.d.ts +18 -10
- package/dist/diag-counters.d.ts.map +1 -1
- package/dist/diag-counters.js +7 -20
- package/dist/heap-estimate.d.ts +5 -20
- package/dist/heap-estimate.d.ts.map +1 -1
- package/dist/heap-estimate.js +6 -33
- package/dist/heavy-alloc-coord.d.ts +131 -11
- package/dist/heavy-alloc-coord.d.ts.map +1 -1
- package/dist/heavy-alloc-coord.js +241 -22
- package/dist/hex.d.ts +20 -0
- package/dist/hex.d.ts.map +1 -0
- package/dist/hex.js +29 -0
- package/dist/install-phase.d.ts +1 -15
- package/dist/install-phase.d.ts.map +1 -1
- package/dist/lexical-path.d.ts +11 -0
- package/dist/lexical-path.d.ts.map +1 -0
- package/dist/lexical-path.js +19 -0
- package/dist/limits.d.ts +4 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +25 -0
- package/dist/module-source.d.ts.map +1 -1
- package/dist/module-source.js +2 -1
- package/dist/oom-classify.d.ts +4 -0
- package/dist/oom-classify.d.ts.map +1 -1
- package/dist/oom-classify.js +6 -0
- package/dist/realm-builtin.d.ts +2 -0
- package/dist/realm-builtin.d.ts.map +1 -0
- package/dist/realm-builtin.js +11 -0
- package/dist/utf8.d.ts +21 -1
- package/dist/utf8.d.ts.map +1 -1
- package/dist/utf8.js +73 -1
- package/dist/w7-frame.d.ts +92 -17
- package/dist/w7-frame.d.ts.map +1 -1
- package/dist/w7-frame.js +87 -35
- package/dist/wave-writer.d.ts +11 -6
- package/dist/wave-writer.d.ts.map +1 -1
- package/dist/wave-writer.js +28 -29
- package/dist/weighted-credit-pool.d.ts +43 -0
- package/dist/weighted-credit-pool.d.ts.map +1 -1
- package/dist/weighted-credit-pool.js +250 -46
- package/dist/worker-code.d.ts +48 -0
- package/dist/worker-code.d.ts.map +1 -0
- package/dist/worker-code.js +10 -0
- package/package.json +2 -2
- package/src/composition.ts +41 -21
- package/src/crc32.ts +9 -12
- package/src/diag-counters.ts +23 -22
- package/src/heap-estimate.ts +11 -40
- package/src/heavy-alloc-coord.ts +275 -20
- package/src/hex.ts +29 -0
- package/src/install-phase.ts +1 -15
- package/src/lexical-path.ts +17 -0
- package/src/limits.ts +29 -0
- package/src/module-source.ts +3 -1
- package/src/oom-classify.ts +8 -0
- package/src/realm-builtin.ts +22 -0
- package/src/utf8.ts +75 -1
- package/src/w7-frame.ts +152 -53
- package/src/wave-writer.ts +28 -27
- package/src/weighted-credit-pool.ts +256 -51
- package/src/worker-code.ts +38 -0
package/src/heavy-alloc-coord.ts
CHANGED
|
@@ -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<
|
|
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<
|
|
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
|
-
/**
|
|
86
|
-
|
|
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,
|
|
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
|
-
|
|
96
|
-
|
|
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):
|
|
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
|
-
|
|
221
|
-
|
|
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):
|
|
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
|
|
231
|
-
*
|
|
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
|
-
|
|
238
|
-
|
|
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
|
+
}
|
package/src/install-phase.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
+
}
|
package/src/limits.ts
CHANGED
|
@@ -197,6 +197,26 @@ export const FACET_OWN_WRITE_MEMORY_BYTES = 32 * 1024 * 1024;
|
|
|
197
197
|
// holds nothing more and the session answers.
|
|
198
198
|
export const WASI_RESIDENT_STORE_BYTES = 32 * 1024 * 1024;
|
|
199
199
|
|
|
200
|
+
// A file too big to hold whole (over WASI_RESIDENT_FILE_CAP_BYTES: a pack, a
|
|
201
|
+
// deep history's .idx) is read through the resident store a page at a time:
|
|
202
|
+
// git's own unit for a paged pack index, and a sixteenth of its 1 MiB pack
|
|
203
|
+
// window, so one window is one batch read.
|
|
204
|
+
export const RESIDENT_PAGE_BYTES = 64 * 1024;
|
|
205
|
+
|
|
206
|
+
// The most a resident store holds of such pages, least recently used out
|
|
207
|
+
// first; in a one-shot's heap, never more than half its budget. Measured on vscode's full history (2.18M objects):
|
|
208
|
+
// a .idx of 57.5 MiB, of which every command touched ~49 MiB, and a .rev of
|
|
209
|
+
// 8.6 MiB, so the bound keeps a deep history's index and its hot pack windows.
|
|
210
|
+
export const RESIDENT_PAGE_CACHE_BYTES = 512 * 1024 * 1024;
|
|
211
|
+
|
|
212
|
+
// The pages are a cache in the session's one storage limit (N18), which the
|
|
213
|
+
// user's files share: they never hold more than this share of what they hold
|
|
214
|
+
// plus what the ledger says is still free at the last grant, so a filling
|
|
215
|
+
// session takes room from the cache, not from the user. Evicted pages are
|
|
216
|
+
// reused by the same facet database; its size, which df counts, stays at its
|
|
217
|
+
// high-water mark until the facet's storage is deleted.
|
|
218
|
+
export const RESIDENT_PAGE_CACHE_FREE_SHARE = 0.25;
|
|
219
|
+
|
|
200
220
|
// Shared allowance for transient allocations in the supervisor DO. With the
|
|
201
221
|
// VFS LRU shrunk to 8 MiB during an active reservation, 40 MiB of admitted
|
|
202
222
|
// payload plus the 9 MiB bundle baseline stays below the 64 MiB soft ceiling
|
|
@@ -204,6 +224,15 @@ export const WASI_RESIDENT_STORE_BYTES = 32 * 1024 * 1024;
|
|
|
204
224
|
// This is 31.25% of the platform's 128 MiB hard isolate ceiling.
|
|
205
225
|
export const SUPERVISOR_IN_FLIGHT_ALLOCATION_BUDGET_BYTES = 40 * 1024 * 1024;
|
|
206
226
|
|
|
227
|
+
// The longest a facet's load waits for that allowance before it is refused
|
|
228
|
+
// (acquireSupervisorLoadAllocation). The wait is FIFO, so a load waits only
|
|
229
|
+
// for the holds ahead of it, each of which ends when its own work does. A
|
|
230
|
+
// load is the one owner that waits while it holds a Dynamic Worker (the
|
|
231
|
+
// loader asks for its code then), so a launch that holds credit while it
|
|
232
|
+
// waits for a worker of the same Durable Object can close a cycle nothing
|
|
233
|
+
// ends: the bound refuses the load, by name, instead of leaving both to hang.
|
|
234
|
+
export const SUPERVISOR_LOAD_WAIT_MS = 60_000;
|
|
235
|
+
|
|
207
236
|
// Reserved slice of that allowance for chunk-sized filesystem reads, so a
|
|
208
237
|
// read is never queued behind a multi-megabyte owner for a wait that has
|
|
209
238
|
// nothing to do with its own cost. A read only draws on this when the shared
|
package/src/module-source.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { utf8Length } from './utf8.js';
|
|
2
|
+
|
|
1
3
|
/** A deployed source asset: its digest, not its pathname, is its identity. */
|
|
2
4
|
export interface ModuleSourceAsset {
|
|
3
5
|
path: string;
|
|
@@ -26,7 +28,7 @@ export class ModuleSource {
|
|
|
26
28
|
private joined: string | undefined;
|
|
27
29
|
readonly byteLength: number;
|
|
28
30
|
constructor(readonly parts: readonly ModuleSourcePart[]) {
|
|
29
|
-
this.byteLength = parts.reduce((bytes, part) => bytes + (typeof part === 'string' ?
|
|
31
|
+
this.byteLength = parts.reduce((bytes, part) => bytes + (typeof part === 'string' ? utf8Length(part) : part.byteLength), 0);
|
|
30
32
|
}
|
|
31
33
|
get text(): string {
|
|
32
34
|
return this.joined ??= this.parts.map((part) => typeof part === 'string' ? part : part.text).join('');
|
package/src/oom-classify.ts
CHANGED
|
@@ -273,6 +273,10 @@ export type DoCallClass =
|
|
|
273
273
|
| 'storage_reset'
|
|
274
274
|
/** The runtime flagged the error `retryable` itself. */
|
|
275
275
|
| 'retryable_flag'
|
|
276
|
+
/** The platform failed the call without saying why ("internal error;
|
|
277
|
+
* reference = …", isUnexplainedPlatformError): whether it ran is unknown.
|
|
278
|
+
* Not a transient class: only a callee that converges repeats it. */
|
|
279
|
+
| 'platform_internal'
|
|
276
280
|
/** The object shed the call under queue pressure. NEVER retried: retrying
|
|
277
281
|
* an overloaded object is what overloaded it. */
|
|
278
282
|
| 'overloaded'
|
|
@@ -317,6 +321,7 @@ export function classifyDoCall(input: unknown): DoCallClass {
|
|
|
317
321
|
const seen = new Set<Error>();
|
|
318
322
|
const messages: string[] = [];
|
|
319
323
|
let flagged = false;
|
|
324
|
+
let unexplained = false;
|
|
320
325
|
let link: Error | null = input;
|
|
321
326
|
while (link !== null && !seen.has(link)) {
|
|
322
327
|
seen.add(link);
|
|
@@ -324,6 +329,7 @@ export function classifyDoCall(input: unknown): DoCallClass {
|
|
|
324
329
|
|| isDoOverloaded(link.message);
|
|
325
330
|
if (overloaded) return 'overloaded';
|
|
326
331
|
if ('retryable' in link && link.retryable === true) flagged = true;
|
|
332
|
+
if (isUnexplainedPlatformError(link)) unexplained = true;
|
|
327
333
|
messages.push(link.message ?? '');
|
|
328
334
|
const cause: unknown = link.cause;
|
|
329
335
|
link = cause instanceof Error ? cause : null;
|
|
@@ -333,6 +339,8 @@ export function classifyDoCall(input: unknown): DoCallClass {
|
|
|
333
339
|
if (pattern.test(chain)) return transient;
|
|
334
340
|
}
|
|
335
341
|
if (flagged) return 'retryable_flag';
|
|
342
|
+
// Only where nothing in the chain explains it: a wrapper's own words are not the platform's.
|
|
343
|
+
if (unexplained) return 'platform_internal';
|
|
336
344
|
return 'permanent';
|
|
337
345
|
}
|
|
338
346
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Node builtin of the realm's own process (`process.getBuiltinModule`),
|
|
3
|
+
* never a program's: a Nimbus runtime that puts a program's process in front
|
|
4
|
+
* of the realm's (node-shims) records the realm's own under
|
|
5
|
+
* `Symbol.for('nimbus.fronts')`, and a warm isolate keeps the last program's
|
|
6
|
+
* process as the global after it ends. Its builtins carry Node's argument
|
|
7
|
+
* checks and errors for programs, which the runtime's own callers pay for
|
|
8
|
+
* on every call. Undefined where the realm has no such builtin.
|
|
9
|
+
*/
|
|
10
|
+
type BuiltinProcess = { getBuiltinModule?(id: string): unknown };
|
|
11
|
+
type Fronts = WeakMap<object, { own: unknown }>;
|
|
12
|
+
|
|
13
|
+
export function realmBuiltin<T>(id: string): T | undefined {
|
|
14
|
+
try {
|
|
15
|
+
const global = globalThis as { process?: BuiltinProcess } & Record<symbol, Fronts | undefined>;
|
|
16
|
+
const current = global.process;
|
|
17
|
+
const own = (current && global[Symbol.for('nimbus.fronts')]?.get(current)?.own) as BuiltinProcess | undefined;
|
|
18
|
+
return (own ?? current)?.getBuiltinModule?.(id) as T | undefined;
|
|
19
|
+
} catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/utf8.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* utf8.ts — the UTF-8 length of a string,
|
|
2
|
+
* utf8.ts — the UTF-8 length of a string, and the order of two strings by
|
|
3
|
+
* their UTF-8 bytes, without encoding them.
|
|
3
4
|
*
|
|
4
5
|
* Bounds on names and payloads are in bytes, and the hot ones (every path a
|
|
5
6
|
* W7 wave or a listing page carries) were measured by encoding the string
|
|
@@ -7,7 +8,21 @@
|
|
|
7
8
|
* TextEncoder would write: one to four per code point, and three for a
|
|
8
9
|
* lone surrogate, which it writes as U+FFFD.
|
|
9
10
|
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* From this many UTF-16 units, a string is counted natively (longUtf8Length),
|
|
14
|
+
* into a scratch buffer of SCRATCH_BYTES. The resident store's count on the
|
|
15
|
+
* launch's captures (worker vfs/resident/cell-cost.ts) keeps the same policy,
|
|
16
|
+
* and tests/unit/utf8-length.mjs holds both to TextEncoder.
|
|
17
|
+
*/
|
|
18
|
+
export const UTF8_LONG_UNITS = 256;
|
|
19
|
+
export const UTF8_SCRATCH_BYTES = 256 * 1024;
|
|
20
|
+
const encoder = new TextEncoder();
|
|
21
|
+
/** Where longUtf8Length's encodes land: written, never read. */
|
|
22
|
+
let scratch: Uint8Array | undefined;
|
|
23
|
+
|
|
10
24
|
export function utf8Length(text: string): number {
|
|
25
|
+
if (text.length >= UTF8_LONG_UNITS) return longUtf8Length(text);
|
|
11
26
|
let bytes = text.length;
|
|
12
27
|
for (let index = 0; index < text.length; index++) {
|
|
13
28
|
const unit = text.charCodeAt(index);
|
|
@@ -29,3 +44,62 @@ export function utf8Length(text: string): number {
|
|
|
29
44
|
}
|
|
30
45
|
return bytes;
|
|
31
46
|
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A long string's UTF-8 length at the engine's speed: encoded a slice at a
|
|
50
|
+
* time into one reused buffer, only the count kept. encodeInto never splits
|
|
51
|
+
* a surrogate pair, so each slice starts on a code point.
|
|
52
|
+
*/
|
|
53
|
+
function longUtf8Length(text: string): number {
|
|
54
|
+
scratch ??= new Uint8Array(UTF8_SCRATCH_BYTES);
|
|
55
|
+
let bytes = 0;
|
|
56
|
+
for (let read = 0; read < text.length;) {
|
|
57
|
+
const done = encoder.encodeInto(read === 0 ? text : text.slice(read), scratch);
|
|
58
|
+
read += done.read;
|
|
59
|
+
bytes += done.written;
|
|
60
|
+
}
|
|
61
|
+
return bytes;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The code point at `index` of `text`: a pair is one past U+FFFF, and a lone
|
|
66
|
+
* surrogate is its own (as WTF-8 writes it, between U+D7FF and U+E000).
|
|
67
|
+
*/
|
|
68
|
+
function codePointAt(text: string, index: number): number {
|
|
69
|
+
const unit = text.charCodeAt(index);
|
|
70
|
+
if (unit < 0xd800 || unit > 0xdbff || index + 1 >= text.length) return unit;
|
|
71
|
+
const next = text.charCodeAt(index + 1);
|
|
72
|
+
return next >= 0xdc00 && next <= 0xdfff ? 0x10000 + ((unit - 0xd800) << 10) + (next - 0xdc00) : unit;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* `a` against `b` (negative, 0, positive) as their UTF-8 bytes order them,
|
|
77
|
+
* as SQLite's `ORDER BY` of a text column and git's index do: code point
|
|
78
|
+
* order. A string UTF-8 cannot write (a lone surrogate) is ordered as WTF-8
|
|
79
|
+
* writes it, its surrogate a code point of its own: the order is total, as a
|
|
80
|
+
* listing's cursor needs, and decided at the first code point that differs,
|
|
81
|
+
* so a directory's entries follow it together. Units equal in both are
|
|
82
|
+
* passed as they are; two that differ, neither a surrogate, are their own
|
|
83
|
+
* code points; the rest, rare in a path, is surrogatesAt's.
|
|
84
|
+
*/
|
|
85
|
+
export function compareUtf8(a: string, b: string): number {
|
|
86
|
+
const n = Math.min(a.length, b.length);
|
|
87
|
+
for (let i = 0; i < n; i++) {
|
|
88
|
+
const x = a.charCodeAt(i);
|
|
89
|
+
const y = b.charCodeAt(i);
|
|
90
|
+
if (x === y) continue;
|
|
91
|
+
if ((x < 0xd800 || x > 0xdfff) && (y < 0xd800 || y > 0xdfff)) return x - y;
|
|
92
|
+
return surrogatesAt(a, b, i);
|
|
93
|
+
}
|
|
94
|
+
return a.length - b.length;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** compareUtf8 of `a` and `b`, equal before `i`, where they differ at `i` and one unit there is a surrogate. */
|
|
98
|
+
function surrogatesAt(a: string, b: string, i: number): number {
|
|
99
|
+
// A low half after a high one the two share: the code points begin at the high one.
|
|
100
|
+
const before = i > 0 ? a.charCodeAt(i - 1) : 0;
|
|
101
|
+
const x = a.charCodeAt(i);
|
|
102
|
+
const y = b.charCodeAt(i);
|
|
103
|
+
const at = before >= 0xd800 && before <= 0xdbff && ((x >= 0xdc00 && x <= 0xdfff) || (y >= 0xdc00 && y <= 0xdfff)) ? i - 1 : i;
|
|
104
|
+
return codePointAt(a, at) - codePointAt(b, at);
|
|
105
|
+
}
|