Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions benchmarks/regexp-construction/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Repeated RegExp construction (#10179)

Run all commands on a build host, with the compiler and runtime/stdlib archives
from the same build. In the issue's Mac lane, wrap every command below in
`GIT_DIR=/tmp/perry-regexp-lane.git ./remote.sh '<command>'`.

`prepare.py` resolves the actual OpenCode v1.18.30 packages under
`$OPENCODE_SRC/node_modules/.bun`: emoji-regex 10.6.0, string-width 7.2.0,
strip-ansi 7.1.2, and get-east-asian-width 1.6.0. Nothing substitutes a shortened
pattern. The current compiler reports 10 modules for this probe.

```sh
export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$PWD/target}"
export RUSTFLAGS="-C force-unwind-tables=yes -C force-frame-pointers=yes"
cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static
export PERRY_RUNTIME_DIR="$CARGO_TARGET_DIR/release"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
python3 benchmarks/regexp-construction/prepare.py /tmp/regexp-probe.ts
"$CARGO_TARGET_DIR/release/perry" compile /tmp/regexp-probe.ts \
-o /tmp/regexp-probe --no-auto-optimize --debug-symbols
/tmp/regexp-probe all 1
/root/claude-opencode/bun/bin/bun /tmp/regexp-probe.ts all 1
PERRY_REGEX_DIAG=1 /tmp/regexp-probe all 1
perf record -q -F 499 -g --call-graph dwarf -o /tmp/regexp.perf \
/tmp/regexp-probe construct 100
perf report --stdio --no-children -g none -i /tmp/regexp.perf \
--percent-limit 1 -F overhead,symbol
```

The first positional argument selects an operation (`construct`, `test-ascii`,
`test-emoji`, `stripAnsi`, `stripAnsi-match`, `ignorable`, `segment`,
`eastAsianWidth`, `stringWidth`, or `all`). The second multiplies iterations.
The emoji test intentionally preserves global `lastIndex` between calls, as
in the issue probe: successful and unsuccessful searches alternate. Compare
identical iteration counts and include the printed checksum. Timing is wall
microseconds per iteration and includes the first operation; larger scales
amortize initial compilation/validation and Bun's warmup.

`measure.py` checks checksums and retains samples, medians, and execution
orders. Native builds run from one fixed pathname/inode; copying happens
outside the timing window. Rotation plus reversal covers all six orders of
before/after/Bun. Use a multiple of six rounds for balanced positions, and
repeat `--case MODE:SCALE` to select individual cases.

```sh
taskset -c 15 python3 benchmarks/regexp-construction/measure.py \
--before /tmp/regexp-probe-before --after /tmp/regexp-probe \
--source /tmp/regexp-probe.ts --bun /root/claude-opencode/bun/bin/bun \
--output /tmp/regexp-probe-results.json --runs 12
```

For existing benchmark regressions, `compare.py` compiles all 12 existing
`benchmarks/app-patterns/kernels/*.ts` with each compiler/archive bundle,
checks outputs against the repository's pinned Node oracle, and alternates
before/after execution order. Each build is copied to the same executable path
outside the timed window. It reports child user CPU as well as wall time;
use identical `--before`/`--after` bundles for an A/A noise control. Both
harnesses also accept `--paired-controls`: two identical-copy labels per build
run in each balanced four-run block (the probe omits Bun in this mode).

```sh
export PATH=/tmp/regexp-oracle/node-v26.5.1-linux-x64/bin:$PATH
python3 benchmarks/regexp-construction/compare.py \
--before /tmp/regexp-baseline-libs --after "$CARGO_TARGET_DIR/release" \
--output /tmp/regexp-app-comparison --runs 12
```

See [current-results.md](current-results.md) for the latest integration comparison,
[final-results.md](final-results.md) for the prior Perex 0.1.4 measurements,
[results.md](results.md) for the original attribution, and
[merged-results.md](merged-results.md) for the earlier integration measurements.
14 changes: 14 additions & 0 deletions benchmarks/regexp-construction/app-samples.json

Large diffs are not rendered by default.

111 changes: 111 additions & 0 deletions benchmarks/regexp-construction/compare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""Interleaved before/after runtime benchmark checks, with the pinned Node oracle.

Each directory must hold a compiler and the static archives from the same build.
Run this on the build host. Timing is child user CPU, avoiding scheduler wait.
"""
import argparse
import json
import os
from pathlib import Path
import resource
import shutil
import statistics
import subprocess
import time


def positive_integer(value):
try:
number = int(value)
except ValueError:
raise argparse.ArgumentTypeError("expected a positive integer") from None
if number < 1:
raise argparse.ArgumentTypeError("expected a positive integer")
return number


parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--before", type=Path, required=True)
parser.add_argument("--after", type=Path)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--runs", type=positive_integer, default=12)
parser.add_argument("--filter", default="")
parser.add_argument("--paired-controls", action="store_true", help="Add two identical-copy labels per build to each round")
args = parser.parse_args()
if args.paired_controls and args.after is None:
parser.error('--paired-controls requires --after')
args.output.mkdir(parents=True, exist_ok=True)
runner = args.output / "run-benchmark"
root = Path(__file__).resolve().parents[2]
rows = []


def run(command, **kwargs):
before = resource.getrusage(resource.RUSAGE_CHILDREN).ru_utime
start = time.monotonic()
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
timeout=180, **kwargs)
cpu = resource.getrusage(resource.RUSAGE_CHILDREN).ru_utime - before
return result, cpu, time.monotonic() - start


for source in sorted((root / "benchmarks/app-patterns/kernels").glob("*.ts")):
if args.filter not in source.stem:
continue
row = {"name": source.stem}
binaries = {}
for label, directory in [("before", args.before), ("after", args.after)]:
if directory is None:
continue
binary = args.output / f"{source.stem}-{label}"
environment = dict(os.environ, PERRY_RUNTIME_DIR=str(directory))
result, _, _ = run([str(directory / "perry"), "compile", str(source),
"--no-auto-optimize", "-o", str(binary)], env=environment)
(args.output / f"{source.stem}-{label}.compile.log").write_bytes(result.stderr)
if result.returncode:
row[label + "_error"] = "compile"
else:
binaries[label] = binary
oracle, _, _ = run(["node", "--experimental-strip-types", str(source)])
expected = oracle.stdout.strip()
row["node_exit"] = oracle.returncode
samples = {label: [] for label in binaries}
if args.paired_controls:
for label in ('before', 'after'):
binaries[label + '_control'] = binaries[label]
samples[label + '_control'] = []
for iteration in range(args.runs + 1):
order = list(binaries)
if args.paired_controls:
order = [('before', 'after', 'after_control', 'before_control'),
('after', 'before', 'before_control', 'after_control'),
('before_control', 'after_control', 'after', 'before'),
('after_control', 'before_control', 'before', 'after')][iteration % 4]
elif iteration % 2:
order.reverse()
for label in order:
# argv[0]/execPath length can change startup allocation and GC
# layout. An A/A control showed ~1.7% for byte-identical binaries
# named *-before and *-after. Copy outside the timed window and
# execute both builds at the same pathname/inode.
shutil.copyfile(binaries[label], runner)
runner.chmod(0o755)
result, cpu, wall = run([str(runner)])
if result.returncode or result.stdout.strip() != expected:
row[label + "_error"] = {
"exit": result.returncode,
"stdout": result.stdout.decode(errors="replace")[:500],
"node": expected.decode(errors="replace")[:500],
}
if iteration:
samples[label].append({"cpu_ms": cpu * 1000, "wall_ms": wall * 1000})
for label, values in samples.items():
row[label] = values
row[label + "_median_cpu_ms"] = statistics.median(v["cpu_ms"] for v in values)
rows.append(row)
(args.output / "results.json").write_text(json.dumps(rows, indent=2) + "\n")
print(json.dumps({k: v for k, v in row.items() if k not in samples}), flush=True)

if any(row["node_exit"] != 0 or any(key.endswith("_error") for key in row) for row in rows):
raise SystemExit("A compiler, runtime, or Node oracle failed; see results.json")
Loading
Loading