Skip to content

Commit fbc5850

Browse files
committed
use RAB
1 parent 64203ac commit fbc5850

File tree

6 files changed

+118
-51
lines changed

6 files changed

+118
-51
lines changed

benchmark/count-utf8.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable no-console */
22
import { utf8CountJs, WASM_AVAILABLE } from "../src/utils/utf8.ts";
3-
import { getWasmError, utf8CountWasm } from "../src/utils/utf8-wasm.ts";
3+
import { getWasmError, utf8CountWasm, RAB_AVAILABLE } from "../src/utils/utf8-wasm.ts";
44

55
// @ts-ignore
66
import Benchmark from "benchmark";
@@ -15,6 +15,7 @@ console.log("WebAssembly Status:");
1515
console.log(` WASM_AVAILABLE: ${WASM_AVAILABLE}`);
1616
if (WASM_AVAILABLE) {
1717
console.log(" js-string-builtins: enabled");
18+
console.log(` RAB_AVAILABLE: ${RAB_AVAILABLE} (resizable ArrayBuffer integration)`);
1819
} else {
1920
const error = getWasmError();
2021
console.log(` Error: ${error?.message || "unknown"}`);

benchmark/decode-string.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable no-console */
22
import { utf8EncodeJs, utf8Count, utf8DecodeJs, utf8DecodeTD, WASM_AVAILABLE } from "../src/utils/utf8.ts";
3-
import { getWasmError, utf8DecodeWasm } from "../src/utils/utf8-wasm.ts";
3+
import { getWasmError, utf8DecodeWasm, RAB_AVAILABLE } from "../src/utils/utf8-wasm.ts";
44

55
// @ts-ignore
66
import Benchmark from "benchmark";
@@ -16,6 +16,7 @@ console.log("WebAssembly Status:");
1616
console.log(` WASM_AVAILABLE: ${WASM_AVAILABLE}`);
1717
if (WASM_AVAILABLE) {
1818
console.log(" js-string-builtins: enabled");
19+
console.log(` RAB_AVAILABLE: ${RAB_AVAILABLE} (resizable ArrayBuffer integration)`);
1920
} else {
2021
const error = getWasmError();
2122
console.log(` Error: ${error?.message || "unknown"}`);

benchmark/encode-string.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable no-console */
22
import { utf8EncodeJs, utf8Count, utf8EncodeTE, WASM_AVAILABLE } from "../src/utils/utf8.ts";
3-
import { getWasmError, utf8EncodeWasm } from "../src/utils/utf8-wasm.ts";
3+
import { getWasmError, utf8EncodeWasm, RAB_AVAILABLE } from "../src/utils/utf8-wasm.ts";
44

55
// @ts-ignore
66
import Benchmark from "benchmark";
@@ -16,6 +16,7 @@ console.log("WebAssembly Status:");
1616
console.log(` WASM_AVAILABLE: ${WASM_AVAILABLE}`);
1717
if (WASM_AVAILABLE) {
1818
console.log(" js-string-builtins: enabled");
19+
console.log(` RAB_AVAILABLE: ${RAB_AVAILABLE} (resizable ArrayBuffer integration)`);
1920
} else {
2021
const error = getWasmError();
2122
console.log(` Error: ${error?.message || "unknown"}`);

src/utils/utf8-wasm-binary.ts

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,27 @@ export const wasmBinary = `
55
AGFzbQEAAAABNQhedwFgAW8Bf2ACb38Bf2ADb2QAfwF/YANkAH9/AWRvYAJ/ZAABf2ABfwFkAGADZA
66
B/fwFvAnsEDndhc206anMtc3RyaW5nBmxlbmd0aAABDndhc206anMtc3RyaW5nCmNoYXJDb2RlQXQA
77
Ag53YXNtOmpzLXN0cmluZxFpbnRvQ2hhckNvZGVBcnJheQADDndhc206anMtc3RyaW5nEWZyb21DaG
8-
FyQ29kZUFycmF5AAQDBgUBAgUGBwUDAQABB1QGBm1lbW9yeQIACXV0ZjhDb3VudAAECnV0ZjhFbmNv
9-
ZGUABRF1dGY4RGVjb2RlVG9BcnJheQAGCmFsbG9jQXJyYXkABw1hcnJheVRvU3RyaW5nAAgKlwkFlA
10-
EBBH8gABAAIQQDQCADIARPRQRAIAAgAxABIgJBgAFJBH8gAUEBagUgAkGAEEkEfyABQQJqBSACQf+3
11-
A00gAkGAsANPcQR/IANBAWoiAiAESQR/IAAgAhABQYD4A3FBgLgDRgR/IAIhAyABQQRqBSABQQNqCw
12-
UgAUEDagsFIAFBA2oLCwshASADQQFqIQMMAQsLIAELwgMCBn8BZAAgASECIAAgABAAIgX7BwAiCEEA
13-
EAIaA0AgBCAFT0UEQCAIIAT7DQAiA0GAAUkEfyACIAM6AAAgAkEBagUgA0GAEEkEfyACIANBBnZBwA
14-
FyOgAAIAJBAWogA0E/cUGAAXI6AAAgAkECagUgA0H/twNNIANBgLADT3EEfyAEQQFqIgYgBUkEfyAI
15-
IAb7DQAiB0GA+ANxQYC4A0YEfyAGIQQgAiADQQp0IAdqQYC4/xprIgNBEnZB8AFyOgAAIAJBAWogA0
16-
EMdkE/cUGAAXI6AAAgAkECaiADQQZ2QT9xQYABcjoAACACQQNqIANBP3FBgAFyOgAAIAJBBGoFIAIg
17-
A0EMdkHgAXI6AAAgAkEBaiADQQZ2QT9xQYABcjoAACACQQJqIANBP3FBgAFyOgAAIAJBA2oLBSACIA
18-
NBDHZB4AFyOgAAIAJBAWogA0EGdkE/cUGAAXI6AAAgAkECaiADQT9xQYABcjoAACACQQNqCwUgAiAD
19-
QQx2QeABcjoAACACQQFqIANBBnZBP3FBgAFyOgAAIAJBAmogA0E/cUGAAXI6AAAgAkEDagsLCyECIA
20-
RBAWohBAwBCwsgAiABawunBAEEfwNAAkAgACADTQ0AIAMtAAAiBEGAAXEEfyAAIANrIQUgBEHgAXFB
21-
wAFGBH8gBUECSQR/IAEgAiAE+w4AIAJBAWohAiADQQFqIQMDQCAAIANNRQRAIAEgAiADLQAA+w4AIA
22-
JBAWohAiADQQFqIQMMAQsLDAMFIAEgAiADQQFqLQAAQT9xIARBH3FBBnRy+w4AIAJBAWohAiADQQJq
23-
CwUgBEHwAXFB4AFGBH8gBUEDSQR/IAEgAiAE+w4AIAJBAWohAiADQQFqIQMDQCAAIANNRQRAIAEgAi
24-
ADLQAA+w4AIAJBAWohAiADQQFqIQMMAQsLDAQFIAEgAiADQQJqLQAAQT9xIARBD3FBDHQgA0EBai0A
25-
AEE/cUEGdHJy+w4AIAJBAWohAiADQQNqCwUgBEH4AXFB8AFGBH8gBUEESQR/IAEgAiAE+w4AIAJBAW
26-
ohAiADQQFqIQMDQCAAIANNRQRAIAEgAiADLQAA+w4AIAJBAWohAiADQQFqIQMMAQsLDAUFIAEgAiAD
27-
QQNqLQAAQT9xIARBB3FBEnQgA0EBai0AAEE/cUEMdHIgA0ECai0AAEE/cUEGdHJyQYCABGsiBEEKdk
28-
GAsANy+w4AIAEgAkEBaiICIARB/wdxQYC4A3L7DgAgAkEBaiECIANBBGoLBSABIAIgBPsOACACQQFq
29-
IQIgA0EBagsLCwUgASACIAT7DgAgAkEBaiECIANBAWoLIQMMAQsLIAILBwAgAPsHAAsKACAAIAEgAh
30-
ADCw==
8+
FyQ29kZUFycmF5AAQDBgUBAgUGBwUFAQEBgCAHVAYGbWVtb3J5AgAJdXRmOENvdW50AAQKdXRmOEVu
9+
Y29kZQAFEXV0ZjhEZWNvZGVUb0FycmF5AAYKYWxsb2NBcnJheQAHDWFycmF5VG9TdHJpbmcACAqXCQ
10+
WUAQEEfyAAEAAhBANAIAMgBE9FBEAgACADEAEiAkGAAUkEfyABQQFqBSACQYAQSQR/IAFBAmoFIAJB
11+
/7cDTSACQYCwA09xBH8gA0EBaiICIARJBH8gACACEAFBgPgDcUGAuANGBH8gAiEDIAFBBGoFIAFBA2
12+
oLBSABQQNqCwUgAUEDagsLCyEBIANBAWohAwwBCwsgAQvCAwIGfwFkACABIQIgACAAEAAiBfsHACII
13+
QQAQAhoDQCAEIAVPRQRAIAggBPsNACIDQYABSQR/IAIgAzoAACACQQFqBSADQYAQSQR/IAIgA0EGdk
14+
HAAXI6AAAgAkEBaiADQT9xQYABcjoAACACQQJqBSADQf+3A00gA0GAsANPcQR/IARBAWoiBiAFSQR/
15+
IAggBvsNACIHQYD4A3FBgLgDRgR/IAYhBCACIANBCnQgB2pBgLj/GmsiA0ESdkHwAXI6AAAgAkEBai
16+
ADQQx2QT9xQYABcjoAACACQQJqIANBBnZBP3FBgAFyOgAAIAJBA2ogA0E/cUGAAXI6AAAgAkEEagUg
17+
AiADQQx2QeABcjoAACACQQFqIANBBnZBP3FBgAFyOgAAIAJBAmogA0E/cUGAAXI6AAAgAkEDagsFIA
18+
IgA0EMdkHgAXI6AAAgAkEBaiADQQZ2QT9xQYABcjoAACACQQJqIANBP3FBgAFyOgAAIAJBA2oLBSAC
19+
IANBDHZB4AFyOgAAIAJBAWogA0EGdkE/cUGAAXI6AAAgAkECaiADQT9xQYABcjoAACACQQNqCwsLIQ
20+
IgBEEBaiEEDAELCyACIAFrC6cEAQR/A0ACQCAAIANNDQAgAy0AACIEQYABcQR/IAAgA2shBSAEQeAB
21+
cUHAAUYEfyAFQQJJBH8gASACIAT7DgAgAkEBaiECIANBAWohAwNAIAAgA01FBEAgASACIAMtAAD7Dg
22+
AgAkEBaiECIANBAWohAwwBCwsMAwUgASACIANBAWotAABBP3EgBEEfcUEGdHL7DgAgAkEBaiECIANB
23+
AmoLBSAEQfABcUHgAUYEfyAFQQNJBH8gASACIAT7DgAgAkEBaiECIANBAWohAwNAIAAgA01FBEAgAS
24+
ACIAMtAAD7DgAgAkEBaiECIANBAWohAwwBCwsMBAUgASACIANBAmotAABBP3EgBEEPcUEMdCADQQFq
25+
LQAAQT9xQQZ0cnL7DgAgAkEBaiECIANBA2oLBSAEQfgBcUHwAUYEfyAFQQRJBH8gASACIAT7DgAgAk
26+
EBaiECIANBAWohAwNAIAAgA01FBEAgASACIAMtAAD7DgAgAkEBaiECIANBAWohAwwBCwsMBQUgASAC
27+
IANBA2otAABBP3EgBEEHcUESdCADQQFqLQAAQT9xQQx0ciADQQJqLQAAQT9xQQZ0cnJBgIAEayIEQQ
28+
p2QYCwA3L7DgAgASACQQFqIgIgBEH/B3FBgLgDcvsOACACQQFqIQIgA0EEagsFIAEgAiAE+w4AIAJB
29+
AWohAiADQQFqCwsLBSABIAIgBPsOACACQQFqIQIgA0EBagshAwwBCwsgAgsHACAA+wcACwoAIAAgAS
30+
ACEAML
3131
`;

src/utils/utf8-wasm.ts

Lines changed: 86 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
*
88
* This implementation uses WASM GC arrays with intoCharCodeArray/fromCharCodeArray
99
* for efficient bulk string operations instead of character-by-character processing.
10+
*
11+
* When available (Node.js 24+ with --experimental-wasm-rab-integration), uses
12+
* resizable ArrayBuffer integration for reduced glue code and auto-tracking views.
1013
*/
1114

1215
import { wasmBinary } from "./utf8-wasm-binary.ts";
@@ -46,6 +49,12 @@ interface WasmExports extends WebAssembly.Exports {
4649
let wasmInstance: WasmExports | null = null;
4750
let wasmInitError: Error | null = null;
4851

52+
// Resizable ArrayBuffer integration (RAB)
53+
// When available, provides auto-tracking views that don't detach on memory.grow()
54+
let wasmResizableBuffer: ArrayBuffer | null = null;
55+
let wasmMemoryView: Uint8Array | null = null;
56+
let useResizableBuffer = false;
57+
4958
function base64ToBytes(base64: string): Uint8Array {
5059
// @ts-expect-error - fromBase64 is not yet supported in TypeScript
5160
if (Uint8Array.fromBase64) {
@@ -65,6 +74,24 @@ function base64ToBytes(base64: string): Uint8Array {
6574
}
6675
}
6776

77+
function tryInitializeResizableBuffer(): void {
78+
if (!wasmInstance) return;
79+
80+
try {
81+
// Check if toResizableBuffer() is available (Node.js 24+ with --experimental-wasm-rab-integration)
82+
const memory = wasmInstance.memory as any;
83+
if (typeof memory.toResizableBuffer === "function") {
84+
wasmResizableBuffer = memory.toResizableBuffer();
85+
// Create auto-tracking view (no explicit length = tracks buffer size)
86+
wasmMemoryView = new Uint8Array(wasmResizableBuffer);
87+
useResizableBuffer = true;
88+
}
89+
} catch {
90+
// RAB integration not available, will use fallback
91+
useResizableBuffer = false;
92+
}
93+
}
94+
6895
function tryInitializeWasmInstance(): void {
6996
if (WASM_MODE === "never") {
7097
wasmInitError = new Error("MSGPACK_WASM=never: wasm disabled");
@@ -82,6 +109,9 @@ function tryInitializeWasmInstance(): void {
82109
const module: WebAssembly.Module = new (WebAssembly.Module as any)(bytes, { builtins: ["js-string"] });
83110
const instance = new WebAssembly.Instance(module);
84111
wasmInstance = instance.exports as WasmExports;
112+
113+
// Try to enable resizable buffer integration for reduced glue code
114+
tryInitializeResizableBuffer();
85115
} catch (e) {
86116
wasmInitError = e instanceof Error ? e : new Error(String(e));
87117

@@ -99,6 +129,12 @@ tryInitializeWasmInstance();
99129
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
100130
export const WASM_AVAILABLE = wasmInstance !== null;
101131

132+
/**
133+
* Whether resizable ArrayBuffer integration is available.
134+
* When true, uses auto-tracking views for reduced glue code.
135+
*/
136+
export const RAB_AVAILABLE = useResizableBuffer;
137+
102138
export function getWasmError(): Error | null {
103139
return wasmInitError;
104140
}
@@ -114,50 +150,77 @@ export function utf8CountWasm(str: string): number {
114150
return wasmInstance!.utf8Count(str);
115151
}
116152

153+
// Page size constant
154+
const PAGE_SIZE = 65536;
155+
156+
/**
157+
* Ensure wasm memory is large enough.
158+
* With RAB integration, the view auto-tracks size changes.
159+
* Without RAB, we need to recreate the view after grow.
160+
*/
161+
function ensureMemorySize(requiredBytes: number): void {
162+
const requiredPages = Math.ceil(requiredBytes / PAGE_SIZE);
163+
const currentPages = wasmInstance!.memory.buffer.byteLength / PAGE_SIZE;
164+
165+
if (requiredPages > currentPages) {
166+
wasmInstance!.memory.grow(requiredPages - currentPages);
167+
// With RAB, wasmMemoryView auto-tracks the new size
168+
// Without RAB, we don't maintain a persistent view
169+
}
170+
}
171+
172+
/**
173+
* Get a view of wasm memory for reading/writing.
174+
* With RAB integration, returns the auto-tracking view.
175+
* Without RAB, creates a fresh view each time.
176+
*/
177+
function getMemoryView(): Uint8Array {
178+
if (useResizableBuffer) {
179+
// Auto-tracking view - no need to recreate
180+
return wasmMemoryView!;
181+
}
182+
// Fallback: create fresh view (handles detached buffer after grow)
183+
return new Uint8Array(wasmInstance!.memory.buffer);
184+
}
185+
117186
/**
118187
* Encode string to UTF-8 bytes in the provided buffer.
119188
* Returns the number of bytes written.
189+
*
190+
* With RAB integration: uses auto-tracking view, minimal glue code.
191+
* Without RAB: recreates view after potential memory growth.
120192
*/
121193
export function utf8EncodeWasm(str: string, output: Uint8Array, outputOffset: number): number {
122-
// Estimate max byte length without a full pass over the string.
123-
// Each UTF-16 code unit can produce at most 3 UTF-8 bytes (BMP chars).
124-
// Surrogate pairs (2 code units) produce 4 bytes, so 3 bytes/code unit is safe.
194+
// Estimate max byte length: each UTF-16 code unit produces at most 3 UTF-8 bytes
125195
const maxByteLength = str.length * 3;
126196

127-
// Ensure wasm memory is large enough
128-
const requiredPages = Math.ceil(maxByteLength / 65536);
129-
const currentPages = wasmInstance!.memory.buffer.byteLength / 65536;
197+
// Ensure memory is large enough (view auto-tracks with RAB)
198+
ensureMemorySize(maxByteLength);
130199

131-
if (requiredPages > currentPages) {
132-
wasmInstance!.memory.grow(requiredPages - currentPages);
133-
}
134-
135-
// Encode to wasm memory (uses intoCharCodeArray for bulk char extraction)
200+
// Encode to wasm memory
136201
const bytesWritten = wasmInstance!.utf8Encode(str, 0);
137202

138203
// Copy from wasm memory to output buffer
139-
const wasmBytes = new Uint8Array(wasmInstance!.memory.buffer, 0, bytesWritten);
140-
output.set(wasmBytes, outputOffset);
204+
const view = getMemoryView();
205+
output.set(view.subarray(0, bytesWritten), outputOffset);
141206

142207
return bytesWritten;
143208
}
144209

145210
/**
146211
* Decode UTF-8 bytes to string.
147212
* Uses GC arrays with fromCharCodeArray for efficient string creation.
213+
*
214+
* With RAB integration: uses auto-tracking view, minimal glue code.
215+
* Without RAB: recreates view after potential memory growth.
148216
*/
149217
export function utf8DecodeWasm(bytes: Uint8Array, inputOffset: number, byteLength: number): string {
150-
// Ensure wasm memory is large enough for UTF-8 input
151-
const requiredPages = Math.ceil(byteLength / 65536);
152-
const currentPages = wasmInstance!.memory.buffer.byteLength / 65536;
153-
154-
if (requiredPages > currentPages) {
155-
wasmInstance!.memory.grow(requiredPages - currentPages);
156-
}
218+
// Ensure memory is large enough (view auto-tracks with RAB)
219+
ensureMemorySize(byteLength);
157220

158-
// Copy UTF-8 bytes to wasm linear memory at offset 0
159-
const wasmBytes = new Uint8Array(wasmInstance!.memory.buffer, 0, byteLength);
160-
wasmBytes.set(bytes.subarray(inputOffset, inputOffset + byteLength));
221+
// Copy UTF-8 bytes to wasm linear memory
222+
const view = getMemoryView();
223+
view.set(bytes.subarray(inputOffset, inputOffset + byteLength), 0);
161224

162225
// Allocate GC array for UTF-16 output (max size = byteLength for ASCII)
163226
const arr = wasmInstance!.allocArray(byteLength);

wasm/utf8.wat

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
(import "wasm:js-string" "fromCharCodeArray"
1919
(func $str_from_array (param (ref $i16_array) i32 i32) (result (ref extern))))
2020

21-
;; Linear memory for UTF-8 bytes (64KB initial)
22-
(memory (export "memory") 1)
21+
;; Linear memory for UTF-8 bytes (64KB initial, 256MB max for RAB integration)
22+
;; Maximum is required for WebAssembly.Memory.toResizableBuffer()
23+
(memory (export "memory") 1 4096)
2324

2425
;; Count UTF-8 byte length of a JS string
2526
(func (export "utf8Count") (param $str externref) (result i32)

0 commit comments

Comments
 (0)