From b80b37deba101e5bd65d3b7db327429b9d2ca3dd Mon Sep 17 00:00:00 2001 From: Haobin Ni Date: Thu, 9 Apr 2026 14:10:38 -0700 Subject: [PATCH 01/31] Save --- src/poach.rs | 63 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/src/poach.rs b/src/poach.rs index 2345986b9..5e153eb07 100644 --- a/src/poach.rs +++ b/src/poach.rs @@ -1,7 +1,8 @@ -use std::path::PathBuf; +use poach::EGraph; -use clap::{Args, Parser, Subcommand}; +use std::{fs::File, path::PathBuf}; +use clap::{Args, Parser, Subcommand}; #[derive(Debug, Parser)] #[command(version, about)] @@ -106,23 +107,67 @@ pub fn poach () { fine_tune(arg); } Commands::Test(arg) => { - println!("test({:?})", arg); + eprintln!("test({:?})", arg); + //TODO: run vanilla egglog tests } } // TODO handle report IO } +/// VanillaEgglog's model is just unit +/// Still, it would create an empty file fn train(arg : TrainArgs) { - println!("train({:?})", arg); - //TODO + let _ = File::create(arg.output_model_file.as_path()); } +/// VanillaEgglog fn serve(arg: ServeArgs) { - println!("serve({:?})", arg); - //TODO + match arg.serve_command { + None => { + let mut egraph = EGraph::default(); + + rayon::ThreadPoolBuilder::new() + .num_threads(1) + .build_global() + .unwrap(); + egraph.repl(poach::RunMode::Normal); +/* + } else { + for input in &args.inputs { + let program = std::fs::read_to_string(input).unwrap_or_else(|_| { + let arg = input.to_string_lossy(); + panic!("Failed to read file {arg}") + }); + + match run_commands( + &mut egraph, + Some(input.to_str().unwrap().into()), + &program, + io::stdout(), + args.mode, + ) { + Ok(None) => {} + _ => std::process::exit(1), + } +*/ + } + Some(cmd) => { + match cmd { + ServeCommands::Single{input_file: _} => { + //TODO + panic!("Single not implemented"); + } + ServeCommands::Batch{input_dir:_, output_dir:_} => { + //TODO + panic!("Batch not implemented"); + } + } + } + } } +/// VanillaEgglog's model is just unit +/// Still, it would create an empty file fn fine_tune(arg: FineTuneArgs) { - println!("fine_tune({:?})", arg); - //TODO + let _ = File::create(arg.output_model_file.as_path()); } From 0a49cf929fe8bf7f2b1c61c398670983363c782d Mon Sep 17 00:00:00 2001 From: Haobin Ni Date: Thu, 9 Apr 2026 14:26:08 -0700 Subject: [PATCH 02/31] VanillaEgglog --- src/poach.rs | 63 +++++++++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/src/poach.rs b/src/poach.rs index 5e153eb07..bdb9a44ab 100644 --- a/src/poach.rs +++ b/src/poach.rs @@ -1,6 +1,6 @@ -use poach::EGraph; +use poach::{EGraph}; -use std::{fs::File, path::PathBuf}; +use std::{fs::File, path::PathBuf, process::exit}; use clap::{Args, Parser, Subcommand}; @@ -126,36 +126,43 @@ fn serve(arg: ServeArgs) { None => { let mut egraph = EGraph::default(); - rayon::ThreadPoolBuilder::new() - .num_threads(1) - .build_global() - .unwrap(); - egraph.repl(poach::RunMode::Normal); -/* - } else { - for input in &args.inputs { - let program = std::fs::read_to_string(input).unwrap_or_else(|_| { - let arg = input.to_string_lossy(); - panic!("Failed to read file {arg}") - }); - - match run_commands( - &mut egraph, - Some(input.to_str().unwrap().into()), - &program, - io::stdout(), - args.mode, - ) { - Ok(None) => {} - _ => std::process::exit(1), + rayon::ThreadPoolBuilder::new() + .num_threads(1) + .build_global() + .unwrap(); + + match egraph.repl(poach::RunMode::Normal) { + Ok(_) => {} + _ => { + exit(-1); + } } -*/ } Some(cmd) => { match cmd { - ServeCommands::Single{input_file: _} => { - //TODO - panic!("Single not implemented"); + ServeCommands::Single{input_file: input} => { + let mut egraph = EGraph::default(); + + rayon::ThreadPoolBuilder::new() + .num_threads(1) + .build_global() + .unwrap(); + + let program = std::fs::read_to_string(input.as_path()).unwrap_or_else(|_| { + let arg = input.to_string_lossy(); + panic!("Failed to read file {arg}") + }); + + match egraph.parse_and_run_program(Some(input.to_str().unwrap().into()), &program) { + Ok(msgs) => { + for msg in msgs { + print!("{msg}"); + } + } + _ => { + exit(-1); + } + } } ServeCommands::Batch{input_dir:_, output_dir:_} => { //TODO From 8a9de86f7a6a399bb0418e5eba655fe7e9e07e6c Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Tue, 28 Apr 2026 14:41:09 -0700 Subject: [PATCH 03/31] fix build error --- src/poach.rs | 64 ++++++++++++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/src/poach.rs b/src/poach.rs index 55e5b79a1..8d2d986a2 100644 --- a/src/poach.rs +++ b/src/poach.rs @@ -122,14 +122,14 @@ pub fn poach() { /// VanillaEgglog's model is just unit /// Still, it would create an empty file -fn train(arg : TrainArgs) { +fn train(arg: TrainArgs) { let _ = File::create(arg.output_model_file.as_path()); } /// VanillaEgglog fn serve(arg: ServeArgs) { - match arg.serve_command { - None => { + match arg.mode { + ServeMode::Streaming => { let mut egraph = EGraph::default(); rayon::ThreadPoolBuilder::new() @@ -144,43 +144,37 @@ fn serve(arg: ServeArgs) { } } } - Some(cmd) => { - match cmd { - ServeCommands::Single { input_file: input } => { - let mut egraph = EGraph::default(); - - rayon::ThreadPoolBuilder::new() - .num_threads(1) - .build_global() - .unwrap(); - - let program = std::fs::read_to_string(input.as_path()).unwrap_or_else(|_| { - let arg = input.to_string_lossy(); - panic!("Failed to read file {arg}") - }); - - match egraph - .parse_and_run_program(Some(input.to_str().unwrap().into()), &program) - { - Ok(msgs) => { - for msg in msgs { - print!("{msg}"); - } - } - _ => { - exit(-1); - } + ServeMode::Single { input_file: input } => { + let mut egraph = EGraph::default(); + + rayon::ThreadPoolBuilder::new() + .num_threads(1) + .build_global() + .unwrap(); + + let program = std::fs::read_to_string(input.as_path()).unwrap_or_else(|_| { + let arg = input.to_string_lossy(); + panic!("Failed to read file {arg}") + }); + + match egraph.parse_and_run_program(Some(input.to_str().unwrap().into()), &program) { + Ok(msgs) => { + for msg in msgs { + print!("{msg}"); } } - ServeCommands::Batch { - input_dir: _, - output_dir: _, - } => { - //TODO - panic!("Batch not implemented"); + _ => { + exit(-1); } } } + ServeMode::Batch { + input_dir: _, + output_dir: _, + } => { + //TODO + panic!("Batch not implemented"); + } } } From 2b4cb7d253b57015e196b9642e02c7f77553371f Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Tue, 28 Apr 2026 15:36:48 -0700 Subject: [PATCH 04/31] nightly --- infra/nightly-resources/web/index.html | 24 ++++++++-- infra/nightly-resources/web/index.js | 61 +++++++++++++++++++++++--- infra/nightly.py | 57 +++++++++++++++++------- src/poach.rs | 28 ++++++++++-- tests/{passing => skipped}/include.egg | 0 5 files changed, 142 insertions(+), 28 deletions(-) rename tests/{passing => skipped}/include.egg (100%) diff --git a/infra/nightly-resources/web/index.html b/infra/nightly-resources/web/index.html index 2a56dbf46..789622937 100644 --- a/infra/nightly-resources/web/index.html +++ b/infra/nightly-resources/web/index.html @@ -11,13 +11,29 @@

Poach Nightly

Loading data/data.json...

- -
- +
+

Summary

+

+
+
+

Reports

