mini_racer_rs 1.1.0 → 1.1.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/Cargo.lock +209 -5
- data/Cargo.toml +2 -1
- data/README.md +71 -48
- data/crates/mini_racer_wire/Cargo.toml +5 -0
- data/crates/mini_racer_wire/benches/wire.rs +293 -0
- data/ext/mini_racer_rs/src/host.rs +25 -32
- data/ext/mini_racer_rs/src/lib.rs +1 -0
- data/ext/mini_racer_rs/src/spin.rs +123 -0
- data/lib/mini_racer_rs/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1cd380b3e8e2ba2c09a4e389bac31dfe9b6c1557660df3d9cadfd32f48e42648
|
|
4
|
+
data.tar.gz: 1bd9456a98bfce434951619a3e5eaf47dc39fd9b7825f8476e02656f8e5d4a5a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7975cc80b2c3d1be558d518da06144c5f06783ff71fe82fd032964bf97012e844e113c26a4237fb0d3810e91089eab544c91ccc64f4f1400ec30a4d205644877
|
|
7
|
+
data.tar.gz: 36361b28bc1d112ed6074c9a5026e4b1e70926ba8eec19e533f9b052d8ae9f4b8fce1f235df2c8ae04ca220a91a65dbacf9f1351f00714739dcec5286140624a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.1.1](https://github.com/andymai/mini_racer_rs/compare/v1.1.0...v1.1.1) (2026-10-21)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Performance
|
|
7
|
+
|
|
8
|
+
* **ext:** park at once for contexts whose answers outlast the spin budget ([#105](https://github.com/andymai/mini_racer_rs/issues/105)) ([66af89b](https://github.com/andymai/mini_racer_rs/commit/66af89b8d22ebe0c4dd28c877155131e3d7e5738))
|
|
9
|
+
* **wire:** add criterion benches for decoding, writing and transcoding ([#106](https://github.com/andymai/mini_racer_rs/issues/106)) ([42fc2b5](https://github.com/andymai/mini_racer_rs/commit/42fc2b50dba2a4a6437456293fbe61aa5d29aef8))
|
|
10
|
+
|
|
3
11
|
## [1.1.0](https://github.com/andymai/mini_racer_rs/compare/v1.0.6...v1.1.0) (2026-10-19)
|
|
4
12
|
|
|
5
13
|
|
data/Cargo.lock
CHANGED
|
@@ -17,6 +17,27 @@ dependencies = [
|
|
|
17
17
|
"memchr",
|
|
18
18
|
]
|
|
19
19
|
|
|
20
|
+
[[package]]
|
|
21
|
+
name = "alloca"
|
|
22
|
+
version = "0.4.0"
|
|
23
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
24
|
+
checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"
|
|
25
|
+
dependencies = [
|
|
26
|
+
"cc",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[[package]]
|
|
30
|
+
name = "anes"
|
|
31
|
+
version = "0.1.6"
|
|
32
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
33
|
+
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
|
34
|
+
|
|
35
|
+
[[package]]
|
|
36
|
+
name = "anstyle"
|
|
37
|
+
version = "1.0.14"
|
|
38
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
39
|
+
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
|
40
|
+
|
|
20
41
|
[[package]]
|
|
21
42
|
name = "autocfg"
|
|
22
43
|
version = "1.5.1"
|
|
@@ -59,6 +80,12 @@ dependencies = [
|
|
|
59
80
|
"displaydoc",
|
|
60
81
|
]
|
|
61
82
|
|
|
83
|
+
[[package]]
|
|
84
|
+
name = "cast"
|
|
85
|
+
version = "0.3.0"
|
|
86
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
87
|
+
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
|
88
|
+
|
|
62
89
|
[[package]]
|
|
63
90
|
name = "cc"
|
|
64
91
|
version = "1.8.0"
|
|
@@ -95,6 +122,33 @@ dependencies = [
|
|
|
95
122
|
"rand_core",
|
|
96
123
|
]
|
|
97
124
|
|
|
125
|
+
[[package]]
|
|
126
|
+
name = "ciborium"
|
|
127
|
+
version = "0.2.2"
|
|
128
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
129
|
+
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
|
|
130
|
+
dependencies = [
|
|
131
|
+
"ciborium-io",
|
|
132
|
+
"ciborium-ll",
|
|
133
|
+
"serde",
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
[[package]]
|
|
137
|
+
name = "ciborium-io"
|
|
138
|
+
version = "0.2.2"
|
|
139
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
140
|
+
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
|
|
141
|
+
|
|
142
|
+
[[package]]
|
|
143
|
+
name = "ciborium-ll"
|
|
144
|
+
version = "0.2.2"
|
|
145
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
146
|
+
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
|
|
147
|
+
dependencies = [
|
|
148
|
+
"ciborium-io",
|
|
149
|
+
"half",
|
|
150
|
+
]
|
|
151
|
+
|
|
98
152
|
[[package]]
|
|
99
153
|
name = "clang-sys"
|
|
100
154
|
version = "1.9.1"
|
|
@@ -106,6 +160,31 @@ dependencies = [
|
|
|
106
160
|
"libloading",
|
|
107
161
|
]
|
|
108
162
|
|
|
163
|
+
[[package]]
|
|
164
|
+
name = "clap"
|
|
165
|
+
version = "4.6.7"
|
|
166
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
167
|
+
checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
|
|
168
|
+
dependencies = [
|
|
169
|
+
"clap_builder",
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
[[package]]
|
|
173
|
+
name = "clap_builder"
|
|
174
|
+
version = "4.6.7"
|
|
175
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
176
|
+
checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
|
|
177
|
+
dependencies = [
|
|
178
|
+
"anstyle",
|
|
179
|
+
"clap_lex",
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
[[package]]
|
|
183
|
+
name = "clap_lex"
|
|
184
|
+
version = "1.1.1"
|
|
185
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
186
|
+
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
|
|
187
|
+
|
|
109
188
|
[[package]]
|
|
110
189
|
name = "core_detect"
|
|
111
190
|
version = "1.0.0"
|
|
@@ -139,6 +218,45 @@ dependencies = [
|
|
|
139
218
|
"cfg-if",
|
|
140
219
|
]
|
|
141
220
|
|
|
221
|
+
[[package]]
|
|
222
|
+
name = "criterion"
|
|
223
|
+
version = "0.8.2"
|
|
224
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
225
|
+
checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3"
|
|
226
|
+
dependencies = [
|
|
227
|
+
"alloca",
|
|
228
|
+
"anes",
|
|
229
|
+
"cast",
|
|
230
|
+
"ciborium",
|
|
231
|
+
"clap",
|
|
232
|
+
"criterion-plot",
|
|
233
|
+
"itertools",
|
|
234
|
+
"num-traits",
|
|
235
|
+
"oorandom",
|
|
236
|
+
"page_size",
|
|
237
|
+
"regex",
|
|
238
|
+
"serde",
|
|
239
|
+
"serde_json",
|
|
240
|
+
"tinytemplate",
|
|
241
|
+
"walkdir",
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
[[package]]
|
|
245
|
+
name = "criterion-plot"
|
|
246
|
+
version = "0.8.2"
|
|
247
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
248
|
+
checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea"
|
|
249
|
+
dependencies = [
|
|
250
|
+
"cast",
|
|
251
|
+
"itertools",
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
[[package]]
|
|
255
|
+
name = "crunchy"
|
|
256
|
+
version = "0.2.4"
|
|
257
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
258
|
+
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
|
259
|
+
|
|
142
260
|
[[package]]
|
|
143
261
|
name = "deno_core_icudata"
|
|
144
262
|
version = "0.78.0"
|
|
@@ -262,6 +380,17 @@ dependencies = [
|
|
|
262
380
|
"crc32fast",
|
|
263
381
|
]
|
|
264
382
|
|
|
383
|
+
[[package]]
|
|
384
|
+
name = "half"
|
|
385
|
+
version = "2.7.1"
|
|
386
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
387
|
+
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
|
388
|
+
dependencies = [
|
|
389
|
+
"cfg-if",
|
|
390
|
+
"crunchy",
|
|
391
|
+
"zerocopy",
|
|
392
|
+
]
|
|
393
|
+
|
|
265
394
|
[[package]]
|
|
266
395
|
name = "home"
|
|
267
396
|
version = "0.5.12"
|
|
@@ -464,7 +593,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
|
|
464
593
|
|
|
465
594
|
[[package]]
|
|
466
595
|
name = "mini_racer_engine"
|
|
467
|
-
version = "1.1.
|
|
596
|
+
version = "1.1.1"
|
|
468
597
|
dependencies = [
|
|
469
598
|
"deno_core_icudata",
|
|
470
599
|
"libc",
|
|
@@ -477,7 +606,7 @@ dependencies = [
|
|
|
477
606
|
|
|
478
607
|
[[package]]
|
|
479
608
|
name = "mini_racer_rs"
|
|
480
|
-
version = "1.1.
|
|
609
|
+
version = "1.1.1"
|
|
481
610
|
dependencies = [
|
|
482
611
|
"magnus",
|
|
483
612
|
"mini_racer_engine",
|
|
@@ -488,7 +617,7 @@ dependencies = [
|
|
|
488
617
|
|
|
489
618
|
[[package]]
|
|
490
619
|
name = "mini_racer_sourcemap"
|
|
491
|
-
version = "1.1.
|
|
620
|
+
version = "1.1.1"
|
|
492
621
|
dependencies = [
|
|
493
622
|
"memchr",
|
|
494
623
|
"serde",
|
|
@@ -498,7 +627,7 @@ dependencies = [
|
|
|
498
627
|
|
|
499
628
|
[[package]]
|
|
500
629
|
name = "mini_racer_sync"
|
|
501
|
-
version = "1.1.
|
|
630
|
+
version = "1.1.1"
|
|
502
631
|
dependencies = [
|
|
503
632
|
"loom",
|
|
504
633
|
"thiserror",
|
|
@@ -506,8 +635,9 @@ dependencies = [
|
|
|
506
635
|
|
|
507
636
|
[[package]]
|
|
508
637
|
name = "mini_racer_wire"
|
|
509
|
-
version = "1.1.
|
|
638
|
+
version = "1.1.1"
|
|
510
639
|
dependencies = [
|
|
640
|
+
"criterion",
|
|
511
641
|
"proptest",
|
|
512
642
|
"thiserror",
|
|
513
643
|
]
|
|
@@ -561,6 +691,22 @@ version = "1.21.4"
|
|
|
561
691
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
562
692
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
563
693
|
|
|
694
|
+
[[package]]
|
|
695
|
+
name = "oorandom"
|
|
696
|
+
version = "11.1.5"
|
|
697
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
698
|
+
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
|
699
|
+
|
|
700
|
+
[[package]]
|
|
701
|
+
name = "page_size"
|
|
702
|
+
version = "0.6.0"
|
|
703
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
704
|
+
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
|
|
705
|
+
dependencies = [
|
|
706
|
+
"libc",
|
|
707
|
+
"winapi",
|
|
708
|
+
]
|
|
709
|
+
|
|
564
710
|
[[package]]
|
|
565
711
|
name = "paste"
|
|
566
712
|
version = "1.0.15"
|
|
@@ -753,6 +899,15 @@ version = "1.0.23"
|
|
|
753
899
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
754
900
|
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
|
755
901
|
|
|
902
|
+
[[package]]
|
|
903
|
+
name = "same-file"
|
|
904
|
+
version = "1.0.6"
|
|
905
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
906
|
+
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
|
907
|
+
dependencies = [
|
|
908
|
+
"winapi-util",
|
|
909
|
+
]
|
|
910
|
+
|
|
756
911
|
[[package]]
|
|
757
912
|
name = "scoped-tls"
|
|
758
913
|
version = "1.0.1"
|
|
@@ -975,6 +1130,16 @@ dependencies = [
|
|
|
975
1130
|
"zerovec",
|
|
976
1131
|
]
|
|
977
1132
|
|
|
1133
|
+
[[package]]
|
|
1134
|
+
name = "tinytemplate"
|
|
1135
|
+
version = "1.2.1"
|
|
1136
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1137
|
+
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
|
|
1138
|
+
dependencies = [
|
|
1139
|
+
"serde",
|
|
1140
|
+
"serde_json",
|
|
1141
|
+
]
|
|
1142
|
+
|
|
978
1143
|
[[package]]
|
|
979
1144
|
name = "tracing"
|
|
980
1145
|
version = "0.1.44"
|
|
@@ -1059,6 +1224,16 @@ version = "0.1.1"
|
|
|
1059
1224
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1060
1225
|
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
|
1061
1226
|
|
|
1227
|
+
[[package]]
|
|
1228
|
+
name = "walkdir"
|
|
1229
|
+
version = "2.5.0"
|
|
1230
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1231
|
+
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
|
1232
|
+
dependencies = [
|
|
1233
|
+
"same-file",
|
|
1234
|
+
"winapi-util",
|
|
1235
|
+
]
|
|
1236
|
+
|
|
1062
1237
|
[[package]]
|
|
1063
1238
|
name = "which"
|
|
1064
1239
|
version = "6.0.3"
|
|
@@ -1087,6 +1262,15 @@ version = "0.4.0"
|
|
|
1087
1262
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1088
1263
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
|
1089
1264
|
|
|
1265
|
+
[[package]]
|
|
1266
|
+
name = "winapi-util"
|
|
1267
|
+
version = "0.1.11"
|
|
1268
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1269
|
+
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
|
1270
|
+
dependencies = [
|
|
1271
|
+
"windows-sys 0.61.2",
|
|
1272
|
+
]
|
|
1273
|
+
|
|
1090
1274
|
[[package]]
|
|
1091
1275
|
name = "winapi-x86_64-pc-windows-gnu"
|
|
1092
1276
|
version = "0.4.0"
|
|
@@ -1225,6 +1409,26 @@ dependencies = [
|
|
|
1225
1409
|
"synstructure",
|
|
1226
1410
|
]
|
|
1227
1411
|
|
|
1412
|
+
[[package]]
|
|
1413
|
+
name = "zerocopy"
|
|
1414
|
+
version = "0.8.63"
|
|
1415
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1416
|
+
checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8"
|
|
1417
|
+
dependencies = [
|
|
1418
|
+
"zerocopy-derive",
|
|
1419
|
+
]
|
|
1420
|
+
|
|
1421
|
+
[[package]]
|
|
1422
|
+
name = "zerocopy-derive"
|
|
1423
|
+
version = "0.8.63"
|
|
1424
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1425
|
+
checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f"
|
|
1426
|
+
dependencies = [
|
|
1427
|
+
"proc-macro2",
|
|
1428
|
+
"quote",
|
|
1429
|
+
"syn 2.0.119",
|
|
1430
|
+
]
|
|
1431
|
+
|
|
1228
1432
|
[[package]]
|
|
1229
1433
|
name = "zerofrom"
|
|
1230
1434
|
version = "0.1.8"
|
data/Cargo.toml
CHANGED
|
@@ -15,9 +15,10 @@ license = "MIT"
|
|
|
15
15
|
publish = false
|
|
16
16
|
repository = "https://github.com/andymai/mini_racer_rs"
|
|
17
17
|
rust-version = "1.91"
|
|
18
|
-
version = "1.1.
|
|
18
|
+
version = "1.1.1"
|
|
19
19
|
|
|
20
20
|
[workspace.dependencies]
|
|
21
|
+
criterion = {version = "0.8", default-features = false, features = ["cargo_bench_support"]}
|
|
21
22
|
deno_core_icudata = "=0.78.0"
|
|
22
23
|
libc = "0.2"
|
|
23
24
|
loom = "0.7.2"
|
data/README.md
CHANGED
|
@@ -15,42 +15,42 @@ A drop-in replacement for mini_racer, implemented in Rust on V8 15.2.
|
|
|
15
15
|
|
|
16
16
|
</div>
|
|
17
17
|
|
|
18
|
-
mini_racer_rs implements the `MiniRacer`
|
|
18
|
+
mini_racer_rs implements the `MiniRacer` API from mini_racer 0.22.1. It defines `MiniRacer` and ships `lib/mini_racer.rb`, so `require "mini_racer"` and ExecJS keep working.
|
|
19
19
|
|
|
20
20
|
> mini_racer_rs is an independent project and is not affiliated with the Discourse staff who maintain mini_racer.
|
|
21
21
|
|
|
22
22
|
## Highlights
|
|
23
23
|
|
|
24
|
-
- **
|
|
25
|
-
- **
|
|
26
|
-
- **
|
|
27
|
-
- **A context holding the MUI bundle uses 44 MB
|
|
28
|
-
- **Forks
|
|
29
|
-
- **V8 15.2.124.1
|
|
30
|
-
- **No compiler
|
|
31
|
-
- **Fiber scheduler
|
|
32
|
-
- **
|
|
33
|
-
- **Difficult values
|
|
34
|
-
- **
|
|
24
|
+
- **Round trips:** 5 to 9 times faster (0.72 µs vs 6.51 µs per call).
|
|
25
|
+
- **Babel through ExecJS:** 1.95x faster (5.15 ms vs 10.05 ms).
|
|
26
|
+
- **Bundle load:** 4.6x faster for a 44 MB production server rendering bundle (215 ms vs 983 ms).
|
|
27
|
+
- **Memory:** A context holding the MUI bundle uses 44 MB vs 110 MB.
|
|
28
|
+
- **Forks:** After loading and rendering React: 20 of 20 clean mini_racer_rs exits vs 20 of 20 mini_racer aborts.
|
|
29
|
+
- **V8:** V8 15.2.124.1 vs V8 13.6.233.17 in mini_racer 0.22.1.
|
|
30
|
+
- **No compiler:** Native gems cover glibc Linux and macOS, both architectures, with Ruby 3.3, 3.4 and 4.0 extensions.
|
|
31
|
+
- **Fiber scheduler:** Three 100 ms loops in separate contexts and fibers finish in 100 ms vs 300 ms.
|
|
32
|
+
- **Upstream suites:** mini_racer passes 171 of 174 with 3 skipped by itself; ExecJS passes 197 of 198 with 1 skipped.
|
|
33
|
+
- **Difficult values:** A `Map` with a function key returns an error Hash instead of aborting with SIGABRT.
|
|
34
|
+
- **Checks:** Script-enforced crate boundaries, four fenced `unsafe` modules, loom, wire-codec fuzzing, nightly sanitizers and mini_racer differential tests.
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
These numbers compare with mini_racer 0.22.1. See [Benchmarks](#benchmarks) for the method.
|
|
37
37
|
|
|
38
38
|
## Install
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
Switch packages with one Gemfile line:
|
|
41
41
|
|
|
42
42
|
```diff
|
|
43
43
|
- gem "mini_racer"
|
|
44
44
|
+ gem "mini_racer_rs"
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
Run `bundle install`. Bundler is required because RubyGems may
|
|
47
|
+
Run `bundle install`. Bundler is required because RubyGems may activate the original `mini_racer` first. Loading both gems raises a `LoadError` naming the conflict. Reverse the Gemfile change to switch back.
|
|
48
48
|
|
|
49
|
-
On x86_64-linux-gnu, aarch64-linux-gnu, arm64-darwin, and x86_64-darwin, `bundle install` installs a native gem
|
|
49
|
+
On x86_64-linux-gnu, aarch64-linux-gnu, arm64-darwin, and x86_64-darwin, `bundle install` installs a native gem without compiling. Elsewhere, the source gem requires Rust 1.91 or newer and libclang to build the extension. For a checkout, follow [CONTRIBUTING.md](CONTRIBUTING.md) and run `bin/setup`.
|
|
50
50
|
|
|
51
51
|
## Quick start
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
Both gems produced exactly these outputs:
|
|
54
54
|
|
|
55
55
|
```ruby check
|
|
56
56
|
require "mini_racer"
|
|
@@ -112,31 +112,35 @@ ExecJS.eval("[1, 2].map(x => x * 2)") # => [2, 4]
|
|
|
112
112
|
| `ensure_gc_after_idle:` | Idle milliseconds before one full garbage collection. `0` disables it. |
|
|
113
113
|
| `snapshot:` | A `MiniRacer::Snapshot` used to initialize the context. |
|
|
114
114
|
|
|
115
|
-
Errors are `MiniRacer::ParseError`, `MiniRacer::RuntimeError
|
|
115
|
+
Errors are `MiniRacer::ParseError`, `MiniRacer::RuntimeError`, `MiniRacer::ScriptTerminatedError`, `MiniRacer::V8OutOfMemoryError`, `MiniRacer::ContextDisposedError`, `MiniRacer::SnapshotError`, and `MiniRacer::PlatformAlreadyInitialized`. `MiniRacer::RuntimeError` includes JavaScript frames in its backtrace. All descend from `MiniRacer::Error`.
|
|
116
116
|
|
|
117
|
-
**Source maps.**
|
|
117
|
+
**Source maps.** Bundles must be UTF-8 Strings of at least 1,024 bytes. Maps must be JSON Strings in source map v3 format. When a context evaluates UTF-8 source containing a registered bundle, mini_racer_rs rewrites `MiniRacer::RuntimeError` backtraces. V8 positions inside the bundle become mapped original file, line, and column positions. V8 function names remain.
|
|
118
|
+
|
|
119
|
+
V8 positions remain for async frames and frames after the tenth. They also remain for `eval`, `new Function`, uncovered positions, and frames outside the bundle. JavaScript-reformatted stacks also keep V8 positions.
|
|
120
|
+
|
|
121
|
+
Detailed stack capture makes creating and throwing errors more expensive in affected contexts. Measured React and MUI server renders were unchanged. Registration is process wide and holds at most eight bundles. Maps cannot be removed. Registering a bundle again replaces its map. Trust bundles and maps like application code because source names appear in Ruby backtraces.
|
|
118
122
|
|
|
119
123
|
## Switching from mini_racer
|
|
120
124
|
|
|
121
125
|
### What changes
|
|
122
126
|
|
|
123
|
-
The public API and require path stay the same.
|
|
127
|
+
The public API and require path stay the same. Changes include native installation, faster calls and Babel transforms, fiber-aware waiting, newer V8 behavior, and safer value conversion. `Object(1n)` returns `1`, and an offset typed array returns its buffer as a binary String. Applications making many small calls gain the most. See [Highlights](#highlights) for measurements.
|
|
124
128
|
|
|
125
129
|
### Check before you ship
|
|
126
130
|
|
|
127
|
-
- You're affected if
|
|
131
|
+
- You're affected if tests or code compare exact JavaScript errors or locale-formatted text. V8 15.2 may differ from V8 13.6.
|
|
128
132
|
|
|
129
|
-
- You're affected if an attached
|
|
133
|
+
- You're affected if an attached callback reenters its context from another fiber, including inside `Enumerator#next`. mini_racer_rs raises `ThreadError`; mini_racer allows it.
|
|
130
134
|
|
|
131
|
-
- You're affected if a callback exits by `throw`, returns
|
|
135
|
+
- You're affected if a callback exits by `throw`, returns from a proc, or receives `Thread#kill`. The script ends with `Error: terminated`. The jump continues after `eval` returns. mini_racer hangs.
|
|
132
136
|
|
|
133
|
-
- You're affected if JavaScript reads a non-ASCII message
|
|
137
|
+
- You're affected if JavaScript reads a callback exception's non-ASCII message. mini_racer_rs passes UTF-8, such as `"café"`; mini_racer passes `"café"`.
|
|
134
138
|
|
|
135
|
-
- You're affected if `fork`, `IO.popen("-")`, or `Process.daemon` runs after V8 starts. Both modes wait up to 500 ms for runners
|
|
139
|
+
- You're affected if `fork`, `IO.popen("-")`, or `Process.daemon` runs after V8 starts. Both modes wait up to 500 ms for runners and interrupt JavaScript after 50 ms. They pause the worker pool and wait up to 100 ms for running tasks. Uninterruptible work may outlast these best-effort waits, and the fork proceeds. Default children reject inherited contexts. New contexts use the child's threads, but are best effort after either timeout. `:single_threaded` children take over idle inherited contexts and reject busy ones. After a runner timeout, they reject all inherited contexts. They accept new contexts.
|
|
136
140
|
|
|
137
|
-
- You're affected if
|
|
141
|
+
- You're affected if `:single_threaded` forks bypass Ruby's `fork`, such as a C-level `fork()`. Inherited contexts are unusable in the child.
|
|
138
142
|
|
|
139
|
-
- You're affected if another thread calls `dispose`
|
|
143
|
+
- You're affected if another thread calls `dispose` during a call. It returns after the call finishes and the isolate is gone. mini_racer returns immediately and frees the isolate later.
|
|
140
144
|
|
|
141
145
|
- You're affected if your code references `MiniRacer::Loader`. It does not exist in mini_racer_rs.
|
|
142
146
|
|
|
@@ -144,26 +148,28 @@ See [COMPATIBILITY.md](COMPATIBILITY.md) for every difference.
|
|
|
144
148
|
|
|
145
149
|
### Stay on mini_racer if
|
|
146
150
|
|
|
147
|
-
- You deploy on musl-based Linux such as Alpine. mini_racer supports it through libv8-node's musl gems
|
|
151
|
+
- You deploy on musl-based Linux such as Alpine. mini_racer supports it through libv8-node's musl gems; mini_racer_rs does not. Neither supports Windows.
|
|
148
152
|
|
|
149
|
-
- Download size matters more than compile time. The largest
|
|
153
|
+
- Download size matters more than compile time. The largest native gem is 65.6 MB because it contains three Ruby extensions. mini_racer downloads 47.1 MB on x86_64 Linux, then compiles.
|
|
150
154
|
|
|
151
155
|
### Gems that depend on mini_racer
|
|
152
156
|
|
|
153
|
-
If another gem
|
|
157
|
+
If another gem adds `mini_racer`, point Bundler to the repository's shim. This empty `mini_racer` gem is version 0.22.1. It depends on the `mini_racer_rs` version from the same checkout.
|
|
154
158
|
|
|
155
159
|
```ruby
|
|
156
160
|
gem "mini_racer_rs"
|
|
157
161
|
gem "mini_racer", github: "andymai/mini_racer_rs", tag: "vX.Y.Z", glob: "shim/*.gemspec"
|
|
158
162
|
```
|
|
159
163
|
|
|
160
|
-
The repository tests shim resolution with
|
|
164
|
+
The repository tests `path:` shim resolution with rtlcss in `test/consumers/transitive/`. The `github:` form has not run end to end. A dependency excluding `mini_racer` 0.22.1 cannot use the shim.
|
|
161
165
|
|
|
162
166
|
### Forking servers such as Puma
|
|
163
167
|
|
|
164
|
-
|
|
168
|
+
Before forking, evaluate each bundle of 1 MB or more twice in the master, then dispose those contexts. Use the renderer's exact source text and file name. The second evaluation builds the process-wide V8 code cache. Matching evaluations compile from it, and forked children inherit it. For Babel's 2.3 MB bundle at commit `5e13c1c`, a forked child's first load took 60.8 ms without a parent load. It took 65.5 ms after one load and 25.0 ms after two. These are medians across 5 trials. Prime a `react-rails` app by building its server renderer twice. Children do not use parent contexts. Only the cache carries over.
|
|
169
|
+
|
|
170
|
+
For Ruby's `fork`, `IO.popen("-")`, or `Process.daemon`, mini_racer_rs waits for context runners, pauses its worker pool and waits for running tasks. An async `WebAssembly.compile` in a new default-mode child completes on the child's own pool. On Ubuntu 24.04 with glibc 2.39, 20 of 20 mini_racer children aborted with signal 6 after React rendering; all mini_racer_rs children exited cleanly. On Fedora, mini_racer hung within 50 `test/stress/fork_busy.rb` forks; mini_racer_rs completed 300 per run on Fedora and Ubuntu.
|
|
165
171
|
|
|
166
|
-
With `mini_racer` 0.22.1, each worker compiles the bundle from source on its first load because
|
|
172
|
+
With `mini_racer` 0.22.1, each worker compiles the bundle from source on its first load because it has no code cache.
|
|
167
173
|
|
|
168
174
|
## Benchmarks
|
|
169
175
|
|
|
@@ -179,13 +185,28 @@ With `mini_racer` 0.22.1, each worker compiles the bundle from source on its fir
|
|
|
179
185
|
| MUI in a new context: load the 8.2 MB bundle and render once, through ExecJS | 167 ms | 105 ms | 1.6x |
|
|
180
186
|
| Load a 44 MB production server rendering bundle into a new context, through ExecJS | 983 ms | 215 ms | 4.6x |
|
|
181
187
|
|
|
182
|
-
Results are medians of 5 runs
|
|
188
|
+
Results are medians of 5 runs. Every workload produced identical output in both gems. The eight library workloads use the release build of commit `220bfe7`. The 44 MB production bundle load uses commit `2513a48`. Benchmarks ran on Ruby 3.4.2 and Linux with an AMD Ryzen 9 7950X3D. The governor was `performance`. Both gems were pinned to CPUs 0 through 7.
|
|
189
|
+
|
|
190
|
+
The per-call and memory figures below were measured on the same machine. Startup plus Babel, Babel context and MUI context figures come from commit `220bfe7`. One watchdog thread serves the whole process, making a call with `timeout:` fast. Scripts of 1 MB or more use a process-wide V8 code cache. Contexts loading the same large script share one source copy.
|
|
191
|
+
|
|
192
|
+
| Measure | mini_racer 0.22.1 | mini_racer_rs |
|
|
193
|
+
|---|---:|---:|
|
|
194
|
+
| eval round trip | 6.91 µs | 1.09 µs |
|
|
195
|
+
| call round trip | 6.51 µs | 0.72 µs |
|
|
196
|
+
| callback round trip | 6.12 µs | 1.12 µs |
|
|
197
|
+
| call on a context with `timeout:` | 20.48 µs | 0.76 µs |
|
|
198
|
+
| Babel inside V8 | 10.3 ms | 5.3 ms |
|
|
199
|
+
| startup plus Babel | 44.4 ms | 13.3 ms |
|
|
200
|
+
| context holding Babel | 28.5 MB | 15.2 MB |
|
|
201
|
+
| context holding the 8.2 MB MUI bundle | 110 MB | 44 MB |
|
|
202
|
+
| context holding the 44 MB production bundle, at commit `2513a48` before sources were shared | 301 MB | 136 MB |
|
|
203
|
+
| peak resident memory with 20,000 contexts left to Ruby's garbage collector | 2,920 MB | 102 MB |
|
|
183
204
|
|
|
184
205
|
```sh
|
|
185
206
|
bundle exec ruby bench/run.rb --cpus 0-7
|
|
186
207
|
```
|
|
187
208
|
|
|
188
|
-
[BENCHMARKS.md](BENCHMARKS.md)
|
|
209
|
+
[BENCHMARKS.md](BENCHMARKS.md) covers the method, per-call costs, memory results, and mini_racer's `benchmark/serde` suite.
|
|
189
210
|
|
|
190
211
|
<!-- compat-table:start -->
|
|
191
212
|
## Compatibility
|
|
@@ -200,7 +221,7 @@ See [COMPATIBILITY.md](COMPATIBILITY.md).
|
|
|
200
221
|
|
|
201
222
|
## How it works
|
|
202
223
|
|
|
203
|
-
mini_racer_rs uses the `v8` crate, rusty_v8 152.2.0, with
|
|
224
|
+
mini_racer_rs uses the `v8` crate, rusty_v8 152.2.0, with magnus and rb-sys Ruby bindings. Each context owns a V8 isolate and native runner thread. Its stack is at least 2 MiB, keeping V8 frames off Ruby thread stacks. A per-context lock allows one dispatch at a time.
|
|
204
225
|
|
|
205
226
|
```text
|
|
206
227
|
Ruby thread runner thread (one per context)
|
|
@@ -216,34 +237,36 @@ mini_racer_rs uses the `v8` crate, rusty_v8 152.2.0, with Ruby bindings through
|
|
|
216
237
|
result
|
|
217
238
|
```
|
|
218
239
|
|
|
219
|
-
A waiting Ruby thread
|
|
240
|
+
A waiting Ruby thread spins with the GVL held, then without it. It then parks with the GVL released. After enough consecutive answers arrive beyond the spin budget, that context skips spinning and parks at once until an answer arrives within the budget. Under a fiber scheduler, only the waiting fiber parks. Values use V8's serialization format and decode directly into Ruby objects. There is no intermediate tree. Repeated objects keep their identity.
|
|
220
241
|
|
|
221
|
-
One watchdog thread per process enforces every context's `timeout:`. In `:single_threaded` mode,
|
|
242
|
+
One watchdog thread per process enforces every context's `timeout:`. In `:single_threaded` mode, forks quiesce runners. Children take over idle contexts; contexts in use raise there. mini_racer_rs gives V8 a copied platform vtable in each process. Worker-thread slots post to its own pool. V8's platform has one worker thread, which receives no tasks. Forking pauses the pool. The parent resumes it afterward. Default multi-threaded children reject inherited contexts.
|
|
222
243
|
|
|
223
|
-
The implementation
|
|
244
|
+
The implementation has four crates:
|
|
224
245
|
|
|
225
246
|
- `mini_racer_wire`: the wire codec, with no unsafe code.
|
|
226
247
|
- `mini_racer_sync`: the thread protocol, with no unsafe code and loom checking.
|
|
227
|
-
- `mini_racer_engine`: V8 process state and runners, and the only crate
|
|
228
|
-
- `mini_racer_rs`: the Ruby extension, and the only crate
|
|
248
|
+
- `mini_racer_engine`: V8 process state and runners, and the only crate naming `v8`.
|
|
249
|
+
- `mini_racer_rs`: the Ruby extension, and the only crate naming magnus or rb-sys.
|
|
250
|
+
|
|
251
|
+
The release pipeline runs the full suites for every released extension.
|
|
229
252
|
|
|
230
|
-
[ARCHITECTURE.md](ARCHITECTURE.md) follows a call end to end and
|
|
253
|
+
[ARCHITECTURE.md](ARCHITECTURE.md) follows a call end to end and explains the design.
|
|
231
254
|
|
|
232
255
|
## Scope
|
|
233
256
|
|
|
234
|
-
mini_racer_rs
|
|
257
|
+
mini_racer_rs does not:
|
|
235
258
|
|
|
236
|
-
- Run on Windows or musl-based Linux such as Alpine.
|
|
259
|
+
- Run on Windows or musl-based Linux such as Alpine. It supports glibc Linux on x86_64 and aarch64, and macOS on arm64 and x86_64.
|
|
237
260
|
|
|
238
|
-
- Provide Node.js APIs. JavaScript runs in plain V8 without file system, network, or process access.
|
|
261
|
+
- Provide Node.js APIs. JavaScript runs in plain V8 without file system, network, or process access. It can reach only Ruby callables you `attach`.
|
|
239
262
|
|
|
240
263
|
- Work reliably without Bundler. RubyGems may activate the original mini_racer first.
|
|
241
264
|
|
|
242
|
-
- Satisfy
|
|
265
|
+
- Satisfy dependencies pinning `mini_racer` versions other than 0.22.1. This includes minibars and its `~> 0.6.4` requirement.
|
|
243
266
|
|
|
244
|
-
- Extend the `MiniRacer` API beyond mini_racer 0.22.1.
|
|
267
|
+
- Extend the `MiniRacer` API beyond mini_racer 0.22.1. [COMPATIBILITY.md](COMPATIBILITY.md) records deliberate differences.
|
|
245
268
|
|
|
246
|
-
Supported
|
|
269
|
+
Supported implementations are CRuby 3.3, 3.4, and 4.0, plus TruffleRuby through mini_racer's unchanged TruffleRuby backend.
|
|
247
270
|
|
|
248
271
|
## Documentation
|
|
249
272
|
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
//! Decoding, writing and transcoding V8's wire format, without V8 or Ruby, on
|
|
2
|
+
//! the shapes the extension sees: seeded server rendering props, and runs of
|
|
3
|
+
//! numbers and strings. Strings take the forms V8's serializer gives them.
|
|
4
|
+
|
|
5
|
+
use std::convert::Infallible;
|
|
6
|
+
use std::hint::black_box;
|
|
7
|
+
|
|
8
|
+
use criterion::{Criterion, Throughput, criterion_group, criterion_main};
|
|
9
|
+
use mini_racer_wire::{Begun, Container, Limits, Scalar, Visitor, Writer, decode_value, text};
|
|
10
|
+
|
|
11
|
+
const WORDS: [&str; 12] = [
|
|
12
|
+
"alpha", "beta", "gamma", "delta", "epsilon", "zeta", "theta", "kappa", "lambda", "sigma",
|
|
13
|
+
"omega", "omicron",
|
|
14
|
+
];
|
|
15
|
+
const INTL: [&str; 5] = ["café", "naïve", "Zürich", "東京", "😀 party"];
|
|
16
|
+
// The words of INTL that V8 keeps one byte a character.
|
|
17
|
+
const LATIN1: [&str; 3] = ["café", "naïve", "Zürich"];
|
|
18
|
+
|
|
19
|
+
/// Reads every value and keeps nothing, so the decoder's own work is what
|
|
20
|
+
/// is measured.
|
|
21
|
+
struct Discard;
|
|
22
|
+
|
|
23
|
+
impl Visitor for Discard {
|
|
24
|
+
type Value = ();
|
|
25
|
+
type Frame = ();
|
|
26
|
+
type Error = Infallible;
|
|
27
|
+
|
|
28
|
+
fn scalar(&mut self, scalar: Scalar<'_>) -> Result<(), Infallible> {
|
|
29
|
+
black_box(scalar);
|
|
30
|
+
Ok(())
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn begin(&mut self, container: Container) -> Result<Begun<(), ()>, Infallible> {
|
|
34
|
+
black_box(container);
|
|
35
|
+
Ok(Begun {
|
|
36
|
+
frame: (),
|
|
37
|
+
value: None,
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fn push(&mut self, (): &mut (), (): ()) -> Result<(), Infallible> {
|
|
42
|
+
Ok(())
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn insert(&mut self, (): &mut (), (): (), (): ()) -> Result<(), Infallible> {
|
|
46
|
+
Ok(())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn end(&mut self, (): ()) -> Result<(), Infallible> {
|
|
50
|
+
Ok(())
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn register(&mut self, id: u32, (): ()) -> Result<(), Infallible> {
|
|
54
|
+
black_box(id);
|
|
55
|
+
Ok(())
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fn back_ref(&mut self, id: u32) -> Result<(), Infallible> {
|
|
59
|
+
black_box(id);
|
|
60
|
+
Ok(())
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
enum Node {
|
|
65
|
+
Int(i64),
|
|
66
|
+
Number(f64),
|
|
67
|
+
Bool(bool),
|
|
68
|
+
Latin1(Vec<u8>),
|
|
69
|
+
Utf16(Vec<u16>),
|
|
70
|
+
Object(Vec<(Vec<u8>, Self)>),
|
|
71
|
+
Array(Vec<Self>),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
impl Node {
|
|
75
|
+
fn text(text: &str) -> Self {
|
|
76
|
+
let latin1: Option<Vec<u8>> = text
|
|
77
|
+
.chars()
|
|
78
|
+
.map(|c| u8::try_from(u32::from(c)).ok())
|
|
79
|
+
.collect();
|
|
80
|
+
latin1.map_or_else(|| Self::Utf16(text.encode_utf16().collect()), Self::Latin1)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fn object(entries: Vec<(&str, Self)>) -> Self {
|
|
84
|
+
Self::Object(
|
|
85
|
+
entries
|
|
86
|
+
.into_iter()
|
|
87
|
+
.map(|(key, value)| (key.as_bytes().to_vec(), value))
|
|
88
|
+
.collect(),
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fn write(&self, writer: &mut Writer<'_>) {
|
|
93
|
+
match self {
|
|
94
|
+
Self::Int(n) => writer.integer(*n),
|
|
95
|
+
Self::Number(n) => writer.number(*n),
|
|
96
|
+
Self::Bool(b) => writer.bool(*b),
|
|
97
|
+
Self::Latin1(bytes) => writer.latin1(bytes),
|
|
98
|
+
Self::Utf16(units) => writer.utf16_aligned(units),
|
|
99
|
+
Self::Object(entries) => {
|
|
100
|
+
writer.begin_object();
|
|
101
|
+
for (key, value) in entries {
|
|
102
|
+
writer.latin1(key);
|
|
103
|
+
value.write(writer);
|
|
104
|
+
}
|
|
105
|
+
writer.end_object(count(entries.len()));
|
|
106
|
+
}
|
|
107
|
+
Self::Array(items) => {
|
|
108
|
+
writer.begin_array(count(items.len()));
|
|
109
|
+
for item in items {
|
|
110
|
+
item.write(writer);
|
|
111
|
+
}
|
|
112
|
+
writer.end_array(count(items.len()));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fn bytes(&self) -> Vec<u8> {
|
|
118
|
+
let mut out = Vec::new();
|
|
119
|
+
self.write(&mut Writer::versioned(&mut out, 15));
|
|
120
|
+
out
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fn count(len: usize) -> u32 {
|
|
125
|
+
u32::try_from(len).unwrap_or(u32::MAX)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// xorshift64, so the payload is the same on every machine.
|
|
129
|
+
struct Seeded(u64);
|
|
130
|
+
|
|
131
|
+
impl Seeded {
|
|
132
|
+
const fn next(&mut self) -> u64 {
|
|
133
|
+
self.0 ^= self.0 << 13;
|
|
134
|
+
self.0 ^= self.0 >> 7;
|
|
135
|
+
self.0 ^= self.0 << 17;
|
|
136
|
+
self.0
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fn below(&mut self, bound: usize) -> usize {
|
|
140
|
+
usize::try_from(self.next()).unwrap_or(0) % bound
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fn words(&mut self, count: usize) -> Node {
|
|
144
|
+
self.words_from(count, &INTL)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fn words_from(&mut self, count: usize, intl: &[&str]) -> Node {
|
|
148
|
+
let words: Vec<&str> = (0..count)
|
|
149
|
+
.map(|_| {
|
|
150
|
+
if self.below(10) == 0 {
|
|
151
|
+
intl[self.below(intl.len())]
|
|
152
|
+
} else {
|
|
153
|
+
WORDS[self.below(WORDS.len())]
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
.collect();
|
|
157
|
+
Node::text(&words.join(" "))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
fn cents(&mut self) -> f64 {
|
|
161
|
+
f64::from(u32::try_from(self.below(100_000)).unwrap_or(0)) / 100.0
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// About 1 MB of props, the size the end-to-end benchmark sends.
|
|
166
|
+
fn props() -> Node {
|
|
167
|
+
let mut seeded = Seeded(42);
|
|
168
|
+
let items = (0..5_000)
|
|
169
|
+
.map(|id| {
|
|
170
|
+
let length = 3 + seeded.below(6);
|
|
171
|
+
Node::object(vec![
|
|
172
|
+
("id", Node::Int(id)),
|
|
173
|
+
(
|
|
174
|
+
"sku",
|
|
175
|
+
Node::text(&format!("SKU-{}", seeded.below(1_000_000))),
|
|
176
|
+
),
|
|
177
|
+
("title", seeded.words(length)),
|
|
178
|
+
("price", Node::Number(seeded.cents())),
|
|
179
|
+
("rating", Node::Number(seeded.cents() / 200.0)),
|
|
180
|
+
("inStock", Node::Bool(seeded.below(4) != 0)),
|
|
181
|
+
(
|
|
182
|
+
"tags",
|
|
183
|
+
Node::Array(vec![seeded.words(1), seeded.words(1), seeded.words(1)]),
|
|
184
|
+
),
|
|
185
|
+
(
|
|
186
|
+
"seller",
|
|
187
|
+
Node::object(vec![
|
|
188
|
+
("name", seeded.words(2)),
|
|
189
|
+
(
|
|
190
|
+
"reviews",
|
|
191
|
+
Node::Int(i64::try_from(seeded.below(5_000)).unwrap_or(0)),
|
|
192
|
+
),
|
|
193
|
+
]),
|
|
194
|
+
),
|
|
195
|
+
])
|
|
196
|
+
})
|
|
197
|
+
.collect();
|
|
198
|
+
Node::object(vec![
|
|
199
|
+
(
|
|
200
|
+
"page",
|
|
201
|
+
Node::object(vec![
|
|
202
|
+
("title", Node::text("Products")),
|
|
203
|
+
("locale", Node::text("en-US")),
|
|
204
|
+
]),
|
|
205
|
+
),
|
|
206
|
+
("items", Node::Array(items)),
|
|
207
|
+
])
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fn numbers() -> Node {
|
|
211
|
+
let mut seeded = Seeded(7);
|
|
212
|
+
Node::Array((0..10_000).map(|_| Node::Number(seeded.cents())).collect())
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
fn latin1_strings() -> Node {
|
|
216
|
+
let mut seeded = Seeded(9);
|
|
217
|
+
Node::Array((0..10_000).map(|_| seeded.words_from(2, &LATIN1)).collect())
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fn two_byte_strings() -> Node {
|
|
221
|
+
Node::Array(
|
|
222
|
+
(0..1_000)
|
|
223
|
+
.map(|n| Node::text(&format!("東京の天気は晴れです {n}")))
|
|
224
|
+
.collect(),
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fn decode(c: &mut Criterion) {
|
|
229
|
+
let mut group = c.benchmark_group("decode");
|
|
230
|
+
for (name, node) in [
|
|
231
|
+
("ssr_props", props()),
|
|
232
|
+
("dense_numbers", numbers()),
|
|
233
|
+
("latin1_strings", latin1_strings()),
|
|
234
|
+
("two_byte_strings", two_byte_strings()),
|
|
235
|
+
] {
|
|
236
|
+
let bytes = node.bytes();
|
|
237
|
+
group.throughput(Throughput::Bytes(
|
|
238
|
+
bytes.len().try_into().unwrap_or(u64::MAX),
|
|
239
|
+
));
|
|
240
|
+
group.bench_function(name, |b| {
|
|
241
|
+
b.iter(|| decode_value(black_box(&bytes), Limits::default(), &mut Discard));
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
group.finish();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
fn write(c: &mut Criterion) {
|
|
248
|
+
let node = props();
|
|
249
|
+
let mut out = Vec::new();
|
|
250
|
+
node.write(&mut Writer::new(&mut out));
|
|
251
|
+
let mut group = c.benchmark_group("write");
|
|
252
|
+
group.throughput(Throughput::Bytes(out.len().try_into().unwrap_or(u64::MAX)));
|
|
253
|
+
group.bench_function("ssr_props", |b| {
|
|
254
|
+
b.iter(|| {
|
|
255
|
+
let mut writer = Writer::new(&mut out);
|
|
256
|
+
black_box(&node).write(&mut writer);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
group.finish();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
fn transcode(c: &mut Criterion) {
|
|
263
|
+
let latin1: Vec<u8> = "Le café de la naïveté à Zürich, déjà vu. "
|
|
264
|
+
.chars()
|
|
265
|
+
.filter_map(|ch| u8::try_from(u32::from(ch)).ok())
|
|
266
|
+
.cycle()
|
|
267
|
+
.take(64 << 10)
|
|
268
|
+
.collect();
|
|
269
|
+
let utf16: Vec<u8> = "東京の天気は晴れです。"
|
|
270
|
+
.encode_utf16()
|
|
271
|
+
.cycle()
|
|
272
|
+
.take(32 << 10)
|
|
273
|
+
.flat_map(u16::to_le_bytes)
|
|
274
|
+
.collect();
|
|
275
|
+
let mut out = vec![0; 192 << 10];
|
|
276
|
+
let mut group = c.benchmark_group("transcode");
|
|
277
|
+
group.throughput(Throughput::Bytes(
|
|
278
|
+
latin1.len().try_into().unwrap_or(u64::MAX),
|
|
279
|
+
));
|
|
280
|
+
group.bench_function("latin1_to_utf8", |b| {
|
|
281
|
+
b.iter(|| text::latin1_to_utf8_into(black_box(&latin1), &mut out));
|
|
282
|
+
});
|
|
283
|
+
group.throughput(Throughput::Bytes(
|
|
284
|
+
utf16.len().try_into().unwrap_or(u64::MAX),
|
|
285
|
+
));
|
|
286
|
+
group.bench_function("utf16le_to_utf8", |b| {
|
|
287
|
+
b.iter(|| text::utf16le_to_utf8_into(black_box(&utf16), &mut out));
|
|
288
|
+
});
|
|
289
|
+
group.finish();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
criterion_group!(benches, decode, write, transcode);
|
|
293
|
+
criterion_main!(benches);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
use std::cell::Cell;
|
|
2
2
|
use std::os::fd::{AsRawFd, IntoRawFd};
|
|
3
|
-
use std::
|
|
4
|
-
use std::time::Duration;
|
|
3
|
+
use std::time::Instant;
|
|
5
4
|
|
|
6
5
|
use magnus::error::ErrorType;
|
|
7
6
|
use magnus::gc::Marker;
|
|
@@ -14,32 +13,9 @@ use mini_racer_engine::{
|
|
|
14
13
|
|
|
15
14
|
use crate::encode;
|
|
16
15
|
use crate::gc_root::OwnedRoot;
|
|
16
|
+
use crate::spin::{self, History};
|
|
17
17
|
use crate::{context, decode, errors, rb_raw};
|
|
18
18
|
|
|
19
|
-
struct SpinBudget {
|
|
20
|
-
gvl: Duration,
|
|
21
|
-
nogvl: Duration,
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Unstable tuning knob: MINI_RACER_RS_SPIN_US=<with GVL>,<without GVL>.
|
|
25
|
-
fn budget() -> &'static SpinBudget {
|
|
26
|
-
static BUDGET: OnceLock<SpinBudget> = OnceLock::new();
|
|
27
|
-
BUDGET.get_or_init(|| {
|
|
28
|
-
let setting = std::env::var("MINI_RACER_RS_SPIN_US").unwrap_or_default();
|
|
29
|
-
let parts: Vec<Option<u64>> = setting
|
|
30
|
-
.split(',')
|
|
31
|
-
.map(|part| part.trim().parse().ok())
|
|
32
|
-
.collect();
|
|
33
|
-
let at = |i: usize, default: u64| {
|
|
34
|
-
Duration::from_micros(parts.get(i).copied().flatten().unwrap_or(default))
|
|
35
|
-
};
|
|
36
|
-
SpinBudget {
|
|
37
|
-
gvl: at(0, 20),
|
|
38
|
-
nogvl: at(1, 50),
|
|
39
|
-
}
|
|
40
|
-
})
|
|
41
|
-
}
|
|
42
|
-
|
|
43
19
|
/// What a context's callbacks run: its attached callables by id, and where
|
|
44
20
|
/// the exception the latest callback raised is kept, as `mini_racer` keeps one
|
|
45
21
|
/// per context and clears it after every dispatch, nested ones included.
|
|
@@ -69,6 +45,7 @@ pub struct RubyHost<'r> {
|
|
|
69
45
|
pub struct Waiter {
|
|
70
46
|
io: OwnedRoot<Value>,
|
|
71
47
|
epoch: Cell<u64>,
|
|
48
|
+
history: History,
|
|
72
49
|
}
|
|
73
50
|
|
|
74
51
|
impl Waiter {
|
|
@@ -268,28 +245,44 @@ fn message(ruby: &Ruby, exception: Exception) -> JsText {
|
|
|
268
245
|
|
|
269
246
|
impl Host for RubyHost<'_> {
|
|
270
247
|
fn wait(&mut self, wait: &WaitHandle<'_>) -> WaitOutcome {
|
|
271
|
-
let budget = budget();
|
|
272
|
-
|
|
273
|
-
|
|
248
|
+
let budget = spin::budget();
|
|
249
|
+
let history = self.waiter.map(|waiter| &waiter.history);
|
|
250
|
+
let spins = history.is_none_or(History::spins);
|
|
251
|
+
// A wait that does not spin times itself to tell whether spinning
|
|
252
|
+
// would have caught the answer.
|
|
253
|
+
let started = (!spins).then(Instant::now);
|
|
254
|
+
let answered = |spun: bool| {
|
|
255
|
+
if let Some(history) = history {
|
|
256
|
+
history.record(started.map_or(spun, |at| at.elapsed() <= budget.total()));
|
|
257
|
+
}
|
|
258
|
+
WaitOutcome::Ready
|
|
259
|
+
};
|
|
260
|
+
if spins && wait.spin(budget.gvl) {
|
|
261
|
+
return answered(true);
|
|
274
262
|
}
|
|
275
263
|
if rb_raw::fiber_scheduler(self.ruby)
|
|
276
264
|
&& let Some(outcome) = self.wait_in_fiber(wait)
|
|
277
265
|
{
|
|
278
|
-
return outcome
|
|
266
|
+
return match outcome {
|
|
267
|
+
WaitOutcome::Ready => answered(false),
|
|
268
|
+
WaitOutcome::Cancel => outcome,
|
|
269
|
+
};
|
|
279
270
|
}
|
|
280
271
|
let unparker = wait.unparker();
|
|
281
272
|
loop {
|
|
282
273
|
let parked = rb_raw::nogvl(
|
|
283
274
|
self.ruby,
|
|
284
275
|
|| {
|
|
285
|
-
|
|
276
|
+
let spun = spins && wait.spin(budget.nogvl);
|
|
277
|
+
if !spun {
|
|
286
278
|
wait.park();
|
|
287
279
|
}
|
|
280
|
+
spun
|
|
288
281
|
},
|
|
289
282
|
&move || unparker.unpark(),
|
|
290
283
|
);
|
|
291
284
|
if wait.ready() {
|
|
292
|
-
return
|
|
285
|
+
return answered(matches!(parked, Some(Ok(true))));
|
|
293
286
|
}
|
|
294
287
|
if parked.as_ref().is_some_and(Result::is_err) {
|
|
295
288
|
self.pending = Some(errors::internal(self.ruby, "panic while waiting"));
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
use std::cell::Cell;
|
|
2
|
+
use std::sync::OnceLock;
|
|
3
|
+
use std::time::Duration;
|
|
4
|
+
|
|
5
|
+
/// How long a Ruby thread spins for a runner's answer before it gives up the
|
|
6
|
+
/// GVL, and then before it parks.
|
|
7
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
8
|
+
pub struct Budget {
|
|
9
|
+
pub gvl: Duration,
|
|
10
|
+
pub nogvl: Duration,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
impl Budget {
|
|
14
|
+
fn parse(setting: &str) -> Self {
|
|
15
|
+
let parts: Vec<Option<u64>> = setting
|
|
16
|
+
.split(',')
|
|
17
|
+
.map(|part| part.trim().parse().ok())
|
|
18
|
+
.collect();
|
|
19
|
+
let at = |i: usize, default: u64| {
|
|
20
|
+
Duration::from_micros(parts.get(i).copied().flatten().unwrap_or(default))
|
|
21
|
+
};
|
|
22
|
+
Self {
|
|
23
|
+
gvl: at(0, 20),
|
|
24
|
+
nogvl: at(1, 50),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pub const fn total(&self) -> Duration {
|
|
29
|
+
self.gvl.saturating_add(self.nogvl)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Unstable tuning knob: MINI_RACER_RS_SPIN_US=<with GVL>,<without GVL>.
|
|
34
|
+
pub fn budget() -> &'static Budget {
|
|
35
|
+
static BUDGET: OnceLock<Budget> = OnceLock::new();
|
|
36
|
+
BUDGET
|
|
37
|
+
.get_or_init(|| Budget::parse(&std::env::var("MINI_RACER_RS_SPIN_US").unwrap_or_default()))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// A context whose answers outlast the budget, such as one rendering pages,
|
|
41
|
+
// gains nothing from spinning: each wait burns the whole budget on the
|
|
42
|
+
// caller's CPU, the first part holding the GVL from other threads, and then
|
|
43
|
+
// parks anyway. A few in a row are enough to tell it from a one-off.
|
|
44
|
+
const STREAK: u8 = 3;
|
|
45
|
+
|
|
46
|
+
/// One context's recent waits: after `STREAK` in a row outlast the budget its
|
|
47
|
+
/// waits park at once, until one is answered within the budget.
|
|
48
|
+
#[derive(Debug, Default)]
|
|
49
|
+
pub struct History {
|
|
50
|
+
slow: Cell<u8>,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
impl History {
|
|
54
|
+
pub const fn spins(&self) -> bool {
|
|
55
|
+
self.slow.get() < STREAK
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
pub fn record(&self, within_budget: bool) {
|
|
59
|
+
let slow = if within_budget {
|
|
60
|
+
0
|
|
61
|
+
} else {
|
|
62
|
+
self.slow.get().saturating_add(1).min(STREAK)
|
|
63
|
+
};
|
|
64
|
+
self.slow.set(slow);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[cfg(test)]
|
|
69
|
+
mod tests {
|
|
70
|
+
use super::*;
|
|
71
|
+
|
|
72
|
+
#[test]
|
|
73
|
+
fn the_budget_defaults_and_reads_the_knob() {
|
|
74
|
+
let us = Duration::from_micros;
|
|
75
|
+
assert_eq!(
|
|
76
|
+
Budget::parse(""),
|
|
77
|
+
Budget {
|
|
78
|
+
gvl: us(20),
|
|
79
|
+
nogvl: us(50)
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
assert_eq!(
|
|
83
|
+
Budget::parse("0,0"),
|
|
84
|
+
Budget {
|
|
85
|
+
gvl: us(0),
|
|
86
|
+
nogvl: us(0)
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
assert_eq!(
|
|
90
|
+
Budget::parse("5, x"),
|
|
91
|
+
Budget {
|
|
92
|
+
gvl: us(5),
|
|
93
|
+
nogvl: us(50)
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
assert_eq!(Budget::parse("").total(), us(70));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#[test]
|
|
100
|
+
fn a_streak_of_slow_answers_stops_spinning() {
|
|
101
|
+
let history = History::default();
|
|
102
|
+
for _ in 1..STREAK {
|
|
103
|
+
history.record(false);
|
|
104
|
+
assert!(history.spins());
|
|
105
|
+
}
|
|
106
|
+
history.record(false);
|
|
107
|
+
assert!(!history.spins());
|
|
108
|
+
history.record(false);
|
|
109
|
+
assert!(!history.spins());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#[test]
|
|
113
|
+
fn one_answer_within_the_budget_brings_spinning_back() {
|
|
114
|
+
let history = History::default();
|
|
115
|
+
for _ in 0..STREAK {
|
|
116
|
+
history.record(false);
|
|
117
|
+
}
|
|
118
|
+
history.record(true);
|
|
119
|
+
assert!(history.spins());
|
|
120
|
+
history.record(false);
|
|
121
|
+
assert!(history.spins());
|
|
122
|
+
}
|
|
123
|
+
}
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mini_racer_rs
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andy Aragon
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-21 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rb_sys
|
|
@@ -82,6 +82,7 @@ files:
|
|
|
82
82
|
- crates/mini_racer_sync/src/termination.rs
|
|
83
83
|
- crates/mini_racer_sync/src/workers.rs
|
|
84
84
|
- crates/mini_racer_wire/Cargo.toml
|
|
85
|
+
- crates/mini_racer_wire/benches/wire.rs
|
|
85
86
|
- crates/mini_racer_wire/src/decode.rs
|
|
86
87
|
- crates/mini_racer_wire/src/lib.rs
|
|
87
88
|
- crates/mini_racer_wire/src/marker.rs
|
|
@@ -104,6 +105,7 @@ files:
|
|
|
104
105
|
- ext/mini_racer_rs/src/rb_raw.rs
|
|
105
106
|
- ext/mini_racer_rs/src/snapshot.rs
|
|
106
107
|
- ext/mini_racer_rs/src/source_maps.rs
|
|
108
|
+
- ext/mini_racer_rs/src/spin.rs
|
|
107
109
|
- ext/mini_racer_rs/v8-archives.sha256
|
|
108
110
|
- lib/mini_racer.rb
|
|
109
111
|
- lib/mini_racer/shared.rb
|