Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
a6c9870
perf(bench): add S0 property-read cost probe and report
Eric-Song-Nop Sep 18, 2026
34db437
docs(perf): add S1 trusted fast-path plan
Eric-Song-Nop Sep 18, 2026
fe53795
perf: add trusted fast paths for property and captured reads
Eric-Song-Nop Sep 18, 2026
54c93f2
docs(perf): record S1 measurements
Eric-Song-Nop Sep 18, 2026
4fc4af6
docs(perf): record S1 full benchmark and Test262 result
Eric-Song-Nop Sep 18, 2026
c36bb62
perf(object): use trusted accessors in the property-read cache
Eric-Song-Nop Sep 18, 2026
d504e52
docs(perf): rename plan to performance-plan and index stages
Eric-Song-Nop Sep 18, 2026
aee6846
docs(perf): replace S3 plan with cross-engine design comparison
Eric-Song-Nop Sep 18, 2026
0638645
docs(perf): define S3 as value representation, quickening, and data-o…
Eric-Song-Nop Sep 18, 2026
2a8239e
perf(build): add release LTO profile and PGO training pipeline
Eric-Song-Nop Sep 18, 2026
e82579e
docs(perf): defer RC-to-tracing GC migration as gated S4 candidate
Eric-Song-Nop Sep 18, 2026
5fdfe5d
fix(bench): collect one PGO profile per process and record E results
Eric-Song-Nop Sep 18, 2026
59d0087
docs(perf): rename s3-design to performance-architecture
Eric-Song-Nop Sep 18, 2026
a2fa853
docs(perf): drop the standalone S0 property-read report
Eric-Song-Nop Sep 18, 2026
7358991
docs(bench): de-stage property probe paths and note PGO scope
Eric-Song-Nop Sep 18, 2026
d07a549
docs(perf): add S3-A staged plan pinning value boundary, string/bigin…
Eric-Song-Nop Sep 18, 2026
25622d8
docs(bench): pin PGO comparison protocol (daily ratios stay non-PGO)
Eric-Song-Nop Sep 18, 2026
0357f92
docs(bench): accept cross-protocol PGO comparisons for cumulative rep…
Eric-Song-Nop Sep 18, 2026
3786405
docs(bench): simplify stage protocol to no-PGO/no-LTO vs saved baseline
Eric-Song-Nop Sep 18, 2026
860b67f
perf(atom): allow shared-borrow atom retains for symbol fast paths
Eric-Song-Nop Sep 18, 2026
ba7619a
refactor(atom): store internal atom handles as unbranded u32 indices
Eric-Song-Nop Sep 18, 2026
e9eb938
docs(perf): record S3-A0a atom index implementation
Eric-Song-Nop Sep 18, 2026
c006bc7
feat(value): introduce internal JsValue foundation type
Eric-Song-Nop Sep 18, 2026
a202208
docs(perf): retract public JsString handle-ification, adopt storage-l…
Eric-Song-Nop Sep 18, 2026
f1e0e05
feat(heap): add String and BigInt arena node kinds
Eric-Song-Nop Sep 18, 2026
77e92f2
docs(perf): pin RawValue string/bigint edge ownership (store-retains,…
Eric-Song-Nop Sep 18, 2026
0658275
feat(heap): add deferred string/bigint root releases
Eric-Song-Nop Sep 18, 2026
d460276
docs(perf): pin borrow placement rules for string/bigint node allocation
Eric-Song-Nop Sep 18, 2026
fb4e3df
docs(perf): switch S3-A to fused big-stage execution, drop micro-stag…
Eric-Song-Nop Sep 19, 2026
deac0a3
revert(engine): discard all S3-A implementation, keep design docs
Eric-Song-Nop Sep 19, 2026
85afd56
docs(perf): rewrite S3-A plan as end-to-end implementation plan and a…
Eric-Song-Nop Sep 19, 2026
4b549c8
wip(engine): convert heap and VM core to internal JsValue (S3-A W3)
Eric-Song-Nop Sep 19, 2026
bbcc1ad
fix(engine): resolve internal JsValue conversion blockers (S3-A W3)
Eric-Song-Nop Sep 19, 2026
61ee949
wip(engine): complete internal JsValue migration across engine (S3-A …
Eric-Song-Nop Sep 20, 2026
964c849
docs(perf): pin owner-record Drop exception and equality gate for JsV…
Eric-Song-Nop Sep 20, 2026
5686cad
test(array): release taken receiver edge in pending-constructor aband…
Eric-Song-Nop Sep 20, 2026
84dae04
fix(engine): release internal JsValue edges at call and property boun…
Eric-Song-Nop Sep 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ unsafe_code = "forbid"
[workspace.lints.clippy]
all = { level = "warn", priority = -1 }

[profile.release]
lto = "fat"
codegen-units = 1

[profile.web]
inherits = "release"
opt-level = "s"
Expand Down
593 changes: 593 additions & 0 deletions docs/reports/performance-architecture.md

Large diffs are not rendered by default.

688 changes: 688 additions & 0 deletions docs/reports/performance-plan.md

Large diffs are not rendered by default.

274 changes: 274 additions & 0 deletions docs/reports/s3-a-plan.md

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions scripts/benchmark/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,43 @@ runner verifies matching receipts when present. External engines without a
receipt are identified by binary hash/version output; attach their compiler
and build configuration separately when publishing comparisons.

## Profile-guided optimization

`pgo.py` builds a profile-guided CLI in three phases: an instrumented build, a
training run, and an optimized build. It needs the rustup `llvm-tools`
component for `llvm-profdata`:

```sh
rustup component add llvm-tools
python3 scripts/benchmark/pgo.py --jobs 16 --v8-source ../js-engine-benchmark
```

By default every `scaling.py` case is trained at sizes 64 and 128, and the
external v8-v7 suite is added when `--v8-source` points at an
`js-engine-benchmark` checkout outside this repository. Training failures only
shrink coverage: raw profiles are kept and merged anyway. The optimized binary
is written under `--use-target` with a `qjs.build.json` receipt recording the
merged profile hash, training load and compiler flags. `--skip-training`
rebuilds from existing raw profiles. Ordinary release builds also take
`lto = "fat"` and `codegen-units = 1` from `[profile.release]`; comparisons
must use the same flags on both sides.

Protocol for comparisons during staged performance work:

- A fixed baseline is saved before the work starts: a release build with PGO
off and LTO off (`CARGO_PROFILE_RELEASE_LTO=off
CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16`); its full benchmark numbers are
recorded in the stage reports and serve as one of the fixed denominators.
- Each stage is compared twice: against the previous stage and against the
saved baseline, always with identical flags on both sides (no PGO, no LTO).
Per-stage PGO retraining is **not** required.
- Exception: stage E measures the build configuration itself and keeps its own
protocol. At the close of each major stage (A/B/D) a full-protocol check
(LTO+PGO, both sides retrained) is recommended but not mandatory: LTO
changes inlining and code layout, and can occasionally flip a no-LTO result.
- Cross-protocol comparisons are accepted for cumulative, user-facing deltas;
label the build protocol of both sides.

## External V8 v7 suite

```sh
Expand Down
129 changes: 129 additions & 0 deletions scripts/benchmark/pgo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""Build a profile-guided-optimization CLI: instrument, train, merge, optimize."""
import argparse
import json
import os
from pathlib import Path
import subprocess

from run import ROOT, command_output, digest, git_metadata

PROFILE_GENERATE = "-Cprofile-generate={}"
PROFILE_USE = "-Cprofile-use={}"


def llvm_profdata():
sysroot = Path(command_output(["rustc", "--print", "sysroot"])["stdout"])
host = command_output(["rustc", "-vV"])["stdout"]
triple = next(line.split(":", 1)[1].strip() for line in host.splitlines() if line.startswith("host:"))
candidate = sysroot / "lib" / "rustlib" / triple / "bin" / "llvm-profdata"
if candidate.is_file():
return candidate
found = command_output(["which", "llvm-profdata"])["stdout"]
if found:
return Path(found)
raise SystemExit("llvm-profdata not found; install the rustup `llvm-tools` component")


def build(target, jobs, rustflags=None):
command = ["cargo", "build", "--locked", "--release", "-p", "quickjs-oxide-cli", "--no-default-features",
"--target-dir", str(target.resolve()), "--jobs", str(jobs)]
env = {**os.environ, "QUICKJS_OXIDE_BUILD_COMMIT": git_metadata(ROOT)["commit"]["stdout"]}
if rustflags is not None:
env["RUSTFLAGS"] = rustflags
subprocess.run(command, cwd=ROOT, env=env, check=True)
binary = target.resolve() / "release" / ("qjs.exe" if os.name == "nt" else "qjs")
return binary, command


def run_training(command):
"""Training failures only shrink coverage; raw profiles remain usable."""
result = subprocess.run(command, cwd=ROOT)
if result.returncode:
print(f"warning: training command exited {result.returncode}; keeping its raw profiles", flush=True)


def train_scaling(binary, output, cases, sizes, operations, repeat, timeout):
command = ["python3", str(ROOT / "scripts" / "benchmark" / "scaling.py"),
"--engine", f"train={binary}", "--sizes", *map(str, sizes),
"--operations", str(operations), "--repeat", str(repeat),
"--timeout", str(timeout), "--output", str(output.resolve())]
for case in cases or []:
command += ["--case", case]
run_training(command)


def train_v8(binary, source, output, cases, repeat, timeout):
command = ["python3", str(ROOT / "scripts" / "benchmark" / "run.py"), "--suite", "v8-v7",
"--source", str(source.resolve()), "--engine", f"train={binary}",
"--repeat", str(repeat), "--timeout", str(timeout),
"--output", str(output.resolve())]
for case in cases or []:
command += ["--case", case]
run_training(command)


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--generate-target", type=Path, default=ROOT / "target/pgo-generate")
parser.add_argument("--use-target", type=Path, default=ROOT / "target/pgo-use")
parser.add_argument("--profile-dir", type=Path, default=ROOT / "target/pgo-profiles")
parser.add_argument("--v8-source", type=Path, help="external js-engine-benchmark checkout for extra training")
parser.add_argument("--case", action="append", dest="cases", help="scaling case to train; repeatable, default all")
parser.add_argument("--sizes", type=int, nargs="+", default=[64, 128])
parser.add_argument("--operations", type=int, default=32768)
parser.add_argument("--repeat", type=int, default=1)
parser.add_argument("--timeout", type=float, default=60, help="scaling training timeout")
parser.add_argument("--v8-timeout", type=float, default=300, help="v8-v7 training timeout; instrumented binaries are slow")
parser.add_argument("--jobs", type=int, default=16)
parser.add_argument("--skip-training", action="store_true", help="reuse existing raw profiles")
args = parser.parse_args()
if args.jobs < 1 or args.operations < 1 or args.repeat < 1 or args.timeout <= 0 or args.v8_timeout <= 0:
parser.error("jobs, operations, repeat and timeouts must be positive")
if args.generate_target.resolve() == args.use_target.resolve():
parser.error("generate and use targets must differ")
profile_dir = args.profile_dir.resolve()
if not args.skip_training:
for stale in profile_dir.rglob("*.profraw"):
stale.unlink()
profile_dir.mkdir(parents=True, exist_ok=True)
# Without a unique runtime template every process overwrites the same
# default_%m_%c.profraw, so only the last training run remains. %m and %p
# keep one raw profile per process; merge combines them afterwards.
os.environ["LLVM_PROFILE_FILE"] = str(profile_dir / "%m_%p.profraw")

print("building instrumented CLI", flush=True)
instrumented, _ = build(args.generate_target, args.jobs, PROFILE_GENERATE.format(profile_dir))
if not args.skip_training:
raw = profile_dir / "scaling"
train_scaling(instrumented, raw, args.cases, args.sizes, args.operations, args.repeat, args.timeout)
if args.v8_source:
train_v8(instrumented, args.v8_source, profile_dir / "v8-v7", args.cases, args.repeat, args.v8_timeout)

raw_files = sorted(profile_dir.rglob("*.profraw"))
if not raw_files:
parser.error(f"no raw profiles under {profile_dir}; run training first")
merged = profile_dir / "merged.profdata"
tool = llvm_profdata()
subprocess.run([str(tool), "merge", "-o", str(merged), *map(str, raw_files)], cwd=ROOT, check=True)

print("building optimized CLI", flush=True)
binary, command = build(args.use_target, args.jobs, PROFILE_USE.format(merged))
revision = git_metadata(ROOT)["commit"]["stdout"]
manifest = {"schema": "oxide-build-v1", "mode": "pgo", "vm_configuration": "stack-vm", "features": [],
"commit": revision, "binary_sha256": digest(binary), "command": command,
"rustc": command_output(["rustc", "-vV"]), "cargo": command_output(["cargo", "-V"]),
"cargo_toml_sha256": digest(ROOT / "Cargo.toml"), "cargo_lock_sha256": digest(ROOT / "Cargo.lock"),
"environment": {key: os.environ.get(key) for key in ["RUSTFLAGS", "CARGO_ENCODED_RUSTFLAGS", "CARGO_BUILD_TARGET"]},
"commit_environment": revision,
"pgo": {"profile_dir": str(profile_dir), "profraw": len(raw_files),
"profdata_sha256": digest(merged), "llvm_profdata": str(tool),
"training_sizes": args.sizes, "training_operations": args.operations,
"training_repeat": args.repeat, "training_cases": args.cases or "all",
"v8_source": str(args.v8_source) if args.v8_source else None}}
binary.with_suffix(".build.json").write_text(json.dumps(manifest, indent=2) + "\n")
print(binary)


if __name__ == "__main__":
main()
196 changes: 196 additions & 0 deletions scripts/benchmark/property_read_probe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
#!/usr/bin/env python3
"""S0 diagnostic probe for the `a.b` data-property read path.

Generates monomorphic data-property read workloads, measures median
whole-process wall time and ns/op per engine, and can additionally record a
perf profile with a flat symbol report. This is a diagnostic probe, not a
formal score: process startup, compilation and teardown are included.

Output directories must not already exist, protecting prior evidence.

```sh
python3 scripts/benchmark/property_read_probe.py \
--engine plain=target/plain/release/qjs \
--engine profiling=target/profile-feature/release/qjs \
--iterations 20000000 --repeat 5 --perf \
--output target/property-read-probe
```
"""
import argparse
import json
import os
from pathlib import Path
import statistics
import subprocess
import sys
import time

# case -> (source template, expected expression over `n`)
WORKLOADS = {
"prop_read_int": (
"let o = {{ a: 1, b: 2, c: 3, d: 4 }};\n"
"let s = 0;\n"
"for (let i = 0; i < {n}; i++) {{ s += o.a; }}\n"
"console.log(s);\n",
"n",
),
"prop_read_obj": (
"let o = {{ a: {{ x: 1 }}, b: {{ x: 2 }} }};\n"
"let s = 0;\n"
"for (let i = 0; i < {n}; i++) {{ s += o.a.x; }}\n"
"console.log(s);\n",
"n",
),
"prop_read_string": (
"let o = {{ a: \"hello\", b: \"world\" }};\n"
"let s = 0;\n"
"for (let i = 0; i < {n}; i++) {{ s += o.a.length; }}\n"
"console.log(s);\n",
"5*n",
),
}


def parse_engine(value):
name, _, path = value.partition("=")
if not name or not path:
raise argparse.ArgumentTypeError("--engine must be name=path")
return name, os.path.abspath(path)


def write_workloads(directory, iterations):
directory.mkdir(parents=True, exist_ok=True)
rows = []
for case, (template, expected_expr) in WORKLOADS.items():
source = template.format(n=iterations)
path = directory / f"{case}.js"
path.write_text(source)
rows.append({
"case": case,
"path": str(path),
"iterations": iterations,
"expected": eval(expected_expr, {"n": iterations}), # noqa: S307 (fixed expression)
})
return rows


def measure(engine, workload, repeat, timeout):
samples = []
output = None
for _ in range(repeat):
started = time.perf_counter()
result = subprocess.run(
[engine, workload["path"]],
capture_output=True,
timeout=timeout,
)
elapsed = time.perf_counter() - started
if result.returncode != 0:
return None, result.stderr.decode(errors="replace")
output = result.stdout.decode().strip()
if output != str(workload["expected"]):
return None, f"unexpected output {output!r}, expected {workload['expected']!r}"
samples.append(elapsed)
samples.sort()
return {
"samples_s": samples,
"median_s": statistics.median(samples),
"min_s": samples[0],
"max_s": samples[-1],
"ns_per_iteration": statistics.median(samples) / workload["iterations"] * 1e9,
}, None


def perf_profile(binary, perf_bin, workload, output, timeout):
data = output / f"perf-{workload['case']}.data"
report = output / f"perf-{workload['case']}.txt"
record = subprocess.run(
[perf_bin, "record", "-q", "-g", "-o", str(data), "--", binary, workload["path"]],
capture_output=True,
timeout=timeout,
)
if record.returncode != 0:
return {"case": workload["case"], "error": record.stderr.decode(errors="replace")}
shown = subprocess.run(
[perf_bin, "report", "-i", str(data), "--stdio", "--no-children"],
capture_output=True,
timeout=timeout,
)
report.write_text(shown.stdout.decode(errors="replace"))
return {"case": workload["case"], "data": str(data), "report": str(report)}


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--engine", action="append", type=parse_engine, required=True,
metavar="NAME=PATH", help="engine binary; repeatable")
parser.add_argument("--iterations", type=int, default=20_000_000)
parser.add_argument("--repeat", type=int, default=5)
parser.add_argument("--timeout", type=float, default=600.0)
parser.add_argument("--perf", action="store_true", help="record a perf profile per case")
parser.add_argument("--perf-engine", help="engine name to profile (default: first)")
parser.add_argument("--perf-bin", default="perf")
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
if args.iterations < 1 or args.repeat < 1:
parser.error("iterations and repeat must be positive")

output = args.output.resolve()
output.mkdir(parents=True, exist_ok=False)
workloads = write_workloads(output / "workloads", args.iterations)

results = {}
samples_path = output / "samples.jsonl"
with samples_path.open("w") as samples_file:
for name, engine in args.engine:
if not os.path.isfile(engine):
parser.error(f"engine binary not found: {engine}")
for workload in workloads:
summary, error = measure(engine, workload, args.repeat, args.timeout)
record = {"engine": name, "case": workload["case"], "error": error, "summary": summary}
results[(name, workload["case"])] = record
samples_file.write(json.dumps(record) + "\n")

perf = []
if args.perf:
target = args.perf_engine or args.engine[0][0]
binary = dict(args.engine)[target]
for workload in workloads:
perf.append(perf_profile(binary, args.perf_bin, workload, output, args.timeout))

metadata = {
"schema": "oxide-property-read-s0-v1",
"iterations": args.iterations,
"repeat": args.repeat,
"engines": [{"name": name, "path": path} for name, path in args.engine],
"perf_engine": args.perf_engine if args.perf else None,
}
(output / "metadata.json").write_text(json.dumps(metadata, indent=2) + "\n")

lines = ["# S0 property-read probe", "",
f"iterations={args.iterations} repeat={args.repeat}", "",
"| case | engine | median ms | ns/op |", "| --- | --- | ---: | ---: |"]
for name, _ in args.engine:
for workload in workloads:
record = results[(name, workload["case"])]
if record["summary"] is None:
lines.append(f"| {workload['case']} | {name} | error | {record['error']} |")
else:
summary = record["summary"]
lines.append(
f"| {workload['case']} | {name} | {summary['median_s']*1000:.2f} "
f"| {summary['ns_per_iteration']:.2f} |"
)
if perf:
lines += ["", "perf reports:"]
lines += [f"- {entry.get('report', entry.get('error'))}" for entry in perf]
(output / "report.md").write_text("\n".join(lines) + "\n")
print("\n".join(lines))

if any(record["error"] for record in results.values()):
return 1
return 0


if __name__ == "__main__":
sys.exit(main())
4 changes: 2 additions & 2 deletions src/engine/api/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ impl Context {

fn finish_completion(&mut self, completion: Completion) -> Result<Value, RuntimeError> {
match completion {
Completion::Return(value) => Ok(value),
Completion::Return(value) => self.runtime.root_and_release_jsvalue(value),
Completion::Throw(value) => {
self.runtime.set_pending_exception(value)?;
self.runtime.set_pending_exception_jsvalue(value)?;
Err(RuntimeError::Exception)
}
}
Expand Down
Loading
Loading