Skip to content

Commit 07a58ce

Browse files
committed
BridgeJS: Optimize string encoding for JS-to-Swift crossings
Two techniques applied to all JS-to-Swift string paths: 1. LRU encoding cache for parameter and return paths - avoids re-encoding repeated strings via a Map<string, Uint8Array> with 256-entry LRU eviction. 2. Direct string retain + encodeInto() for stack ABI paths (arrays, structs, enums, dictionaries) - skips the intermediate Uint8Array allocation entirely by retaining the JS string and encoding directly into the WASM linear memory buffer. _swift_js_init_memory now returns the actual byte count written, which the stack ABI path needs since it passes a worst-case buffer size (string.length * 3) rather than the exact UTF-8 byte count. Benchmarks (100k iterations, Node.js): StringRoundtrip/takeString: -21% ArrayRoundtrip/takeStringArray: -35% ArrayRoundtrip/roundtripStringArray: -29%
1 parent 5e96639 commit 07a58ce

53 files changed

Lines changed: 573 additions & 249 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,9 @@ public struct BridgeJSLink {
331331
"let \(JSGlueVariableScope.reservedDecodeString);",
332332
"const \(JSGlueVariableScope.reservedTextDecoder) = new TextDecoder(\"utf-8\");",
333333
"const \(JSGlueVariableScope.reservedTextEncoder) = new TextEncoder(\"utf-8\");",
334+
"const \(JSGlueVariableScope.reservedStrEncCache) = new Map();",
335+
"const \(JSGlueVariableScope.reservedStrEncCacheMax) = 4096;",
336+
"function \(JSGlueVariableScope.reservedCachedEncode)(s) { let b = \(JSGlueVariableScope.reservedStrEncCache).get(s); if (b) { \(JSGlueVariableScope.reservedStrEncCache).delete(s); \(JSGlueVariableScope.reservedStrEncCache).set(s, b); return b; } b = \(JSGlueVariableScope.reservedTextEncoder).encode(s); if (\(JSGlueVariableScope.reservedStrEncCache).size >= \(JSGlueVariableScope.reservedStrEncCacheMax)) { \(JSGlueVariableScope.reservedStrEncCache).delete(\(JSGlueVariableScope.reservedStrEncCache).keys().next().value); } \(JSGlueVariableScope.reservedStrEncCache).set(s, b); return b; };",
334337
"let \(JSGlueVariableScope.reservedStorageToReturnString);",
335338
"let \(JSGlueVariableScope.reservedStorageToReturnBytes);",
336339
"let \(JSGlueVariableScope.reservedStorageToReturnException);",
@@ -393,7 +396,15 @@ public struct BridgeJSLink {
393396
printer.write(
394397
"const bytes = new Uint8Array(\(JSGlueVariableScope.reservedMemory).buffer, bytesPtr);"
395398
)
399+
printer.write("if (typeof source === 'string') {")
400+
printer.indent {
401+
printer.write(
402+
"return \(JSGlueVariableScope.reservedTextEncoder).encodeInto(source, bytes).written;"
403+
)
404+
}
405+
printer.write("}")
396406
printer.write("bytes.set(source);")
407+
printer.write("return source.length;")
397408
}
398409
printer.write("}")
399410
printer.write("bjs[\"swift_js_make_js_string\"] = function(ptr, len) {")

Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ final class JSGlueVariableScope {
2424
static let reservedStorageToReturnOptionalHeapObject = "tmpRetOptionalHeapObject"
2525
static let reservedTextEncoder = "textEncoder"
2626
static let reservedTextDecoder = "textDecoder"
27+
static let reservedCachedEncode = "_cachedEncode"
28+
static let reservedStrEncCache = "_strEncCache"
29+
static let reservedStrEncCacheMax = "_strEncCacheMax"
2730
static let reservedStringStack = "strStack"
2831
static let reservedI32Stack = "i32Stack"
2932
static let reservedI64Stack = "i64Stack"
@@ -53,6 +56,9 @@ final class JSGlueVariableScope {
5356
reservedStorageToReturnOptionalHeapObject,
5457
reservedTextEncoder,
5558
reservedTextDecoder,
59+
reservedCachedEncode,
60+
reservedStrEncCache,
61+
reservedStrEncCacheMax,
5662
reservedStringStack,
5763
reservedI32Stack,
5864
reservedI64Stack,
@@ -263,7 +269,7 @@ struct IntrinsicJSFragment: Sendable {
263269
let argument = arguments[0]
264270
let bytesLabel = scope.variable("\(argument)Bytes")
265271
let bytesIdLabel = scope.variable("\(argument)Id")
266-
printer.write("const \(bytesLabel) = \(JSGlueVariableScope.reservedTextEncoder).encode(\(argument));")
272+
printer.write("const \(bytesLabel) = \(JSGlueVariableScope.reservedCachedEncode)(\(argument));")
267273
printer.write("const \(bytesIdLabel) = \(JSGlueVariableScope.reservedSwift).memory.retain(\(bytesLabel));")
268274
return [bytesIdLabel, "\(bytesLabel).length"]
269275
}
@@ -296,7 +302,7 @@ struct IntrinsicJSFragment: Sendable {
296302
printCode: { arguments, context in
297303
let printer = context.printer
298304
printer.write(
299-
"\(JSGlueVariableScope.reservedStorageToReturnBytes) = \(JSGlueVariableScope.reservedTextEncoder).encode(\(arguments[0]));"
305+
"\(JSGlueVariableScope.reservedStorageToReturnBytes) = \(JSGlueVariableScope.reservedCachedEncode)(\(arguments[0]));"
300306
)
301307
return ["\(JSGlueVariableScope.reservedStorageToReturnBytes).length"]
302308
}
@@ -2116,15 +2122,11 @@ struct IntrinsicJSFragment: Sendable {
21162122
printCode: { arguments, context in
21172123
let (scope, printer) = (context.scope, context.printer)
21182124
let value = arguments[0]
2119-
let bytesVar = scope.variable("bytes")
21202125
let idVar = scope.variable("id")
21212126
printer.write(
2122-
"const \(bytesVar) = \(JSGlueVariableScope.reservedTextEncoder).encode(\(value));"
2123-
)
2124-
printer.write(
2125-
"const \(idVar) = \(JSGlueVariableScope.reservedSwift).memory.retain(\(bytesVar));"
2127+
"const \(idVar) = \(JSGlueVariableScope.reservedSwift).memory.retain(\(value));"
21262128
)
2127-
scope.emitPushI32Parameter("\(bytesVar).length", printer: printer)
2129+
scope.emitPushI32Parameter("\(value).length * 3", printer: printer)
21282130
scope.emitPushI32Parameter(idVar, printer: printer)
21292131
return []
21302132
}

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayTypes.js

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ export async function createInstantiator(options, swift) {
2424
let decodeString;
2525
const textDecoder = new TextDecoder("utf-8");
2626
const textEncoder = new TextEncoder("utf-8");
27+
const _strEncCache = new Map();
28+
const _strEncCacheMax = 4096;
29+
function _cachedEncode(s) { let b = _strEncCache.get(s); if (b) { _strEncCache.delete(s); _strEncCache.set(s, b); return b; } b = textEncoder.encode(s); if (_strEncCache.size >= _strEncCacheMax) { _strEncCache.delete(_strEncCache.keys().next().value); } _strEncCache.set(s, b); return b; };
2730
let tmpRetString;
2831
let tmpRetBytes;
2932
let tmpRetException;
@@ -70,7 +73,11 @@ export async function createInstantiator(options, swift) {
7073
const source = swift.memory.getObject(sourceId);
7174
swift.memory.release(sourceId);
7275
const bytes = new Uint8Array(memory.buffer, bytesPtr);
76+
if (typeof source === 'string') {
77+
return textEncoder.encodeInto(source, bytes).written;
78+
}
7379
bytes.set(source);
80+
return source.length;
7481
}
7582
bjs["swift_js_make_js_string"] = function(ptr, len) {
7683
return swift.memory.retain(decodeString(ptr, len));
@@ -301,9 +308,8 @@ export async function createInstantiator(options, swift) {
301308
arrayResult.reverse();
302309
let ret = imports.importProcessStrings(arrayResult);
303310
for (const elem of ret) {
304-
const bytes = textEncoder.encode(elem);
305-
const id = swift.memory.retain(bytes);
306-
i32Stack.push(bytes.length);
311+
const id = swift.memory.retain(elem);
312+
i32Stack.push(elem.length * 3);
307313
i32Stack.push(id);
308314
}
309315
i32Stack.push(ret.length);
@@ -411,9 +417,8 @@ export async function createInstantiator(options, swift) {
411417
}
412418
i32Stack.push(nums.length);
413419
for (const elem1 of strs) {
414-
const bytes = textEncoder.encode(elem1);
415-
const id = swift.memory.retain(bytes);
416-
i32Stack.push(bytes.length);
420+
const id = swift.memory.retain(elem1);
421+
i32Stack.push(elem1.length * 3);
417422
i32Stack.push(id);
418423
}
419424
i32Stack.push(strs.length);
@@ -466,9 +471,8 @@ export async function createInstantiator(options, swift) {
466471
},
467472
processStringArray: function bjs_processStringArray(values) {
468473
for (const elem of values) {
469-
const bytes = textEncoder.encode(elem);
470-
const id = swift.memory.retain(bytes);
471-
i32Stack.push(bytes.length);
474+
const id = swift.memory.retain(elem);
475+
i32Stack.push(elem.length * 3);
472476
i32Stack.push(id);
473477
}
474478
i32Stack.push(values.length);
@@ -570,7 +574,7 @@ export async function createInstantiator(options, swift) {
570574
structHelpers.Point.lower(elem);
571575
}
572576
i32Stack.push(points.length);
573-
const matchingBytes = textEncoder.encode(matching);
577+
const matchingBytes = _cachedEncode(matching);
574578
const matchingId = swift.memory.retain(matchingBytes);
575579
instance.exports.bjs_findFirstPoint(matchingId, matchingBytes.length);
576580
const structValue = structHelpers.Point.lift();
@@ -651,9 +655,8 @@ export async function createInstantiator(options, swift) {
651655
for (const elem of values) {
652656
const isSome = elem != null ? 1 : 0;
653657
if (isSome) {
654-
const bytes = textEncoder.encode(elem);
655-
const id = swift.memory.retain(bytes);
656-
i32Stack.push(bytes.length);
658+
const id = swift.memory.retain(elem);
659+
i32Stack.push(elem.length * 3);
657660
i32Stack.push(id);
658661
}
659662
i32Stack.push(isSome);
@@ -807,9 +810,8 @@ export async function createInstantiator(options, swift) {
807810
processNestedStringArray: function bjs_processNestedStringArray(values) {
808811
for (const elem of values) {
809812
for (const elem1 of elem) {
810-
const bytes = textEncoder.encode(elem1);
811-
const id = swift.memory.retain(bytes);
812-
i32Stack.push(bytes.length);
813+
const id = swift.memory.retain(elem1);
814+
i32Stack.push(elem1.length * 3);
813815
i32Stack.push(id);
814816
}
815817
i32Stack.push(elem.length);
@@ -976,9 +978,8 @@ export async function createInstantiator(options, swift) {
976978
}
977979
i32Stack.push(nums.length);
978980
for (const elem1 of strs) {
979-
const bytes = textEncoder.encode(elem1);
980-
const id = swift.memory.retain(bytes);
981-
i32Stack.push(bytes.length);
981+
const id = swift.memory.retain(elem1);
982+
i32Stack.push(elem1.length * 3);
982983
i32Stack.push(id);
983984
}
984985
i32Stack.push(strs.length);
@@ -997,9 +998,8 @@ export async function createInstantiator(options, swift) {
997998
const isSome1 = b != null;
998999
if (isSome1) {
9991000
for (const elem1 of b) {
1000-
const bytes = textEncoder.encode(elem1);
1001-
const id = swift.memory.retain(bytes);
1002-
i32Stack.push(bytes.length);
1001+
const id = swift.memory.retain(elem1);
1002+
i32Stack.push(elem1.length * 3);
10031003
i32Stack.push(id);
10041004
}
10051005
i32Stack.push(b.length);

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ export async function createInstantiator(options, swift) {
1111
let decodeString;
1212
const textDecoder = new TextDecoder("utf-8");
1313
const textEncoder = new TextEncoder("utf-8");
14+
const _strEncCache = new Map();
15+
const _strEncCacheMax = 4096;
16+
function _cachedEncode(s) { let b = _strEncCache.get(s); if (b) { _strEncCache.delete(s); _strEncCache.set(s, b); return b; } b = textEncoder.encode(s); if (_strEncCache.size >= _strEncCacheMax) { _strEncCache.delete(_strEncCache.keys().next().value); } _strEncCache.set(s, b); return b; };
1417
let tmpRetString;
1518
let tmpRetBytes;
1619
let tmpRetException;
@@ -45,7 +48,11 @@ export async function createInstantiator(options, swift) {
4548
const source = swift.memory.getObject(sourceId);
4649
swift.memory.release(sourceId);
4750
const bytes = new Uint8Array(memory.buffer, bytesPtr);
51+
if (typeof source === 'string') {
52+
return textEncoder.encodeInto(source, bytes).written;
53+
}
4854
bytes.set(source);
55+
return source.length;
4956
}
5057
bjs["swift_js_make_js_string"] = function(ptr, len) {
5158
return swift.memory.retain(decodeString(ptr, len));
@@ -216,7 +223,7 @@ export async function createInstantiator(options, swift) {
216223
return ret1;
217224
},
218225
asyncRoundTripString: function bjs_asyncRoundTripString(v) {
219-
const vBytes = textEncoder.encode(v);
226+
const vBytes = _cachedEncode(v);
220227
const vId = swift.memory.retain(vBytes);
221228
const ret = instance.exports.bjs_asyncRoundTripString(vId, vBytes.length);
222229
const ret1 = swift.memory.getObject(ret);

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/AsyncImport.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ export async function createInstantiator(options, swift) {
1111
let decodeString;
1212
const textDecoder = new TextDecoder("utf-8");
1313
const textEncoder = new TextEncoder("utf-8");
14+
const _strEncCache = new Map();
15+
const _strEncCacheMax = 4096;
16+
function _cachedEncode(s) { let b = _strEncCache.get(s); if (b) { _strEncCache.delete(s); _strEncCache.set(s, b); return b; } b = textEncoder.encode(s); if (_strEncCache.size >= _strEncCacheMax) { _strEncCache.delete(_strEncCache.keys().next().value); } _strEncCache.set(s, b); return b; };
1417
let tmpRetString;
1518
let tmpRetBytes;
1619
let tmpRetException;
@@ -160,7 +163,11 @@ export async function createInstantiator(options, swift) {
160163
const source = swift.memory.getObject(sourceId);
161164
swift.memory.release(sourceId);
162165
const bytes = new Uint8Array(memory.buffer, bytesPtr);
166+
if (typeof source === 'string') {
167+
return textEncoder.encodeInto(source, bytes).written;
168+
}
163169
bytes.set(source);
170+
return source.length;
164171
}
165172
bjs["swift_js_make_js_string"] = function(ptr, len) {
166173
return swift.memory.retain(decodeString(ptr, len));
@@ -360,7 +367,7 @@ export async function createInstantiator(options, swift) {
360367
}
361368
bjs["make_swift_closure_TestModule_10TestModulesSS_y"] = function(boxPtr, file, line) {
362369
const lower_closure_TestModule_10TestModulesSS_y = function(param0) {
363-
const param0Bytes = textEncoder.encode(param0);
370+
const param0Bytes = _cachedEncode(param0);
364371
const param0Id = swift.memory.retain(param0Bytes);
365372
instance.exports.invoke_swift_closure_TestModule_10TestModulesSS_y(boxPtr, param0Id, param0Bytes.length);
366373
if (tmpRetException) {

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/AsyncStaticImport.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ export async function createInstantiator(options, swift) {
1111
let decodeString;
1212
const textDecoder = new TextDecoder("utf-8");
1313
const textEncoder = new TextEncoder("utf-8");
14+
const _strEncCache = new Map();
15+
const _strEncCacheMax = 4096;
16+
function _cachedEncode(s) { let b = _strEncCache.get(s); if (b) { _strEncCache.delete(s); _strEncCache.set(s, b); return b; } b = textEncoder.encode(s); if (_strEncCache.size >= _strEncCacheMax) { _strEncCache.delete(_strEncCache.keys().next().value); } _strEncCache.set(s, b); return b; };
1417
let tmpRetString;
1518
let tmpRetBytes;
1619
let tmpRetException;
@@ -159,7 +162,11 @@ export async function createInstantiator(options, swift) {
159162
const source = swift.memory.getObject(sourceId);
160163
swift.memory.release(sourceId);
161164
const bytes = new Uint8Array(memory.buffer, bytesPtr);
165+
if (typeof source === 'string') {
166+
return textEncoder.encodeInto(source, bytes).written;
167+
}
162168
bytes.set(source);
169+
return source.length;
163170
}
164171
bjs["swift_js_make_js_string"] = function(ptr, len) {
165172
return swift.memory.retain(decodeString(ptr, len));

0 commit comments

Comments
 (0)