mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 16:07:24 +00:00
Large semantic dumps retain a JSON object graph with allocated field names
and string node references for every AX node before formatting the output.
Build compact typed nodes with numeric references and pass them directly
from the renderer to the CLI. Keep one AX traversal for both typed nodes
and CDP payloads, converting protocol nodes individually as they are built.
Preserve omitted/empty fields, field insertion order, list markers, and
frame-root attachment order. Render text iteratively and trim in place.
Add compatibility coverage and a standalone benchmark that stages both
executables on the same filesystem, alternates runs, records peak RSS and
wall time, and rejects changes to dump output bytes.
Against main at 5f5e1fff3 on a generated 40,000-row list (three interleaved
runs, medians):
- semantic_tree_text: 718.42 -> 390.96 MiB; 2.919 -> 2.204 s.
- semantic_tree: 815.71 -> 483.81 MiB; 2.484 -> 2.369 s.
- markdown control: 95.52 -> 95.59 MiB; 0.895 -> 0.913 s.
A generated 200,000-row list also completes with identical bytes (one run):
- semantic_tree_text: 3473.02 -> 1775.07 MiB; 13.245 -> 10.015 s.
- semantic_tree: 3803.45 -> 2137.26 MiB; 12.626 -> 10.397 s.
JSON/text comparisons also match the baseline on a mixed fixture containing
hidden/ignored nodes, form values, list markers, shadow DOM, and child frames.
These generated fixtures are independent of the external MOL-269 evidence.
Validation:
- cargo fmt --all
- cargo clippy --workspace --all-targets --all-features -- -D warnings
- cargo nextest run --no-fail-fast (19,885 passed, 17 skipped)
- cargo build --release -p moli
Refs: MOL-269
142 lines
5.5 KiB
Python
142 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Compare semantic dump time/RSS and verify output bytes on a generated list.
|
|
|
|
Build both revisions with `cargo build --release -p moli`, then run:
|
|
python3 moli-benchmark/scripts/semantic-dump-memory.py \
|
|
--before /tmp/moli-before --after target/release/moli \
|
|
--output-dir /tmp/semantic-dump-results
|
|
|
|
Requires Unix wait4; RSS is reported in bytes on both Linux and macOS.
|
|
Both executables are copied into the output directory so executable mappings
|
|
have the same filesystem backing when their RSS is compared.
|
|
"""
|
|
|
|
import argparse
|
|
import functools
|
|
import hashlib
|
|
import http.server
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
import time
|
|
|
|
|
|
class QuietHandler(http.server.SimpleHTTPRequestHandler):
|
|
def log_message(self, *_args):
|
|
pass
|
|
|
|
|
|
def positive_int(value):
|
|
result = int(value)
|
|
if result <= 0:
|
|
raise argparse.ArgumentTypeError("must be positive")
|
|
return result
|
|
|
|
|
|
def measure(binary, label, fmt, repeat, rows, url, output_dir, env):
|
|
output = output_dir / f"{label}-{fmt}.out"
|
|
errors = output_dir / f"{label}-{fmt}.stderr"
|
|
command = [
|
|
str(binary), "fetch", "--timeout", "120000", "--dump", fmt, url
|
|
]
|
|
started = time.monotonic()
|
|
with output.open("wb") as out, errors.open("wb") as err:
|
|
with subprocess.Popen(command, stdout=out, stderr=err, env=env) as process:
|
|
_, status, usage = os.wait4(process.pid, 0)
|
|
process.returncode = os.waitstatus_to_exitcode(status)
|
|
wall_seconds = time.monotonic() - started
|
|
with output.open("rb") as stream:
|
|
digest = hashlib.file_digest(stream, "sha256").hexdigest()
|
|
record = {
|
|
"binary": label,
|
|
"executable": str(binary),
|
|
"rows": rows,
|
|
"format": fmt,
|
|
"repeat": repeat,
|
|
"returncode": process.returncode,
|
|
"maxrss_bytes": usage.ru_maxrss * (1 if sys.platform == "darwin" else 1024),
|
|
"wall_seconds": wall_seconds,
|
|
"output_bytes": output.stat().st_size,
|
|
"sha256": digest,
|
|
}
|
|
with (output_dir / "results.jsonl").open("a") as results:
|
|
results.write(json.dumps(record) + "\n")
|
|
print(json.dumps(record), flush=True)
|
|
if process.returncode:
|
|
raise RuntimeError(f"{label} {fmt} failed; see {errors}")
|
|
return record["output_bytes"], digest
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--before", required=True, type=Path)
|
|
parser.add_argument("--after", required=True, type=Path)
|
|
parser.add_argument("--output-dir", required=True, type=Path)
|
|
parser.add_argument("--rows", type=positive_int, default=40000)
|
|
parser.add_argument("--runs", type=positive_int, default=3)
|
|
args = parser.parse_args()
|
|
binaries = [("before", args.before.resolve()), ("after", args.after.resolve())]
|
|
for label, binary in binaries:
|
|
if not os.access(binary, os.X_OK):
|
|
parser.error(f"{label} binary is not executable: {binary}")
|
|
args.output_dir.mkdir(parents=True, exist_ok=True)
|
|
staged_binaries = []
|
|
manifest = {"rows": args.rows, "runs": args.runs, "binaries": {}}
|
|
for label, binary in binaries:
|
|
staged = (args.output_dir / f"moli-{label}").resolve()
|
|
if binary != staged:
|
|
shutil.copy2(binary, staged)
|
|
with staged.open("rb") as stream:
|
|
digest = hashlib.file_digest(stream, "sha256").hexdigest()
|
|
manifest["binaries"][label] = {
|
|
"source": str(binary), "staged": str(staged), "sha256": digest
|
|
}
|
|
staged_binaries.append((label, staged))
|
|
binaries = staged_binaries
|
|
(args.output_dir / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
|
|
fixture = args.output_dir / "large-list.html"
|
|
with fixture.open("w") as page:
|
|
page.write("<!doctype html><html><head><title>Large list</title></head><body><main>\n")
|
|
for index in range(args.rows):
|
|
page.write(
|
|
f'<div class="row"><a href="/item/{index}">Item {index}</a>'
|
|
f'<span> description {index}</span></div>\n'
|
|
)
|
|
page.write("</main></body></html>\n")
|
|
handler = functools.partial(QuietHandler, directory=str(args.output_dir.resolve()))
|
|
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
url = f"http://127.0.0.1:{server.server_port}/{fixture.name}"
|
|
env = {
|
|
key: value for key, value in os.environ.items()
|
|
if key.lower() not in ("http_proxy", "https_proxy", "all_proxy")
|
|
}
|
|
try:
|
|
for fmt in ("semantic_tree_text", "semantic_tree", "markdown"):
|
|
expected = None
|
|
for repeat in range(args.runs):
|
|
# Alternate the order to reduce warm-cache/order bias.
|
|
order = binaries if repeat % 2 == 0 else list(reversed(binaries))
|
|
for label, binary in order:
|
|
actual = measure(
|
|
binary, label, fmt, repeat, args.rows, url, args.output_dir, env
|
|
)
|
|
if expected is None:
|
|
expected = actual
|
|
elif actual != expected:
|
|
raise RuntimeError(f"{fmt} output differs: {label}, run {repeat}")
|
|
print("All dump outputs are byte-for-byte identical.", flush=True)
|
|
finally:
|
|
server.shutdown()
|
|
thread.join()
|
|
server.server_close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|