From 323891948ad53be1937fb6927117a87447805451 Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Fri, 11 Sep 2026 17:10:30 +0200 Subject: [PATCH 1/2] fix(runtime): reduce arguments object construction overhead --- benchmarks/issue-10063/.gitattributes | 1 + benchmarks/issue-10063/README.md | 123 ++++ benchmarks/issue-10063/function-arguments.ts | 94 +++ benchmarks/issue-10063/measure.py | 66 ++ benchmarks/issue-10063/package.json | 1 + benchmarks/issue-10063/results-windows.json | 599 ++++++++++++++++++ changelog.d/10063-arguments-construction.md | 7 + .../src/gc/tests/arguments_objects.rs | 160 +++++ crates/perry-runtime/src/gc/tests/mod.rs | 1 + crates/perry-runtime/src/object/arguments.rs | 175 +++-- .../src/object/descriptor_state.rs | 25 + 11 files changed, 1198 insertions(+), 54 deletions(-) create mode 100644 benchmarks/issue-10063/.gitattributes create mode 100644 benchmarks/issue-10063/README.md create mode 100644 benchmarks/issue-10063/function-arguments.ts create mode 100644 benchmarks/issue-10063/measure.py create mode 100644 benchmarks/issue-10063/package.json create mode 100644 benchmarks/issue-10063/results-windows.json create mode 100644 changelog.d/10063-arguments-construction.md create mode 100644 crates/perry-runtime/src/gc/tests/arguments_objects.rs diff --git a/benchmarks/issue-10063/.gitattributes b/benchmarks/issue-10063/.gitattributes new file mode 100644 index 0000000000..71fd53bdf5 --- /dev/null +++ b/benchmarks/issue-10063/.gitattributes @@ -0,0 +1 @@ +function-arguments.ts text eol=lf diff --git a/benchmarks/issue-10063/README.md b/benchmarks/issue-10063/README.md new file mode 100644 index 0000000000..a50fed1c46 --- /dev/null +++ b/benchmarks/issue-10063/README.md @@ -0,0 +1,123 @@ +# Arguments construction benchmark (#10063) + +`function-arguments.ts` is the unchanged standalone workload from +[issue #10063](https://github.com/PerryTS/perry/issues/10063). Its SHA-256 is +`f1ae42e13cdf29dec7829aa402c3daf22f093f4a17911dabbc0743c4e105ba73`. +The local `package.json` preserves the original standalone file's CommonJS +context, including sloppy mapped arguments, despite the repository's root +`"type": "module"` setting. + +## Reproduce + +Use Node v26.5.1 and the same Rust/LLVM toolchain for both Perry builds. In +separate clean worktrees for the baseline and patched revisions, build the CLI +and both runtime archives together: + +```sh +cargo build --release --locked -p perry -p perry-runtime-static -p perry-stdlib-static +``` + +For each build, point `PERRY_RUNTIME_DIR` at that worktree's `target/release` and +compile this same source with `--no-auto-optimize` into separate executables: + +```sh +PERRY_RUNTIME_DIR=/absolute/baseline/target/release \ + /absolute/baseline/target/release/perry compile \ + /absolute/patched/benchmarks/issue-10063/function-arguments.ts \ + --no-auto-optimize -o /absolute/results/before +PERRY_RUNTIME_DIR=/absolute/patched/target/release \ + /absolute/patched/target/release/perry compile \ + /absolute/patched/benchmarks/issue-10063/function-arguments.ts \ + --no-auto-optimize -o /absolute/results/after +python benchmarks/issue-10063/measure.py \ + --source benchmarks/issue-10063/function-arguments.ts \ + --before /absolute/results/before --after /absolute/results/after \ + --output /absolute/results/comparison.json --rounds 3 +``` + +On Windows, use `perry.exe`, `.exe` output paths, and PowerShell's +`$env:PERRY_RUNTIME_DIR = 'C:\absolute\worktree\target\release'` syntax. +Ensure LLVM's binaries are on `PATH`. Do not set a different GC backend for one +build or run other builds or benchmarks during the comparison. + +The driver serializes every process, alternates the two native builds' order, +enforces a 60-second whole-process timeout, and checks all five checksums. The +workload retains its original warmup (at least 200 ms and five runs), seven +samples, and at least 20 ms per sample. Setup, checksum validation, and process +startup are outside the reported `ms_per_run`; `wall_seconds` includes them. + +## Diagnosis + +A Windows program-counter sample of the baseline's 10,000-element workload +recorded 1,298 samples. Among the 60 most frequent symbols, shape bookkeeping +accounted for 445 self samples, GC for 252, and descriptor helpers for 85. +Symbols were resolved from the native linker's map; these are self samples, +not inclusive stack costs. The hot path repeatedly appended argument keys, +registered redundant all-true indexed descriptors, and changed semantic shapes. + +The fix constructs ordinary arguments objects in bulk. A bounded, GC-traced +cache shares immutable key arrays for arities 0 through 64. Each call still gets +its own values, identity, descriptors, and mapped boxes; key mutation uses the +existing copy-on-write mechanism. Indexed descriptors use their existing +all-true defaults, and the two initial sloppy descriptors share one invalidation +and shape transition. + +## Measurements + +Measured on 2026-09-11 on Windows x86_64, AMD Ryzen 5 7640HS (12 logical +processors), Node v26.5.1, LLVM 22.1.8, and the Rust nightly 2026-08-20 toolchain. +Both Perry builds report 0.5.1532 and use the same release profile (optimization +level 3, thin LTO, one codegen unit). The baseline is +`603b074ace01464bc66fc07cc8d532f26ccf5a0f`. Timing used the default native +statepoint backend and no concurrent builds or test suites. + +Each cell is the median of three processes' reported medians, in milliseconds +per workload run. The million-call baseline produced no result within the +60-second process budget in any round; its speedup cannot be quantified here. + +| Calls | Node | Before | After | Speedup | +| ---: | ---: | ---: | ---: | ---: | +| 100 | 0.002337 | 0.7342 | 0.2108 | 3.48x | +| 1,000 | 0.02261 | 7.413 | 2.228 | 3.33x | +| 10,000 | 0.2254 | 75.15 | 21.02 | 3.57x | +| 100,000 | 1.940 | 1,697 | 302.5 | 5.61x | +| 1,000,000 | 18.54 | timeout (3/3) | 3,317 | n/a | + +All patched runs and all completed baseline runs matched Node's checksums. +The patched million-call processes took 40.109, 40.984, and 42.046 seconds in +total, including warmup and all seven samples. Their reported medians ranged +from 3,311.6 to 3,507.8 ms per workload run. Perry still has considerable overhead +relative to Node; these results establish the improvement on this Windows host, +not a direct comparison with the issue's original macOS measurements. + +See [results-windows.json](results-windows.json) for every observation, timeouts, +toolchain details, and artifact/source hashes. + +## Semantic validation + +`cargo test --release --locked --lib -p perry-runtime arguments -- --test-threads=1` +passes all 10 selected tests, including the four new construction and moving-GC +regressions. The full runtime library suite passes 3,439 tests, ignores four, +and fails `emergency_full_trace_is_excluded_from_ordinary_pause_stats` on this +Windows host. That unchanged telemetry test expects allocator trimming to be +unsupported, whereas the unchanged mimalloc path reports that it executed; it +also fails in isolation. + +With Test262 pinned at `4249661388e5d3f92a85186213da140a6481490f`, both builds +pass 246 of 261 `language/arguments-object` cases. All 15 remaining failure +paths, buckets, and reasons match exactly. Both builds also pass eight of the +nine repository fixtures selected by `run_parity_tests.sh --filter arguments`; +`test_issue_3580_arguments_object_semantics` produces the same existing mismatch. + +For those conformance runs, set `PYTHONUTF8=1` so Python writes valid UTF-8 +harness files on Windows, and `PERRY_RS4GC=0` for both builds. The default native +statepoint backend currently rejects the Windows exception-handling code used +by most of these tests. The timings above use the default backend, and the new +Rust GC tests independently assert actual evacuation of cached keys, escaping +arguments objects, and indexed heap references. + +Runtime formatting, test registration, file-size, GC store, address-class, +root-holder, rekey, and raw-handle audits pass. `pre-tag-check.sh --quick` reports +two remaining checkout/host limitations: workspace formatting exceeds Windows' +command-length limit, and the public benchmark evidence is stale (also observed +on the baseline). No workspace version or release metadata was changed. diff --git a/benchmarks/issue-10063/function-arguments.ts b/benchmarks/issue-10063/function-arguments.ts new file mode 100644 index 0000000000..c7315a3a02 --- /dev/null +++ b/benchmarks/issue-10063/function-arguments.ts @@ -0,0 +1,94 @@ +// @runtime {"name": "function-arguments", "category": "functions-async", "verification": "checksum", "sources": [{"file": "crates/perry-runtime/src/object/arguments.rs", "function": "js_arguments_object_alloc"}], "hypothesis": "Arguments objects install indexed properties and descriptors individually.", "notes": "", "asynchronous": false, "output_stderr": false, "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 sumArgs(a: number, b: number, c: number): number { + let h = 0; + for (let j = 0; j < arguments.length; j++) h += arguments[j] * (j + 1); + return h; +} +function setup(n: number): number[] { return numbers(n); } +function run(input: number[]): number { + let h = 0; + for (let i = 0; i < input.length; i++) h = (h + sumArgs(input[i], i % 7, i % 11)) % 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.log(JSON.stringify({name: "function-arguments", category: "functions-async", n, + ms_per_run: samples[3], runs, checksum})); +} +benchmarkMain(); diff --git a/benchmarks/issue-10063/measure.py b/benchmarks/issue-10063/measure.py new file mode 100644 index 0000000000..1a1f17d44a --- /dev/null +++ b/benchmarks/issue-10063/measure.py @@ -0,0 +1,66 @@ +"""Compare builds of the unchanged #10063 workload, with serialized 60s runs.""" + +import argparse +import hashlib +import json +import os +from pathlib import Path +import subprocess +import time + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source", type=Path, required=True) + parser.add_argument("--before", type=Path, required=True) + parser.add_argument("--after", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--rounds", type=int, default=3) + args = parser.parse_args() + node = subprocess.check_output(["node", "--version"], text=True).strip() + if node != "v26.5.1": + parser.error(f"use the issue's Node v26.5.1 oracle, found {node}") + if args.rounds < 1: + parser.error("--rounds must be positive") + source = args.source.resolve() + expected = {100: 53207531, 1000: 509027806, 10000: 6382792, + 100000: 66481643, 1000000: 475838285} + commands = {"node": ["node", str(source)], + "before": [str(args.before.resolve())], + "after": [str(args.after.resolve())]} + report = {"node": node, "source_sha256": hashlib.sha256(source.read_bytes()).hexdigest(), + "timeout_seconds": 60, "rows": []} + env = {**os.environ, "TZ": "UTC", "LC_ALL": "en_US.UTF-8"} + failed = False + for repeat in range(args.rounds): + # Alternate the native order to reduce order/thermal bias. Never run + # two benchmark processes concurrently on the measurement host. + engines = ["node", "before", "after"] if repeat % 2 == 0 else ["node", "after", "before"] + for size, checksum in expected.items(): + for engine in engines: + row = {"repeat": repeat, "engine": engine, "n": size} + started = time.monotonic() + try: + process = subprocess.run(commands[engine] + [str(size)], env=env, + capture_output=True, text=True, timeout=60) + row["exit_code"] = process.returncode + if process.returncode: + row.update(status="error", stdout=process.stdout, stderr=process.stderr) + failed = True + else: + result = json.loads(process.stdout) + row.update(result) + row["status"] = "ok" if result["checksum"] == checksum else "checksum-mismatch" + failed |= row["status"] != "ok" + except subprocess.TimeoutExpired: + row["status"] = "timeout" + failed |= engine != "before" + row["wall_seconds"] = time.monotonic() - started + report["rows"].append(row) + args.output.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") + print(json.dumps(row), flush=True) + return int(failed) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/benchmarks/issue-10063/package.json b/benchmarks/issue-10063/package.json new file mode 100644 index 0000000000..729ac4d93b --- /dev/null +++ b/benchmarks/issue-10063/package.json @@ -0,0 +1 @@ +{"type":"commonjs"} diff --git a/benchmarks/issue-10063/results-windows.json b/benchmarks/issue-10063/results-windows.json new file mode 100644 index 0000000000..8d9bb818a4 --- /dev/null +++ b/benchmarks/issue-10063/results-windows.json @@ -0,0 +1,599 @@ +{ + "node": "v26.5.1", + "source_sha256": "f1ae42e13cdf29dec7829aa402c3daf22f093f4a17911dabbc0743c4e105ba73", + "timeout_seconds": 60, + "rows": [ + { + "repeat": 0, + "engine": "node", + "n": 100, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 0.0023365842277180463, + "runs": 58617, + "checksum": 53207531, + "status": "ok", + "wall_seconds": 0.4539999999979045 + }, + { + "repeat": 0, + "engine": "before", + "n": 100, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 0.7341607142857113, + "runs": 193, + "checksum": 53207531, + "status": "ok", + "wall_seconds": 0.48399999999674037 + }, + { + "repeat": 0, + "engine": "after", + "n": 100, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 0.2107642105263189, + "runs": 580, + "checksum": 53207531, + "status": "ok", + "wall_seconds": 0.9839999999967404 + }, + { + "repeat": 0, + "engine": "node", + "n": 1000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 0.022608474576271603, + "runs": 6152, + "checksum": 509027806, + "status": "ok", + "wall_seconds": 0.4220000000059372 + }, + { + "repeat": 0, + "engine": "before", + "n": 1000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 7.413366666666661, + "runs": 20, + "checksum": 509027806, + "status": "ok", + "wall_seconds": 0.5 + }, + { + "repeat": 0, + "engine": "after", + "n": 1000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 2.2280999999999898, + "runs": 58, + "checksum": 509027806, + "status": "ok", + "wall_seconds": 0.7029999999940628 + }, + { + "repeat": 0, + "engine": "node", + "n": 10000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 0.2253595505617973, + "runs": 625, + "checksum": 6382792, + "status": "ok", + "wall_seconds": 0.4220000000059372 + }, + { + "repeat": 0, + "engine": "before", + "n": 10000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 75.1547999999998, + "runs": 7, + "checksum": 6382792, + "status": "ok", + "wall_seconds": 1.6720000000059372 + }, + { + "repeat": 0, + "engine": "after", + "n": 10000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 21.02355, + "runs": 9, + "checksum": 6382792, + "status": "ok", + "wall_seconds": 0.5149999999994179 + }, + { + "repeat": 0, + "engine": "node", + "n": 100000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 1.9385727272727318, + "runs": 75, + "checksum": 66481643, + "status": "ok", + "wall_seconds": 0.45300000000861473 + }, + { + "repeat": 0, + "engine": "before", + "n": 100000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 1697.4748, + "runs": 7, + "checksum": 66481643, + "status": "ok", + "wall_seconds": 21.25 + }, + { + "repeat": 0, + "engine": "after", + "n": 100000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 302.50329999999985, + "runs": 7, + "checksum": 66481643, + "status": "ok", + "wall_seconds": 3.7660000000032596 + }, + { + "repeat": 0, + "engine": "node", + "n": 1000000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 18.407749999999993, + "runs": 14, + "checksum": 475838285, + "status": "ok", + "wall_seconds": 0.5779999999940628 + }, + { + "repeat": 0, + "engine": "before", + "n": 1000000, + "status": "timeout", + "wall_seconds": 60.187999999994645 + }, + { + "repeat": 0, + "engine": "after", + "n": 1000000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 3311.606799999994, + "runs": 7, + "checksum": 475838285, + "status": "ok", + "wall_seconds": 40.10900000001129 + }, + { + "repeat": 1, + "engine": "node", + "n": 100, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 0.002336936726593666, + "runs": 59134, + "checksum": 53207531, + "status": "ok", + "wall_seconds": 0.4529999999940628 + }, + { + "repeat": 1, + "engine": "after", + "n": 100, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 0.20371212121212254, + "runs": 512, + "checksum": 53207531, + "status": "ok", + "wall_seconds": 0.40600000000267755 + }, + { + "repeat": 1, + "engine": "before", + "n": 100, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 0.8483416666666651, + "runs": 160, + "checksum": 53207531, + "status": "ok", + "wall_seconds": 0.4379999999946449 + }, + { + "repeat": 1, + "engine": "node", + "n": 1000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 0.023203828306264468, + "runs": 6012, + "checksum": 509027806, + "status": "ok", + "wall_seconds": 0.45300000000861473 + }, + { + "repeat": 1, + "engine": "after", + "n": 1000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 2.2510200000000053, + "runs": 61, + "checksum": 509027806, + "status": "ok", + "wall_seconds": 0.4379999999946449 + }, + { + "repeat": 1, + "engine": "before", + "n": 1000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 10.192633333333314, + "runs": 17, + "checksum": 509027806, + "status": "ok", + "wall_seconds": 0.46799999999348074 + }, + { + "repeat": 1, + "engine": "node", + "n": 10000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 0.2248449438202187, + "runs": 618, + "checksum": 6382792, + "status": "ok", + "wall_seconds": 0.4380000000091968 + }, + { + "repeat": 1, + "engine": "after", + "n": 10000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 22.617099999999994, + "runs": 7, + "checksum": 6382792, + "status": "ok", + "wall_seconds": 0.4369999999908032 + }, + { + "repeat": 1, + "engine": "before", + "n": 10000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 76.77169999999978, + "runs": 7, + "checksum": 6382792, + "status": "ok", + "wall_seconds": 1.7030000000086147 + }, + { + "repeat": 1, + "engine": "node", + "n": 100000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 1.9399454545454555, + "runs": 76, + "checksum": 66481643, + "status": "ok", + "wall_seconds": 0.4539999999979045 + }, + { + "repeat": 1, + "engine": "after", + "n": 100000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 307.49230000000034, + "runs": 7, + "checksum": 66481643, + "status": "ok", + "wall_seconds": 3.7339999999967404 + }, + { + "repeat": 1, + "engine": "before", + "n": 100000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 1697.548200000001, + "runs": 7, + "checksum": 66481643, + "status": "ok", + "wall_seconds": 21.01600000000326 + }, + { + "repeat": 1, + "engine": "node", + "n": 1000000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 18.61205000000004, + "runs": 13, + "checksum": 475838285, + "status": "ok", + "wall_seconds": 0.5779999999940628 + }, + { + "repeat": 1, + "engine": "after", + "n": 1000000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 3316.5742000000027, + "runs": 7, + "checksum": 475838285, + "status": "ok", + "wall_seconds": 40.98400000001129 + }, + { + "repeat": 1, + "engine": "before", + "n": 1000000, + "status": "timeout", + "wall_seconds": 60.171999999991385 + }, + { + "repeat": 2, + "engine": "node", + "n": 100, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 0.0023856427378966807, + "runs": 59254, + "checksum": 53207531, + "status": "ok", + "wall_seconds": 0.4370000000053551 + }, + { + "repeat": 2, + "engine": "before", + "n": 100, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 0.7285892857142845, + "runs": 196, + "checksum": 53207531, + "status": "ok", + "wall_seconds": 0.4850000000005821 + }, + { + "repeat": 2, + "engine": "after", + "n": 100, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 0.21888260869565226, + "runs": 538, + "checksum": 53207531, + "status": "ok", + "wall_seconds": 0.3899999999994179 + }, + { + "repeat": 2, + "engine": "node", + "n": 1000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 0.02261005649717439, + "runs": 6206, + "checksum": 509027806, + "status": "ok", + "wall_seconds": 0.42199999999138527 + }, + { + "repeat": 2, + "engine": "before", + "n": 1000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 7.192933333333333, + "runs": 22, + "checksum": 509027806, + "status": "ok", + "wall_seconds": 0.5 + }, + { + "repeat": 2, + "engine": "after", + "n": 1000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 2.020549999999986, + "runs": 62, + "checksum": 509027806, + "status": "ok", + "wall_seconds": 0.4210000000020955 + }, + { + "repeat": 2, + "engine": "node", + "n": 10000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 0.22649550561797485, + "runs": 623, + "checksum": 6382792, + "status": "ok", + "wall_seconds": 0.4379999999946449 + }, + { + "repeat": 2, + "engine": "before", + "n": 10000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 72.89239999999995, + "runs": 7, + "checksum": 6382792, + "status": "ok", + "wall_seconds": 1.6720000000059372 + }, + { + "repeat": 2, + "engine": "after", + "n": 10000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 20.576850000000007, + "runs": 9, + "checksum": 6382792, + "status": "ok", + "wall_seconds": 0.5310000000026776 + }, + { + "repeat": 2, + "engine": "node", + "n": 100000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 1.9426909090909135, + "runs": 76, + "checksum": 66481643, + "status": "ok", + "wall_seconds": 0.4529999999940628 + }, + { + "repeat": 2, + "engine": "before", + "n": 100000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 1675.4102000000003, + "runs": 7, + "checksum": 66481643, + "status": "ok", + "wall_seconds": 21.14100000000326 + }, + { + "repeat": 2, + "engine": "after", + "n": 100000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 299.93660000000045, + "runs": 7, + "checksum": 66481643, + "status": "ok", + "wall_seconds": 3.7189999999973224 + }, + { + "repeat": 2, + "engine": "node", + "n": 1000000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 18.538849999999996, + "runs": 13, + "checksum": 475838285, + "status": "ok", + "wall_seconds": 0.5779999999940628 + }, + { + "repeat": 2, + "engine": "before", + "n": 1000000, + "status": "timeout", + "wall_seconds": 60.17200000000594 + }, + { + "repeat": 2, + "engine": "after", + "n": 1000000, + "exit_code": 0, + "name": "function-arguments", + "category": "functions-async", + "ms_per_run": 3507.7536000000036, + "runs": 7, + "checksum": 475838285, + "status": "ok", + "wall_seconds": 42.046000000002095 + } + ], + "environment": { + "date": "2026-09-11", + "os": "Windows x86_64", + "cpu": "AMD Ryzen 5 7640HS, 12 logical processors", + "rust": "rustc 1.100.0-nightly (2026-08-20 toolchain)", + "llvm": "22.1.8", + "profile": "release: opt-level=3, lto=thin, codegen-units=1", + "perry_version": "0.5.1532 (both builds; no version bump)", + "baseline_revision": "603b074ace01464bc66fc07cc8d532f26ccf5a0f", + "gc_backend": "default native statepoints (no PERRY_RS4GC override)", + "auto_optimize": false, + "source_context": "standalone CommonJS, identical bytes for both builds and Node", + "execution": "serialized; no concurrent cargo, rustc, parity tests, or other benchmark processes" + }, + "native_artifact_sha256": { + "perry.exe": "34cb2b63a468675df7b3cd29d35453057cd3045942a869ce794661a5c2b26103", + "perry_runtime.lib": "060fda314587ded050aac1157003bc3f030f2a82224f4710c8ddcf2d75ec0c0e", + "perry_stdlib.lib": "0c9a20efc46f460405c64037550d9cf9e6a139dba4395eca7b91621c7217e245", + "before.exe": "ddfeb77ba94e1b42bc8a07e745d819e1f776687e553398a47bf7c1fb7aa43193", + "after.exe": "781f7ade34ea38f7dd8cc2ac89efe7afe7fc3069106bfa69e3b19297d6c9d16f" + }, + "patched_source_sha256": { + "crates/perry-runtime/src/object/arguments.rs": "2903ad34ef9ca003875e45d899ce1d0a2dd581dbadfa433e8f8a7a5afc74275a", + "crates/perry-runtime/src/object/descriptor_state.rs": "7b3723a0372daaf9f01902d0bd4042cdb78b53d3cdd9f9ba20f9777bf2866e07" + } +} diff --git a/changelog.d/10063-arguments-construction.md b/changelog.d/10063-arguments-construction.md new file mode 100644 index 0000000000..3386edd76c --- /dev/null +++ b/changelog.d/10063-arguments-construction.md @@ -0,0 +1,7 @@ +Reduce arguments-object construction overhead by building indexed fields in bulk, +sharing a bounded cache of immutable key layouts, and omitting redundant default +indexed-property descriptors. Batch the initial sloppy `length` and `callee` +descriptors into one shape transition. Arguments objects retain independent +identity, values, descriptors, and mapped parameter boxes. The key cache is traced +and rewritten by the arguments GC scanner, with regression tests for copy-on-write +keys and reachability through moving collections. Fixes #10063. diff --git a/crates/perry-runtime/src/gc/tests/arguments_objects.rs b/crates/perry-runtime/src/gc/tests/arguments_objects.rs new file mode 100644 index 0000000000..039d0bf9b8 --- /dev/null +++ b/crates/perry-runtime/src/gc/tests/arguments_objects.rs @@ -0,0 +1,160 @@ +//! Arguments construction must preserve per-call state and trace shared keys. + +use super::super::*; +use super::support::{ptr_bits, CopyingNurseryTestGuard, GcTestIsolationGuard}; +use crate::object::*; +use crate::value::JSValue; + +fn key(name: &str) -> *const crate::StringHeader { + crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32) +} + +fn arguments(values: &[f64], callee: f64, restricted: bool) -> *mut ObjectHeader { + let mut array = crate::array::js_array_alloc(values.len() as u32); + for &value in values { + array = crate::array::js_array_push_f64(array, value); + } + js_arguments_object_alloc( + crate::value::js_nanbox_pointer(array as i64), + callee, + restricted as i32, + ) +} + +fn get(obj: *const ObjectHeader, name: &str) -> JSValue { + js_object_get_field_by_name(obj, key(name)) +} + +fn register_scanners() { + gc_register_mutable_root_scanner(scan_arguments_object_roots_mut); + gc_register_mutable_root_scanner(descriptor_state::scan_descriptor_roots_mut); +} + +#[test] +fn arguments_share_keys_but_keep_identity_values_and_descriptors_private() { + let _guard = GcTestIsolationGuard::with_realm_bootstrapped(); + test_clear_arguments_object_roots(); + let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); + let first = arguments(&[1.0, 2.0, 3.0], undefined, false); + let second = arguments(&[4.0, 5.0, 6.0], undefined, false); + assert_ne!(first, second); + unsafe { + assert_eq!(object_keys_array(first), object_keys_array(second)); + } + // All-true indexed attributes are implicit, but reflection still exposes + // the complete default descriptor. This also checks the optimization ran. + assert!(get_property_attrs(first as usize, "0").is_none()); + let descriptor = unsafe { arguments_object_descriptor(first, key("0")) }.unwrap(); + let descriptor = crate::value::js_nanbox_get_pointer(descriptor) as *const ObjectHeader; + assert_eq!(get(descriptor, "value").bits(), 1.0f64.to_bits()); + for name in ["writable", "enumerable", "configurable"] { + assert_eq!(get(descriptor, name).bits(), crate::value::TAG_TRUE); + } + let length_attrs = get_property_attrs(first as usize, "length").unwrap(); + assert!(length_attrs.writable() && !length_attrs.enumerable() && length_attrs.configurable()); + let callee_attrs = get_property_attrs(first as usize, "callee").unwrap(); + assert!(callee_attrs.writable() && !callee_attrs.enumerable() && callee_attrs.configurable()); + assert_eq!(get(first, "length").bits(), 3.0f64.to_bits()); + + js_object_set_field_by_name(first, key("0"), 8.0); + assert_eq!(get(second, "0").bits(), 4.0f64.to_bits()); + set_property_attrs( + first as usize, + "0".into(), + PropertyAttrs::new(false, false, false), + ); + assert!(get_property_attrs(second as usize, "0").is_none()); + assert_eq!(js_object_delete_field(first, key("1")), 1); + js_object_set_field_by_name(first, key("extra"), 9.0); + assert!(get(first, "1").is_undefined()); + assert_eq!(get(second, "1").bits(), 5.0f64.to_bits()); + assert!(get(second, "extra").is_undefined()); + let third = arguments(&[10.0, 11.0, 12.0], undefined, false); + assert_eq!(get(third, "1").bits(), 11.0f64.to_bits()); + assert!(get(third, "extra").is_undefined()); + unsafe { + assert_eq!(object_keys_array(second), object_keys_array(third)); + assert_ne!(object_keys_array(first), object_keys_array(second)); + } +} + +#[test] +fn arguments_bulk_construction_handles_empty_and_uncached_arities() { + let _guard = GcTestIsolationGuard::with_realm_bootstrapped(); + let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); + for len in [0, 1, 3, 64, 65, 100] { + let values: Vec = (0..len).map(|i| i as f64 + 0.5).collect(); + let args = arguments(&values, undefined, true); + assert_eq!(get(args, "length").bits(), (len as f64).to_bits()); + for (i, value) in values.iter().enumerate() { + assert_eq!(get(args, &i.to_string()).bits(), value.to_bits()); + } + assert!(get(args, &len.to_string()).is_undefined()); + let attrs = get_property_attrs(args as usize, "callee").unwrap(); + assert!(!attrs.writable() && !attrs.enumerable() && !attrs.configurable()); + let accessor = get_accessor_descriptor(args as usize, "callee").unwrap(); + assert_eq!(accessor.get, accessor.set); + assert_ne!(accessor.get, 0); + } +} + +#[test] +fn arguments_shared_keys_survive_moving_gc_without_a_live_arguments_owner() { + let _guard = CopyingNurseryTestGuard::new(0); + register_scanners(); + let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); + let before = arguments(&[1.0, 2.0, 3.0], undefined, false); + let before_keys = unsafe { object_keys_array(before) }; + gc_collect_minor(); + let after = arguments(&[4.0, 5.0, 6.0], undefined, false); + let after_keys = unsafe { object_keys_array(after) }; + assert_ne!( + before_keys, after_keys, + "the cached keys must actually evacuate" + ); + assert_eq!(get(after, "0").bits(), 4.0f64.to_bits()); + assert_eq!(get(after, "length").bits(), 3.0f64.to_bits()); +} + +#[test] +fn arguments_values_callee_and_mapping_survive_moving_gc() { + let _guard = CopyingNurseryTestGuard::new(1); + register_scanners(); + let child = js_object_alloc(0, 1); + js_object_set_field(child, 0, JSValue::number(42.0)); + let child_value = crate::value::js_nanbox_pointer(child as i64); + let text = crate::string::js_string_from_bytes(b"arguments payload".as_ptr(), 17); + let text_value = crate::value::js_nanbox_string(text as i64); + let args = arguments(&[child_value, text_value, 7.0], child_value, false); + let mapped = crate::r#box::js_box_alloc(8.0); + js_arguments_object_map_index(args, 2, mapped); + js_shadow_slot_set(0, ptr_bits(args as usize)); + + gc_collect_minor(); + + let moved = (js_shadow_slot_get(0) & POINTER_MASK) as *mut ObjectHeader; + assert_ne!( + moved, args, + "the escaping arguments object must actually evacuate" + ); + assert!(is_arguments_object(moved)); + let moved_child = get(moved, "0").as_pointer::(); + assert_ne!( + moved_child, child, + "indexed heap references must actually evacuate" + ); + assert_eq!( + js_object_get_field(moved_child, 0).bits(), + 42.0f64.to_bits() + ); + assert_eq!(get(moved, "callee").bits(), get(moved, "0").bits()); + let moved_text = get(moved, "1"); + assert!(unsafe { crate::string::js_string_key_matches(moved_text, key("arguments payload")) }); + assert_eq!(get(moved, "2").bits(), 8.0f64.to_bits()); + js_object_set_field_by_name(moved, key("2"), 9.0); + assert_eq!(crate::r#box::js_box_get(mapped), 9.0); + assert_eq!(get(moved, "length").bits(), 3.0f64.to_bits()); + assert!(!get_property_attrs(moved as usize, "length") + .unwrap() + .enumerable()); +} diff --git a/crates/perry-runtime/src/gc/tests/mod.rs b/crates/perry-runtime/src/gc/tests/mod.rs index 4a387d68ff..85f22246c4 100644 --- a/crates/perry-runtime/src/gc/tests/mod.rs +++ b/crates/perry-runtime/src/gc/tests/mod.rs @@ -1,5 +1,6 @@ mod alloc; mod arena_right_size; +mod arguments_objects; mod array_pointer_slot_enumeration; mod barrier; mod barrier_arming; diff --git a/crates/perry-runtime/src/object/arguments.rs b/crates/perry-runtime/src/object/arguments.rs index 58f716b8e6..cf1ad044f6 100644 --- a/crates/perry-runtime/src/object/arguments.rs +++ b/crates/perry-runtime/src/object/arguments.rs @@ -15,6 +15,10 @@ struct ArgumentsMeta { crate::perry_thread_local! { static ARGUMENTS_OBJECTS: RefCell> = RefCell::new(crate::fast_hash::new_ptr_hash_map()); + // Bounded, agent-local cache of immutable ordered keys. Values, descriptors, + // and mapped boxes still belong to each individual arguments object. + static ARGUMENTS_KEYS: RefCell<[*mut ArrayHeader; 65]> = + RefCell::new([std::ptr::null_mut(); 65]); } /// Latched by the one and only registry insert (`js_arguments_object_create`). @@ -45,6 +49,11 @@ fn arguments_registry_never_used() -> bool { } pub fn scan_arguments_object_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + ARGUMENTS_KEYS.with(|cache| { + for keys in cache.borrow_mut().iter_mut() { + visitor.visit_raw_mut_ptr_slot(keys); + } + }); let mut moved = Vec::new(); ARGUMENTS_OBJECTS.with(|m| { let mut map = m.borrow_mut(); @@ -88,6 +97,7 @@ pub(crate) fn prune_dead_arguments_object_entries(is_dead_owner: &dyn Fn(usize) #[cfg(test)] pub(crate) fn test_clear_arguments_object_roots() { ARGUMENTS_OBJECTS.with(|m| m.borrow_mut().clear()); + ARGUMENTS_KEYS.with(|cache| cache.borrow_mut().fill(std::ptr::null_mut())); } #[cfg(test)] @@ -158,81 +168,138 @@ fn thrower_closure_value() -> f64 { crate::value::js_nanbox_pointer(closure as i64) } +/// Build the complete own-key layout once for common arities. Larger calls use +/// the same bulk construction without retaining an unbounded cache of keys. +fn arguments_keys(len: u32) -> *mut ArrayHeader { + if let Some(keys) = ARGUMENTS_KEYS.with(|cache| cache.borrow().get(len as usize).copied()) { + if !keys.is_null() { + return keys; + } + } + let scope = crate::gc::RuntimeHandleScope::new(); + let keys = scope.root_raw_mut_ptr(crate::array::js_array_alloc(len.saturating_add(2))); + for i in 0..len { + let key = intern_key(&i.to_string()); + let array = keys.with_mut_ptr(|array| { + crate::array::js_array_push_f64(array, crate::value::js_nanbox_string(key as i64)) + }); + keys.set_raw_mut_ptr(array); + } + for name in ["length", "callee"] { + let key = intern_key(name); + let array = keys.with_mut_ptr(|array| { + crate::array::js_array_push_f64(array, crate::value::js_nanbox_string(key as i64)) + }); + keys.set_raw_mut_ptr(array); + } + keys.with_mut_ptr(|keys| unsafe { + // Every receiver must copy before adding/deleting keys, including the + // first receiver: later calls can reuse the cached layout after it dies. + let header = crate::value::addr_class::try_read_tracked_gc_header(keys as usize) + .expect("arguments keys have a tracked array header"); + (*header.as_ptr()).gc_flags |= crate::gc::GC_FLAG_SHAPE_SHARED; + ARGUMENTS_KEYS.with(|cache| { + if let Some(slot) = cache.borrow_mut().get_mut(len as usize) { + // GC_STORE_AUDIT(ROOT): the arguments scanner traces and rewrites this slot. + crate::gc::runtime_store_root_raw_mut_ptr_slot(slot, keys); + } + }); + keys + }) +} + #[no_mangle] pub extern "C" fn js_arguments_object_alloc( raw_args: f64, callee: f64, restricted_callee: i32, ) -> *mut ObjectHeader { - let arr_ptr = crate::array::clean_arr_ptr( - crate::value::js_nanbox_get_pointer(raw_args) as *const crate::array::ArrayHeader - ); + let scope = crate::gc::RuntimeHandleScope::new(); + let raw_args = scope.root_nanbox_f64(raw_args); + let callee = scope.root_nanbox_f64(callee); + let arr_ptr = crate::array::clean_arr_ptr(crate::value::js_nanbox_get_pointer( + raw_args.get_nanbox_f64(), + ) as *const ArrayHeader); let len = if arr_ptr.is_null() { 0 } else { crate::array::js_array_length(arr_ptr) }; - let obj = js_object_alloc(0, len.saturating_add(2)); + let keys = scope.root_raw_mut_ptr(arguments_keys(len)); + let obj = scope.root_raw_mut_ptr(js_object_alloc(0, len.saturating_add(2))); + obj.with_mut_ptr(|obj| { + keys.with_mut_ptr(|keys| unsafe { + set_object_keys_array_with_live(obj, keys, len.saturating_add(2)); + }); + }); for i in 0..len { - let name = i.to_string(); - let key = intern_key(&name); - let value = if arr_ptr.is_null() { - f64::from_bits(crate::value::TAG_UNDEFINED) - } else { - crate::array::js_array_get_f64(arr_ptr, i) - }; - js_object_set_field_by_name(obj, key, value); - set_property_attrs(obj as usize, name, PropertyAttrs::new(true, true, true)); - } - - let length_key = intern_key("length"); - js_object_set_field_by_name(obj, length_key, len as f64); - set_property_attrs( - obj as usize, - "length".to_string(), - PropertyAttrs::new(true, false, true), - ); + let arr_ptr = crate::array::clean_arr_ptr(crate::value::js_nanbox_get_pointer( + raw_args.get_nanbox_f64(), + ) as *const ArrayHeader); + let value = crate::array::js_array_get(arr_ptr, i); + // Creation defines own data properties: no inherited setter can + // intercept them. The field helper also maintains the GC slot layout. + obj.with_mut_ptr(|obj| js_object_set_field(obj, i, value)); + } + + // Indexed properties already have the default all-true attributes. Storing + // those defaults individually created a semantic shape transition, several + // descriptor-table entries, and later GC work for every supplied argument. + obj.with_mut_ptr(|obj| { + js_object_set_field(obj, len, JSValue::number(len as f64)); + }); - let callee_key = intern_key("callee"); if restricted_callee != 0 { - js_object_set_field_by_name(obj, callee_key, f64::from_bits(crate::value::TAG_UNDEFINED)); let thrower = thrower_closure_value(); - set_accessor_descriptor( - obj as usize, - "callee".to_string(), - AccessorDescriptor { - get: thrower.to_bits(), - set: thrower.to_bits(), - }, - ); - set_property_attrs( - obj as usize, - "callee".to_string(), - PropertyAttrs::new(false, false, false), - ); + obj.with_mut_ptr::(|obj| { + set_property_attrs( + obj as usize, + "length".to_string(), + PropertyAttrs::new(true, false, true), + ); + super::descriptor_state::install_fresh_accessor_property( + obj as usize, + "callee".to_string(), + AccessorDescriptor { + get: thrower.to_bits(), + set: thrower.to_bits(), + }, + PropertyAttrs::new(false, false, false), + ); + }); } else { - js_object_set_field_by_name(obj, callee_key, callee); - set_property_attrs( - obj as usize, - "callee".to_string(), - PropertyAttrs::new(true, false, true), - ); + obj.with_mut_ptr(|obj| { + js_object_set_field( + obj, + len + 1, + JSValue::from_bits(callee.get_nanbox_f64().to_bits()), + ); + super::descriptor_state::set_property_attrs_batch( + obj as usize, + &[ + ("length", PropertyAttrs::new(true, false, true)), + ("callee", PropertyAttrs::new(true, false, true)), + ], + ); + }); } // Latch BEFORE the insert, so no probe can observe a populated registry // through a `false` flag. - ARGUMENTS_OBJECTS_EVER_USED.store(true, std::sync::atomic::Ordering::Relaxed); - ARGUMENTS_OBJECTS.with(|m| { - m.borrow_mut().insert( - obj as usize, - ArgumentsMeta { - mapped: HashMap::new(), - restricted_callee: restricted_callee != 0, - }, - ); - }); - obj + obj.with_mut_ptr(|obj| { + ARGUMENTS_OBJECTS_EVER_USED.store(true, std::sync::atomic::Ordering::Relaxed); + ARGUMENTS_OBJECTS.with(|m| { + m.borrow_mut().insert( + obj as usize, + ArgumentsMeta { + mapped: HashMap::new(), + restricted_callee: restricted_callee != 0, + }, + ); + }); + obj + }) } #[no_mangle] diff --git a/crates/perry-runtime/src/object/descriptor_state.rs b/crates/perry-runtime/src/object/descriptor_state.rs index 21966ee59e..3f8eaf433b 100644 --- a/crates/perry-runtime/src/object/descriptor_state.rs +++ b/crates/perry-runtime/src/object/descriptor_state.rs @@ -990,6 +990,31 @@ pub(crate) fn set_property_attrs(obj: usize, key: String, attrs: PropertyAttrs) .insert((obj, key), attrs); } +/// Install a group of data descriptors without exposing intermediate states. +/// No JS runs between entries, so one plan invalidation and semantic shape +/// transition retire all prior observations just as repeated installs would. +/// The per-key guard, owner index, and GC bookkeeping still run for every key. +pub(crate) fn set_property_attrs_batch(obj: usize, entries: &[(&str, PropertyAttrs)]) { + if entries.is_empty() { + return; + } + super::prop_plan::prop_plan_epoch_bump(); + note_descriptor_target(obj); + let st = state(); + st.descriptors.property_attrs_in_use.set(true); + GLOBAL_DESCRIPTORS_IN_USE.store(true, Ordering::Relaxed); + for &(key, attrs) in entries { + disable_inline_guards_for_descriptor_target(obj, key); + note_meta_descriptor_key(obj, key, false); + note_young_descriptor_owner(st, obj, None); + owner_index_add(&st.descriptors.attr_keys_by_owner, obj, key); + st.descriptors + .property_descriptors + .borrow_mut() + .insert((obj, key.to_string()), attrs); + } +} + /// Remove a customized property descriptor for (obj, key), restoring default /// data-property attributes for subsequent writes and reflection. pub(crate) fn clear_property_attrs(obj: usize, key: &str) { From ebab2bd591966df75f0e59e755e4db3b1afb662d Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Fri, 11 Sep 2026 17:14:00 +0200 Subject: [PATCH 2/2] chore: associate arguments changelog with PR 10081 --- benchmarks/issue-10063/README.md | 2 +- ...rguments-construction.md => 10081-arguments-construction.md} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename changelog.d/{10063-arguments-construction.md => 10081-arguments-construction.md} (100%) diff --git a/benchmarks/issue-10063/README.md b/benchmarks/issue-10063/README.md index a50fed1c46..6548958f6e 100644 --- a/benchmarks/issue-10063/README.md +++ b/benchmarks/issue-10063/README.md @@ -120,4 +120,4 @@ Runtime formatting, test registration, file-size, GC store, address-class, root-holder, rekey, and raw-handle audits pass. `pre-tag-check.sh --quick` reports two remaining checkout/host limitations: workspace formatting exceeds Windows' command-length limit, and the public benchmark evidence is stale (also observed -on the baseline). No workspace version or release metadata was changed. +on the baseline). The workspace version, CLAUDE.md, and CHANGELOG.md are unchanged. diff --git a/changelog.d/10063-arguments-construction.md b/changelog.d/10081-arguments-construction.md similarity index 100% rename from changelog.d/10063-arguments-construction.md rename to changelog.d/10081-arguments-construction.md