+ + + + + + + + + + + + +
PathWall TimeRule RunningExtractionOther# Timing Steps
+
- diff --git a/infra/nightly-resources/web/index.js b/infra/nightly-resources/web/index.js index c04090882..51030c86d 100644 --- a/infra/nightly-resources/web/index.js +++ b/infra/nightly-resources/web/index.js @@ -13,17 +13,66 @@ async function load() { const data = await response.json(); statusNode.textContent = "Loaded data/data.json"; - renderSummary(data.summary); - renderBenchmarks(data); + renderSummary(data); + renderBenchmarks(data.reports); } catch (error) { statusNode.textContent = `Failed to load data/data.json: ${error}`; } } -function renderSummary(summary) { - // TODO +function renderSummary(data) { + let ruleRunningMillis = 0; + let extractionMillis = 0; + let otherMillis = 0; + + for (const { report } of data.reports) { + const totals = getTimingTotals(report); + ruleRunningMillis += totals.ruleRunningMillis; + extractionMillis += totals.extractionMillis; + otherMillis += totals.otherMillis; + } + + document.querySelector("#summary-text").textContent = + `${data.reports.length} benchmarks | ` + + `Nightly time: ${data.summary.total_time_seconds.toFixed(1)} s | ` + + `Rule running: ${ruleRunningMillis} ms | ` + + `Extraction: ${extractionMillis} ms | ` + + `Other: ${otherMillis} ms`; +} + +function renderBenchmarks(reports) { + document.querySelector("#benchmarks-body").innerHTML = reports + .map(({ path, time_seconds, report }) => { + const totals = getTimingTotals(report); + + return ` + + ${path} + ${time_seconds.toFixed(3)} s + ${formatMillis(totals.ruleRunningMillis)} + ${formatMillis(totals.extractionMillis)} + ${formatMillis(totals.otherMillis)} + ${report.timings.length} + + `; + }) + .join(""); } -function renderBenchmarks(data) { - // TODO +function getTimingTotals(report) { + let ruleRunningMillis = 0; + let extractionMillis = 0; + let otherMillis = 0; + + for (const timing of report.timings) { + if (timing.tags.includes("running_rules")) { + ruleRunningMillis += timing.total; + } else if (timing.tags.includes("extraction")) { + extractionMillis += timing.total; + } else { + otherMillis += timing.total; + } + } + + return { ruleRunningMillis, extractionMillis, otherMillis }; } diff --git a/infra/nightly.py b/infra/nightly.py index 85ad4a1b9..a118a4678 100644 --- a/infra/nightly.py +++ b/infra/nightly.py @@ -19,6 +19,7 @@ REPORT_OUTPUT_DIR = DATA_DIR / "reports" DATA_JSON_PATH = DATA_DIR / "data.json" POACH_BIN = REPO_ROOT / "target" / "release" / "poach" +MODEL_FILE = REPO_ROOT / "empty.model" def main() -> None: @@ -27,7 +28,7 @@ def main() -> None: benchmark_dir = (REPO_ROOT / sys.argv[1]).resolve() - benchmark_files = sorted(benchmark_dir.rglob("*.egg")) + benchmark_files = list(benchmark_dir.rglob("*.egg")) if not benchmark_files: raise SystemExit( f"No .egg benchmark files found under {benchmark_dir}." @@ -40,10 +41,16 @@ def main() -> None: DATA_JSON_PATH.write_text(json.dumps(data, indent=2), encoding="utf-8") print(f"Wrote {DATA_JSON_PATH}") -def run_command(command: list[str], *, cwd: Path) -> dict[str, Any]: +def run_command(command: list[str], *, cwd: Path, report_path: Path) -> dict[str, Any]: started = time.perf_counter() try: - completed = subprocess.run(command, check=True, cwd=cwd) + completed = subprocess.run( + command, + check=True, + cwd=cwd, + capture_output=True, + text=True, + ) except subprocess.CalledProcessError as err: time_seconds = time.perf_counter() - started print( @@ -52,11 +59,13 @@ def run_command(command: list[str], *, cwd: Path) -> dict[str, Any]: ) raise err - # TODO: collect stderr and stdout from completed + report_path.parent.mkdir(parents=True, exist_ok=True) + report_path.write_text(completed.stderr, encoding="utf-8") return { "argv": command, "cwd": str(cwd), + "report_path": str(report_path), "returncode": completed.returncode, "time_seconds": time.perf_counter() - started, } @@ -67,34 +76,52 @@ def run_benchmarks(benchmark_dir: Path) -> list[dict[str, Any]]: shutil.rmtree(REPORT_OUTPUT_DIR) REPORT_OUTPUT_DIR.mkdir(parents=True, exist_ok=True) - # TODO: poach serve streaming is the default mode - - command = [ - str(POACH_BIN), # poach binary - # fill in other args - str(benchmark_dir), # input files - ] - print("Running benchmarks:", " ".join(command)) - return [run_command(command, cwd=REPO_ROOT)] + results = [] + for benchmark_file in benchmark_dir.rglob("*.egg"): + relative_benchmark = benchmark_file.relative_to(benchmark_dir) + report_path = REPORT_OUTPUT_DIR / relative_benchmark.with_suffix(".report.json") + command = [ + str(POACH_BIN), + "serve", + "--debug", + str(MODEL_FILE), + "single", + str(benchmark_file), + ] + print("Running benchmark:", " ".join(command)) + results.append(run_command(command, cwd=REPO_ROOT, report_path=report_path)) + return results def aggregate_reports( benchmark_dir: Path, command_results: list[dict[str, Any]] ) -> dict[str, Any]: - report_files = sorted(REPORT_OUTPUT_DIR.rglob("*.report.json")) + report_files = list(REPORT_OUTPUT_DIR.rglob("*.report.json")) if not report_files: raise SystemExit(f"No report files were generated under {REPORT_OUTPUT_DIR}") return { "generated_at": datetime.now(timezone.utc).isoformat(), "suite": benchmark_dir.name, - "benchmark_root": str(benchmark_dir.relative_to(REPO_ROOT)), + "benchmark_root": ( + str(benchmark_dir.relative_to(REPO_ROOT)) + if benchmark_dir.is_absolute() + else str(benchmark_dir) + ), "summary": { "commands": command_results, "total_time_seconds": sum( result["time_seconds"] for result in command_results ), }, + "reports": [ + { + "path": str(Path(result["report_path"]).relative_to(OUTPUT_DIR)), + "time_seconds": result["time_seconds"], + "report": json.loads(Path(result["report_path"]).read_text(encoding="utf-8")), + } + for result in command_results + ], } diff --git a/src/poach.rs b/src/poach.rs index 8d2d986a2..a53a5f20f 100644 --- a/src/poach.rs +++ b/src/poach.rs @@ -1,6 +1,6 @@ -use poach::EGraph; +use poach::{EGraph, report::Reporter}; -use std::{fs::File, path::PathBuf, process::exit}; +use std::{fs::File, io::Write, path::PathBuf, process::exit}; use clap::{Args, Parser, Subcommand}; @@ -157,7 +157,29 @@ fn serve(arg: ServeArgs) { panic!("Failed to read file {arg}") }); - match egraph.parse_and_run_program(Some(input.to_str().unwrap().into()), &program) { + let result: Result<_, poach::Error> = if arg.debug { + let filename = Some(input.to_str().unwrap().into()); + match egraph.parser.get_program_from_string(filename, &program) { + Ok(parsed) => { + let mut reporter = Reporter::new(); + let result = egraph.run_program_with_reporter(parsed, &mut reporter); + if result.is_ok() { + let mut stderr = std::io::stderr().lock(); + serde_json::to_writer(&mut stderr, &reporter.build_report()) + .expect("failed to serialize debug report"); + writeln!(stderr).expect("failed to terminate debug report"); + } + result + } + Err(err) => Err(err.into()), + } + } else { + egraph + .parse_and_run_program(Some(input.to_str().unwrap().into()), &program) + .map_err(Into::into) + }; + + match result { Ok(msgs) => { for msg in msgs { print!("{msg}"); diff --git a/tests/passing/include.egg b/tests/skipped/include.egg similarity index 100% rename from tests/passing/include.egg rename to tests/skipped/include.egg From 43acac4dabf330752a450e3e02c32a9c5ff54298 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Tue, 28 Apr 2026 17:55:30 -0700 Subject: [PATCH 05/31] smaller data.json --- infra/nightly-resources/web/index.js | 39 +++++++--------------------- infra/nightly.py | 28 ++++++++++++++++++-- src/report.rs | 20 ++++++++++++-- 3 files changed, 53 insertions(+), 34 deletions(-) diff --git a/infra/nightly-resources/web/index.js b/infra/nightly-resources/web/index.js index 51030c86d..f664a725a 100644 --- a/infra/nightly-resources/web/index.js +++ b/infra/nightly-resources/web/index.js @@ -25,11 +25,10 @@ function renderSummary(data) { let extractionMillis = 0; let otherMillis = 0; - for (const { report } of data.reports) { - const totals = getTimingTotals(report); - ruleRunningMillis += totals.ruleRunningMillis; - extractionMillis += totals.extractionMillis; - otherMillis += totals.otherMillis; + for (const { timing_summary } of data.reports) { + ruleRunningMillis += timing_summary.rule_running_millis; + extractionMillis += timing_summary.extraction_millis; + otherMillis += timing_summary.other_millis; } document.querySelector("#summary-text").textContent = @@ -42,37 +41,17 @@ function renderSummary(data) { function renderBenchmarks(reports) { document.querySelector("#benchmarks-body").innerHTML = reports - .map(({ path, time_seconds, report }) => { - const totals = getTimingTotals(report); - + .map(({ path, time_seconds, timing_summary }) => { return ` ${path} ${time_seconds.toFixed(3)} s - ${formatMillis(totals.ruleRunningMillis)} - ${formatMillis(totals.extractionMillis)} - ${formatMillis(totals.otherMillis)} - ${report.timings.length} + ${formatMillis(timing_summary.rule_running_millis)} + ${formatMillis(timing_summary.extraction_millis)} + ${formatMillis(timing_summary.other_millis)} + ${timing_summary.timing_steps} `; }) .join(""); } - -function getTimingTotals(report) { - let ruleRunningMillis = 0; - let extractionMillis = 0; - let otherMillis = 0; - - for (const timing of report.timings) { - if (timing.tags.includes("running_rules")) { - ruleRunningMillis += timing.total; - } else if (timing.tags.includes("extraction")) { - extractionMillis += timing.total; - } else { - otherMillis += timing.total; - } - } - - return { ruleRunningMillis, extractionMillis, otherMillis }; -} diff --git a/infra/nightly.py b/infra/nightly.py index a118a4678..3e7e31572 100644 --- a/infra/nightly.py +++ b/infra/nightly.py @@ -93,6 +93,27 @@ def run_benchmarks(benchmark_dir: Path) -> list[dict[str, Any]]: return results +def summarize_report(report: dict[str, Any]) -> dict[str, int]: + rule_running_millis = 0 + extraction_millis = 0 + other_millis = 0 + + for timing in report["timings"]: + if "running_rules" in timing["tags"]: + rule_running_millis += timing["total"] + elif "extraction" in timing["tags"]: + extraction_millis += timing["total"] + else: + other_millis += timing["total"] + + return { + "rule_running_millis": rule_running_millis, + "extraction_millis": extraction_millis, + "other_millis": other_millis, + "timing_steps": len(report["timings"]), + } + + def aggregate_reports( benchmark_dir: Path, command_results: list[dict[str, Any]] ) -> dict[str, Any]: @@ -109,7 +130,6 @@ def aggregate_reports( else str(benchmark_dir) ), "summary": { - "commands": command_results, "total_time_seconds": sum( result["time_seconds"] for result in command_results ), @@ -118,7 +138,11 @@ def aggregate_reports( { "path": str(Path(result["report_path"]).relative_to(OUTPUT_DIR)), "time_seconds": result["time_seconds"], - "report": json.loads(Path(result["report_path"]).read_text(encoding="utf-8")), + "timing_summary": summarize_report( + json.loads( + Path(result["report_path"]).read_text(encoding="utf-8") + ) + ), } for result in command_results ], diff --git a/src/report.rs b/src/report.rs index 3bf7c3b39..0d85dc6fd 100644 --- a/src/report.rs +++ b/src/report.rs @@ -1,7 +1,7 @@ use std::fmt::{self, Display, Formatter}; use std::time::{Duration, Instant}; -use serde::Serialize; +use serde::{Serialize, Serializer}; type TimerHandle = usize; @@ -30,6 +30,7 @@ struct TimingStep { tags: Vec, #[serde(with = "serde_millis")] total: Duration, + #[serde(serialize_with = "serialize_duration_breakdown")] breakdown: Vec, } @@ -109,11 +110,26 @@ impl Display for TimingStep { writeln!(f, " name: {}", self.name)?; writeln!(f, " tags: {:?}", self.tags)?; writeln!(f, " total: {}", self.total.as_secs_f64())?; - writeln!(f, " breakdown: {:?}", self.breakdown)?; + writeln!( + f, + " breakdown: {:?}", + self.breakdown + .iter() + .map(Duration::as_millis) + .collect::>() + )?; Ok(()) } } +fn serialize_duration_breakdown(breakdown: &[Duration], serializer: S) -> Result +where + S: Serializer, +{ + let breakdown_millis: Vec<_> = breakdown.iter().map(Duration::as_millis).collect(); + breakdown_millis.serialize(serializer) +} + fn write_metric_section(f: &mut Formatter<'_>, title: &str, metrics: &[T]) -> fmt::Result where T: Display, From 98ab672b42a723d60b4279869b2aa403090f0d82 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Wed, 29 Apr 2026 09:31:55 -0700 Subject: [PATCH 06/31] nightlies with real benchmarks --- AGENTS.md | 1 + infra/nightly-resources/web/index.html | 2 + infra/nightly-resources/web/index.js | 60 ++++++++++-- infra/nightly-resources/web/style.css | 38 ++++++++ infra/nightly.py | 122 +++++++++++++++---------- infra/nightly.sh | 9 +- 6 files changed, 173 insertions(+), 59 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a9f87946d..8fb28664e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,7 @@ ## Conventions + Style - Avoid defensive code that adds complexity without a clear, likely payoff. +- Avoid introducing local variables that are only used once or twice when inlining keeps the code clear. ## Response format diff --git a/infra/nightly-resources/web/index.html b/infra/nightly-resources/web/index.html index 789622937..20b84bb35 100644 --- a/infra/nightly-resources/web/index.html +++ b/infra/nightly-resources/web/index.html @@ -17,6 +17,8 @@

Summary

Reports

+
+
diff --git a/infra/nightly-resources/web/index.js b/infra/nightly-resources/web/index.js index f664a725a..49bb4a6a0 100644 --- a/infra/nightly-resources/web/index.js +++ b/infra/nightly-resources/web/index.js @@ -1,10 +1,12 @@ import { formatMillis } from "./util.js"; -const statusNode = document.querySelector("#status"); - +let suites = []; +let activeSuiteName = null; load(); async function load() { + const statusNode = document.querySelector("#status"); + try { const response = await fetch("./data/data.json"); if (!response.ok) { @@ -12,9 +14,11 @@ async function load() { } const data = await response.json(); + suites = data.suites; + activeSuiteName = suites[0]?.name ?? null; statusNode.textContent = "Loaded data/data.json"; renderSummary(data); - renderBenchmarks(data.reports); + renderSuites(); } catch (error) { statusNode.textContent = `Failed to load data/data.json: ${error}`; } @@ -32,19 +36,59 @@ function renderSummary(data) { } document.querySelector("#summary-text").textContent = - `${data.reports.length} benchmarks | ` + + `${data.summary.benchmark_count} benchmarks across ${data.suites.length} suites | ` + `Nightly time: ${data.summary.total_time_seconds.toFixed(1)} s | ` + `Rule running: ${ruleRunningMillis} ms | ` + `Extraction: ${extractionMillis} ms | ` + `Other: ${otherMillis} ms`; } -function renderBenchmarks(reports) { - document.querySelector("#benchmarks-body").innerHTML = reports - .map(({ path, time_seconds, timing_summary }) => { +function renderSuites() { + document.querySelector("#suite-tabs").innerHTML = suites + .map((suite) => { + return ` + + `; + }) + .join(""); + + for (const button of document.querySelectorAll(".suite-tab")) { + button.addEventListener("click", () => { + activeSuiteName = button.dataset.suiteName; + renderSuites(); + }); + } + + const activeSuite = suites.find((suite) => suite.name === activeSuiteName); + if (!activeSuite) { + document.querySelector("#active-suite-summary").textContent = ""; + document.querySelector("#benchmarks-body").innerHTML = ""; + return; + } + + document.querySelector("#active-suite-summary").innerHTML = ` +
+

${activeSuite.name}

+

${activeSuite.reports.length} benchmarks | ${activeSuite.summary.total_time_seconds.toFixed(1)} s

+
+ `; + document.querySelector("#benchmarks-body").innerHTML = renderRows( + activeSuite.reports, + ); +} + +function renderRows(reports) { + return reports + .map(({ benchmark_path, time_seconds, timing_summary }) => { return ` - + diff --git a/infra/nightly-resources/web/style.css b/infra/nightly-resources/web/style.css index 99bb78f65..8eb1262d4 100644 --- a/infra/nightly-resources/web/style.css +++ b/infra/nightly-resources/web/style.css @@ -31,6 +31,44 @@ h1 { margin: 0 0 16px; } +.suite { + margin: 0 0 24px; +} + +.suite-tab { + appearance: none; + border: 1px solid #ccc; + background: #f0f0f0; + color: inherit; + padding: 8px 12px; + margin: 0 8px 12px 0; + font: inherit; + cursor: pointer; +} + +.suite-tab.is-active { + background: #111; + border-color: #111; + color: #fff; +} + +.suite-header { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 16px; + margin: 0 0 8px; +} + +.suite-header p { + margin: 0; +} + +h3 { + margin: 0; + font-size: 1rem; +} + h2 { margin: 0 0 8px; font-size: 1.1rem; diff --git a/infra/nightly.py b/infra/nightly.py index 3e7e31572..c696980f2 100644 --- a/infra/nightly.py +++ b/infra/nightly.py @@ -26,16 +26,17 @@ def main() -> None: if len(sys.argv) != 2: raise SystemExit(f"Usage: {Path(sys.argv[0]).name} ") - benchmark_dir = (REPO_ROOT / sys.argv[1]).resolve() - - benchmark_files = list(benchmark_dir.rglob("*.egg")) - if not benchmark_files: - raise SystemExit( - f"No .egg benchmark files found under {benchmark_dir}." - ) - - command_results = run_benchmarks(benchmark_dir) - data = aggregate_reports(benchmark_dir, command_results) + benchmark_root = (REPO_ROOT / sys.argv[1]).resolve() + benchmark_dirs = list( + path + for path in benchmark_root.iterdir() + if path.is_dir() and any(path.rglob("*.egg")) + ) + if not benchmark_dirs: + raise SystemExit(f"No benchmark suite directories found under {benchmark_root}.") + + benchmark_results = run_benchmarks(benchmark_dirs) + data = aggregate_reports(benchmark_dirs, benchmark_results) DATA_DIR.mkdir(parents=True, exist_ok=True) DATA_JSON_PATH.write_text(json.dumps(data, indent=2), encoding="utf-8") @@ -70,26 +71,32 @@ def run_command(command: list[str], *, cwd: Path, report_path: Path) -> dict[str "time_seconds": time.perf_counter() - started, } - -def run_benchmarks(benchmark_dir: Path) -> list[dict[str, Any]]: +def run_benchmarks(benchmark_dirs: list[Path]) -> list[dict[str, Any]]: if REPORT_OUTPUT_DIR.exists(): shutil.rmtree(REPORT_OUTPUT_DIR) REPORT_OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + benchmark_root = benchmark_dirs[0].parent results = [] - for benchmark_file in benchmark_dir.rglob("*.egg"): - relative_benchmark = benchmark_file.relative_to(benchmark_dir) - report_path = REPORT_OUTPUT_DIR / relative_benchmark.with_suffix(".report.json") - command = [ - str(POACH_BIN), - "serve", - "--debug", - str(MODEL_FILE), - "single", - str(benchmark_file), - ] - print("Running benchmark:", " ".join(command)) - results.append(run_command(command, cwd=REPO_ROOT, report_path=report_path)) + for benchmark_dir in benchmark_dirs: + benchmark_files = list(benchmark_dir.rglob("*.egg")) + for benchmark_file in benchmark_files: + report_path = REPORT_OUTPUT_DIR / benchmark_file.relative_to( + benchmark_root + ).with_suffix(".report.json") + command = [ + str(POACH_BIN), + "serve", + "--debug", + str(MODEL_FILE), + "single", + str(benchmark_file), + ] + print("Running benchmark:", " ".join(command)) + result = run_command(command, cwd=REPO_ROOT, report_path=report_path) + result["suite"] = benchmark_dir.name + result["benchmark_path"] = str(benchmark_file.relative_to(benchmark_dir)) + results.append(result) return results @@ -115,37 +122,58 @@ def summarize_report(report: dict[str, Any]) -> dict[str, int]: def aggregate_reports( - benchmark_dir: Path, command_results: list[dict[str, Any]] + benchmark_dirs: list[Path], benchmark_results: list[dict[str, Any]] ) -> dict[str, Any]: - report_files = list(REPORT_OUTPUT_DIR.rglob("*.report.json")) - if not report_files: + if not benchmark_results: raise SystemExit(f"No report files were generated under {REPORT_OUTPUT_DIR}") + benchmark_root = benchmark_dirs[0].parent + suites = [] + for benchmark_dir in benchmark_dirs: + suite_results = [ + result for result in benchmark_results if result["suite"] == benchmark_dir.name + ] + suites.append( + { + "name": benchmark_dir.name, + "benchmark_root": str(benchmark_dir.relative_to(REPO_ROOT)), + "summary": { + "total_time_seconds": sum( + result["time_seconds"] for result in suite_results + ), + }, + "reports": [ + { + "suite": benchmark_dir.name, + "benchmark_path": result["benchmark_path"], + "path": str( + Path(result["report_path"]).relative_to(OUTPUT_DIR) + ), + "time_seconds": result["time_seconds"], + "timing_summary": summarize_report( + json.loads( + Path(result["report_path"]).read_text( + encoding="utf-8" + ) + ) + ), + } + for result in suite_results + ], + } + ) + return { "generated_at": datetime.now(timezone.utc).isoformat(), - "suite": benchmark_dir.name, - "benchmark_root": ( - str(benchmark_dir.relative_to(REPO_ROOT)) - if benchmark_dir.is_absolute() - else str(benchmark_dir) - ), + "benchmark_root": str(benchmark_root.relative_to(REPO_ROOT)), "summary": { + "benchmark_count": len(benchmark_results), "total_time_seconds": sum( - result["time_seconds"] for result in command_results + result["time_seconds"] for result in benchmark_results ), }, - "reports": [ - { - "path": str(Path(result["report_path"]).relative_to(OUTPUT_DIR)), - "time_seconds": result["time_seconds"], - "timing_summary": summarize_report( - json.loads( - Path(result["report_path"]).read_text(encoding="utf-8") - ) - ), - } - for result in command_results - ], + "suites": suites, + "reports": [report for suite in suites for report in suite["reports"]], } diff --git a/infra/nightly.sh b/infra/nightly.sh index fd581c521..2807f9bba 100644 --- a/infra/nightly.sh +++ b/infra/nightly.sh @@ -20,14 +20,15 @@ rm -rf nightly mkdir -p nightly/output mkdir -p nightly/tmp -# TODO: use real benchmarks -# git clone https://github.com/ajpal/poach-benchmarks.git +BENCHMARKS_DIR="nightly/tmp/poach-benchmarks" + +git clone https://github.com/ajpal/poach-benchmarks.git "$BENCHMARKS_DIR" # Build in release mode before running nightly.py cargo build --release # This script runs all of the benchmarks/experiments -python3 infra/nightly.py tests/passing +python3 infra/nightly.py "$BENCHMARKS_DIR" # Abort if nightly.py failed to produce data.json. Without this check, # the nightly runner will report the nightly as successful even though the @@ -40,4 +41,4 @@ fi cp infra/nightly-resources/web/* nightly/output # Uncomment for local development -# cd nightly/output && python3 -m http.server 8002 +cd nightly/output && python3 -m http.server 8002 From 5bc67d3a12bec98fc73a0736569d33cfe32acbc9 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Wed, 29 Apr 2026 09:43:07 -0700 Subject: [PATCH 07/31] fail faster --- infra/nightly.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infra/nightly.sh b/infra/nightly.sh index 2807f9bba..becf2026e 100644 --- a/infra/nightly.sh +++ b/infra/nightly.sh @@ -1,4 +1,6 @@ #!/bin/bash +set -euo pipefail + REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" echo "Beginning POACH nightly script..." From f1979b311440bee6e1e2371477967a768144dab6 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Wed, 29 Apr 2026 09:51:31 -0700 Subject: [PATCH 08/31] oops --- infra/nightly.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/nightly.sh b/infra/nightly.sh index becf2026e..795e73dd2 100644 --- a/infra/nightly.sh +++ b/infra/nightly.sh @@ -43,4 +43,4 @@ fi cp infra/nightly-resources/web/* nightly/output # Uncomment for local development -cd nightly/output && python3 -m http.server 8002 +# cd nightly/output && python3 -m http.server 8002 From b861b1650720dd55ab5ae60c30652e9bfd5e7447 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Wed, 29 Apr 2026 10:53:22 -0700 Subject: [PATCH 09/31] update paths --- infra/nightly.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/nightly.py b/infra/nightly.py index c696980f2..e66944b8e 100644 --- a/infra/nightly.py +++ b/infra/nightly.py @@ -30,7 +30,7 @@ def main() -> None: benchmark_dirs = list( path for path in benchmark_root.iterdir() - if path.is_dir() and any(path.rglob("*.egg")) + if path.is_dir() and any((path / "train").glob("*.egg")) ) if not benchmark_dirs: raise SystemExit(f"No benchmark suite directories found under {benchmark_root}.") @@ -79,7 +79,7 @@ def run_benchmarks(benchmark_dirs: list[Path]) -> list[dict[str, Any]]: benchmark_root = benchmark_dirs[0].parent results = [] for benchmark_dir in benchmark_dirs: - benchmark_files = list(benchmark_dir.rglob("*.egg")) + benchmark_files = list((benchmark_dir / "train").glob("*.egg")) for benchmark_file in benchmark_files: report_path = REPORT_OUTPUT_DIR / benchmark_file.relative_to( benchmark_root From d152534ecd099563175797b1abb933e5865ca149 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Wed, 29 Apr 2026 12:07:57 -0700 Subject: [PATCH 10/31] sortable tables --- infra/nightly-resources/web/index.html | 14 +++---- infra/nightly-resources/web/index.js | 51 +++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/infra/nightly-resources/web/index.html b/infra/nightly-resources/web/index.html index 20b84bb35..ed9d726eb 100644 --- a/infra/nightly-resources/web/index.html +++ b/infra/nightly-resources/web/index.html @@ -21,13 +21,13 @@

Reports

${path}${benchmark_path} ${time_seconds.toFixed(3)} s ${formatMillis(timing_summary.rule_running_millis)} ${formatMillis(timing_summary.extraction_millis)}
- - - - - - - + + + + + + + diff --git a/infra/nightly-resources/web/index.js b/infra/nightly-resources/web/index.js index 49bb4a6a0..51c4d3238 100644 --- a/infra/nightly-resources/web/index.js +++ b/infra/nightly-resources/web/index.js @@ -2,7 +2,10 @@ import { formatMillis } from "./util.js"; let suites = []; let activeSuiteName = null; +let sortKey = "benchmark_path"; +let sortDir = "asc"; load(); +installHeaderSortHandlers(); async function load() { const statusNode = document.querySelector("#status"); @@ -14,7 +17,7 @@ async function load() { } const data = await response.json(); - suites = data.suites; + suites = [...data.suites].sort((a, b) => a.name.localeCompare(b.name)); activeSuiteName = suites[0]?.name ?? null; statusNode.textContent = "Loaded data/data.json"; renderSummary(data); @@ -79,8 +82,52 @@ function renderSuites() { `; document.querySelector("#benchmarks-body").innerHTML = renderRows( - activeSuite.reports, + sortReports(activeSuite.reports), ); + updateHeaderIndicators(); +} + +function getSortValue(report, key) { + return key.split(".").reduce((acc, part) => acc?.[part], report) ?? 0; +} + +function sortReports(reports) { + const sorted = [...reports]; + const dir = sortDir === "asc" ? 1 : -1; + sorted.sort((a, b) => { + const av = getSortValue(a, sortKey); + const bv = getSortValue(b, sortKey); + if (typeof av === "string" || typeof bv === "string") { + return String(av).localeCompare(String(bv)) * dir; + } + return (av - bv) * dir; + }); + return sorted; +} + +function installHeaderSortHandlers() { + for (const th of document.querySelectorAll("#benchmarks-header th")) { + th.style.cursor = "pointer"; + th.addEventListener("click", () => { + const key = th.dataset.sortKey; + if (sortKey === key) { + sortDir = sortDir === "asc" ? "desc" : "asc"; + } else { + sortKey = key; + sortDir = "asc"; + } + renderSuites(); + }); + } +} + +function updateHeaderIndicators() { + for (const th of document.querySelectorAll("#benchmarks-header th")) { + const label = th.textContent.replace(/[ ▲▼]+$/, ""); + const arrow = + th.dataset.sortKey === sortKey ? (sortDir === "asc" ? " ▲" : " ▼") : ""; + th.textContent = label + arrow; + } } function renderRows(reports) { From 6440e81ebfd272171497e0baef266cf7e650b8fc Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Thu, 30 Apr 2026 10:47:19 -0700 Subject: [PATCH 11/31] Split nightly setup into its own script Extract toolchain update + benchmarks clone into infra/setup.sh, and have nightly.sh skip that setup when run by the combined-nightly orchestrator (which sets POACH_NIGHTLY_COMBINED=1 and supplies POACH_BENCHMARKS_DIR). Standalone behavior is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- infra/nightly.sh | 24 +++++++++++++----------- infra/setup.sh | 13 +++++++++++++ 2 files changed, 26 insertions(+), 11 deletions(-) mode change 100644 => 100755 infra/nightly.sh create mode 100755 infra/setup.sh diff --git a/infra/nightly.sh b/infra/nightly.sh old mode 100644 new mode 100755 index 795e73dd2..8a04ee749 --- a/infra/nightly.sh +++ b/infra/nightly.sh @@ -6,25 +6,27 @@ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" echo "Beginning POACH nightly script..." ############################################################################### -# This script generates the data for the nightly frontend +# This script generates the data for the nightly frontend ############################################################################### export PATH=~/.cargo/bin:$PATH -rustup update - -cargo install rustfilt - # Ensure we start from a clean slate rm -rf nightly +mkdir -p nightly/output nightly/tmp -# Set Up -mkdir -p nightly/output -mkdir -p nightly/tmp - -BENCHMARKS_DIR="nightly/tmp/poach-benchmarks" +# Standalone runs do their own setup (toolchain + benchmarks clone). When +# driven by the combined orchestrator, POACH_NIGHTLY_COMBINED=1 and the +# benchmarks dir is supplied via POACH_BENCHMARKS_DIR. +if [ -z "${POACH_NIGHTLY_COMBINED:-}" ]; then + bash infra/setup.sh +fi -git clone https://github.com/ajpal/poach-benchmarks.git "$BENCHMARKS_DIR" +BENCHMARKS_DIR="${POACH_BENCHMARKS_DIR:-nightly/tmp/poach-benchmarks}" +if [ ! -d "$BENCHMARKS_DIR" ]; then + echo "ERROR: benchmarks dir $BENCHMARKS_DIR not found" >&2 + exit 1 +fi # Build in release mode before running nightly.py cargo build --release diff --git a/infra/setup.sh b/infra/setup.sh new file mode 100755 index 000000000..a55c5c13a --- /dev/null +++ b/infra/setup.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# One-time setup for the nightly run: rust toolchain update + benchmarks clone. +# Skipped when invoked by the combined-nightly orchestrator, which performs +# these steps once at the top level before driving each branch's nightly.sh. +set -euo pipefail + +export PATH=~/.cargo/bin:$PATH + +rustup update +cargo install rustfilt + +mkdir -p nightly/tmp +git clone https://github.com/ajpal/poach-benchmarks.git nightly/tmp/poach-benchmarks From 90b12c16253b6b77e4f235031c50efeaae0c41e3 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Thu, 30 Apr 2026 11:01:33 -0700 Subject: [PATCH 12/31] Tolerate benchmarks dir outside REPO_ROOT in nightly.py When the combined-nightly orchestrator runs this branch in a worktree, it passes POACH_BENCHMARKS_DIR pointing at a shared clone above the worktree (so we don't re-clone per branch). The benchmark_root.relative_to(REPO_ROOT) call then raises ValueError. Add display_path() that falls back to the absolute path when the input isn't a subpath of REPO_ROOT. Co-Authored-By: Claude Opus 4.7 (1M context) --- infra/nightly.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/infra/nightly.py b/infra/nightly.py index e66944b8e..bd184174b 100644 --- a/infra/nightly.py +++ b/infra/nightly.py @@ -100,6 +100,17 @@ def run_benchmarks(benchmark_dirs: list[Path]) -> list[dict[str, Any]]: return results +def display_path(p: Path) -> str: + # Used for serializing benchmark roots into data.json. Falls back to the + # absolute path when the benchmarks dir lives outside REPO_ROOT (e.g. when + # the combined-nightly orchestrator points POACH_BENCHMARKS_DIR at a + # shared clone above the worktree). + try: + return str(p.relative_to(REPO_ROOT)) + except ValueError: + return str(p) + + def summarize_report(report: dict[str, Any]) -> dict[str, int]: rule_running_millis = 0 extraction_millis = 0 @@ -136,7 +147,7 @@ def aggregate_reports( suites.append( { "name": benchmark_dir.name, - "benchmark_root": str(benchmark_dir.relative_to(REPO_ROOT)), + "benchmark_root": display_path(benchmark_dir), "summary": { "total_time_seconds": sum( result["time_seconds"] for result in suite_results @@ -165,7 +176,7 @@ def aggregate_reports( return { "generated_at": datetime.now(timezone.utc).isoformat(), - "benchmark_root": str(benchmark_root.relative_to(REPO_ROOT)), + "benchmark_root": display_path(benchmark_root), "summary": { "benchmark_count": len(benchmark_results), "total_time_seconds": sum( From 14fecb744932355f2d62ed8530fb02abea43882f Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Thu, 30 Apr 2026 12:27:01 -0700 Subject: [PATCH 13/31] Drop train/ prefix from benchmark_path Emit benchmark_path relative to the suite's train/ subdir (matching the serialize branch's convention) so the combined-nightly comparison table can join rows by benchmark_path without the orchestrator having to strip the prefix. Co-Authored-By: Claude Opus 4.7 (1M context) --- infra/nightly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/nightly.py b/infra/nightly.py index bd184174b..dcd460039 100644 --- a/infra/nightly.py +++ b/infra/nightly.py @@ -95,7 +95,7 @@ def run_benchmarks(benchmark_dirs: list[Path]) -> list[dict[str, Any]]: print("Running benchmark:", " ".join(command)) result = run_command(command, cwd=REPO_ROOT, report_path=report_path) result["suite"] = benchmark_dir.name - result["benchmark_path"] = str(benchmark_file.relative_to(benchmark_dir)) + result["benchmark_path"] = str(benchmark_file.relative_to(benchmark_dir / "train")) results.append(result) return results From 2296180d26dab9f96f120b01841af5e089cbd831 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Thu, 30 Apr 2026 12:28:52 -0700 Subject: [PATCH 14/31] Strip .egg suffix from benchmark_path Match the serialize branch exactly so the combined-nightly merger can join rows by benchmark_path with no normalization. Co-Authored-By: Claude Opus 4.7 (1M context) --- infra/nightly.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/infra/nightly.py b/infra/nightly.py index dcd460039..2ee209253 100644 --- a/infra/nightly.py +++ b/infra/nightly.py @@ -95,7 +95,9 @@ def run_benchmarks(benchmark_dirs: list[Path]) -> list[dict[str, Any]]: print("Running benchmark:", " ".join(command)) result = run_command(command, cwd=REPO_ROOT, report_path=report_path) result["suite"] = benchmark_dir.name - result["benchmark_path"] = str(benchmark_file.relative_to(benchmark_dir / "train")) + result["benchmark_path"] = str( + benchmark_file.relative_to(benchmark_dir / "train").with_suffix("") + ) results.append(result) return results From 37c519a34aa568217f1cd31c4b0f7246230d8887 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Thu, 30 Apr 2026 13:32:25 -0700 Subject: [PATCH 15/31] Revert .egg stripping in benchmark_path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Serialize emits `.egg`, not `` — the previous edit over-corrected. Drop the .with_suffix("") so vanilla and serialize both emit `.egg` and the merger joins rows correctly. Co-Authored-By: Claude Opus 4.7 (1M context) --- infra/nightly.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/infra/nightly.py b/infra/nightly.py index 2ee209253..dcd460039 100644 --- a/infra/nightly.py +++ b/infra/nightly.py @@ -95,9 +95,7 @@ def run_benchmarks(benchmark_dirs: list[Path]) -> list[dict[str, Any]]: print("Running benchmark:", " ".join(command)) result = run_command(command, cwd=REPO_ROOT, report_path=report_path) result["suite"] = benchmark_dir.name - result["benchmark_path"] = str( - benchmark_file.relative_to(benchmark_dir / "train").with_suffix("") - ) + result["benchmark_path"] = str(benchmark_file.relative_to(benchmark_dir / "train")) results.append(result) return results From 85356380f32b7fcff4c3015b8268d5573b636cec Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Mon, 4 May 2026 09:52:38 -0700 Subject: [PATCH 16/31] no lock needed --- src/poach.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/poach.rs b/src/poach.rs index a53a5f20f..5de7cc439 100644 --- a/src/poach.rs +++ b/src/poach.rs @@ -164,10 +164,9 @@ fn serve(arg: ServeArgs) { let mut reporter = Reporter::new(); let result = egraph.run_program_with_reporter(parsed, &mut reporter); if result.is_ok() { - let mut stderr = std::io::stderr().lock(); - serde_json::to_writer(&mut stderr, &reporter.build_report()) + serde_json::to_writer(&mut std::io::stderr(), &reporter.build_report()) .expect("failed to serialize debug report"); - writeln!(stderr).expect("failed to terminate debug report"); + writeln!(std::io::stderr()).expect("failed to terminate debug report"); } result } From 7899210c15a741922141aca99e30d93bc6c217e9 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Tue, 12 May 2026 14:22:53 -0700 Subject: [PATCH 17/31] nightly script --- infra/nightly.py | 291 ++++++++++++++++++----------------------------- infra/nightly.sh | 2 +- 2 files changed, 109 insertions(+), 184 deletions(-) diff --git a/infra/nightly.py b/infra/nightly.py index dcd460039..9af258dba 100644 --- a/infra/nightly.py +++ b/infra/nightly.py @@ -1,196 +1,121 @@ -#!/usr/bin/env python3 - -from __future__ import annotations +#!/user/bin/env python3 import json -import shutil import subprocess import sys import time from datetime import datetime, timezone from pathlib import Path -from typing import Any - - -REPO_ROOT = Path(__file__).resolve().parent.parent -NIGHTLY_DIR = REPO_ROOT / "nightly" -OUTPUT_DIR = NIGHTLY_DIR / "output" -DATA_DIR = OUTPUT_DIR / "data" -REPORT_OUTPUT_DIR = DATA_DIR / "reports" -DATA_JSON_PATH = DATA_DIR / "data.json" -POACH_BIN = REPO_ROOT / "target" / "release" / "poach" -MODEL_FILE = REPO_ROOT / "empty.model" - - -def main() -> None: - if len(sys.argv) != 2: - raise SystemExit(f"Usage: {Path(sys.argv[0]).name} ") - - benchmark_root = (REPO_ROOT / sys.argv[1]).resolve() - benchmark_dirs = list( - path - for path in benchmark_root.iterdir() - if path.is_dir() and any((path / "train").glob("*.egg")) - ) - if not benchmark_dirs: - raise SystemExit(f"No benchmark suite directories found under {benchmark_root}.") - - benchmark_results = run_benchmarks(benchmark_dirs) - data = aggregate_reports(benchmark_dirs, benchmark_results) - - DATA_DIR.mkdir(parents=True, exist_ok=True) - DATA_JSON_PATH.write_text(json.dumps(data, indent=2), encoding="utf-8") - print(f"Wrote {DATA_JSON_PATH}") - -def run_command(command: list[str], *, cwd: Path, report_path: Path) -> dict[str, Any]: - started = time.perf_counter() - try: - completed = subprocess.run( - command, - check=True, - cwd=cwd, - capture_output=True, - text=True, - ) - except subprocess.CalledProcessError as err: - time_seconds = time.perf_counter() - started - print( - f"Command failed after {time_seconds:.3f}s: {' '.join(command)}", - file=sys.stderr, - ) - raise err - - report_path.parent.mkdir(parents=True, exist_ok=True) - report_path.write_text(completed.stderr, encoding="utf-8") - - return { - "argv": command, - "cwd": str(cwd), - "report_path": str(report_path), - "returncode": completed.returncode, - "time_seconds": time.perf_counter() - started, - } - -def run_benchmarks(benchmark_dirs: list[Path]) -> list[dict[str, Any]]: - if REPORT_OUTPUT_DIR.exists(): - shutil.rmtree(REPORT_OUTPUT_DIR) - REPORT_OUTPUT_DIR.mkdir(parents=True, exist_ok=True) - - benchmark_root = benchmark_dirs[0].parent - results = [] - for benchmark_dir in benchmark_dirs: - benchmark_files = list((benchmark_dir / "train").glob("*.egg")) - for benchmark_file in benchmark_files: - report_path = REPORT_OUTPUT_DIR / benchmark_file.relative_to( - benchmark_root - ).with_suffix(".report.json") - command = [ - str(POACH_BIN), - "serve", - "--debug", - str(MODEL_FILE), - "single", - str(benchmark_file), - ] - print("Running benchmark:", " ".join(command)) - result = run_command(command, cwd=REPO_ROOT, report_path=report_path) - result["suite"] = benchmark_dir.name - result["benchmark_path"] = str(benchmark_file.relative_to(benchmark_dir / "train")) - results.append(result) - return results - - -def display_path(p: Path) -> str: - # Used for serializing benchmark roots into data.json. Falls back to the - # absolute path when the benchmarks dir lives outside REPO_ROOT (e.g. when - # the combined-nightly orchestrator points POACH_BENCHMARKS_DIR at a - # shared clone above the worktree). - try: - return str(p.relative_to(REPO_ROOT)) - except ValueError: - return str(p) - - -def summarize_report(report: dict[str, Any]) -> dict[str, int]: - rule_running_millis = 0 - extraction_millis = 0 - other_millis = 0 - - for timing in report["timings"]: - if "running_rules" in timing["tags"]: - rule_running_millis += timing["total"] - elif "extraction" in timing["tags"]: - extraction_millis += timing["total"] - else: - other_millis += timing["total"] - - return { - "rule_running_millis": rule_running_millis, - "extraction_millis": extraction_millis, - "other_millis": other_millis, - "timing_steps": len(report["timings"]), - } - - -def aggregate_reports( - benchmark_dirs: list[Path], benchmark_results: list[dict[str, Any]] -) -> dict[str, Any]: - if not benchmark_results: - raise SystemExit(f"No report files were generated under {REPORT_OUTPUT_DIR}") - - benchmark_root = benchmark_dirs[0].parent - suites = [] - for benchmark_dir in benchmark_dirs: - suite_results = [ - result for result in benchmark_results if result["suite"] == benchmark_dir.name - ] - suites.append( - { - "name": benchmark_dir.name, - "benchmark_root": display_path(benchmark_dir), - "summary": { - "total_time_seconds": sum( - result["time_seconds"] for result in suite_results - ), - }, - "reports": [ - { - "suite": benchmark_dir.name, - "benchmark_path": result["benchmark_path"], - "path": str( - Path(result["report_path"]).relative_to(OUTPUT_DIR) - ), - "time_seconds": result["time_seconds"], - "timing_summary": summarize_report( - json.loads( - Path(result["report_path"]).read_text( - encoding="utf-8" - ) - ) - ), - } - for result in suite_results - ], - } - ) +# Determine directories +SCRIPT_DIR = Path(__file__).resolve().parent +POACH_ROOT = SCRIPT_DIR.parent +NIGHTLY_DIR = POACH_ROOT / "nightly" +POACH_BINARY = POACH_ROOT / "target" / "release" / "poach" + +def main(benchmark_dir): + print(benchmark_dir) + + (benchmark_results, failing_benchmarks) = run_benchmarks(benchmark_dir) + + data = { + "generated_at": datetime.now(timezone.utc).isoformat(), + "failing_benchmarks": [str(b) for b in failing_benchmarks], + "data": benchmark_results + } + data_out_path = NIGHTLY_DIR / "output" / "data" / "data.json" + data_out_path.parent.mkdir(parents=True, exist_ok=True) + data_out_path.write_text(json.dumps(data, indent=2), encoding="utf-8") + +def run_command(cmd): + started = time.perf_counter() + cmd_result = subprocess.run( + cmd, + cwd=POACH_ROOT, + capture_output=True, + text=True # decode stderr/stdout as string instead of raw bytes + ) + if cmd_result.returncode != 0: + time_seconds = time.perf_counter() - started + print(f"Command failed after {time_seconds:.2f}s: {' '.join(cmd)}", file=sys.stderr) return { - "generated_at": datetime.now(timezone.utc).isoformat(), - "benchmark_root": display_path(benchmark_root), - "summary": { - "benchmark_count": len(benchmark_results), - "total_time_seconds": sum( - result["time_seconds"] for result in benchmark_results - ), - }, - "suites": suites, - "reports": [report for suite in suites for report in suite["reports"]], + "cmd": cmd, + "status": "error" } + report = json.loads(cmd_result.stderr) + + return { + "cmd": " ".join(cmd), + "status": "success", + "report": summarize_report(report), + "wall_time_s": time.perf_counter() - started + } + +def summarize_report(report): + # aggregate timing steps by type + rule_ms = 0 + extraction_ms = 0 + other_ms = 0 + total_ms = 0 + for time_step in report["timings"]: + total_ms += time_step["total"] + if "running_rules" in time_step["tags"]: + rule_ms += time_step["total"] + elif "extraction" in time_step["tags"]: + extraction_ms += time_step["total"] + else: + other_ms += time_step["total"] + + # No sizes in vanilla egglog reports + + return { + "rule_ms": rule_ms, + "extraction_ms": extraction_ms, + "other_ms": other_ms, + "timing_steps": len(report["timings"]) + } + +def run_benchmarks(benchmark_dir): + report_dir = NIGHTLY_DIR / "reports" + report_dir.mkdir(parents=True, exist_ok=True) + + # Find benchmarks + # benchmark_dir is the root of the benchmark directory + benchmarks = list(Path(benchmark_dir).rglob("train/*.egg")) + # For this treatment, we don't do anything at train time, + # we just use the train benchmarks at serve time + + results = [] + failing_benchmarks = [] + for benchmark in benchmarks: + if len(results) > 10: + break + relative_path = benchmark.relative_to(benchmark_dir) + suite_name = str(relative_path.parent) + benchmark_name = relative_path.name + command = [ + str(POACH_BINARY), + "serve", + "--debug", + "EMPTY.MODEL", + "single", + str(benchmark) + ] + + result = run_command(command) + result["benchmark_name"] = benchmark_name + result["suite_name"] = suite_name + if result["status"] == "success": + print(f"Success: {benchmark_name}") + results.append(result) + else: + failing_benchmarks.append(relative_path) + + return (results, failing_benchmarks) if __name__ == "__main__": - try: - main() - except subprocess.CalledProcessError as err: - print(f"Command failed with exit code {err.returncode}: {err.cmd}", file=sys.stderr) - raise + if len(sys.argv) != 2: + raise SystemExit(f"Usage: nightly.py ") + + main(sys.argv[1]) \ No newline at end of file diff --git a/infra/nightly.sh b/infra/nightly.sh index 8a04ee749..f4ce9ffe0 100755 --- a/infra/nightly.sh +++ b/infra/nightly.sh @@ -13,7 +13,7 @@ export PATH=~/.cargo/bin:$PATH # Ensure we start from a clean slate rm -rf nightly -mkdir -p nightly/output nightly/tmp +mkdir -p nightly # Standalone runs do their own setup (toolchain + benchmarks clone). When # driven by the combined orchestrator, POACH_NIGHTLY_COMBINED=1 and the From 4a8edc95475d5ff1889caef8dd39af5804bfbd81 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Tue, 12 May 2026 14:23:18 -0700 Subject: [PATCH 18/31] remove early exit --- infra/nightly.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/infra/nightly.py b/infra/nightly.py index 9af258dba..c55a1139a 100644 --- a/infra/nightly.py +++ b/infra/nightly.py @@ -89,8 +89,6 @@ def run_benchmarks(benchmark_dir): results = [] failing_benchmarks = [] for benchmark in benchmarks: - if len(results) > 10: - break relative_path = benchmark.relative_to(benchmark_dir) suite_name = str(relative_path.parent) benchmark_name = relative_path.name From cce3f12dd73d2c28eb027ceea179d8ae8149c335 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Tue, 12 May 2026 15:26:55 -0700 Subject: [PATCH 19/31] frontend --- infra/nightly-resources/web/index.html | 14 +- infra/nightly-resources/web/index.js | 218 +++++++++++-------------- infra/nightly-resources/web/table.js | 45 +++++ infra/nightly.py | 2 +- 4 files changed, 145 insertions(+), 134 deletions(-) create mode 100644 infra/nightly-resources/web/table.js diff --git a/infra/nightly-resources/web/index.html b/infra/nightly-resources/web/index.html index ed9d726eb..45a0b73f9 100644 --- a/infra/nightly-resources/web/index.html +++ b/infra/nightly-resources/web/index.html @@ -19,19 +19,7 @@

Summary

Reports

-
PathWall TimeRule RunningExtractionOther# Timing Steps
PathWall TimeRule RunningExtractionOther# Timing Steps
- - - - - - - - - - - -
PathWall TimeRule RunningExtractionOther# Timing Steps
+
diff --git a/infra/nightly-resources/web/index.js b/infra/nightly-resources/web/index.js index 51c4d3238..96826b66c 100644 --- a/infra/nightly-resources/web/index.js +++ b/infra/nightly-resources/web/index.js @@ -1,148 +1,126 @@ -import { formatMillis } from "./util.js"; +import { convertToTable } from "./table.js"; + +const STATE = { + activeSuite: null, +}; + +const GLOBAL_DATA = { + data: null, + suites: null, +}; -let suites = []; -let activeSuiteName = null; -let sortKey = "benchmark_path"; -let sortDir = "asc"; load(); -installHeaderSortHandlers(); async function load() { const statusNode = document.querySelector("#status"); - try { - const response = await fetch("./data/data.json"); - if (!response.ok) { - throw new Error(`Failed to load data.json (${response.status})`); - } - - const data = await response.json(); - suites = [...data.suites].sort((a, b) => a.name.localeCompare(b.name)); - activeSuiteName = suites[0]?.name ?? null; - statusNode.textContent = "Loaded data/data.json"; - renderSummary(data); - renderSuites(); - } catch (error) { + const response = await fetch("./data/data.json"); + if (!response.ok) { statusNode.textContent = `Failed to load data/data.json: ${error}`; + return; } -} -function renderSummary(data) { - let ruleRunningMillis = 0; - let extractionMillis = 0; - let otherMillis = 0; + GLOBAL_DATA.data = await response.json(); + GLOBAL_DATA.suites = [ + ...new Set(GLOBAL_DATA.data.passing_benchmarks.map((x) => x.suite_name)), + ].sort(); + STATE.activeSuite = GLOBAL_DATA.suites[0] ?? null; - for (const { timing_summary } of data.reports) { - ruleRunningMillis += timing_summary.rule_running_millis; - extractionMillis += timing_summary.extraction_millis; - otherMillis += timing_summary.other_millis; - } + statusNode.textContent = "Loaded data/data.json"; + renderSummary(); - document.querySelector("#summary-text").textContent = - `${data.summary.benchmark_count} benchmarks across ${data.suites.length} suites | ` + - `Nightly time: ${data.summary.total_time_seconds.toFixed(1)} s | ` + - `Rule running: ${ruleRunningMillis} ms | ` + - `Extraction: ${extractionMillis} ms | ` + - `Other: ${otherMillis} ms`; + renderSuiteSelectors(); + renderTable(); } -function renderSuites() { - document.querySelector("#suite-tabs").innerHTML = suites - .map((suite) => { - return ` - - `; - }) - .join(""); +function renderSummary() { + let ruleMs = 0; + let extractMs = 0; + let otherMs = 0; - for (const button of document.querySelectorAll(".suite-tab")) { - button.addEventListener("click", () => { - activeSuiteName = button.dataset.suiteName; - renderSuites(); - }); + for (const reportSummary of GLOBAL_DATA.data.passing_benchmarks) { + ruleMs += reportSummary.report.rule_ms; + extractMs += reportSummary.report.extraction_ms; + otherMs += reportSummary.report.other_ms; } - const activeSuite = suites.find((suite) => suite.name === activeSuiteName); - if (!activeSuite) { - document.querySelector("#active-suite-summary").textContent = ""; - document.querySelector("#benchmarks-body").innerHTML = ""; - return; - } + const numPassing = GLOBAL_DATA.data.passing_benchmarks.length; + const numFailing = GLOBAL_DATA.data.failing_benchmarks.length; + const totalTime = GLOBAL_DATA.data.passing_benchmarks + .map((x) => x.wall_time_s) + .reduce((a, b) => a + b, 0); - document.querySelector("#active-suite-summary").innerHTML = ` -
-

${activeSuite.name}

-

${activeSuite.reports.length} benchmarks | ${activeSuite.summary.total_time_seconds.toFixed(1)} s

-
- `; - document.querySelector("#benchmarks-body").innerHTML = renderRows( - sortReports(activeSuite.reports), - ); - updateHeaderIndicators(); + document.querySelector("#summary-text").textContent = + `Passing Benchmarks: ${numPassing} | ` + + `Failing Benchmarks: ${numFailing} | ` + + `Nightly time: ${totalTime.toFixed(1)} s | ` + + `Rule running: ${(ruleMs / 1000).toFixed(1)} s | ` + + `Extraction: ${(extractMs / 1000).toFixed(1)} s | ` + + `Other: ${(otherMs / 1000).toFixed(1)} s`; } -function getSortValue(report, key) { - return key.split(".").reduce((acc, part) => acc?.[part], report) ?? 0; -} +function renderSuiteSelectors() { + document.querySelector("#suite-tabs").innerHTML = GLOBAL_DATA.suites + .map( + (suite) => + ` + `, + ) + .join(""); -function sortReports(reports) { - const sorted = [...reports]; - const dir = sortDir === "asc" ? 1 : -1; - sorted.sort((a, b) => { - const av = getSortValue(a, sortKey); - const bv = getSortValue(b, sortKey); - if (typeof av === "string" || typeof bv === "string") { - return String(av).localeCompare(String(bv)) * dir; - } - return (av - bv) * dir; - }); - return sorted; -} + for (const button of document.querySelectorAll(".suite-tab")) { + button.addEventListener("click", () => { + STATE.activeSuite = button.dataset.suiteName; -function installHeaderSortHandlers() { - for (const th of document.querySelectorAll("#benchmarks-header th")) { - th.style.cursor = "pointer"; - th.addEventListener("click", () => { - const key = th.dataset.sortKey; - if (sortKey === key) { - sortDir = sortDir === "asc" ? "desc" : "asc"; - } else { - sortKey = key; - sortDir = "asc"; + for (const btn of document.querySelectorAll(".suite-tab")) { + btn.classList.toggle( + "is-active", + btn.dataset.suiteName === STATE.activeSuite, + ); } - renderSuites(); + + renderTable(); }); } } -function updateHeaderIndicators() { - for (const th of document.querySelectorAll("#benchmarks-header th")) { - const label = th.textContent.replace(/[ ▲▼]+$/, ""); - const arrow = - th.dataset.sortKey === sortKey ? (sortDir === "asc" ? " ▲" : " ▼") : ""; - th.textContent = label + arrow; - } -} +function renderTable() { + const benchmarks = GLOBAL_DATA.data.passing_benchmarks.filter( + (x) => x.suite_name === STATE.activeSuite, + ); + const totalTime = benchmarks + .map((x) => x.wall_time_s) + .reduce((a, b) => a + b, 0); -function renderRows(reports) { - return reports - .map(({ benchmark_path, time_seconds, timing_summary }) => { - return ` - - ${benchmark_path} - ${time_seconds.toFixed(3)} s - ${formatMillis(timing_summary.rule_running_millis)} - ${formatMillis(timing_summary.extraction_millis)} - ${formatMillis(timing_summary.other_millis)} - ${timing_summary.timing_steps} - - `; - }) - .join(""); + document.querySelector("#active-suite-summary").innerHTML = ` +
+

${STATE.activeSuite}

+

${benchmarks.length} benchmarks | ${totalTime} s

+
`; + + const columns = [ + "Benchmark", + "Wall Time (s)", + "Rules (ms)", + "Extraction (ms)", + "Other (ms)", + ]; + + const rows = benchmarks.map((b) => ({ + Benchmark: b.benchmark_name, + "Wall Time (s)": b.wall_time_s, + "Rules (ms)": b.report.rule_ms, + "Extraction (ms)": b.report.extraction_ms, + "Other (ms)": b.report.other_ms, + })); + + const tableDiv = document.querySelector("#active-suite-table"); + tableDiv.innerHTML = ""; + tableDiv.appendChild(convertToTable(columns, rows)); } diff --git a/infra/nightly-resources/web/table.js b/infra/nightly-resources/web/table.js new file mode 100644 index 000000000..f2c1e583a --- /dev/null +++ b/infra/nightly-resources/web/table.js @@ -0,0 +1,45 @@ +/** + * Given a list of column names and a list of row objects, produce an + * HTML table that displays the data + * + * @param {String[]} columns + * Defines the set of columns to be displayed and their order. + * If empty, an empty table element will be returned + * + * @param {Object[]} rows + * Each object corresponds to one row in the table. + * Each object maps column name -> cell value. + * Values should be numbers or strings only. + * Rows may contain keys not listed in `columns` and may be missing `columns` + * Extra values are ignored. Missing values are displayed as `-` in the table + * If empty, a table with only the header row will be returned + * + * @return An HTML DOM element + */ +export function convertToTable(columns, rows) { + const table = document.createElement("table"); + + const thead = document.createElement("thead"); + const headerRow = document.createElement("tr"); + for (const column of columns) { + const th = document.createElement("th"); + th.textContent = column; + headerRow.appendChild(th); + } + thead.appendChild(headerRow); + table.appendChild(thead); + + const tbody = document.createElement("tbody"); + for (const row of rows) { + const tr = document.createElement("tr"); + for (const column of columns) { + const td = document.createElement("td"); + td.textContent = row[column] ?? "-"; // We want to leave 0 and "" intact, so don't use || + tr.appendChild(td); + } + tbody.appendChild(tr); + } + table.appendChild(tbody); + + return table; +} diff --git a/infra/nightly.py b/infra/nightly.py index c55a1139a..d100c8898 100644 --- a/infra/nightly.py +++ b/infra/nightly.py @@ -21,7 +21,7 @@ def main(benchmark_dir): data = { "generated_at": datetime.now(timezone.utc).isoformat(), "failing_benchmarks": [str(b) for b in failing_benchmarks], - "data": benchmark_results + "passing_benchmarks": benchmark_results } data_out_path = NIGHTLY_DIR / "output" / "data" / "data.json" data_out_path.parent.mkdir(parents=True, exist_ok=True) From c2bae370ba852f7303b14e40170827b39f24c616 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Tue, 12 May 2026 15:46:21 -0700 Subject: [PATCH 20/31] sortable table --- infra/nightly-resources/web/table.js | 83 ++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 16 deletions(-) diff --git a/infra/nightly-resources/web/table.js b/infra/nightly-resources/web/table.js index f2c1e583a..e63e36b4b 100644 --- a/infra/nightly-resources/web/table.js +++ b/infra/nightly-resources/web/table.js @@ -15,31 +15,82 @@ * If empty, a table with only the header row will be returned * * @return An HTML
DOM element + * Sortable by column, defaults to sorted by first column */ export function convertToTable(columns, rows) { const table = document.createElement("table"); - const thead = document.createElement("thead"); - const headerRow = document.createElement("tr"); - for (const column of columns) { - const th = document.createElement("th"); - th.textContent = column; - headerRow.appendChild(th); + const STATE = { + sortCol: columns[0], + sortDir: "asc", + }; + + function sortedRows() { + const dir = STATE.sortDir === "asc" ? 1 : -1; + return [...rows].sort((a, b) => { + // Look up value to sort by in each row + const av = a[STATE.sortCol]; + const bv = b[STATE.sortCol]; + + // First check if either/both are missing. Sort missing values to the bottom + const aMissing = av === undefined || av === null; + const bMissing = bv === undefined || bv === null; + if (aMissing && bMissing) return 0; + if (aMissing) return 1; + if (bMissing) return -1; + + // Compare + return (av > bv ? 1 : av < bv ? -1 : 0) * dir; + }); } - thead.appendChild(headerRow); - table.appendChild(thead); - const tbody = document.createElement("tbody"); - for (const row of rows) { - const tr = document.createElement("tr"); + function toggleSortDir() { + if (STATE.sortDir === "asc") { + STATE.sortDir = "desc"; + } else { + STATE.sortDir = "asc"; + } + } + + function render() { + // Reset table element before rebuilding + table.innerHTML = ""; + + const thead = document.createElement("thead"); + const headerRow = document.createElement("tr"); for (const column of columns) { - const td = document.createElement("td"); - td.textContent = row[column] ?? "-"; // We want to leave 0 and "" intact, so don't use || - tr.appendChild(td); + const th = document.createElement("th"); + const arrow = + column === STATE.sortCol ? (STATE.sortDir === "asc" ? " ▲" : " ▼") : ""; + th.textContent = column + arrow; + th.style.cursor = "pointer"; + th.addEventListener("click", () => { + if (STATE.sortCol === column) { + toggleSortDir(); + } else { + STATE.sortCol = column; + STATE.sortDir = "asc"; + } + render(); + }); + headerRow.appendChild(th); + } + thead.appendChild(headerRow); + table.appendChild(thead); + + const tbody = document.createElement("tbody"); + for (const row of sortedRows()) { + const tr = document.createElement("tr"); + for (const column of columns) { + const td = document.createElement("td"); + td.textContent = row[column] ?? "-"; // We want to leave 0 and "" intact, so don't use || + tr.appendChild(td); + } + tbody.appendChild(tr); } - tbody.appendChild(tr); + table.appendChild(tbody); } - table.appendChild(tbody); + render(); return table; } From 93d080bd1579493f133426d4f648a4c64d1ca916 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Tue, 12 May 2026 16:35:41 -0700 Subject: [PATCH 21/31] round wall time --- infra/nightly-resources/web/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/nightly-resources/web/index.js b/infra/nightly-resources/web/index.js index 96826b66c..8c47b30fe 100644 --- a/infra/nightly-resources/web/index.js +++ b/infra/nightly-resources/web/index.js @@ -114,7 +114,7 @@ function renderTable() { const rows = benchmarks.map((b) => ({ Benchmark: b.benchmark_name, - "Wall Time (s)": b.wall_time_s, + "Wall Time (s)": b.wall_time_s.toFixed(2), "Rules (ms)": b.report.rule_ms, "Extraction (ms)": b.report.extraction_ms, "Other (ms)": b.report.other_ms, From 5a742be185d10d84c05494f5558f1c10fd3057be Mon Sep 17 00:00:00 2001 From: Noah Huck Date: Tue, 12 May 2026 17:57:34 -0700 Subject: [PATCH 22/31] register custom scheduler with egraph --- src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 9db873cf0..5db3a2e5d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -363,8 +363,10 @@ impl Default for EGraph { eg.rulesets .insert("".into(), Ruleset::Rules(Default::default())); - // support get-size! macro for Herbie + // support get-size! macro, custom scheduler for Herbie eg.add_primitive(get_size_prim::GetSizePrimitive); + eg.add_command("run-schedule".into(), Arc::new(RunExtendedSchedule)) + .unwrap(); eg } From bd34aa41ec411aea38581003d6f0ca4fac56bea3 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Tue, 12 May 2026 21:00:03 -0700 Subject: [PATCH 23/31] serialize durations as micros --- src/report.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/report.rs b/src/report.rs index 0d85dc6fd..36b4412dc 100644 --- a/src/report.rs +++ b/src/report.rs @@ -28,7 +28,7 @@ pub struct RunReport { struct TimingStep { name: String, tags: Vec, - #[serde(with = "serde_millis")] + #[serde(serialize_with = "serialize_duration_total")] total: Duration, #[serde(serialize_with = "serialize_duration_breakdown")] breakdown: Vec, @@ -122,11 +122,18 @@ impl Display for TimingStep { } } +fn serialize_duration_total(total: &Duration, serializer: S) -> Result +where + S: Serializer, +{ + total.as_micros().serialize(serializer) +} + fn serialize_duration_breakdown(breakdown: &[Duration], serializer: S) -> Result where S: Serializer, { - let breakdown_millis: Vec<_> = breakdown.iter().map(Duration::as_millis).collect(); + let breakdown_millis: Vec<_> = breakdown.iter().map(Duration::as_micros).collect(); breakdown_millis.serialize(serializer) } From f94ca002d009e161d111d70a05f4460b741a424e Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Tue, 12 May 2026 21:26:58 -0700 Subject: [PATCH 24/31] micros everywhere --- infra/nightly-resources/web/index.js | 40 ++++++++++++++-------------- infra/nightly.py | 34 ++++++++++++----------- src/report.rs | 12 ++++++--- 3 files changed, 46 insertions(+), 40 deletions(-) diff --git a/infra/nightly-resources/web/index.js b/infra/nightly-resources/web/index.js index 8c47b30fe..a4752efb1 100644 --- a/infra/nightly-resources/web/index.js +++ b/infra/nightly-resources/web/index.js @@ -34,29 +34,29 @@ async function load() { } function renderSummary() { - let ruleMs = 0; - let extractMs = 0; - let otherMs = 0; + let ruleMicros = 0; + let extractMicros = 0; + let otherMicros = 0; for (const reportSummary of GLOBAL_DATA.data.passing_benchmarks) { - ruleMs += reportSummary.report.rule_ms; - extractMs += reportSummary.report.extraction_ms; - otherMs += reportSummary.report.other_ms; + ruleMicros += reportSummary.report.rule_micros; + extractMicros += reportSummary.report.extraction_micros; + otherMicros += reportSummary.report.other_micros; } const numPassing = GLOBAL_DATA.data.passing_benchmarks.length; const numFailing = GLOBAL_DATA.data.failing_benchmarks.length; const totalTime = GLOBAL_DATA.data.passing_benchmarks - .map((x) => x.wall_time_s) + .map((x) => x.wall_time_micros) .reduce((a, b) => a + b, 0); document.querySelector("#summary-text").textContent = `Passing Benchmarks: ${numPassing} | ` + `Failing Benchmarks: ${numFailing} | ` + - `Nightly time: ${totalTime.toFixed(1)} s | ` + - `Rule running: ${(ruleMs / 1000).toFixed(1)} s | ` + - `Extraction: ${(extractMs / 1000).toFixed(1)} s | ` + - `Other: ${(otherMs / 1000).toFixed(1)} s`; + `Nightly time: ${totalTime} μs | ` + + `Rule running: ${ruleMicros} μs | ` + + `Extraction: ${extractMicros} μs | ` + + `Other: ${otherMicros} μs`; } function renderSuiteSelectors() { @@ -95,7 +95,7 @@ function renderTable() { (x) => x.suite_name === STATE.activeSuite, ); const totalTime = benchmarks - .map((x) => x.wall_time_s) + .map((x) => x.wall_time_micros) .reduce((a, b) => a + b, 0); document.querySelector("#active-suite-summary").innerHTML = ` @@ -106,18 +106,18 @@ function renderTable() { const columns = [ "Benchmark", - "Wall Time (s)", - "Rules (ms)", - "Extraction (ms)", - "Other (ms)", + "Wall Time (μs)", + "Rules (μs)", + "Extraction (μs)", + "Other (μs)", ]; const rows = benchmarks.map((b) => ({ Benchmark: b.benchmark_name, - "Wall Time (s)": b.wall_time_s.toFixed(2), - "Rules (ms)": b.report.rule_ms, - "Extraction (ms)": b.report.extraction_ms, - "Other (ms)": b.report.other_ms, + "Wall Time (μs)": b.wall_time_micros, + "Rules (μs)": b.report.rule_micros, + "Extraction (μs)": b.report.extraction_micros, + "Other (μs)": b.report.other_micros, })); const tableDiv = document.querySelector("#active-suite-table"); diff --git a/infra/nightly.py b/infra/nightly.py index d100c8898..7802fddcc 100644 --- a/infra/nightly.py +++ b/infra/nightly.py @@ -28,19 +28,21 @@ def main(benchmark_dir): data_out_path.write_text(json.dumps(data, indent=2), encoding="utf-8") def run_command(cmd): - started = time.perf_counter() + started = time.perf_counter_ns() cmd_result = subprocess.run( cmd, cwd=POACH_ROOT, capture_output=True, text=True # decode stderr/stdout as string instead of raw bytes ) + # Clock granularity is ~50-100 ns. + # Report as micros to avoid reporting false precision. + time_micros = (time.perf_counter_ns() - started) // 1000 if cmd_result.returncode != 0: - time_seconds = time.perf_counter() - started - print(f"Command failed after {time_seconds:.2f}s: {' '.join(cmd)}", file=sys.stderr) return { "cmd": cmd, - "status": "error" + "status": "error", + "wall_time_micros": time_micros } report = json.loads(cmd_result.stderr) @@ -49,30 +51,30 @@ def run_command(cmd): "cmd": " ".join(cmd), "status": "success", "report": summarize_report(report), - "wall_time_s": time.perf_counter() - started + "wall_time_micros": time_micros } def summarize_report(report): # aggregate timing steps by type - rule_ms = 0 - extraction_ms = 0 - other_ms = 0 - total_ms = 0 + rule_micros = 0 + extraction_micros = 0 + other_micros = 0 + total_micros = 0 for time_step in report["timings"]: - total_ms += time_step["total"] + total_micros += time_step["total"] if "running_rules" in time_step["tags"]: - rule_ms += time_step["total"] + rule_micros += time_step["total"] elif "extraction" in time_step["tags"]: - extraction_ms += time_step["total"] + extraction_micros += time_step["total"] else: - other_ms += time_step["total"] + other_micros += time_step["total"] # No sizes in vanilla egglog reports return { - "rule_ms": rule_ms, - "extraction_ms": extraction_ms, - "other_ms": other_ms, + "rule_micros": rule_micros, + "extraction_micros": extraction_micros, + "other_micros": other_micros, "timing_steps": len(report["timings"]) } diff --git a/src/report.rs b/src/report.rs index 36b4412dc..657664418 100644 --- a/src/report.rs +++ b/src/report.rs @@ -109,13 +109,13 @@ impl Display for TimingStep { writeln!(f, "timing:")?; writeln!(f, " name: {}", self.name)?; writeln!(f, " tags: {:?}", self.tags)?; - writeln!(f, " total: {}", self.total.as_secs_f64())?; + writeln!(f, " total: {}", self.total.as_micros())?; writeln!( f, " breakdown: {:?}", self.breakdown .iter() - .map(Duration::as_millis) + .map(Duration::as_micros) .collect::>() )?; Ok(()) @@ -126,6 +126,8 @@ fn serialize_duration_total(total: &Duration, serializer: S) -> Result(breakdown: &[Duration], serializer: S) -> Res where S: Serializer, { - let breakdown_millis: Vec<_> = breakdown.iter().map(Duration::as_micros).collect(); - breakdown_millis.serialize(serializer) + // Clock granularity is ~50-100 ns. + // Serialize as micros to avoid reporting false precision. + let breakdown_micros: Vec<_> = breakdown.iter().map(Duration::as_micros).collect(); + breakdown_micros.serialize(serializer) } fn write_metric_section(f: &mut Formatter<'_>, title: &str, metrics: &[T]) -> fmt::Result From cacb7ed3adcaed793afcdc354af6a074924c2315 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Tue, 12 May 2026 21:45:28 -0700 Subject: [PATCH 25/31] toggle time display --- infra/nightly-resources/web/index.html | 5 ++ infra/nightly-resources/web/index.js | 73 +++++++++++++++++++------- infra/nightly-resources/web/style.css | 15 ++++++ 3 files changed, 73 insertions(+), 20 deletions(-) diff --git a/infra/nightly-resources/web/index.html b/infra/nightly-resources/web/index.html index 45a0b73f9..d4a2d12c2 100644 --- a/infra/nightly-resources/web/index.html +++ b/infra/nightly-resources/web/index.html @@ -9,6 +9,11 @@

Poach Nightly

+
+ Time Display + + +

Loading data/data.json...

diff --git a/infra/nightly-resources/web/index.js b/infra/nightly-resources/web/index.js index a4752efb1..9b5a80545 100644 --- a/infra/nightly-resources/web/index.js +++ b/infra/nightly-resources/web/index.js @@ -2,6 +2,7 @@ import { convertToTable } from "./table.js"; const STATE = { activeSuite: null, + timeDisplay: "raw", }; const GLOBAL_DATA = { @@ -21,16 +22,49 @@ async function load() { } GLOBAL_DATA.data = await response.json(); + statusNode.textContent = "Loaded data/data.json"; + GLOBAL_DATA.suites = [ ...new Set(GLOBAL_DATA.data.passing_benchmarks.map((x) => x.suite_name)), ].sort(); STATE.activeSuite = GLOBAL_DATA.suites[0] ?? null; - statusNode.textContent = "Loaded data/data.json"; - renderSummary(); + // Set up interactive elements + setupSuiteSelectors(); + setupTimeDisplaySelector(); - renderSuiteSelectors(); - renderTable(); + render(); +} + +function setupTimeDisplaySelector() { + for (const radio of document.querySelectorAll('input[name="time-display"]')) { + radio.addEventListener("change", () => { + if (radio.checked) { + STATE.timeDisplay = radio.value; + render(); + } + }); + } +} + +function displayTime(rawValue) { + const ONE_MIN = 60000000; + const ONE_SEC = 1000000; + const ONE_MILLI = 1000; + if (STATE.timeDisplay === "raw") { + return `${rawValue} μs`; + } else { + console.assert(STATE.timeDisplay === "readable"); + if (rawValue >= ONE_MIN) { + return `${(rawValue / ONE_MIN).toFixed(2)} min`; + } else if (rawValue >= ONE_SEC) { + return `${(rawValue / ONE_SEC).toFixed(2)} s`; + } else if (rawValue >= ONE_MILLI) { + return `${(rawValue / ONE_MILLI).toFixed(2)} ms`; + } else { + return `${rawValue} μs`; + } + } } function renderSummary() { @@ -53,13 +87,13 @@ function renderSummary() { document.querySelector("#summary-text").textContent = `Passing Benchmarks: ${numPassing} | ` + `Failing Benchmarks: ${numFailing} | ` + - `Nightly time: ${totalTime} μs | ` + - `Rule running: ${ruleMicros} μs | ` + - `Extraction: ${extractMicros} μs | ` + - `Other: ${otherMicros} μs`; + `Nightly time: ${displayTime(totalTime)} | ` + + `Rule running: ${displayTime(ruleMicros)} | ` + + `Extraction: ${displayTime(extractMicros)} | ` + + `Other: ${displayTime(otherMicros)}`; } -function renderSuiteSelectors() { +function setupSuiteSelectors() { document.querySelector("#suite-tabs").innerHTML = GLOBAL_DATA.suites .map( (suite) => @@ -104,23 +138,22 @@ function renderTable() {

${benchmarks.length} benchmarks | ${totalTime} s

`; - const columns = [ - "Benchmark", - "Wall Time (μs)", - "Rules (μs)", - "Extraction (μs)", - "Other (μs)", - ]; + const columns = ["Benchmark", "Wall Time", "Rules", "Extraction", "Other"]; const rows = benchmarks.map((b) => ({ Benchmark: b.benchmark_name, - "Wall Time (μs)": b.wall_time_micros, - "Rules (μs)": b.report.rule_micros, - "Extraction (μs)": b.report.extraction_micros, - "Other (μs)": b.report.other_micros, + "Wall Time": displayTime(b.wall_time_micros), + Rules: displayTime(b.report.rule_micros), + Extraction: displayTime(b.report.extraction_micros), + Other: displayTime(b.report.other_micros), })); const tableDiv = document.querySelector("#active-suite-table"); tableDiv.innerHTML = ""; tableDiv.appendChild(convertToTable(columns, rows)); } + +function render() { + renderSummary(); + renderTable(); +} diff --git a/infra/nightly-resources/web/style.css b/infra/nightly-resources/web/style.css index 8eb1262d4..dd70666f4 100644 --- a/infra/nightly-resources/web/style.css +++ b/infra/nightly-resources/web/style.css @@ -12,6 +12,21 @@ body { .page { width: 100%; padding: 24px; + position: relative; +} + +#time-display-selector { + position: absolute; + top: 24px; + right: 24px; + display: flex; + gap: 12px; + border: 1px solid #ccc; + padding: 8px 12px; +} + +#time-display-selector legend { + padding: 0 4px; } h1 { From ac3765cf2fe5e153cce1b53f6040ad50bb0f02e6 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Tue, 12 May 2026 21:51:35 -0700 Subject: [PATCH 26/31] fix sorting --- infra/nightly-resources/web/index.js | 17 ++++++++++++----- infra/nightly-resources/web/table.js | 16 ++++++++++++++-- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/infra/nightly-resources/web/index.js b/infra/nightly-resources/web/index.js index 9b5a80545..09f01a708 100644 --- a/infra/nightly-resources/web/index.js +++ b/infra/nightly-resources/web/index.js @@ -142,15 +142,22 @@ function renderTable() { const rows = benchmarks.map((b) => ({ Benchmark: b.benchmark_name, - "Wall Time": displayTime(b.wall_time_micros), - Rules: displayTime(b.report.rule_micros), - Extraction: displayTime(b.report.extraction_micros), - Other: displayTime(b.report.other_micros), + "Wall Time": b.wall_time_micros, + Rules: b.report.rule_micros, + Extraction: b.report.extraction_micros, + Other: b.report.other_micros, })); + const displayFns = { + "Wall Time": displayTime, + Rules: displayTime, + Extraction: displayTime, + Other: displayTime, + }; + const tableDiv = document.querySelector("#active-suite-table"); tableDiv.innerHTML = ""; - tableDiv.appendChild(convertToTable(columns, rows)); + tableDiv.appendChild(convertToTable(columns, rows, displayFns)); } function render() { diff --git a/infra/nightly-resources/web/table.js b/infra/nightly-resources/web/table.js index e63e36b4b..9666695e0 100644 --- a/infra/nightly-resources/web/table.js +++ b/infra/nightly-resources/web/table.js @@ -14,10 +14,15 @@ * Extra values are ignored. Missing values are displayed as `-` in the table * If empty, a table with only the header row will be returned * + * @param {Object} [displayFns] + * Optional map from column name -> formatter function (rawValue) => string. + * Sorting still uses the raw value from `rows`; rendering uses the formatted + * string. Columns not listed are rendered as-is. + * * @return An HTML
DOM element * Sortable by column, defaults to sorted by first column */ -export function convertToTable(columns, rows) { +export function convertToTable(columns, rows, displayFns = {}) { const table = document.createElement("table"); const STATE = { @@ -83,7 +88,14 @@ export function convertToTable(columns, rows) { const tr = document.createElement("tr"); for (const column of columns) { const td = document.createElement("td"); - td.textContent = row[column] ?? "-"; // We want to leave 0 and "" intact, so don't use || + const rawValue = row[column]; + if (rawValue === undefined || rawValue === null) { + td.textContent = "-"; + } else if (displayFns[column]) { + td.textContent = displayFns[column](rawValue); + } else { + td.textContent = rawValue; + } tr.appendChild(td); } tbody.appendChild(tr); From c2584bef7e25e41a63f313b3acc90290760e73bf Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Tue, 12 May 2026 21:52:43 -0700 Subject: [PATCH 27/31] switch default --- infra/nightly-resources/web/index.html | 4 ++-- infra/nightly-resources/web/index.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/infra/nightly-resources/web/index.html b/infra/nightly-resources/web/index.html index d4a2d12c2..d8dda7f72 100644 --- a/infra/nightly-resources/web/index.html +++ b/infra/nightly-resources/web/index.html @@ -11,8 +11,8 @@

Poach Nightly

Time Display - - + +

Loading data/data.json...

diff --git a/infra/nightly-resources/web/index.js b/infra/nightly-resources/web/index.js index 09f01a708..3615bbbf4 100644 --- a/infra/nightly-resources/web/index.js +++ b/infra/nightly-resources/web/index.js @@ -2,7 +2,7 @@ import { convertToTable } from "./table.js"; const STATE = { activeSuite: null, - timeDisplay: "raw", + timeDisplay: "readable", }; const GLOBAL_DATA = { From c3865b2d98482038945861042c154486c9728834 Mon Sep 17 00:00:00 2001 From: Haobin Ni Date: Mon, 18 May 2026 10:20:43 -0700 Subject: [PATCH 28/31] Keep main up-to-date with PTP_Vanilla's nightly improvements --- infra/nightly.py | 8 ++++- src/poach.rs | 89 +++++------------------------------------------- 2 files changed, 15 insertions(+), 82 deletions(-) diff --git a/infra/nightly.py b/infra/nightly.py index 7802fddcc..95eef4469 100644 --- a/infra/nightly.py +++ b/infra/nightly.py @@ -69,7 +69,7 @@ def summarize_report(report): else: other_micros += time_step["total"] - # No sizes in vanilla egglog reports + # Add size and other information to the report here return { "rule_micros": rule_micros, @@ -88,6 +88,12 @@ def run_benchmarks(benchmark_dir): # For this treatment, we don't do anything at train time, # we just use the train benchmarks at serve time + # TODO: invoke the poach commands appropriate for this branch (e.g. + # `poach train ...` and/or `poach serve ...`) for each benchmark file + # and append one result dict per command to `results`. Each result + # must include at least: "suite_name", "benchmark_name", "status", + # "wall_time_micros" (plus any branch-specific fields like "phase"). + results = [] failing_benchmarks = [] for benchmark in benchmarks: diff --git a/src/poach.rs b/src/poach.rs index 5de7cc439..1ade70033 100644 --- a/src/poach.rs +++ b/src/poach.rs @@ -1,6 +1,4 @@ -use poach::{EGraph, report::Reporter}; - -use std::{fs::File, io::Write, path::PathBuf, process::exit}; +use std::path::PathBuf; use clap::{Args, Parser, Subcommand}; @@ -113,94 +111,23 @@ pub fn poach() { fine_tune(arg); } Commands::Test(arg) => { - eprintln!("test({:?})", arg); - //TODO: run vanilla egglog tests + println!("test({:?})", arg); } } // TODO handle report IO } -/// VanillaEgglog's model is just unit -/// Still, it would create an empty file fn train(arg: TrainArgs) { - let _ = File::create(arg.output_model_file.as_path()); + println!("train({:?})", arg); + //TODO } -/// VanillaEgglog fn serve(arg: ServeArgs) { - match arg.mode { - ServeMode::Streaming => { - let mut egraph = EGraph::default(); - - rayon::ThreadPoolBuilder::new() - .num_threads(1) - .build_global() - .unwrap(); - - match egraph.repl(poach::RunMode::Normal) { - Ok(_) => {} - _ => { - exit(-1); - } - } - } - ServeMode::Single { input_file: input } => { - let mut egraph = EGraph::default(); - - rayon::ThreadPoolBuilder::new() - .num_threads(1) - .build_global() - .unwrap(); - - let program = std::fs::read_to_string(input.as_path()).unwrap_or_else(|_| { - let arg = input.to_string_lossy(); - panic!("Failed to read file {arg}") - }); - - let result: Result<_, poach::Error> = if arg.debug { - let filename = Some(input.to_str().unwrap().into()); - match egraph.parser.get_program_from_string(filename, &program) { - Ok(parsed) => { - let mut reporter = Reporter::new(); - let result = egraph.run_program_with_reporter(parsed, &mut reporter); - if result.is_ok() { - serde_json::to_writer(&mut std::io::stderr(), &reporter.build_report()) - .expect("failed to serialize debug report"); - writeln!(std::io::stderr()).expect("failed to terminate debug report"); - } - result - } - Err(err) => Err(err.into()), - } - } else { - egraph - .parse_and_run_program(Some(input.to_str().unwrap().into()), &program) - .map_err(Into::into) - }; - - match result { - Ok(msgs) => { - for msg in msgs { - print!("{msg}"); - } - } - _ => { - exit(-1); - } - } - } - ServeMode::Batch { - input_dir: _, - output_dir: _, - } => { - //TODO - panic!("Batch not implemented"); - } - } + println!("serve({:?})", arg); + //TODO } -/// VanillaEgglog's model is just unit -/// Still, it would create an empty file fn fine_tune(arg: FineTuneArgs) { - let _ = File::create(arg.output_model_file.as_path()); + println!("fine_tune({:?})", arg); + //TODO } From 2aa3dded9a95e6b7026ba356fd3e452bc1e34cd9 Mon Sep 17 00:00:00 2001 From: Haobin Ni Date: Mon, 18 May 2026 10:51:36 -0700 Subject: [PATCH 29/31] Fixing obvious erros --- Cargo.lock | 10 ---------- Cargo.toml | 1 - infra/nightly-resources/web/index.js | 2 +- infra/nightly.py | 4 ++-- 4 files changed, 3 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 79e7c8882..b2ef14f63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1219,7 +1219,6 @@ dependencies = [ "rustc-hash", "serde", "serde_json", - "serde_millis", "testing_logger", "thiserror", "tracing", @@ -1496,15 +1495,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_millis" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e2dc780ca5ee2c369d1d01d100270203c4ff923d2a4264812d723766434d00" -dependencies = [ - "serde", -] - [[package]] name = "sha2" version = "0.10.9" diff --git a/Cargo.toml b/Cargo.toml index 465755148..cdcbd20ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,7 +150,6 @@ lazy_static = "1.5.0" serde = { workspace = true } tracing-subscriber = "0.3.23" tracing = "0.1.44" -serde_millis = "0.1.1" [build-dependencies] chrono = { workspace = true, features = ["now"], optional = true } diff --git a/infra/nightly-resources/web/index.js b/infra/nightly-resources/web/index.js index 3615bbbf4..dcf54fbe8 100644 --- a/infra/nightly-resources/web/index.js +++ b/infra/nightly-resources/web/index.js @@ -135,7 +135,7 @@ function renderTable() { document.querySelector("#active-suite-summary").innerHTML = `

${STATE.activeSuite}

-

${benchmarks.length} benchmarks | ${totalTime} s

+

${benchmarks.length} benchmarks | ${displayTime(totalTime)}

`; const columns = ["Benchmark", "Wall Time", "Rules", "Extraction", "Other"]; diff --git a/infra/nightly.py b/infra/nightly.py index 95eef4469..420cad8e6 100644 --- a/infra/nightly.py +++ b/infra/nightly.py @@ -40,7 +40,7 @@ def run_command(cmd): time_micros = (time.perf_counter_ns() - started) // 1000 if cmd_result.returncode != 0: return { - "cmd": cmd, + "cmd": " ".join(cmd), "status": "error", "wall_time_micros": time_micros } @@ -124,4 +124,4 @@ def run_benchmarks(benchmark_dir): if len(sys.argv) != 2: raise SystemExit(f"Usage: nightly.py ") - main(sys.argv[1]) \ No newline at end of file + main(sys.argv[1]) From f1b4dbd7de170d349ff4217a0317c687d29a846e Mon Sep 17 00:00:00 2001 From: Haobin Ni Date: Mon, 18 May 2026 14:24:48 -0700 Subject: [PATCH 30/31] Fix --- infra/nightly-resources/web/index.js | 2 +- infra/nightly.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/nightly-resources/web/index.js b/infra/nightly-resources/web/index.js index dcf54fbe8..0c71de62a 100644 --- a/infra/nightly-resources/web/index.js +++ b/infra/nightly-resources/web/index.js @@ -17,7 +17,7 @@ async function load() { const response = await fetch("./data/data.json"); if (!response.ok) { - statusNode.textContent = `Failed to load data/data.json: ${error}`; + statusNode.textContent = `Failed to load data/data.json: ${response.status} ${response.statusText}`; return; } diff --git a/infra/nightly.py b/infra/nightly.py index 420cad8e6..a70647ad4 100644 --- a/infra/nightly.py +++ b/infra/nightly.py @@ -1,4 +1,4 @@ -#!/user/bin/env python3 +#!/usr/bin/env python3 import json import subprocess From 08b2b102b91c420248b899a666fffb0a440b9ca8 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Mon, 18 May 2026 17:21:54 -0700 Subject: [PATCH 31/31] remove extra prints --- infra/nightly.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/infra/nightly.py b/infra/nightly.py index 6efb3fcb0..30166c93c 100644 --- a/infra/nightly.py +++ b/infra/nightly.py @@ -154,7 +154,6 @@ def run_benchmarks(benchmark_dir): ] train_result = run_command(train_command, summarize_train_report) if train_result["status"] == "success": - print(f"Train Success: {benchmark_name}") report["train"] = train_result else: print(f"Failure: {benchmark_name}") @@ -172,7 +171,6 @@ def run_benchmarks(benchmark_dir): serve_result = run_command(serve_command, summarize_serve_report) if serve_result["status"] == "success": - print(f"Serve Success: {benchmark_name}") report["serve"] = serve_result reports.append(report) else: