expo-modules-jsi 57.0.6 โ 57.0.8
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/CHANGELOG.md +27 -0
- package/CLAUDE.md +35 -0
- package/apple/Benchmarks/BenchmarkRunner.swift +115 -0
- package/apple/Benchmarks/FunctionCallBenchmarks.swift +45 -0
- package/apple/Benchmarks/HostFunctionBenchmarks.swift +217 -0
- package/apple/Benchmarks/StringConversionBenchmarks.swift +245 -0
- package/apple/Benchmarks/ValueAccessBenchmarks.swift +132 -0
- package/apple/Package.swift +10 -0
- package/apple/Sources/ExpoModulesJSI/Contexts/HostCallbackContext.swift +28 -0
- package/apple/Sources/ExpoModulesJSI/Contexts/HostFunctionContext.swift +18 -6
- package/apple/Sources/ExpoModulesJSI/Contexts/HostObjectContext.swift +7 -3
- package/apple/Sources/ExpoModulesJSI/Protocols/JSIRepresentable.swift +20 -13
- package/apple/Sources/ExpoModulesJSI/Protocols/JavaScriptRepresentable.swift +15 -3
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptPropNameID.swift +11 -7
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +80 -58
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptValuesBuffer.swift +7 -2
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptArray.swift +2 -2
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptBigInt.swift +1 -2
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptObject.swift +18 -8
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptUnownedValue.swift +2 -2
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptValue.swift +27 -9
- package/apple/Sources/ExpoModulesJSI/Utilities/String+JSI.swift +130 -0
- package/apple/Sources/ExpoModulesJSI-Cxx/include/CppError.h +9 -3
- package/apple/Sources/ExpoModulesJSI-Cxx/include/HostFunctionClosure.h +6 -3
- package/apple/Sources/ExpoModulesJSI-Cxx/include/HostObject.h +2 -1
- package/apple/Sources/ExpoModulesJSI-Cxx/include/HostObjectCallbacks.h +5 -3
- package/apple/Sources/ExpoModulesJSI-Cxx/include/JSIUtils.h +18 -7
- package/apple/Tests/JavaScriptArrayBufferTests.swift +2 -2
- package/apple/Tests/JavaScriptNativeStateTests.swift +10 -10
- package/apple/Tests/JavaScriptObjectTests.swift +79 -0
- package/apple/Tests/JavaScriptPropNameIDTests.swift +123 -0
- package/apple/Tests/JavaScriptRuntimeTests.swift +29 -0
- package/apple/Tests/JavaScriptTypedArrayTests.swift +1 -1
- package/apple/Tests/JavaScriptUnownedValueTests.swift +9 -0
- package/apple/Tests/JavaScriptValueTests.swift +102 -0
- package/apple/Tests/JavaScriptValuesBufferTests.swift +2 -2
- package/apple/Tests/JavaScriptWeakObjectTests.swift +2 -2
- package/apple/scripts/build-xcframework.sh +10 -0
- package/apple/scripts/generate-modulemap.sh +10 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,33 @@
|
|
|
10
10
|
|
|
11
11
|
### ๐ก Others
|
|
12
12
|
|
|
13
|
+
## 57.0.8 โ 2026-09-04
|
|
14
|
+
|
|
15
|
+
### ๐ New features
|
|
16
|
+
|
|
17
|
+
- [iOS] Add `JavaScriptRuntime.collectGarbage(cause:)` that requests a full garbage collection through the runtime's JSI instrumentation. It is a no-op on engines whose runtime doesn't implement GC instrumentation, unlike the Hermes-only `gc()` global that the tests used to evaluate. ([#48446](https://github.com/expo/expo/pull/48446) by [@tsapeta](https://github.com/tsapeta))
|
|
18
|
+
|
|
19
|
+
### ๐ Bug fixes
|
|
20
|
+
|
|
21
|
+
- [iOS] Fixed `JavaScriptPropNameID(_:string:)` and the array's string-keyed subscript truncating non-ASCII property keys: they passed `String.count` (the grapheme-cluster count) as the UTF-8 byte length to `PropNameID::forUtf8`, so keys like `"cafรฉ"` or `"๐"` were built from mangled bytes and no longer matched the intended property. ([#48329](https://github.com/expo/expo/pull/48329) by [@tsapeta](https://github.com/tsapeta))
|
|
22
|
+
- [iOS] Fixed the prebuilt `ExpoModulesJSI.xcframework` shipping with code coverage instrumentation: building through the auto-generated SwiftPM scheme made Xcode pass `-profile-generate -profile-coverage-mapping` to swiftc even for a plain Release `build`, adding a counter increment to every function on the host function call path and about 40% to the binary size. The benchmark target had the same instrumentation and now runs without it. ([#49637](https://github.com/expo/expo/pull/49637) by [@tsapeta](https://github.com/tsapeta))
|
|
23
|
+
- [iOS] Fixed property names with non-ASCII characters being mangled when accessed by name from Swift, such as `getProperty`, `setProperty`, `hasProperty`, the array string subscript and dictionary conversions. ([#49679](https://github.com/expo/expo/pull/49679) by [@tsapeta](https://github.com/tsapeta))
|
|
24
|
+
|
|
25
|
+
### ๐ก Others
|
|
26
|
+
|
|
27
|
+
- [iOS] Added an opt-in benchmark target that measures value access, host function calls, and JS function calls; run it with `pnpm benchmark`. ([#49579](https://github.com/expo/expo/pull/49579) by [@tsapeta](https://github.com/tsapeta))
|
|
28
|
+
- [iOS] Reduced the native overhead of synchronous host function calls and host object property accessors by removing the per-call weak and unowned runtime reference traffic on the call path. ([#49631](https://github.com/expo/expo/pull/49631) by [@tsapeta](https://github.com/tsapeta))
|
|
29
|
+
- [iOS] Made passing strings between JavaScript and Swift faster, up to ~3.8ร for long strings. ([#49678](https://github.com/expo/expo/pull/49678) by [@tsapeta](https://github.com/tsapeta))
|
|
30
|
+
- [iOS] Values returned to Swift from property reads, array reads and function calls are now taken over instead of cloned through the engine, making `toJavaScriptValue(in:)` ~1.16ร faster. ([#49688](https://github.com/expo/expo/pull/49688) by [@tsapeta](https://github.com/tsapeta))
|
|
31
|
+
- [iOS] Made decoding non-ASCII JS strings up to 512 UTF-16 code units long ~1.5ร faster. ([#49691](https://github.com/expo/expo/pull/49691) by [@tsapeta](https://github.com/tsapeta))
|
|
32
|
+
|
|
33
|
+
## 57.0.7 โ 2026-09-01
|
|
34
|
+
|
|
35
|
+
### ๐ก Others
|
|
36
|
+
|
|
37
|
+
- [iOS] `JavaScriptValue.undefined` and `JavaScriptValue.null` now return shared immortal instances instead of allocating a new value on each access, removing one allocation from every void-returning host call. ([#49545](https://github.com/expo/expo/pull/49545) by [@tsapeta](https://github.com/tsapeta))
|
|
38
|
+
- [iOS] `CppError::tryCatch` now takes a C++ callable instead of an Objective-C block. Every caller already passes a pure C++ body, so the block bridged no Swift closure and only added a non-inlinable indirect call and an Objective-C runtime dependency on the JS call/eval error-handling path. ([#48333](https://github.com/expo/expo/pull/48333) by [@tsapeta](https://github.com/tsapeta))
|
|
39
|
+
|
|
13
40
|
## 57.0.6 โ 2026-08-26
|
|
14
41
|
|
|
15
42
|
### ๐ก Others
|
package/CLAUDE.md
CHANGED
|
@@ -65,6 +65,41 @@ Tests are in `apple/Tests/` and each file covers one type. Some suites use the g
|
|
|
65
65
|
|
|
66
66
|
Run them with `pnpm test` from the package root, which calls `apple/scripts/test.sh`. The script needs an installed host app's `Pods` directory (defaults to `$EXPO_ROOT_DIR/apps/bare-expo/ios/Pods`); override with `PODS_ROOT`. It symlinks React / hermesvm / ReactNativeDependencies xcframeworks into `apple/.test-frameworks/` so SPM can resolve them as relative-path binary targets, generates the `jsi` modulemap, and runs `xcodebuild test` against an iOS Simulator (override with `DESTINATION`). Extra args pass through to xcodebuild — e.g. `pnpm test -only-testing TestName`.
|
|
67
67
|
|
|
68
|
+
## Benchmarks
|
|
69
|
+
|
|
70
|
+
Performance benchmarks live in `apple/Benchmarks/` as a separate opt-in test target. Run them with
|
|
71
|
+
`pnpm benchmark` from the package root; it reuses the test harness (`apple/scripts/test.sh`, same
|
|
72
|
+
Pods requirements as tests) but builds in the Release configuration, which is required for
|
|
73
|
+
meaningful numbers. Regular test runs build the target but skip every suite: the suites are gated
|
|
74
|
+
on the `EXPO_BENCHMARK` environment variable, which the script forwards to the test runner via
|
|
75
|
+
`TEST_RUNNER_EXPO_BENCHMARK=1`.
|
|
76
|
+
|
|
77
|
+
Benchmarks default to the Mac Catalyst destination (the prebuilt xcframeworks ship catalyst
|
|
78
|
+
slices): they run natively on the Mac with no simulator, and measured spreads drop to 1-2% versus
|
|
79
|
+
roughly 10-25% on a simulator. Set `DESTINATION` to override, e.g. for a simulator or device run.
|
|
80
|
+
|
|
81
|
+
Each benchmark prints one line to the xcodebuild log:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
[benchmark] <name>: median <X> ns/op, min <Y> ns/op (<iterations> iterations, <samples> samples)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Every benchmark is a test in the single `JSIBenchmarks` suite (extended across the files), which
|
|
88
|
+
is `.serialized` so only one benchmark runs at a time: the `JavaScriptActor` executor runs jobs
|
|
89
|
+
synchronously on the calling thread, so parallel suites would disturb each other's measurements.
|
|
90
|
+
Each test wraps its content in `benchmarkCase { runtime in ... }`, which creates a fresh runtime
|
|
91
|
+
and runs the case in a high-priority task; at the testing library's default priority the scheduler
|
|
92
|
+
sometimes places the run on efficiency cores, which produced multi-fold run-to-run swings.
|
|
93
|
+
|
|
94
|
+
Inside a case, the `benchmark(_:runtime:samples:_:)` helper calibrates the iteration count to
|
|
95
|
+
~50ms per sample (two-phase: tenfold growth to get an estimate, then a rescale from the warmup
|
|
96
|
+
run), and collects garbage between samples. The body receives the iteration count and must perform
|
|
97
|
+
exactly that many operations: loop in Swift for micro benchmarks, or pass the count to a
|
|
98
|
+
JavaScript driver function to measure a full JS-to-native round trip (see
|
|
99
|
+
`HostFunctionBenchmarks.swift`, which includes an empty-driver-loop baseline to read the other
|
|
100
|
+
results against). Expect run-to-run medians to agree within roughly ยฑ10-25% on a workstation;
|
|
101
|
+
compare medians, and treat anything within that band as noise.
|
|
102
|
+
|
|
68
103
|
## Formatting
|
|
69
104
|
|
|
70
105
|
Swift sources are formatted with swift-format. From the package root, `pnpm swift:format` rewrites files in place and `pnpm swift:lint` checks without modifying; both delegate to the repo-root `scripts/swift-format.sh`, which reads the repo-root `.swift-format` config and only touches tracked `.swift` files. CI enforces this via `.github/workflows/swift-format.yml`, which pins a specific swift-format version — mismatched local versions can produce different output, so prefer the pinned one. Style conventions beyond what the formatter enforces live in [`guides/Swift Style Guide.md`](../../guides/Swift%20Style%20Guide.md) at the repo root.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Copyright 2026-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesJSI
|
|
4
|
+
import Foundation
|
|
5
|
+
import Testing
|
|
6
|
+
|
|
7
|
+
/// Whether benchmark suites are enabled for this test run. Benchmarks are opt-in:
|
|
8
|
+
/// regular test runs skip them. Run `pnpm benchmark`, which enables them by forwarding
|
|
9
|
+
/// the `EXPO_BENCHMARK` environment variable to the test runner and builds the package
|
|
10
|
+
/// in the Release configuration so the numbers are meaningful. It also turns code coverage
|
|
11
|
+
/// off: `xcodebuild test` instruments every function and closure with profile counters
|
|
12
|
+
/// otherwise, even in Release, which adds a few nanoseconds to each measured call.
|
|
13
|
+
let benchmarksEnabled = ProcessInfo.processInfo.environment["EXPO_BENCHMARK"] == "1"
|
|
14
|
+
|
|
15
|
+
/// Umbrella suite that all benchmarks belong to (as extensions in the other files).
|
|
16
|
+
/// It's serialized so only one benchmark runs at a time: the `JavaScriptActor` executor
|
|
17
|
+
/// runs jobs synchronously on the calling thread, so separate suites would execute
|
|
18
|
+
/// concurrently on the parallel testing pool and disturb each other's measurements.
|
|
19
|
+
@Suite(.enabled(if: benchmarksEnabled), .serialized)
|
|
20
|
+
struct JSIBenchmarks {}
|
|
21
|
+
|
|
22
|
+
/// Runs one benchmark case inside a high-priority task, with a fresh runtime created
|
|
23
|
+
/// for it. The priority matters: the testing library runs tests at default priority,
|
|
24
|
+
/// whose threads the scheduler sometimes places on efficiency cores, which showed up
|
|
25
|
+
/// as multi-fold run-to-run swings in the results. Create the runtime, the measured
|
|
26
|
+
/// values, and the `benchmark(_:runtime:samples:_:)` calls inside the closure.
|
|
27
|
+
func benchmarkCase(_ body: @escaping @JavaScriptActor (JavaScriptRuntime) throws -> Void) async throws {
|
|
28
|
+
try await Task(priority: .high) { @JavaScriptActor in
|
|
29
|
+
let runtime = JavaScriptRuntime()
|
|
30
|
+
try body(runtime)
|
|
31
|
+
}.value
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Runs the given body repeatedly and prints a single-line report with the median and
|
|
35
|
+
/// minimum time per operation. The body receives the number of operations to perform,
|
|
36
|
+
/// either in a Swift loop or by passing the count to a JavaScript driver function.
|
|
37
|
+
///
|
|
38
|
+
/// The iteration count is calibrated so that a single sample takes roughly 50ms, and the
|
|
39
|
+
/// garbage collector runs between samples so collection pauses triggered by garbage from
|
|
40
|
+
/// one sample don't land in the next one's measurement.
|
|
41
|
+
@JavaScriptActor
|
|
42
|
+
func benchmark(
|
|
43
|
+
_ name: String,
|
|
44
|
+
runtime: JavaScriptRuntime,
|
|
45
|
+
samples sampleCount: Int = 7,
|
|
46
|
+
_ body: (_ iterations: Int) throws -> Void
|
|
47
|
+
) rethrows {
|
|
48
|
+
let clock = ContinuousClock()
|
|
49
|
+
var iterations = try calibrateIterations(clock: clock, body)
|
|
50
|
+
// The warmup run doubles as the second calibration phase: it runs at the estimated
|
|
51
|
+
// iteration count, which is more representative than the short runs the estimate came
|
|
52
|
+
// from (caches are warm, one-time costs are paid), so rescale the count from it.
|
|
53
|
+
let warmupElapsed = try clock.measure {
|
|
54
|
+
try body(iterations)
|
|
55
|
+
}
|
|
56
|
+
iterations = rescaleIterations(iterations, elapsed: warmupElapsed)
|
|
57
|
+
var nanosecondsPerOperation = [Double]()
|
|
58
|
+
for _ in 0..<sampleCount {
|
|
59
|
+
runtime.collectGarbage()
|
|
60
|
+
let elapsed = try clock.measure {
|
|
61
|
+
try body(iterations)
|
|
62
|
+
}
|
|
63
|
+
nanosecondsPerOperation.append(Double(elapsed.nanoseconds) / Double(iterations))
|
|
64
|
+
}
|
|
65
|
+
nanosecondsPerOperation.sort()
|
|
66
|
+
let median = format(nanosecondsPerOperation[nanosecondsPerOperation.count / 2])
|
|
67
|
+
let minimum = format(nanosecondsPerOperation[0])
|
|
68
|
+
print(
|
|
69
|
+
"[benchmark] \(name): median \(median) ns/op, min \(minimum) ns/op"
|
|
70
|
+
+ " (\(iterations) iterations, \(sampleCount) samples)"
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// How long a single measured sample should take.
|
|
75
|
+
private let targetSampleNanoseconds = 50_000_000.0
|
|
76
|
+
|
|
77
|
+
/// Finds an iteration count that makes one sample take roughly 50ms. Starts at a single
|
|
78
|
+
/// iteration and grows tenfold until the measured time is long enough to extrapolate from,
|
|
79
|
+
/// so even sub-nanosecond bodies calibrate quickly without overshooting.
|
|
80
|
+
@JavaScriptActor
|
|
81
|
+
private func calibrateIterations(
|
|
82
|
+
clock: ContinuousClock,
|
|
83
|
+
_ body: (_ iterations: Int) throws -> Void
|
|
84
|
+
) rethrows -> Int {
|
|
85
|
+
var iterations = 1
|
|
86
|
+
while true {
|
|
87
|
+
let elapsed = try clock.measure {
|
|
88
|
+
try body(iterations)
|
|
89
|
+
}
|
|
90
|
+
if elapsed >= .milliseconds(5) || iterations >= 100_000_000 {
|
|
91
|
+
return rescaleIterations(iterations, elapsed: elapsed)
|
|
92
|
+
}
|
|
93
|
+
iterations *= 10
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Rescales an iteration count so the next run of that many iterations takes roughly
|
|
98
|
+
/// the target sample duration, given how long `iterations` of the body just took.
|
|
99
|
+
private func rescaleIterations(_ iterations: Int, elapsed: Duration) -> Int {
|
|
100
|
+
let nanoseconds = max(elapsed.nanoseconds, 1)
|
|
101
|
+
let rescaled = Int(Double(iterations) * targetSampleNanoseconds / Double(nanoseconds))
|
|
102
|
+
return min(max(rescaled, 1), 100_000_000)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Formats a nanosecond quantity with a single decimal place.
|
|
106
|
+
private func format(_ nanoseconds: Double) -> String {
|
|
107
|
+
return String(format: "%.1f", nanoseconds)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
extension Duration {
|
|
111
|
+
/// The duration expressed as a whole number of nanoseconds.
|
|
112
|
+
var nanoseconds: Int64 {
|
|
113
|
+
return components.seconds * 1_000_000_000 + components.attoseconds / 1_000_000_000
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Copyright 2026-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesJSI
|
|
4
|
+
import Testing
|
|
5
|
+
|
|
6
|
+
/// Benchmarks for Swift calling into JavaScript functions, the opposite direction
|
|
7
|
+
/// of the host function benchmarks. Each operation covers encoding the arguments,
|
|
8
|
+
/// the engine's function invocation, and wrapping the returned value.
|
|
9
|
+
extension JSIBenchmarks {
|
|
10
|
+
@Test
|
|
11
|
+
func `call noop JS function`() async throws {
|
|
12
|
+
try await benchmarkCase { runtime in
|
|
13
|
+
let fn = try runtime.eval("(function() {})").getFunction()
|
|
14
|
+
try benchmark("JavaScriptFunction.call(): noop", runtime: runtime) { iterations in
|
|
15
|
+
for _ in 0..<iterations {
|
|
16
|
+
_ = try fn.call()
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Test
|
|
23
|
+
func `call JS function with two numbers`() async throws {
|
|
24
|
+
try await benchmarkCase { runtime in
|
|
25
|
+
let fn = try runtime.eval("(function(a, b) { return a + b; })").getFunction()
|
|
26
|
+
try benchmark("JavaScriptFunction.call(): add two numbers", runtime: runtime) { iterations in
|
|
27
|
+
for _ in 0..<iterations {
|
|
28
|
+
_ = try fn.call(arguments: 3.5, 38.5)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Test
|
|
35
|
+
func `call JS function with two strings`() async throws {
|
|
36
|
+
try await benchmarkCase { runtime in
|
|
37
|
+
let fn = try runtime.eval("(function(a, b) { return a + b; })").getFunction()
|
|
38
|
+
try benchmark("JavaScriptFunction.call(): concat two strings", runtime: runtime) { iterations in
|
|
39
|
+
for _ in 0..<iterations {
|
|
40
|
+
_ = try fn.call(arguments: "expo ", "modules")
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Copyright 2026-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesJSI
|
|
4
|
+
import Testing
|
|
5
|
+
|
|
6
|
+
/// End-to-end benchmarks for JavaScript calling into Swift host functions.
|
|
7
|
+
/// A JavaScript driver function runs the measured loop, so each operation covers
|
|
8
|
+
/// the full round trip: the engine's call dispatch, argument decoding in Swift,
|
|
9
|
+
/// and encoding the result back to JavaScript.
|
|
10
|
+
extension JSIBenchmarks {
|
|
11
|
+
/// Measures the raw JavaScript loop that drives the other host function benchmarks,
|
|
12
|
+
/// so their results can be read relative to the loop's own overhead.
|
|
13
|
+
@Test
|
|
14
|
+
func `empty JS loop`() async throws {
|
|
15
|
+
try await benchmarkCase { runtime in
|
|
16
|
+
let driver = try runtime.eval("(function(n) { for (var i = 0; i < n; i++); })").getFunction()
|
|
17
|
+
try benchmark("host function: empty driver loop", runtime: runtime) { iterations in
|
|
18
|
+
_ = try driver.call(arguments: iterations)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Test
|
|
24
|
+
func `noop host function`() async throws {
|
|
25
|
+
try await benchmarkCase { runtime in
|
|
26
|
+
let fn = runtime.createFunction("noop") { _, _ in
|
|
27
|
+
return .undefined
|
|
28
|
+
}
|
|
29
|
+
runtime.global().setProperty("benchFn", value: fn)
|
|
30
|
+
let driver = try runtime.eval("(function(n) { for (var i = 0; i < n; i++) benchFn(); })").getFunction()
|
|
31
|
+
try benchmark("host function: noop", runtime: runtime) { iterations in
|
|
32
|
+
_ = try driver.call(arguments: iterations)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Same as `noop host function`, but through the unowned-`this` closure form that
|
|
38
|
+
/// macro-generated `@JS` functions bind to.
|
|
39
|
+
@Test
|
|
40
|
+
func `noop unowned-this host function`() async throws {
|
|
41
|
+
try await benchmarkCase { runtime in
|
|
42
|
+
let fn = runtime.createFunction("noop") {
|
|
43
|
+
(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer) in
|
|
44
|
+
return .undefined
|
|
45
|
+
}
|
|
46
|
+
runtime.global().setProperty("benchFn", value: fn)
|
|
47
|
+
let driver = try runtime.eval("(function(n) { for (var i = 0; i < n; i++) benchFn(); })").getFunction()
|
|
48
|
+
try benchmark("host function: noop unowned-this", runtime: runtime) { iterations in
|
|
49
|
+
_ = try driver.call(arguments: iterations)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Test
|
|
55
|
+
func `host function concatenating two strings`() async throws {
|
|
56
|
+
try await benchmarkCase { runtime in
|
|
57
|
+
let fn = runtime.createFunction("concat") { [runtime] _, arguments in
|
|
58
|
+
let a = try arguments[0].asString()
|
|
59
|
+
let b = try arguments[1].asString()
|
|
60
|
+
return JavaScriptValue(runtime, a + b)
|
|
61
|
+
}
|
|
62
|
+
runtime.global().setProperty("benchFn", value: fn)
|
|
63
|
+
let driver = try runtime.eval(
|
|
64
|
+
"(function(n) { for (var i = 0; i < n; i++) benchFn('expo ', 'modules'); })"
|
|
65
|
+
).getFunction()
|
|
66
|
+
try benchmark("host function: concat two strings", runtime: runtime) { iterations in
|
|
67
|
+
_ = try driver.call(arguments: iterations)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Test
|
|
73
|
+
func `host function adding two numbers`() async throws {
|
|
74
|
+
try await benchmarkCase { runtime in
|
|
75
|
+
let fn = runtime.createFunction("add") { [runtime] _, arguments in
|
|
76
|
+
let a = try arguments[0].asDouble()
|
|
77
|
+
let b = try arguments[1].asDouble()
|
|
78
|
+
return JavaScriptValue(runtime, a + b)
|
|
79
|
+
}
|
|
80
|
+
runtime.global().setProperty("benchFn", value: fn)
|
|
81
|
+
let driver = try runtime.eval(
|
|
82
|
+
"(function(n) { for (var i = 0; i < n; i++) benchFn(3.5, 38.5); })"
|
|
83
|
+
).getFunction()
|
|
84
|
+
try benchmark("host function: add two numbers", runtime: runtime) { iterations in
|
|
85
|
+
_ = try driver.call(arguments: iterations)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// MARK: - Unowned-this form with string arguments
|
|
91
|
+
|
|
92
|
+
@Test
|
|
93
|
+
func `concat two strings unowned-this`() async throws {
|
|
94
|
+
try await benchmarkCase { runtime in
|
|
95
|
+
let fn = runtime.createFunction("concat") {
|
|
96
|
+
[runtime] (this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer) in
|
|
97
|
+
let a = try String.decode(arguments.unownedValue(at: 0), in: runtime)
|
|
98
|
+
let b = try String.decode(arguments.unownedValue(at: 1), in: runtime)
|
|
99
|
+
return try String.encode(a + b, in: runtime)
|
|
100
|
+
}
|
|
101
|
+
runtime.global().setProperty("benchFn", value: fn)
|
|
102
|
+
let driver = try runtime.eval(
|
|
103
|
+
"""
|
|
104
|
+
(function(n) { for (var i = 0; i < n; i++) benchFn('expo ', 'modules'); })
|
|
105
|
+
"""
|
|
106
|
+
).getFunction()
|
|
107
|
+
try benchmark("host function: concat two strings unowned-this", runtime: runtime) { iterations in
|
|
108
|
+
_ = try driver.call(arguments: iterations)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Like `concat two strings unowned-this`, with inputs and result longer than libc++'s 22-byte
|
|
114
|
+
/// `std::string` inline capacity, so every `std::string` on the path has to allocate.
|
|
115
|
+
@Test
|
|
116
|
+
func `concat two long strings unowned-this`() async throws {
|
|
117
|
+
try await benchmarkCase { runtime in
|
|
118
|
+
let fn = runtime.createFunction("concat") {
|
|
119
|
+
[runtime] (this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer) in
|
|
120
|
+
let a = try String.decode(arguments.unownedValue(at: 0), in: runtime)
|
|
121
|
+
let b = try String.decode(arguments.unownedValue(at: 1), in: runtime)
|
|
122
|
+
return try String.encode(a + b, in: runtime)
|
|
123
|
+
}
|
|
124
|
+
runtime.global().setProperty("benchFn", value: fn)
|
|
125
|
+
let driver = try runtime.eval(
|
|
126
|
+
"""
|
|
127
|
+
(function(n) {
|
|
128
|
+
for (var i = 0; i < n; i++) benchFn('expo modules are quite fast, ', 'and this string is also long');
|
|
129
|
+
})
|
|
130
|
+
"""
|
|
131
|
+
).getFunction()
|
|
132
|
+
try benchmark("host function: concat two long strings unowned-this", runtime: runtime) { iterations in
|
|
133
|
+
_ = try driver.call(arguments: iterations)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@Test
|
|
139
|
+
func `concat two 256B strings unowned-this`() async throws {
|
|
140
|
+
try await benchmarkCase { runtime in
|
|
141
|
+
let fn = runtime.createFunction("concat") {
|
|
142
|
+
[runtime] (this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer) in
|
|
143
|
+
let a = try String.decode(arguments.unownedValue(at: 0), in: runtime)
|
|
144
|
+
let b = try String.decode(arguments.unownedValue(at: 1), in: runtime)
|
|
145
|
+
return try String.encode(a + b, in: runtime)
|
|
146
|
+
}
|
|
147
|
+
runtime.global().setProperty("benchFn", value: fn)
|
|
148
|
+
let driver = try runtime.eval(
|
|
149
|
+
"(function(n) { var a = 'a'.repeat(128); var b = 'b'.repeat(128); for (var i = 0; i < n; i++) benchFn(a, b); })"
|
|
150
|
+
).getFunction()
|
|
151
|
+
try benchmark("host function: concat two 256B strings unowned-this", runtime: runtime) { iterations in
|
|
152
|
+
_ = try driver.call(arguments: iterations)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@Test
|
|
158
|
+
func `concat two 4KB strings unowned-this`() async throws {
|
|
159
|
+
try await benchmarkCase { runtime in
|
|
160
|
+
let fn = runtime.createFunction("concat") {
|
|
161
|
+
[runtime] (this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer) in
|
|
162
|
+
let a = try String.decode(arguments.unownedValue(at: 0), in: runtime)
|
|
163
|
+
let b = try String.decode(arguments.unownedValue(at: 1), in: runtime)
|
|
164
|
+
return try String.encode(a + b, in: runtime)
|
|
165
|
+
}
|
|
166
|
+
runtime.global().setProperty("benchFn", value: fn)
|
|
167
|
+
let driver = try runtime.eval(
|
|
168
|
+
"""
|
|
169
|
+
(function(n) { var a = 'a'.repeat(2048); var b = 'b'.repeat(2048); for (var i = 0; i < n; i++) benchFn(a, b); })
|
|
170
|
+
"""
|
|
171
|
+
).getFunction()
|
|
172
|
+
try benchmark("host function: concat two 4KB strings unowned-this", runtime: runtime) { iterations in
|
|
173
|
+
_ = try driver.call(arguments: iterations)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@Test
|
|
179
|
+
func `concat two 4KB non-ASCII strings unowned-this`() async throws {
|
|
180
|
+
try await benchmarkCase { runtime in
|
|
181
|
+
let fn = runtime.createFunction("concat") {
|
|
182
|
+
[runtime] (this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer) in
|
|
183
|
+
let a = try String.decode(arguments.unownedValue(at: 0), in: runtime)
|
|
184
|
+
let b = try String.decode(arguments.unownedValue(at: 1), in: runtime)
|
|
185
|
+
return try String.encode(a + b, in: runtime)
|
|
186
|
+
}
|
|
187
|
+
runtime.global().setProperty("benchFn", value: fn)
|
|
188
|
+
let driver = try runtime.eval(
|
|
189
|
+
"""
|
|
190
|
+
(function(n) { var a = 'ฤ
'.repeat(2048); var b = 'ฤ
'.repeat(2048); for (var i = 0; i < n; i++) benchFn(a, b); })
|
|
191
|
+
"""
|
|
192
|
+
).getFunction()
|
|
193
|
+
try benchmark("host function: concat two 4KB non-ASCII strings unowned-this", runtime: runtime) { iterations in
|
|
194
|
+
_ = try driver.call(arguments: iterations)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@Test
|
|
200
|
+
func `concat two 12B non-ASCII strings unowned-this`() async throws {
|
|
201
|
+
try await benchmarkCase { runtime in
|
|
202
|
+
let fn = runtime.createFunction("concat") {
|
|
203
|
+
[runtime] (this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer) in
|
|
204
|
+
let a = try String.decode(arguments.unownedValue(at: 0), in: runtime)
|
|
205
|
+
let b = try String.decode(arguments.unownedValue(at: 1), in: runtime)
|
|
206
|
+
return try String.encode(a + b, in: runtime)
|
|
207
|
+
}
|
|
208
|
+
runtime.global().setProperty("benchFn", value: fn)
|
|
209
|
+
let driver = try runtime.eval(
|
|
210
|
+
"(function(n) { var a = 'ฤ
ฤรณ'.repeat(2); var b = 'ฤ
ฤรณ'.repeat(2); for (var i = 0; i < n; i++) benchFn(a, b); })"
|
|
211
|
+
).getFunction()
|
|
212
|
+
try benchmark("host function: concat two 12B non-ASCII strings unowned-this", runtime: runtime) { iterations in
|
|
213
|
+
_ = try driver.call(arguments: iterations)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|