Some checks are pending
build-prerelease / Publish to rpm.lair.cafe (unstable) (push) Blocked by required conditions
build-prerelease / Resolve version stamps + change detection (push) Successful in 31s
build-prerelease / Lint (fmt + clippy) (push) Successful in 2m21s
build-prerelease / Build cortex binary (push) Successful in 2m27s
build-prerelease / Build helexa-bench binary (push) Successful in 2m44s
build-prerelease / Test (push) Successful in 4m32s
build-prerelease / Build neuron-ampere (push) Successful in 2m7s
build-prerelease / Build neuron-ada (push) Successful in 2m28s
build-prerelease / Build neuron-blackwell (push) Successful in 2m59s
build-prerelease / Package cortex RPM (push) Successful in 1m20s
build-prerelease / Package helexa-bench RPM (push) Successful in 1m19s
build-prerelease / Package helexa-neuron-ada RPM (push) Successful in 1m39s
build-prerelease / Package helexa-neuron-ampere RPM (push) Successful in 1m39s
build-prerelease / Package helexa-neuron-blackwell RPM (push) Successful in 1m42s
Part A — helexa-bench read API: - [api] config (enabled, listen :13132); WAL on the store so API reads never block the sweep writer. - store read methods: summary, series (chronological per-build medians), runs (filtered), dimensions, run_count. - api.rs: axum /api/health|dimensions|summary|series|runs, permissive CORS (UI is a separate origin). The `run` daemon binds the API alongside the sweep; new `serve` subcommand serves API-only. - listener plumbing (bench gains a port): data/helexa-bench-firewalld.xml, spec install, deploy-bench /api/health probe + firewalld step, sudoers firewall-cmd grants, [api] in example + bob.toml. - 5 API tests + serve smoke. Part B — bench/ Vite + React-SWC-TS app (router, react-bootstrap, recharts): Overview (summary table), Trends (decode tok/s & TTFT across build SHAs), Runs (filterable explorer). Typed API client with VITE_API_BASE + dev proxy to bob. npm build/typecheck clean. Hosted separately from the API (per design); .gitignore excludes node_modules/dist. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
154 lines
5.1 KiB
Rust
154 lines
5.1 KiB
Rust
//! helexa-bench CLI.
|
|
//!
|
|
//! - `run` — continuous daemon (systemd default): sweep, sleep, repeat.
|
|
//! - `once` — a single sweep, then exit (manual / CI).
|
|
//! - `report` — render the SQLite store as a results table.
|
|
//!
|
|
//! Runs on a single-threaded runtime: the workload is batch-1 sequential
|
|
//! (one request at a time, the regime we measure), and it lets the
|
|
//! SQLite connection live across awaits without `Sync` gymnastics.
|
|
|
|
use anyhow::{Context, Result};
|
|
use clap::{Parser, Subcommand};
|
|
use helexa_bench::api;
|
|
use helexa_bench::config::BenchConfig;
|
|
use helexa_bench::report;
|
|
use helexa_bench::store::Store;
|
|
use helexa_bench::sweep::Sweeper;
|
|
use tracing_subscriber::EnvFilter;
|
|
|
|
#[derive(Parser)]
|
|
#[command(name = "helexa-bench")]
|
|
#[command(about = "Continuous version-aware benchmark harness for the neuron fleet")]
|
|
#[command(version)]
|
|
struct Cli {
|
|
#[command(subcommand)]
|
|
command: Command,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum Command {
|
|
/// Run sweeps continuously, pausing `sweep_interval_secs` between them.
|
|
Run {
|
|
#[arg(short, long, default_value = "helexa-bench.toml")]
|
|
config: String,
|
|
},
|
|
/// Run a single sweep over all targets, then exit.
|
|
Once {
|
|
#[arg(short, long, default_value = "helexa-bench.toml")]
|
|
config: String,
|
|
},
|
|
/// Serve the read-only JSON API only (no sweeping).
|
|
Serve {
|
|
#[arg(short, long, default_value = "helexa-bench.toml")]
|
|
config: String,
|
|
},
|
|
/// Render recorded results. Uses `--db` if given, else the db_path
|
|
/// from `--config`.
|
|
Report {
|
|
#[arg(short, long, default_value = "helexa-bench.toml")]
|
|
config: String,
|
|
/// Override the SQLite path (skips reading the config file).
|
|
#[arg(long)]
|
|
db: Option<String>,
|
|
/// Output format.
|
|
#[arg(long, default_value = "md")]
|
|
format: Format,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Copy, clap::ValueEnum)]
|
|
enum Format {
|
|
Md,
|
|
Json,
|
|
}
|
|
|
|
fn main() -> Result<()> {
|
|
tracing_subscriber::fmt()
|
|
.with_env_filter(
|
|
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
|
|
)
|
|
.init();
|
|
|
|
let cli = Cli::parse();
|
|
let rt = tokio::runtime::Builder::new_current_thread()
|
|
.enable_all()
|
|
.build()
|
|
.context("building tokio runtime")?;
|
|
rt.block_on(run(cli))
|
|
}
|
|
|
|
async fn run(cli: Cli) -> Result<()> {
|
|
match cli.command {
|
|
Command::Run { config } => {
|
|
let cfg = load_config(&config)?;
|
|
require_targets(&cfg)?;
|
|
// Bind the read API alongside the sweep loop (one bob service
|
|
// does both). Its own store connection; WAL keeps the sweep
|
|
// writer and the API readers from blocking each other.
|
|
if cfg.api.enabled {
|
|
let state = api::open_state(&cfg.bench.db_path)?;
|
|
let listen = cfg.api.listen.clone();
|
|
tokio::spawn(async move {
|
|
if let Err(e) = api::serve(&listen, state).await {
|
|
tracing::error!(error = %format!("{e:#}"), "bench API server exited");
|
|
}
|
|
});
|
|
}
|
|
let sweeper = Sweeper::new(cfg)?;
|
|
tracing::info!("helexa-bench started; entering continuous sweep loop");
|
|
sweeper.run_forever().await
|
|
}
|
|
Command::Serve { config } => {
|
|
let cfg = load_config(&config)?;
|
|
if !cfg.api.enabled {
|
|
anyhow::bail!("[api] enabled = false — nothing to serve");
|
|
}
|
|
let state = api::open_state(&cfg.bench.db_path)?;
|
|
tracing::info!("helexa-bench serving API only");
|
|
api::serve(&cfg.api.listen, state).await
|
|
}
|
|
Command::Once { config } => {
|
|
let cfg = load_config(&config)?;
|
|
require_targets(&cfg)?;
|
|
let sweeper = Sweeper::new(cfg)?;
|
|
let summary = sweeper.run_once().await?;
|
|
tracing::info!(
|
|
measured = summary.measured,
|
|
skipped = summary.skipped,
|
|
failed = summary.failed,
|
|
unreachable = summary.targets_unreachable,
|
|
"single sweep complete"
|
|
);
|
|
Ok(())
|
|
}
|
|
Command::Report { config, db, format } => {
|
|
let db_path = match db {
|
|
Some(p) => p,
|
|
None => load_config(&config)?.bench.db_path,
|
|
};
|
|
let store = Store::open(&db_path)?;
|
|
let rows = store.report_rows()?;
|
|
let rendered = match format {
|
|
Format::Md => report::render_markdown(&rows),
|
|
Format::Json => report::render_json(&rows)?,
|
|
};
|
|
println!("{rendered}");
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn load_config(path: &str) -> Result<BenchConfig> {
|
|
BenchConfig::load(path)
|
|
.map_err(|e| anyhow::anyhow!("{e}"))
|
|
.with_context(|| format!("loading config {path}"))
|
|
}
|
|
|
|
fn require_targets(cfg: &BenchConfig) -> Result<()> {
|
|
if cfg.targets.is_empty() {
|
|
anyhow::bail!("no targets configured — add at least one [[targets]] entry");
|
|
}
|
|
Ok(())
|
|
}
|