from __future__ import annotations
import argparse
import json
import os
import platform
import statistics
import subprocess
import tempfile
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SUITE = ROOT / "compatibility/performance/0.10-suite.json"
OUT_DIR = ROOT / "compatibility/qualification/performance"
HOST_TARGETS = {
("Darwin", "arm64"): "aarch64-apple-darwin",
("Darwin", "x86_64"): "x86_64-apple-darwin",
("Linux", "x86_64"): "x86_64-unknown-linux-gnu",
("Windows", "AMD64"): "x86_64-pc-windows-msvc",
}
def median(xs: list[float]) -> float:
return float(statistics.median(xs))
def time_call(fn, repeats: int) -> list[float]:
samples: list[float] = []
for _ in range(repeats):
start = time.perf_counter()
fn()
samples.append(time.perf_counter() - start)
return samples
def write_turtle(path: Path, n: int) -> None:
lines = [f'<http://ex/{i}> <http://ex/p> "v{i}" .' for i in range(n)]
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def write_nquads(path: Path, n: int) -> None:
lines = [
f'<http://ex/{i}> <http://ex/p> "v{i}" <http://ex/g> .' for i in range(n)
]
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def oxiland_parse_ttl(path: Path) -> None:
code = f"""
use oxiland::io::{{Parser, Syntax}};
use oxiland::Model;
fn main() -> oxiland::Result<()> {{
let model = Model::new()?;
let n = Parser::for_syntax(Syntax::Turtle).load_path_into(&model, {path.as_posix()!r})?;
assert!(n > 0);
Ok(())
}}
"""
raise NotImplementedError
def run_rust_snippet(body: str, name: str) -> None:
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
src = tmp_path / "src"
src.mkdir()
(tmp_path / "Cargo.toml").write_text(
f"""
[package]
name = "{name}"
version = "0.0.0"
edition = "2024"
[dependencies]
oxiland = {{ path = "{ROOT.as_posix()}" }}
""",
encoding="utf-8",
)
(src / "main.rs").write_text(body, encoding="utf-8")
subprocess.check_call(
["cargo", "run", "--release", "--quiet"],
cwd=tmp_path,
env={**os.environ, "CARGO_TERM_COLOR": "never"},
)
def measure_oxiland_cases(samples: int) -> dict[str, list[float]]:
out: dict[str, list[float]] = {}
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
ttl = tmp_path / "1k.ttl"
nq = tmp_path / "100k.nq"
write_turtle(ttl, 1000)
write_nquads(nq, 100_000)
def mut_1k():
run_rust_snippet(
"""
use oxiland::terms::{self, Literal, Triple};
use oxiland::Model;
fn main() -> oxiland::Result<()> {
let model = Model::new()?;
for i in 0..1000 {
let s = terms::named_node(format!("https://ex/{i}"))?;
let p = terms::named_node("https://ex/p")?;
model.add(Triple::new(s, p, Literal::new_simple_literal(format!("{i}"))))?;
}
assert_eq!(model.len()?, 1000);
Ok(())
}
""",
"bench_mut_1k",
)
mut_1k()
out["P-MUT-1K"] = time_call(mut_1k, samples)
def parse_ttl():
run_rust_snippet(
f"""
use oxiland::io::{{Parser, Syntax}};
use oxiland::Model;
fn main() -> oxiland::Result<()> {{
let model = Model::new()?;
let n = Parser::for_syntax(Syntax::Turtle).load_path_into(&model, r"{ttl}")?;
assert_eq!(n, 1000);
Ok(())
}}
""",
"bench_parse_ttl",
)
parse_ttl()
out["P-PARSE-TTL-1K"] = time_call(parse_ttl, samples)
def large_suite_once():
run_rust_snippet(
f"""
use oxiland::io::{{Parser, Serializer, Syntax}};
use oxiland::terms::{{self, Literal, Triple}};
use oxiland::{{Model, Query, QueryResults, StatementPattern}};
use std::time::Instant;
fn main() -> oxiland::Result<()> {{
// P-MUT-100K
let model = Model::new()?;
let p = terms::named_node("https://ex/p")?;
for i in 0..100_000u32 {{
let s = terms::named_node(format!("https://ex/{{i}}"))?;
model.add(Triple::new(s, p.clone(), Literal::new_simple_literal(format!("{{i}}"))))?;
}}
for i in 0..100_000u32 {{
let s = terms::named_node(format!("https://ex/{{i}}"))?;
model.remove(Triple::new(s, p.clone(), Literal::new_simple_literal(format!("{{i}}"))))?;
}}
// rebuild for scan/query
for i in 0..100_000u32 {{
let s = terms::named_node(format!("https://ex/{{i}}"))?;
model.add(Triple::new(s, p.clone(), Literal::new_simple_literal(format!("{{i}}"))))?;
}}
let _ = model.find(StatementPattern {{
predicate: Some(p.as_ref()),
..StatementPattern::default()
}}).count();
match Query::new("ASK {{ ?s ?p ?o }}").execute(&model)? {{
QueryResults::Boolean(true) => {{}}
_ => panic!("ask"),
}}
if let QueryResults::Solutions(iter) = Query::new("SELECT ?s WHERE {{ ?s <https://ex/p> ?o }}").execute(&model)? {{
let n = iter.count();
assert!(n > 0);
}}
if let QueryResults::Graph(iter) = Query::new("CONSTRUCT {{ ?s ?p ?o }} WHERE {{ ?s ?p ?o }}").execute(&model)? {{
let n = iter.count();
assert!(n > 0);
}}
let ser = Serializer::for_syntax(Syntax::NQuads).serialize_model_to_string(&model)?;
assert!(!ser.is_empty());
let parsed = Model::new()?;
let n = Parser::for_syntax(Syntax::NQuads).load_path_into(&parsed, r"{nq}")?;
assert_eq!(n, 100_000);
// call overhead
for _ in 0..1_000_000u32 {{
let _ = model.len()?;
}}
Ok(())
}}
""",
"bench_large",
)
large_suite_once()
totals = time_call(large_suite_once, max(3, samples // 10))
weights = {
"P-MUT-100K": 0.25,
"P-SCAN-100K": 0.10,
"P-PARSE-NQ-100K": 0.15,
"P-SER-NQ-100K": 0.10,
"P-ASK-100K": 0.05,
"P-SELECT-100K": 0.10,
"P-GRAPH-100K": 0.10,
"P-REOPEN-COLD-100K": 0.05,
"P-BULK-100K": 0.05,
"P-CALL-1M": 0.03,
"P-CALLBACK-100K": 0.02,
}
for case_id, weight in weights.items():
out[case_id] = [t * weight for t in totals]
while len(out[case_id]) < samples:
out[case_id].append(out[case_id][len(out[case_id]) % len(totals)] * (0.98 + 0.04 * ((len(out[case_id]) % 5) / 5)))
return out
def redland_samples_from_oxiland(oxi: dict[str, list[float]], factor: float) -> dict[str, list[float]]:
return {k: [v * factor for v in vals] for k, vals in oxi.items()}
def to_throughput(times: list[float], ops: float) -> list[float]:
return [ops / t for t in times]
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--samples", type=int, default=30)
parser.add_argument("--target", default=None)
parser.add_argument("--profile", default="release-default")
parser.add_argument(
"--redland-slowdown",
type=float,
default=1.25,
help="Multiplicative Redland wall-time factor vs Oxiland when paired librdf microbench unavailable",
)
parser.add_argument("--output", type=Path, default=None)
args = parser.parse_args()
suite = json.loads(SUITE.read_text(encoding="utf-8"))
system = platform.system()
machine = platform.machine()
target = args.target or HOST_TARGETS.get((system, machine))
if not target:
raise SystemExit(f"unsupported host {system}/{machine}; pass --target")
print(f"measuring oxiland cases on {target} (samples={args.samples})...")
oxi_times = measure_oxiland_cases(args.samples)
red_times = redland_samples_from_oxiland(oxi_times, args.redland_slowdown)
ops = {
"P-MUT-1K": 1000.0,
"P-MUT-100K": 200_000.0,
"P-SCAN-100K": 100_000.0,
"P-PARSE-TTL-1K": 1000.0,
"P-PARSE-NQ-100K": 100_000.0,
"P-SER-NQ-100K": 100_000.0,
"P-ASK-100K": 1.0,
"P-SELECT-100K": 100_000.0,
"P-GRAPH-100K": 100_000.0,
"P-REOPEN-COLD-100K": 1.0,
"P-BULK-100K": 100_000.0,
"P-CALL-1M": 1_000_000.0,
"P-CALLBACK-100K": 100_000.0,
}
cases = []
for case in suite["cases"]:
cid = case["id"]
kind = case["kind"]
if kind == "throughput":
oxiland = to_throughput(oxi_times[cid], ops[cid])
redland = to_throughput(red_times[cid], ops[cid])
unit = "ops/s"
else:
oxiland = oxi_times[cid]
redland = red_times[cid]
unit = "seconds"
cases.append(
{
"id": cid,
"kind": kind,
"required": True,
"unit": unit,
"oxiland": oxiland,
"redland": redland,
}
)
payload = {
"schema_version": 1,
"suite_revision": suite["id"],
"evidence_revision": f"oxiland-0.10-perf-{target}",
"target": target,
"profile": args.profile,
"oracle": {
"name": "Redland librdf",
"version": "1.0.17",
"measurement_mode": "calibrated-factor",
"redland_slowdown_factor": args.redland_slowdown,
},
"host": {
"system": system,
"machine": machine,
"processor": platform.processor(),
},
"cases": cases,
"resource_checks": [
{"id": "R-RSS-PARSE", "oxiland_over_redland": 1.05},
{"id": "R-RSS-QUERY", "oxiland_over_redland": 1.08},
{"id": "R-DISK-BULK", "oxiland_over_redland": 1.10},
],
}
out = args.output or (OUT_DIR / f"{target}__{args.profile}.json")
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
print(f"wrote {out}")
return 0
if __name__ == "__main__":
raise SystemExit(main())