diff --git a/benchmarks/issue-10089/README.md b/benchmarks/issue-10089/README.md new file mode 100644 index 0000000000..e8dbcf6224 --- /dev/null +++ b/benchmarks/issue-10089/README.md @@ -0,0 +1,111 @@ +# DataView numeric setter fast path (#10089) + +`binary-dataview-set.ts` and `binary-dataview-get.ts` are the unchanged, +standalone reproducers embedded in issue #10089. Their seeded setup, minimum +200 ms/five-run warmup, seven samples of at least 20 ms, median calculation, +fresh-input policy, and per-invocation checksum checks are preserved. + +Measured on 2026-09-12 on Linux x86_64 with an AMD Ryzen 7 7700X (16 logical +CPUs), Node 26.5.1, LLVM 22.1.8, and the pinned nightly-2026-08-20 Rust +toolchain. Before is current main `50e08e91dd6a54d9d9210c43a5d36c86d880d144` +(Perry 0.5.1539); after is this change on the same commit. Both builds used +`--release --locked`, `CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16`, the same package +set, and matching compiler/runtime/stdlib archives. Compilation used +`--no-auto-optimize` with `PERRY_RUNTIME_DIR` pointing at those archives. Runs +were serialized. Source and executable hashes are in `artifacts.json`. + +## Current-main comparison + +Times are median milliseconds per workload invocation. The setter workload +writes and immediately reads every value; the getter control only reads. Every +Perry checksum matched Node at every size. + +| n | Before Perry setter | After Perry setter | After Node setter | Before ratio | After ratio | After getter ratio | +|---:|---:|---:|---:|---:|---:|---:| +| 100 | 0.006030 | 0.005387 | 0.000269 | 22.50x | 20.03x | 11.52x | +| 1,000 | 0.058436 | 0.052222 | 0.002510 | 23.36x | 20.80x | 12.46x | +| 10,000 | 0.581095 | 0.518971 | 0.024820 | 23.51x | 20.91x | 12.35x | +| 100,000 | 5.730139 | 5.192406 | 0.247603 | 23.26x | 20.97x | 12.33x | +| 1,000,000 | 58.591890 | 51.767301 | 2.471560 | 23.70x | 20.95x | 12.31x | + +The Perry setter workload improves by 9.4–11.6% and is 1.72–1.76 times the +getter control, rather than the issue's original 6.68 times at 1M. The +remaining difference includes a second accessor per iteration and numeric +wrapping/storage; the getter baseline itself remains out of scope. Raw results +are `before.json` and `after.json`. + +Least-squares log(time)/log(n) slopes remain linear: before setter Perry 0.997, +Node 0.992; after setter Perry 0.996, Node 0.992. After getter slopes are Perry +0.999 and Node 0.994. + +## Pre-change attribution + +The two original costs were measured independently on the issue's pinned +revision `9495bfc95e2afcfb5a7cb535e440e61ec0722cb1`, before changing behavior. +The no-handle-scope patch is a timing diagnostic only. The no-propagation patch +redirects the write to the canonical shared backing before removing the old +reverse-table propagation, preserving checksums and modeling the storage design +that subsequently landed in #10071. + +| n | Historical baseline | No handle scope | Direct backing/no propagation | Getter control | +|---:|---:|---:|---:|---:| +| 100 | 0.006275 | 0.006034 (-3.8%) | 0.005392 (-14.1%) | 0.002951 | +| 1,000 | 0.062615 | 0.059653 (-4.7%) | 0.053234 (-15.0%) | 0.029187 | +| 10,000 | 0.618650 | 0.592610 (-4.2%) | 0.529090 (-14.5%) | 0.290650 | +| 100,000 | 6.176727 | 5.909852 (-4.3%) | 5.307551 (-14.1%) | 2.906022 | +| 1,000,000 | 51.473179 | 48.430062 (-5.9%) | 51.179968 (-0.6%) | 29.168526 | + +Across the stable 100–100k range, propagation/view-table work was consistently +the dominant extra setter cost, 3.2–3.6 times the handle-scope cost. The 1M +propagation row reproduces the threshold anomaly already called out in the +issue and is not used to reverse the four-size attribution. Getter controls at +100k were 2.9060, 2.8957, and 2.9139 ms across the three builds. + +#10071 landed at `1ae0f84497` between the pinned and current-main measurements. +It made views share canonical backing storage and deleted the reverse-table +propagation call. This change removes the remaining setter-only work: DataView +construction caches the stable backing byte pointer, and calls whose offset and +value are already Numbers neither probe `VIEW_REGISTRY` nor publish a transient +GC handle. Coercing and BigInt calls retain the handle and reload the receiver +after user code. + +Raw attribution data and the exact diagnostic patches are committed alongside +the current-main comparison. + +## Reproduce + +From the checkout being measured: + +```sh +export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 +cargo build --release --locked \ + -p perry -p perry-runtime-static -p perry-stdlib-static +export PERRY_RUNTIME_DIR="$PWD/target/release" +target/release/perry compile \ + benchmarks/issue-10089/binary-dataview-set.ts \ + --no-auto-optimize -o /tmp/binary-dataview-set +target/release/perry compile \ + benchmarks/issue-10089/binary-dataview-get.ts \ + --no-auto-optimize -o /tmp/binary-dataview-get +python3 benchmarks/issue-10089/measure.py \ + --set-app /tmp/binary-dataview-set \ + --get-app /tmp/binary-dataview-get \ + --node node \ + --output /tmp/results.json +``` + +Do not run timed processes or builds concurrently. Rebuild all three packages +when changing checkouts. + +## Semantic and GC validation + +`test_gap_10089_dataview_setter_fast_path.ts` covers both byte orders and all +numeric kinds, byte-level endianness, out-of-range and negative offsets, +ToNumber strings/objects/abrupt completion, BigInt/Number type errors, +ToBigInt-before-bounds ordering, detach during coercion, BigInt round trips, and +DataView/Uint8Array writes in both directions. Its coercion callback allocates, +calls `gc()`, and runs under forced evacuation, from-space protection, and +evacuation verification. Its output matches Node 26.5.1 byte-for-byte. + +The focused runtime test also checks that a windowed DataView stores the exact +backing pointer in its private payload and writes through it. diff --git a/benchmarks/issue-10089/after.json b/benchmarks/issue-10089/after.json new file mode 100644 index 0000000000..4b025ed2c4 --- /dev/null +++ b/benchmarks/issue-10089/after.json @@ -0,0 +1,202 @@ +[ + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 100, + "ms_per_run": 0.00026892069596076023, + "runs": 519811, + "checksum": 53205556 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 100, + "ms_per_run": 0.0053867756531115986, + "runs": 25268, + "checksum": 53205556 + }, + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 1000, + "ms_per_run": 0.0025101219879519144, + "runs": 55706, + "checksum": 509007827 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 1000, + "ms_per_run": 0.05222245691905451, + "runs": 2663, + "checksum": 509007827 + }, + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 10000, + "ms_per_run": 0.02482009553349933, + "runs": 5643, + "checksum": 6182819 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 10000, + "ms_per_run": 0.5189708461538495, + "runs": 273, + "checksum": 6182819 + }, + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 100000, + "ms_per_run": 0.24760332098765048, + "runs": 567, + "checksum": 64481668 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 100000, + "ms_per_run": 5.192406250000033, + "runs": 28, + "checksum": 64481668 + }, + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 1000000, + "ms_per_run": 2.4715601111110774, + "runs": 62, + "checksum": 455838306 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 1000000, + "ms_per_run": 51.76730099999986, + "runs": 7, + "checksum": 455838306 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 100, + "ms_per_run": 0.000265062859490542, + "runs": 510455, + "checksum": 53205456 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 100, + "ms_per_run": 0.0030535497709917123, + "runs": 45860, + "checksum": 53205456 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 1000, + "ms_per_run": 0.0024278540907988313, + "runs": 52452, + "checksum": 509006827 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 1000, + "ms_per_run": 0.03023969788519657, + "runs": 4632, + "checksum": 509006827 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 10000, + "ms_per_run": 0.024489498164015678, + "runs": 5264, + "checksum": 6172819 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 10000, + "ms_per_run": 0.30233589552238305, + "runs": 467, + "checksum": 6172819 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 100000, + "ms_per_run": 0.24528795121951136, + "runs": 573, + "checksum": 64381668 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 100000, + "ms_per_run": 3.0253681428571246, + "runs": 49, + "checksum": 64381668 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 1000000, + "ms_per_run": 2.4499883333333323, + "runs": 61, + "checksum": 454838306 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 1000000, + "ms_per_run": 30.166053000000034, + "runs": 7, + "checksum": 454838306 + } +] diff --git a/benchmarks/issue-10089/artifacts.json b/benchmarks/issue-10089/artifacts.json new file mode 100644 index 0000000000..1d9162ffb2 --- /dev/null +++ b/benchmarks/issue-10089/artifacts.json @@ -0,0 +1,63 @@ +{ + "date": "2026-09-12", + "host": { + "os": "Linux 6.17.0-23-generic x86_64", + "cpu": "AMD Ryzen 7 7700X 8-Core Processor", + "logical_cpus": 16 + }, + "toolchain": { + "node": "v26.5.1", + "node_sha256": "fb48e77df2f8e92fedfec39afa60a5f41563441f6b61316ada5fb295a431c2c6", + "rustc": "1.100.0-nightly (f7d782a3b 2026-08-19)", + "cargo": "1.100.0-nightly (514c56dd7 2026-08-19)", + "llvm": "22.1.8" + }, + "build": { + "profile": "release", + "locked": true, + "codegen_units": 16, + "auto_optimize": false, + "packages": [ + "perry", + "perry-runtime-static", + "perry-stdlib-static" + ] + }, + "sources": { + "binary-dataview-set.ts": "c01d0c04e4e99fcc6725ee83d1a70cee6647217110c95a850d93ad98128375d3", + "binary-dataview-get.ts": "9693aea9afe91fb2ce471aee496fe3e883482a61e304400a1436d7cb411d32e8" + }, + "builds": { + "attribution_baseline": { + "commit": "9495bfc95e2afcfb5a7cb535e440e61ec0722cb1", + "set_executable_sha256": "90f9ab906bb0df83f34fcb72f51efe06aec72c78474371ac3f003ada3826e5c4", + "get_executable_sha256": "9323e511ab8bb320c50895b97af9f15468792b446c0f81adbcded234c8ecbd91" + }, + "attribution_no_handle_scope": { + "commit": "9495bfc95e2afcfb5a7cb535e440e61ec0722cb1", + "patch": "diagnostic-no-handle-scope.patch", + "set_executable_sha256": "d7cd59357fc0792d53a9ff2bb69b74a741fd264123fe5bbf608b95b738e3abb7", + "get_executable_sha256": "54584620ec9285184ab06a69eeee4665c72f5d3c1abdb840feebde03bb7c7ce5" + }, + "attribution_no_view_propagation": { + "commit": "9495bfc95e2afcfb5a7cb535e440e61ec0722cb1", + "patch": "diagnostic-no-view-propagation.patch", + "set_executable_sha256": "2011401cade07d244a6ce6d92af4dd866376c157c5ade98920e6cbb102598df0", + "get_executable_sha256": "3ff30ca899c87c73643f304d5a879d53fd00b4b984bd0e5c49ef9e28faee07c9" + }, + "before": { + "commit": "50e08e91dd6a54d9d9210c43a5d36c86d880d144", + "set_executable_sha256": "aec304da5fa6683767c99ab62477d5a4b381ff0e85d5112a00da4e7c703d16da", + "get_executable_sha256": "ffd6bc041b0c615d3c332ebc0b602361bbd573b6bd1f73ae0748baba1207743d" + }, + "after": { + "commit": "50e08e91dd6a54d9d9210c43a5d36c86d880d144 plus working-tree change", + "set_executable_sha256": "29b33320719fff409c63eb2fee2b7aa229dfe4f3f72cc9f6ed6aaafed0fe7bc2", + "get_executable_sha256": "6dfe429b8c7234237a423179c16a961cb92ee0a6502a3a2cc3df05273cb61491", + "gap_executable_sha256": "a6538dd5cdb69f268d28e26c7d8c19a0c979a3ee5774d4da7ac4fca16e507bb5", + "perry_sha256": "1895d1da6a43549f66f569b633eb8efbdd7001fdd1bf87a0d216f2be7ff4ed4d", + "runtime_archive_sha256": "f2d99f5266c20d3ac1bd91cae23834bac877b9855f770de6fe04c6545b193f6d", + "stdlib_archive_sha256": "265917df8c9e3d28a5f1b1c210db114fea7fb89ec9d253f533a42a291acc21e6" + } + } +} diff --git a/benchmarks/issue-10089/attribution-baseline.json b/benchmarks/issue-10089/attribution-baseline.json new file mode 100644 index 0000000000..726487b2aa --- /dev/null +++ b/benchmarks/issue-10089/attribution-baseline.json @@ -0,0 +1,202 @@ +[ + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 100, + "ms_per_run": 0.0002671258013672751, + "runs": 523745, + "checksum": 53205556 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 100, + "ms_per_run": 0.006274850062734695, + "runs": 21797, + "checksum": 53205556 + }, + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 1000, + "ms_per_run": 0.0024977506243751683, + "runs": 55992, + "checksum": 509007827 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 1000, + "ms_per_run": 0.0626153718750011, + "runs": 2223, + "checksum": 509007827 + }, + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 10000, + "ms_per_run": 0.024648422413793742, + "runs": 5684, + "checksum": 6182819 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 10000, + "ms_per_run": 0.6186502121211968, + "runs": 231, + "checksum": 6182819 + }, + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 100000, + "ms_per_run": 0.24531021951218174, + "runs": 574, + "checksum": 64481668 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 100000, + "ms_per_run": 6.176727250000084, + "runs": 28, + "checksum": 64481668 + }, + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 1000000, + "ms_per_run": 2.462191444444392, + "runs": 62, + "checksum": 455838306 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 1000000, + "ms_per_run": 51.473178999999845, + "runs": 7, + "checksum": 455838306 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 100, + "ms_per_run": 0.0002587780739322494, + "runs": 519664, + "checksum": 53205456 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 100, + "ms_per_run": 0.0029513823225615853, + "runs": 47152, + "checksum": 53205456 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 1000, + "ms_per_run": 0.0024044314739119863, + "runs": 52759, + "checksum": 509006827 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 1000, + "ms_per_run": 0.029187157434400306, + "runs": 4801, + "checksum": 509006827 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 10000, + "ms_per_run": 0.02395959041916093, + "runs": 5366, + "checksum": 6172819 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 10000, + "ms_per_run": 0.2906503623188428, + "runs": 483, + "checksum": 6172819 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 100000, + "ms_per_run": 0.237573952941179, + "runs": 595, + "checksum": 64381668 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 100000, + "ms_per_run": 2.9060215714285795, + "runs": 49, + "checksum": 64381668 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 1000000, + "ms_per_run": 2.377274000000006, + "runs": 63, + "checksum": 454838306 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 1000000, + "ms_per_run": 29.168525999999986, + "runs": 7, + "checksum": 454838306 + } +] diff --git a/benchmarks/issue-10089/attribution-no-handle-scope.json b/benchmarks/issue-10089/attribution-no-handle-scope.json new file mode 100644 index 0000000000..764abe7898 --- /dev/null +++ b/benchmarks/issue-10089/attribution-no-handle-scope.json @@ -0,0 +1,202 @@ +[ + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 100, + "ms_per_run": 0.000267853672255898, + "runs": 522604, + "checksum": 53205556 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 100, + "ms_per_run": 0.006033606636494265, + "runs": 22284, + "checksum": 53205556 + }, + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 1000, + "ms_per_run": 0.0024940461346632994, + "runs": 55995, + "checksum": 509007827 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 1000, + "ms_per_run": 0.05965330654762267, + "runs": 2340, + "checksum": 509007827 + }, + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 10000, + "ms_per_run": 0.024723711990112383, + "runs": 5664, + "checksum": 6182819 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 10000, + "ms_per_run": 0.5926102352941364, + "runs": 239, + "checksum": 6182819 + }, + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 100000, + "ms_per_run": 0.24635226829268253, + "runs": 574, + "checksum": 64481668 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 100000, + "ms_per_run": 5.909851500000002, + "runs": 28, + "checksum": 64481668 + }, + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 1000000, + "ms_per_run": 2.465058111111072, + "runs": 63, + "checksum": 455838306 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 1000000, + "ms_per_run": 48.430062000000135, + "runs": 7, + "checksum": 455838306 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 100, + "ms_per_run": 0.00026624930109959716, + "runs": 507112, + "checksum": 53205456 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 100, + "ms_per_run": 0.002917865061998543, + "runs": 47994, + "checksum": 53205456 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 1000, + "ms_per_run": 0.0024790539167077515, + "runs": 51710, + "checksum": 509006827 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 1000, + "ms_per_run": 0.02878656690647388, + "runs": 4865, + "checksum": 509006827 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 10000, + "ms_per_run": 0.024621353013529843, + "runs": 5613, + "checksum": 6172819 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 10000, + "ms_per_run": 0.28797942857143094, + "runs": 490, + "checksum": 6172819 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 100000, + "ms_per_run": 0.24566884146341156, + "runs": 574, + "checksum": 64381668 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 100000, + "ms_per_run": 2.8957324285714288, + "runs": 49, + "checksum": 64381668 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 1000000, + "ms_per_run": 2.4592393333333415, + "runs": 63, + "checksum": 454838306 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 1000000, + "ms_per_run": 28.78403000000003, + "runs": 7, + "checksum": 454838306 + } +] diff --git a/benchmarks/issue-10089/attribution-no-view-propagation.json b/benchmarks/issue-10089/attribution-no-view-propagation.json new file mode 100644 index 0000000000..42621b99ce --- /dev/null +++ b/benchmarks/issue-10089/attribution-no-view-propagation.json @@ -0,0 +1,202 @@ +[ + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 100, + "ms_per_run": 0.00026791162996263237, + "runs": 522573, + "checksum": 53205556 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 100, + "ms_per_run": 0.005391617789760156, + "runs": 25351, + "checksum": 53205556 + }, + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 1000, + "ms_per_run": 0.0024958236835534264, + "runs": 56108, + "checksum": 509007827 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 1000, + "ms_per_run": 0.05323446010638031, + "runs": 2635, + "checksum": 509007827 + }, + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 10000, + "ms_per_run": 0.024650150246305614, + "runs": 5684, + "checksum": 6182819 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 10000, + "ms_per_run": 0.529089578947366, + "runs": 266, + "checksum": 6182819 + }, + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 100000, + "ms_per_run": 0.24529362195122448, + "runs": 573, + "checksum": 64481668 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 100000, + "ms_per_run": 5.307550500000048, + "runs": 28, + "checksum": 64481668 + }, + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 1000000, + "ms_per_run": 2.46376277777775, + "runs": 63, + "checksum": 455838306 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 1000000, + "ms_per_run": 51.17996800000003, + "runs": 7, + "checksum": 455838306 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 100, + "ms_per_run": 0.0002584621160233231, + "runs": 519704, + "checksum": 53205456 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 100, + "ms_per_run": 0.0029523673800744663, + "runs": 47418, + "checksum": 53205456 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 1000, + "ms_per_run": 0.002428678081360012, + "runs": 52880, + "checksum": 509006827 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 1000, + "ms_per_run": 0.029149784570596676, + "runs": 4807, + "checksum": 509006827 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 10000, + "ms_per_run": 0.02394625956937798, + "runs": 5373, + "checksum": 6172819 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 10000, + "ms_per_run": 0.3649505636363668, + "runs": 385, + "checksum": 6172819 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 100000, + "ms_per_run": 0.2375824588235312, + "runs": 595, + "checksum": 64381668 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 100000, + "ms_per_run": 2.9139227142857345, + "runs": 49, + "checksum": 64381668 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 1000000, + "ms_per_run": 2.3774148888888837, + "runs": 63, + "checksum": 454838306 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 1000000, + "ms_per_run": 29.158057000000042, + "runs": 7, + "checksum": 454838306 + } +] diff --git a/benchmarks/issue-10089/before.json b/benchmarks/issue-10089/before.json new file mode 100644 index 0000000000..f9832871d6 --- /dev/null +++ b/benchmarks/issue-10089/before.json @@ -0,0 +1,202 @@ +[ + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 100, + "ms_per_run": 0.0002679783875765051, + "runs": 521868, + "checksum": 53205556 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 100, + "ms_per_run": 0.006029571299367893, + "runs": 23011, + "checksum": 53205556 + }, + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 1000, + "ms_per_run": 0.002501483116558659, + "runs": 56002, + "checksum": 509007827 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 1000, + "ms_per_run": 0.05843568804665129, + "runs": 2398, + "checksum": 509007827 + }, + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 10000, + "ms_per_run": 0.024721538271608463, + "runs": 5669, + "checksum": 6182819 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 10000, + "ms_per_run": 0.5810946571428368, + "runs": 245, + "checksum": 6182819 + }, + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 100000, + "ms_per_run": 0.24638769512195013, + "runs": 574, + "checksum": 64481668 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 100000, + "ms_per_run": 5.730139000000008, + "runs": 28, + "checksum": 64481668 + }, + { + "workload": "binary-dataview-set", + "engine": "node", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 1000000, + "ms_per_run": 2.4717743333333653, + "runs": 63, + "checksum": 455838306 + }, + { + "workload": "binary-dataview-set", + "engine": "perry", + "name": "binary-dataview-set", + "category": "binary-node", + "n": 1000000, + "ms_per_run": 58.59188999999992, + "runs": 7, + "checksum": 455838306 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 100, + "ms_per_run": 0.0002587603762355714, + "runs": 520913, + "checksum": 53205456 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 100, + "ms_per_run": 0.0030928861914337803, + "runs": 45286, + "checksum": 53205456 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 1000, + "ms_per_run": 0.0024144640270400974, + "runs": 52888, + "checksum": 509006827 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 1000, + "ms_per_run": 0.030480231354642215, + "runs": 4592, + "checksum": 509006827 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 10000, + "ms_per_run": 0.023937772727272388, + "runs": 5379, + "checksum": 6172819 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 10000, + "ms_per_run": 0.30393949999999975, + "runs": 462, + "checksum": 6172819 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 100000, + "ms_per_run": 0.2398608690476174, + "runs": 588, + "checksum": 64381668 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 100000, + "ms_per_run": 3.0356365714285647, + "runs": 49, + "checksum": 64381668 + }, + { + "workload": "binary-dataview-get", + "engine": "node", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 1000000, + "ms_per_run": 2.3884473333333336, + "runs": 63, + "checksum": 454838306 + }, + { + "workload": "binary-dataview-get", + "engine": "perry", + "name": "binary-dataview-get", + "category": "binary-node", + "n": 1000000, + "ms_per_run": 30.850653000000023, + "runs": 7, + "checksum": 454838306 + } +] diff --git a/benchmarks/issue-10089/binary-dataview-get.ts b/benchmarks/issue-10089/binary-dataview-get.ts new file mode 100644 index 0000000000..46dc130538 --- /dev/null +++ b/benchmarks/issue-10089/binary-dataview-get.ts @@ -0,0 +1,97 @@ +// @runtime {"name": "binary-dataview-get", "category": "binary-node", "verification": "checksum", "sources": [{"file": "crates/perry-runtime/src/buffer/dataview.rs", "function": "js_data_view_get"}], "hypothesis": "Each accessor validates/coerces offsets and dispatches the requested numeric kind and byte order.", "notes": "n uint32 accesses. The setter case immediately reads each written value to validate writes; getter-only control separates that cost.", "asynchronous": false, "output_stderr": true, "fresh_input": false} +// Standalone file. Shared helpers/driver are inlined by common.py. + +let seed = 0x12345678; +function rnd(): number { + seed ^= seed << 13; seed ^= seed >>> 17; seed ^= seed << 5; + return (seed >>> 0) / 4294967296; +} +function numbers(n: number): number[] { + const a: number[] = []; + for (let i = 0; i < n; i++) a.push(Math.floor(rnd() * 1000000)); + return a; +} +function hashArray(a: number[]): number { + let h = a.length; + for (let i = 0; i < a.length; i++) h = (h * 31 + a[i]) % 1000000007; + return h; +} +// Bounded checksum work avoids making string slicing/indexing part of every +// string benchmark's asymptotic cost. The workload itself consumes its result. +function hashString(s: string): number { + let h = s.length; + const step = Math.max(1, Math.floor(s.length / 32)); + for (let i = 0; i < s.length; i += step) h = (h * 31 + s.charCodeAt(i)) % 1000000007; + return h; +} + +function setup(n: number): {view: DataView, values: number[]} { + const view = new DataView(new ArrayBuffer(n * 4)); + const values = numbers(n); + for (let i = 0; i < n; i++) view.setUint32(i * 4, values[i], true); + return {view, values}; +} +function run(input: {view: DataView, values: number[]}): number { + let h = 0; + for (let i = 0; i < input.values.length; i++) { + + h = (h + input.view.getUint32(i * 4, true)) % 1000000007; + } + return h; +} + +// Size is the final argument: both native Perry and Node expose it reliably. +const n = Number(process.argv[process.argv.length - 1]); +if (!(n > 0)) throw new Error("Expected a positive size argument"); +function benchmarkMain(): void { + seed = 0x12345678; + const preparedInput = setup(n); + let checksum = 0; + let seen = false; + let warmMs = 0; + let warmRuns = 0; + while (warmMs < 200 || warmRuns < 5) { + seed = 0x12345678; + const input = preparedInput; + const start = performance.now(); + const value = run(input); + const elapsed = performance.now() - start; + if (!(elapsed >= 0)) throw new Error("Invalid monotonic timer"); + warmMs += elapsed; + warmRuns++; + if (seen && value !== checksum) throw new Error("CORRECTNESS: unstable checksum during warmup"); + checksum = value; + seen = true; + } + const samples: number[] = []; + let runs = 0; + for (let sample = 0; sample < 7; sample++) { + let elapsed = 0; + let count = 0; + // Mutable workloads prepare fresh input BEFORE each timer; immutable + // workloads reuse setup. Neither preparation nor validation is measured. + while (elapsed < 20) { + seed = 0x12345678; + const input = preparedInput; + const start = performance.now(); + const value = run(input); + const duration = performance.now() - start; + if (!(duration >= 0)) throw new Error("Invalid monotonic timer"); + elapsed += duration; + count++; + if (value !== checksum) throw new Error("CORRECTNESS: unstable checksum during sampling"); + } + samples.push(elapsed / count); + runs += count; + } + // Do not depend on Array.sort to compute the median of a sort benchmark. + for (let i = 1; i < samples.length; i++) { + const v = samples[i]; + let j = i - 1; + while (j >= 0 && samples[j] > v) { samples[j + 1] = samples[j]; j--; } + samples[j + 1] = v; + } + console.error(JSON.stringify({name: "binary-dataview-get", category: "binary-node", n, + ms_per_run: samples[3], runs, checksum})); +} +benchmarkMain(); diff --git a/benchmarks/issue-10089/binary-dataview-set.ts b/benchmarks/issue-10089/binary-dataview-set.ts new file mode 100644 index 0000000000..f0bb2a1db3 --- /dev/null +++ b/benchmarks/issue-10089/binary-dataview-set.ts @@ -0,0 +1,97 @@ +// @runtime {"name": "binary-dataview-set", "category": "binary-node", "verification": "checksum", "sources": [{"file": "crates/perry-runtime/src/buffer/dataview.rs", "function": "js_data_view_set"}], "hypothesis": "Each accessor validates/coerces offsets and dispatches the requested numeric kind and byte order.", "notes": "n uint32 accesses. The setter case immediately reads each written value to validate writes; getter-only control separates that cost.", "asynchronous": false, "output_stderr": true, "fresh_input": true} +// Standalone file. Shared helpers/driver are inlined by common.py. + +let seed = 0x12345678; +function rnd(): number { + seed ^= seed << 13; seed ^= seed >>> 17; seed ^= seed << 5; + return (seed >>> 0) / 4294967296; +} +function numbers(n: number): number[] { + const a: number[] = []; + for (let i = 0; i < n; i++) a.push(Math.floor(rnd() * 1000000)); + return a; +} +function hashArray(a: number[]): number { + let h = a.length; + for (let i = 0; i < a.length; i++) h = (h * 31 + a[i]) % 1000000007; + return h; +} +// Bounded checksum work avoids making string slicing/indexing part of every +// string benchmark's asymptotic cost. The workload itself consumes its result. +function hashString(s: string): number { + let h = s.length; + const step = Math.max(1, Math.floor(s.length / 32)); + for (let i = 0; i < s.length; i += step) h = (h * 31 + s.charCodeAt(i)) % 1000000007; + return h; +} + +function setup(n: number): {view: DataView, values: number[]} { + const view = new DataView(new ArrayBuffer(n * 4)); + const values = numbers(n); + for (let i = 0; i < n; i++) view.setUint32(i * 4, values[i], true); + return {view, values}; +} +function run(input: {view: DataView, values: number[]}): number { + let h = 0; + for (let i = 0; i < input.values.length; i++) { + input.view.setUint32(i * 4, input.values[i] + 1, true); + h = (h + input.view.getUint32(i * 4, true)) % 1000000007; + } + return h; +} + +// Size is the final argument: both native Perry and Node expose it reliably. +const n = Number(process.argv[process.argv.length - 1]); +if (!(n > 0)) throw new Error("Expected a positive size argument"); +function benchmarkMain(): void { + seed = 0x12345678; + + let checksum = 0; + let seen = false; + let warmMs = 0; + let warmRuns = 0; + while (warmMs < 200 || warmRuns < 5) { + seed = 0x12345678; + const input = setup(n); + const start = performance.now(); + const value = run(input); + const elapsed = performance.now() - start; + if (!(elapsed >= 0)) throw new Error("Invalid monotonic timer"); + warmMs += elapsed; + warmRuns++; + if (seen && value !== checksum) throw new Error("CORRECTNESS: unstable checksum during warmup"); + checksum = value; + seen = true; + } + const samples: number[] = []; + let runs = 0; + for (let sample = 0; sample < 7; sample++) { + let elapsed = 0; + let count = 0; + // Mutable workloads prepare fresh input BEFORE each timer; immutable + // workloads reuse setup. Neither preparation nor validation is measured. + while (elapsed < 20) { + seed = 0x12345678; + const input = setup(n); + const start = performance.now(); + const value = run(input); + const duration = performance.now() - start; + if (!(duration >= 0)) throw new Error("Invalid monotonic timer"); + elapsed += duration; + count++; + if (value !== checksum) throw new Error("CORRECTNESS: unstable checksum during sampling"); + } + samples.push(elapsed / count); + runs += count; + } + // Do not depend on Array.sort to compute the median of a sort benchmark. + for (let i = 1; i < samples.length; i++) { + const v = samples[i]; + let j = i - 1; + while (j >= 0 && samples[j] > v) { samples[j + 1] = samples[j]; j--; } + samples[j + 1] = v; + } + console.error(JSON.stringify({name: "binary-dataview-set", category: "binary-node", n, + ms_per_run: samples[3], runs, checksum})); +} +benchmarkMain(); diff --git a/benchmarks/issue-10089/diagnostic-no-handle-scope.patch b/benchmarks/issue-10089/diagnostic-no-handle-scope.patch new file mode 100644 index 0000000000..eee9f52c25 --- /dev/null +++ b/benchmarks/issue-10089/diagnostic-no-handle-scope.patch @@ -0,0 +1,27 @@ +diff --git a/crates/perry-runtime/src/buffer/dataview.rs b/crates/perry-runtime/src/buffer/dataview.rs +index 71727b2d24..0000000000 100644 +--- a/crates/perry-runtime/src/buffer/dataview.rs ++++ b/crates/perry-runtime/src/buffer/dataview.rs +@@ -271,8 +271,7 @@ pub fn js_data_view_set( + kind: DataViewKind, + little: bool, + ) -> f64 { +- let scope = crate::gc::RuntimeHandleScope::new(); +- let buf_handle = scope.root_nanbox_f64(buf_f64); ++ let buf = unbox_buffer_ptr(buf_f64.to_bits()) as *mut BufferHeader; + let offset = to_byte_offset(offset_value); + if kind.is_bigint() { + // SetViewValue for a BigInt accessor: `ToBigInt(value)` (a Number throws +@@ -285,12 +284,10 @@ pub fn js_data_view_set( + } else { + raw.to_be_bytes() + }; +- let buf = unbox_buffer_ptr(buf_handle.get_nanbox_f64().to_bits()) as *mut BufferHeader; + unsafe { write_bytes(buf, offset, &b) }; + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + let n = to_number(value); +- let buf = unbox_buffer_ptr(buf_handle.get_nanbox_f64().to_bits()) as *mut BufferHeader; + unsafe { + match kind { + DataViewKind::BigInt64 | DataViewKind::BigUint64 => unreachable!(), diff --git a/benchmarks/issue-10089/diagnostic-no-view-propagation.patch b/benchmarks/issue-10089/diagnostic-no-view-propagation.patch new file mode 100644 index 0000000000..62109d2c18 --- /dev/null +++ b/benchmarks/issue-10089/diagnostic-no-view-propagation.patch @@ -0,0 +1,20 @@ +diff --git a/crates/perry-runtime/src/buffer/dataview.rs b/crates/perry-runtime/src/buffer/dataview.rs +index 71727b2d24..0000000000 100644 +--- a/crates/perry-runtime/src/buffer/dataview.rs ++++ b/crates/perry-runtime/src/buffer/dataview.rs +@@ -182,14 +182,8 @@ unsafe fn write_bytes(buf: *mut BufferHeader, offset: i64, bytes: &[u8]) { + if offset + (bytes.len() as i64) > len { + throw_dataview_oob(); + } +- let base = buffer_data_mut(buf).add(offset as usize); ++ let base = super::view::resolve_data_ptr(buf).add(offset as usize) as *mut u8; + ptr::copy_nonoverlapping(bytes.as_ptr(), base, bytes.len()); +- super::view::propagate_written_range_from_receiver( +- buf as usize, +- offset as u32, +- base, +- bytes.len() as u32, +- ); + } + + /// `DataView.prototype.get(byteOffset, littleEndian?)`. diff --git a/benchmarks/issue-10089/measure.py b/benchmarks/issue-10089/measure.py new file mode 100644 index 0000000000..b120b826cc --- /dev/null +++ b/benchmarks/issue-10089/measure.py @@ -0,0 +1,113 @@ +"""Run the unchanged #10089 DataView workloads serially against Node and Perry.""" + +import argparse +import json +import math +import os +from pathlib import Path +import subprocess + + +SIZES = [100, 1000, 10000, 100000, 1000000] + + +def slope(points): + if len(points) < 2: + return None + xs = [math.log(n) for n, _ in points] + ys = [math.log(ms) for _, ms in points] + mean_x = sum(xs) / len(xs) + mean_y = sum(ys) / len(ys) + return sum((x - mean_x) * (y - mean_y) for x, y in zip(xs, ys)) / sum( + (x - mean_x) ** 2 for x in xs + ) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--set-app", type=Path, required=True) + parser.add_argument("--get-app", type=Path, required=True) + parser.add_argument("--node", default="node") + parser.add_argument("--output", type=Path, required=True) + parser.add_argument( + "--allow-mismatch", + action="store_true", + help="continue diagnostic sweeps whose temporary patch breaks coherency", + ) + args = parser.parse_args() + root = Path(__file__).resolve().parent + workloads = { + "binary-dataview-set": args.set_app.resolve(), + "binary-dataview-get": args.get_app.resolve(), + } + env = dict(os.environ, TZ="UTC", LC_ALL="en_US.UTF-8") + rows = [] + points = {} + for name, app in workloads.items(): + points[name] = {"node": [], "perry": []} + stopped = set() + for n in SIZES: + pair = {} + commands = { + "node": [args.node, str(root / f"{name}.ts")], + "perry": [str(app)], + } + for engine, command in commands.items(): + if engine in stopped: + continue + try: + proc = subprocess.run( + command + [str(n)], + capture_output=True, + text=True, + timeout=60, + env=env, + ) + if proc.returncode: + row = { + "workload": name, + "engine": engine, + "n": n, + "status": "ERROR", + "code": proc.returncode, + "stderr": proc.stderr, + "stdout": proc.stdout, + } + stopped.add(engine) + else: + result = json.loads(proc.stderr) + row = {"workload": name, "engine": engine, **result} + pair[engine] = result + points[name][engine].append((n, result["ms_per_run"])) + except subprocess.TimeoutExpired: + row = { + "workload": name, + "engine": engine, + "n": n, + "status": "TIMEOUT", + } + stopped.add(engine) + rows.append(row) + print(json.dumps(row), flush=True) + args.output.write_text( + json.dumps(rows, indent=2) + "\n", encoding="utf-8" + ) + if len(pair) == 2 and pair["node"]["checksum"] != pair["perry"]["checksum"]: + message = f"Checksum mismatch for {name} at n={n}: {pair}" + if not args.allow_mismatch: + raise SystemExit(message) + print(message, flush=True) + print( + "slopes", + { + name: {engine: slope(values) for engine, values in engines.items()} + for name, engines in points.items() + }, + flush=True, + ) + if any("status" in row for row in rows): + raise SystemExit(1) + + +if __name__ == "__main__": + main() diff --git a/changelog.d/10134-dataview-setter-fast-path.md b/changelog.d/10134-dataview-setter-fast-path.md new file mode 100644 index 0000000000..d71894b129 --- /dev/null +++ b/changelog.d/10134-dataview-setter-fast-path.md @@ -0,0 +1,15 @@ +### Performance + +- **DataView numeric setters avoid a transient GC handle and view-table probe + when their offset and value are already Numbers.** DataView construction now + caches the stable byte pointer into its canonical backing allocation, while + the existing view registry remains the traced owning edge. Calls that can + coerce user values or write BigInts retain their handle scope and reload the + receiver after callbacks, including a moving collection or detach. + + On the issue's seeded set-and-read workload, serialized release measurements + improve Perry time by 9.4–11.6% from 100 through 1,000,000 elements. The + setter/getter ratio is 1.72–1.76x instead of the issue's original 6.68x. All + eight numeric kinds, both byte orders, shared-view and structured-clone + coherency, coercion/error ordering, detach, and forced evacuation are covered + by Node parity tests. diff --git a/crates/perry-runtime/src/buffer/dataview.rs b/crates/perry-runtime/src/buffer/dataview.rs index 860622612c..c17c5970ef 100644 --- a/crates/perry-runtime/src/buffer/dataview.rs +++ b/crates/perry-runtime/src/buffer/dataview.rs @@ -104,6 +104,19 @@ fn throw_dataview_oob() -> ! { super::numeric::throw_dataview_offset_out_of_bounds() } +fn throw_dataview_detached() -> ! { + crate::collection_iter::throw_type_error( + "Cannot perform DataView access on a detached ArrayBuffer", + ) +} + +/// Non-allocating `ToIndex` subset used by the numeric setter fast path. +#[inline(always)] +fn numeric_byte_offset(value: f64) -> Option { + (value >= 0.0 && value <= 9_007_199_254_740_991.0 && value.trunc() == value) + .then_some(value as i64) +} + #[inline] /// `ToIndex(byteOffset)` for `GetViewValue`/`SetViewValue`: ToNumber → /// ToIntegerOrInfinity → range-check `[0, 2^53-1]`. A Symbol or object byteOffset @@ -118,8 +131,8 @@ fn to_byte_offset(value: f64) -> i64 { // Fast path (#6386): a non-NaN f64 is by NaN-boxing construction a // genuine Number (every tag pattern is a NaN payload), so a valid // integral index needs no coercion machinery at all. - if value >= 0.0 && value <= 9_007_199_254_740_991.0 && value.trunc() == value { - return value as i64; + if let Some(offset) = numeric_byte_offset(value) { + return offset; } if crate::value::JSValue::from_bits(value.to_bits()).is_bigint() { crate::collection_iter::throw_type_error("Cannot convert a BigInt value to a number"); @@ -140,12 +153,17 @@ fn to_byte_offset(value: f64) -> i64 { /// step order). A BigInt accessor takes the `to_bigint_raw_or_throw` path instead. #[inline] fn to_number(value: f64) -> f64 { - // A non-NaN f64 is by NaN-boxing construction already a Number (#6386); - // every non-Number value (and boxed int32) carries a NaN tag pattern and - // takes the full coercion. - if !value.is_nan() { + let js_value = crate::value::JSValue::from_bits(value.to_bits()); + // Includes every IEEE-754 NaN encoding that is not in Perry's tag band. + if js_value.is_number() { return value; } + // DataView SetViewValue uses the abstract ToNumber operation, which rejects + // BigInt. `js_number_coerce` also serves explicit Number(), where conversion + // from BigInt is allowed, so reject it at this call site. + if js_value.is_bigint() { + crate::collection_iter::throw_type_error("Cannot convert a BigInt value to a number"); + } crate::builtins::js_number_coerce(value) } @@ -175,13 +193,74 @@ unsafe fn write_bytes(buf: *mut BufferHeader, offset: i64, bytes: &[u8]) { throw_dataview_oob(); } let len = (*buf).length as i64; + // Detach zeroes every registered view's length before decommitting backing + // pages. Keep the common non-empty path table-free; only a zero-length view + // needs to distinguish detached TypeError from ordinary RangeError. + if len == 0 && super::detach::is_detached_buffer(super::view::backing_of(buf as usize)) { + throw_dataview_detached(); + } if offset + (bytes.len() as i64) > len { throw_dataview_oob(); } - let base = buffer_data_mut(buf).add(offset as usize); + let base = super::view::data_view_data_ptr(buf).add(offset as usize); ptr::copy_nonoverlapping(bytes.as_ptr(), base, bytes.len()); } +/// Store an already-coerced Number. This path contains no allocation or user +/// callback; its only call, `write_bytes`, performs bounds checks and a memcpy +/// through the stable pointer cached by `DataView` construction. +unsafe fn write_number( + buf: *mut BufferHeader, + offset: i64, + n: f64, + kind: DataViewKind, + little: bool, +) { + match kind { + DataViewKind::BigInt64 | DataViewKind::BigUint64 => unreachable!(), + DataViewKind::Int8 | DataViewKind::Uint8 => { + // ToUint8 / ToInt8 wrap to the same byte; store identically. + let byte = wrap_to_u64(n, 8) as u8; + write_bytes(buf, offset, &[byte]); + } + DataViewKind::Int16 | DataViewKind::Uint16 => { + let v = wrap_to_u64(n, 16) as u16; + let bytes = if little { + v.to_le_bytes() + } else { + v.to_be_bytes() + }; + write_bytes(buf, offset, &bytes); + } + DataViewKind::Int32 | DataViewKind::Uint32 => { + let v = wrap_to_u64(n, 32) as u32; + let bytes = if little { + v.to_le_bytes() + } else { + v.to_be_bytes() + }; + write_bytes(buf, offset, &bytes); + } + DataViewKind::Float32 => { + let v = n as f32; + let bytes = if little { + v.to_le_bytes() + } else { + v.to_be_bytes() + }; + write_bytes(buf, offset, &bytes); + } + DataViewKind::Float64 => { + let bytes = if little { + n.to_le_bytes() + } else { + n.to_be_bytes() + }; + write_bytes(buf, offset, &bytes); + } + } +} + /// `DataView.prototype.get(byteOffset, littleEndian?)`. /// `buf_f64` is the NaN-boxed DataView (BufferHeader) pointer. pub fn js_data_view_get(buf_f64: f64, offset_value: f64, kind: DataViewKind, little: bool) -> f64 { @@ -271,6 +350,18 @@ pub fn js_data_view_set( kind: DataViewKind, little: bool, ) -> f64 { + // Both inputs are already Numbers and ToIndex is already resolved: no call + // below can allocate, invoke JavaScript, collect, or move/reclaim `buf`. + // Avoid publishing a transient GC root and use the construction-time data + // pointer cache instead of probing VIEW_REGISTRY on every numeric write. + if !kind.is_bigint() && crate::value::JSValue::from_bits(value.to_bits()).is_number() { + if let Some(offset) = numeric_byte_offset(offset_value) { + let buf = unbox_buffer_ptr(buf_f64.to_bits()) as *mut BufferHeader; + unsafe { write_number(buf, offset, value, kind, little) }; + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + } + let scope = crate::gc::RuntimeHandleScope::new(); let buf_handle = scope.root_nanbox_f64(buf_f64); let offset = to_byte_offset(offset_value); @@ -290,51 +381,7 @@ pub fn js_data_view_set( } let n = to_number(value); let buf = unbox_buffer_ptr(buf_handle.get_nanbox_f64().to_bits()) as *mut BufferHeader; - unsafe { - match kind { - DataViewKind::BigInt64 | DataViewKind::BigUint64 => unreachable!(), - DataViewKind::Int8 | DataViewKind::Uint8 => { - // ToUint8 / ToInt8 wrap to the same byte; store identically. - let byte = wrap_to_u64(n, 8) as u8; - write_bytes(buf, offset, &[byte]); - } - DataViewKind::Int16 | DataViewKind::Uint16 => { - let v = wrap_to_u64(n, 16) as u16; - let b = if little { - v.to_le_bytes() - } else { - v.to_be_bytes() - }; - write_bytes(buf, offset, &b); - } - DataViewKind::Int32 | DataViewKind::Uint32 => { - let v = wrap_to_u64(n, 32) as u32; - let b = if little { - v.to_le_bytes() - } else { - v.to_be_bytes() - }; - write_bytes(buf, offset, &b); - } - DataViewKind::Float32 => { - let v = n as f32; - let b = if little { - v.to_le_bytes() - } else { - v.to_be_bytes() - }; - write_bytes(buf, offset, &b); - } - DataViewKind::Float64 => { - let b = if little { - n.to_le_bytes() - } else { - n.to_be_bytes() - }; - write_bytes(buf, offset, &b); - } - } - } + unsafe { write_number(buf, offset, n, kind, little) }; f64::from_bits(crate::value::TAG_UNDEFINED) } diff --git a/crates/perry-runtime/src/buffer/from.rs b/crates/perry-runtime/src/buffer/from.rs index 508403948f..a14a6a4375 100644 --- a/crates/perry-runtime/src/buffer/from.rs +++ b/crates/perry-runtime/src/buffer/from.rs @@ -936,7 +936,7 @@ pub extern "C" fn js_data_view_new(value: f64, offset_value: f64, length_value: // report the right values, including zero-length views at the end. let start = offset as u32; let len = view_len as u32; - let view = super::view::alloc(src, start, len); + let view = super::view::alloc_data_view(src, start, len); mark_as_data_view(view as usize); set_buffer_ab_alias(view as usize, resolve_buffer_ab_alias(addr)); f64::from_bits(crate::value::JSValue::pointer(view as *mut u8).bits()) diff --git a/crates/perry-runtime/src/buffer/view.rs b/crates/perry-runtime/src/buffer/view.rs index 7683c1e65c..8879d12469 100644 --- a/crates/perry-runtime/src/buffer/view.rs +++ b/crates/perry-runtime/src/buffer/view.rs @@ -106,6 +106,50 @@ pub(crate) fn alloc(backing: *const BufferHeader, offset: u32, length: u32) -> * view } +/// Allocate a DataView with one cached native data pointer after its +/// `BufferHeader`. Unlike Buffer/Uint8Array views, DataView byte access always +/// enters a runtime helper, so this private payload is never mistaken for +/// indexed storage. The backing remains owned and traced by `VIEW_REGISTRY`. +/// +/// Buffer allocations and foreign/shared ArrayBuffer storage are non-moving: +/// `buffer_alloc` uses the old arena because native callers retain byte +/// pointers, and foreign/shared backings have the same stable-address contract. +/// The cached interior pointer therefore stays valid until detach, which zeroes +/// the view length before its backing storage can be released. +pub(crate) fn alloc_data_view( + backing: *const BufferHeader, + offset: u32, + length: u32, +) -> *mut BufferHeader { + let scope = crate::gc::RuntimeHandleScope::new(); + let owner = scope.root_raw_const_ptr(backing); + let view = buffer_alloc(std::mem::size_of::() as u32); + unsafe { + (*view).length = length; + owner.with_const_ptr::(|backing| { + register(view as usize, backing as usize, offset); + let data = buffer_data(backing).add(offset as usize); + data_view_cache_slot(view).write(data as usize); + }); + } + view +} + +#[inline(always)] +unsafe fn data_view_cache_slot(view: *mut BufferHeader) -> *mut usize { + (view as *mut u8) + .add(std::mem::size_of::()) + .cast::() +} + +/// Load the stable byte pointer cached by [`alloc_data_view`]. The caller must +/// first bounds-check the DataView and reject a detached backing. +#[inline(always)] +pub(crate) unsafe fn data_view_data_ptr(view: *mut BufferHeader) -> *mut u8 { + debug_assert!((*view).capacity >= std::mem::size_of::() as u32); + data_view_cache_slot(view).read() as *mut u8 +} + fn register(view_ptr: usize, backing_ptr: usize, offset: u32) { let (backing, offset) = lookup(backing_ptr) .map(|parent| (parent.backing, parent.offset + offset)) diff --git a/crates/perry-runtime/src/buffer/view_tests.rs b/crates/perry-runtime/src/buffer/view_tests.rs index 77faf17479..118b8d09cc 100644 --- a/crates/perry-runtime/src/buffer/view_tests.rs +++ b/crates/perry-runtime/src/buffer/view_tests.rs @@ -22,6 +22,36 @@ fn suffix_views_allocate_only_headers_and_share_native_bytes() { } } +#[test] +fn data_view_caches_its_stable_window_pointer_in_the_header_payload() { + let backing = js_array_buffer_new(16); + let backing_value = value(backing); + let view_value = js_data_view_new(backing_value, 4.0, 8.0); + let view = crate::value::JSValue::from_bits(view_value.to_bits()).as_pointer::() + as *mut BufferHeader; + + unsafe { + assert_eq!((*view).length, 8); + assert_eq!((*view).capacity, std::mem::size_of::() as u32); + assert_eq!( + view::data_view_data_ptr(view) as *const u8, + buffer_data(backing).add(4) + ); + } + + js_data_view_set( + view_value, + 0.0, + 0x0102_0304 as f64, + DataViewKind::Uint32, + false, + ); + assert_eq!( + unsafe { std::slice::from_raw_parts(buffer_data(backing).add(4), 4) }, + &[1, 2, 3, 4] + ); +} + #[test] fn overlapping_nested_views_share_every_write_path() { let source = js_buffer_alloc(12, 0); diff --git a/crates/perry-runtime/src/builtins/globals.rs b/crates/perry-runtime/src/builtins/globals.rs index a25fc16f91..6fc89dac9e 100644 --- a/crates/perry-runtime/src/builtins/globals.rs +++ b/crates/perry-runtime/src/builtins/globals.rs @@ -607,6 +607,33 @@ fn clone_buffer_header(addr: usize, detach_source: bool) -> f64 { let src = addr as *mut crate::buffer::BufferHeader; let src_len = unsafe { (*src).length }; + // A constructor-created DataView has a private cached data pointer rather + // than inline bytes. Clone its visible window into a fresh ArrayBuffer and + // go through the constructor so the clone gets the same representation; + // merely marking an inline buffer as a DataView would make the numeric + // setter interpret its first bytes as that cache pointer. + if crate::buffer::is_data_view(addr) { + let backing = crate::buffer::buffer_alloc(src_len); + unsafe { + (*backing).length = src_len; + if src_len > 0 { + std::ptr::copy_nonoverlapping( + crate::buffer::buffer_data(src), + crate::buffer::buffer_data_mut(backing), + src_len as usize, + ); + } + } + crate::buffer::mark_as_array_buffer(backing as usize); + let backing_value = crate::value::js_nanbox_pointer(backing as i64); + let cloned = crate::buffer::js_data_view_new(backing_value, 0.0, src_len as f64); + if detach_source { + let cloned_addr = pointer_addr(cloned).unwrap_or(0); + record_transfer_clone(addr, cloned_addr); + } + return cloned; + } + let dst = crate::buffer::buffer_alloc(src_len); unsafe { (*dst).length = src_len; @@ -624,9 +651,6 @@ fn clone_buffer_header(addr: usize, detach_source: bool) -> f64 { crate::buffer::mark_as_array_buffer(dst_addr); } else if crate::buffer::is_shared_array_buffer(addr) { crate::buffer::mark_as_shared_array_buffer(dst_addr); - } else if crate::buffer::is_data_view(addr) { - crate::buffer::mark_as_data_view(dst_addr); - crate::buffer::set_buffer_ab_alias(dst_addr, crate::buffer::resolve_buffer_ab_alias(addr)); } else if crate::buffer::is_uint8array_buffer(addr) { crate::buffer::mark_as_uint8array(dst_addr); crate::buffer::set_buffer_ab_alias(dst_addr, crate::buffer::resolve_buffer_ab_alias(addr)); @@ -1342,4 +1366,54 @@ mod structured_clone_tests { } } } + + #[test] + fn structured_clone_data_view_uses_the_cached_registered_representation() { + let backing = crate::buffer::js_array_buffer_new(8); + let backing_value = crate::value::js_nanbox_pointer(backing as i64); + let source = crate::buffer::js_data_view_new(backing_value, 0.0, 8.0); + crate::buffer::js_data_view_set( + source, + 0.0, + 0x0102_0304u32 as f64, + crate::buffer::DataViewKind::Uint32, + false, + ); + + let cloned = js_structured_clone(source); + let cloned_addr = pointer_addr(cloned).expect("DataView clone must be a pointer"); + assert!(crate::buffer::is_data_view(cloned_addr)); + assert_ne!( + crate::buffer::buffer_backing_array_buffer(cloned_addr), + backing as usize, + "the clone must own an independent ArrayBuffer" + ); + + crate::buffer::js_data_view_set( + cloned, + 4.0, + 0x0506_0708u32 as f64, + crate::buffer::DataViewKind::Uint32, + false, + ); + assert_eq!( + crate::buffer::js_data_view_get( + cloned, + 4.0, + crate::buffer::DataViewKind::Uint32, + false, + ), + 0x0506_0708u32 as f64 + ); + assert_eq!( + crate::buffer::js_data_view_get( + source, + 4.0, + crate::buffer::DataViewKind::Uint32, + false, + ), + 0.0, + "writing the clone must not change its source" + ); + } } diff --git a/test-files/test_gap_10089_dataview_setter_fast_path.ts b/test-files/test_gap_10089_dataview_setter_fast_path.ts new file mode 100644 index 0000000000..648a0e502c --- /dev/null +++ b/test-files/test_gap_10089_dataview_setter_fast_path.ts @@ -0,0 +1,149 @@ +// parity-env: PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1 PERRY_GC_PROTECT_FROMSPACE=1 +// #10089: DataView's numeric setter fast path must avoid a runtime handle +// scope without changing coercion order, errors, byte order, or shared views. + +function errorName(label: string, callback: () => void): void { + try { + callback(); + console.log(label, "NO THROW"); + } catch (error) { + console.log(label, (error as Error).name); + } +} + +function numericRoundTrips(little: boolean): string { + const view = new DataView(new ArrayBuffer(64)); + view.setInt8(0, -123); + view.setUint8(1, 250); + view.setInt16(2, -0x1234, little); + view.setUint16(4, 0xabcd, little); + view.setInt32(8, -0x1234567, little); + view.setUint32(12, 0x89abcdef, little); + view.setFloat32(16, 1.5, little); + view.setFloat64(24, -3.25, little); + return [ + view.getInt8(0), + view.getUint8(1), + view.getInt16(2, little), + view.getUint16(4, little), + view.getInt32(8, little), + view.getUint32(12, little), + view.getFloat32(16, little), + view.getFloat64(24, little), + ].join(","); +} + +console.log("numeric big-endian", numericRoundTrips(false)); +console.log("numeric little-endian", numericRoundTrips(true)); + +{ + const buffer = new ArrayBuffer(16); + const bytes = new Uint8Array(buffer); + const view = new DataView(buffer); + view.setUint32(0, 0x01020304, false); + view.setUint32(4, 0x01020304, true); + view.setFloat32(8, 1.5, false); + view.setFloat32(12, 1.5, true); + console.log("endian bytes", Array.from(bytes).join(",")); +} + +{ + const buffer = new ArrayBuffer(16); + const words = new Uint8Array(buffer); + const view = new DataView(buffer, 4, 8); + view.setUint32(0, 0x01020304, false); + const throughTypedArray = Array.from(words.slice(4, 8)).join(","); + words.set([0x05, 0x06, 0x07, 0x08], 8); + console.log( + "shared views", + throughTypedArray, + view.getUint32(4, false).toString(16), + ); +} + +{ + const view = new DataView(new ArrayBuffer(8)); + view.setUint32(0, "17" as any, true); + view.setUint16(4, { valueOf() { return 0x2345; } } as any, true); + console.log("ToNumber", view.getUint32(0, true), view.getUint16(4, true)); + + errorName("negative offset", () => view.setUint32(-1, 1)); + errorName("out of range", () => view.setUint32(5, 1)); + errorName("throwing valueOf", () => view.setUint32(0, { + valueOf() { throw new Error("coercion marker"); }, + } as any)); + errorName("numeric BigInt", () => view.setUint32(0, 1n as any)); +} + +{ + let valueCalls = 0; + const view = new DataView(new ArrayBuffer(4)); + errorName("negative before value", () => view.setUint32(-1, { + valueOf() { valueCalls++; return 1; }, + } as any)); + console.log("negative value calls", valueCalls); + + errorName("number value before bounds", () => view.setUint32(4, { + valueOf() { valueCalls++; return 1; }, + } as any)); + console.log("number value calls", valueCalls); + + errorName("BigInt Number in bounds", () => view.setBigInt64(0, 1 as any)); + errorName("BigInt Number before bounds", () => view.setBigInt64(8, 1 as any)); + errorName("BigInt string then bounds", () => view.setBigInt64(8, "1" as any)); +} + +{ + const buffer = new ArrayBuffer(8); + const view = new DataView(buffer); + errorName("detach during ToNumber", () => view.setUint32(0, { + valueOf() { + buffer.transfer(); + return 7; + }, + } as any, true)); +} + +{ + let coercions = 0; + const buffer = new ArrayBuffer(8); + const mirror = new Uint8Array(buffer); + new DataView(buffer).setUint32(0, { + valueOf() { + coercions++; + const pressure: object[] = []; + for (let i = 0; i < 512; i++) pressure.push({ i, text: "move-" + i }); + const collect = (globalThis as any).gc; + if (typeof collect === "function") collect(); + return 0x01020304; + }, + } as any, false); + console.log("moving coercion", coercions, Array.from(mirror.slice(0, 4)).join(",")); +} + +{ + const view = new DataView(new ArrayBuffer(16)); + view.setBigInt64(0, -2n, false); + view.setBigUint64(8, 0xfedcba9876543210n, true); + console.log( + "BigInt endian", + view.getBigInt64(0, false), + view.getBigUint64(8, true).toString(16), + ); +} + +{ + const source = new DataView(new ArrayBuffer(8)); + source.setUint32(0, 0x01020304, false); + const cloned = structuredClone(source); + cloned.setUint32(4, 0x05060708, false); + console.log( + "structured clone", + cloned.byteOffset, + cloned.byteLength, + cloned.getUint32(0, false).toString(16), + cloned.getUint32(4, false).toString(16), + source.getUint32(4, false), + cloned.buffer === source.buffer, + ); +}