-
-
Notifications
You must be signed in to change notification settings - Fork 161
perf(regex): cache compiled programs and speed up test/replace #10193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
proggeramlug
wants to merge
12
commits into
PerryTS:main
from
proggeramlug:perf/regexp-construction-cache
Closed
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
62bddb2
perf(regex): share compiled programs and reduce repeated call overhead
proggeramlug a33b824
chore: integrate current main regex binding reuse
proggeramlug f900ea7
bench: balance regex probe order and preserve integration evidence
proggeramlug c0f4136
Merge current Perex witness support and remove overlapping binding cache
proggeramlug 3314315
test: account for shared validation in the search-position witness
proggeramlug 2ee54bf
perf(regex): retain bounded scalar scratch hints with program cells
proggeramlug bc9212f
fix(regex): access scratch metadata through its rooted allocation owner
proggeramlug 7be32ca
perf(regex): drop scratch hints after existing-benchmark regression
proggeramlug d555543
bench(regex): preserve final measurements and unresolved performance …
proggeramlug 5b42a59
Merge upstream cross-call positions and GC generation tracking
proggeramlug 1d0a60c
docs(regex): record current-main verification and balanced controls
proggeramlug 15e2a76
Merge origin/main into perf/regexp-construction-cache
proggeramlug File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| 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. | ||
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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") |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.