slice 7: before/after benchmark runner
Executes a benchmark harness inside the mission's team container
and records the resulting metrics as a benchmark_snapshots row keyed
on (phase_id, iteration). Baseline pass (iteration=0) captures
before_metrics; each post-iteration call captures after_metrics +
computes delta vs baseline.
Rust surface:
- cm_db::repo::missions::upsert_benchmark_snapshot / benchmark_snapshots_for
- cm_api::benchmark_runner::{baseline, after_iteration, run}
- Harness enum: Auto | Criterion | CargoBench | VitestBench |
PytestBench | Shell (each with a command() vector)
- Auto detection peeks at the repo layout inside the container
(Cargo.toml → CargoBench, package.json → VitestBench, pyproject
→ PytestBench). Falls back to a Shell echo when nothing
identifiable.
- Bencher-format line parser extracts (name, ns_per_iter,
plusminus) so criterion + `cargo bench` output become structured
samples the canvas can diff.
- compute_delta pairs samples by name, emits {before_ns, after_ns,
delta_pct, direction: improved|regressed}.
API:
- POST /api/missions/{id}/benchmark { phase_id, slot, iteration? }
triggers baseline or after run and returns the mission's full
snapshot list.
- GET /api/missions/{id} now includes `benchmarks[]` in the detail
payload.
Frontend:
- New Benchmarks tab on MissionCanvas with iteration + driver
header, plus a 4-column grid (bench / before / after / Δ%) when
delta samples are present. Improved deltas render green,
regressions red.
- TS types + triggerBenchmark() helper in lib/api/missions.ts.
Wiring notes:
- team_container_for_mission reads teams.zeroclaw_container — that's
populated by topology_worker::try_team_gateway_url on first run,
so trigger baseline AFTER the mission's first phase spawns the
container.
- Not auto-fired yet by phase execution; that's the "template phase
executor" work that spans Slices 4-8. Manual API trigger works
today; automated hook is a follow-up.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
3ac3d53da7
commit
f843c9ddb1
@@ -8,7 +8,7 @@
|
||||
//
|
||||
// This replaces ResearchCanvas + LoopsCanvas after Slice 9's cutover.
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { FileText, Play, RefreshCw } from "lucide-react";
|
||||
|
||||
import {
|
||||
@@ -61,7 +61,7 @@ const TEMPLATE_LABEL: Record<TemplateKind, string> = {
|
||||
custom: "Custom",
|
||||
};
|
||||
|
||||
type Tab = "overview" | "phases" | "tasks" | "artifacts";
|
||||
type Tab = "overview" | "phases" | "tasks" | "artifacts" | "benchmarks";
|
||||
|
||||
export function MissionCanvas({
|
||||
selectedId,
|
||||
@@ -230,7 +230,7 @@ export function MissionCanvas({
|
||||
</p>
|
||||
)}
|
||||
<div style={{ display: "flex", gap: 4, marginTop: 4 }}>
|
||||
{(["overview", "phases", "tasks", "artifacts"] as Tab[]).map((t) => {
|
||||
{(["overview", "phases", "tasks", "artifacts", "benchmarks"] as Tab[]).map((t) => {
|
||||
const active = tab === t;
|
||||
const badge =
|
||||
t === "tasks"
|
||||
@@ -239,7 +239,9 @@ export function MissionCanvas({
|
||||
? mission.artifacts.length
|
||||
: t === "phases"
|
||||
? mission.phases.length
|
||||
: null;
|
||||
: t === "benchmarks"
|
||||
? mission.benchmarks.length
|
||||
: null;
|
||||
return (
|
||||
<button
|
||||
key={t}
|
||||
@@ -493,6 +495,139 @@ export function MissionCanvas({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === "benchmarks" && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{mission.benchmarks.length === 0 ? (
|
||||
<Empty label="no benchmark snapshots yet — trigger a baseline via /api/missions/{id}/benchmark or a workflow with benchmark = { mode = "before_after" }" />
|
||||
) : (
|
||||
mission.benchmarks.map((s) => {
|
||||
const delta = s.delta as
|
||||
| { kind?: string; samples?: Array<Record<string, unknown>> }
|
||||
| null
|
||||
| undefined;
|
||||
return (
|
||||
<div
|
||||
key={s.id}
|
||||
style={{
|
||||
padding: 12,
|
||||
borderRadius: 10,
|
||||
border: "1px solid rgba(255,255,255,.07)",
|
||||
background: "#101014",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: mono,
|
||||
fontSize: 10.5,
|
||||
color: s.iteration === 0 ? "#7cd6e0" : "#5fd08a",
|
||||
letterSpacing: ".08em",
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
{s.iteration === 0 ? "baseline" : `iter ${s.iteration}`}
|
||||
</span>
|
||||
{s.driver && (
|
||||
<span
|
||||
style={{
|
||||
fontFamily: mono,
|
||||
fontSize: 10,
|
||||
color: "#8a8a92",
|
||||
marginLeft: 4,
|
||||
}}
|
||||
>
|
||||
{s.driver}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
marginLeft: "auto",
|
||||
fontFamily: mono,
|
||||
fontSize: 10.5,
|
||||
color: "#6a6a72",
|
||||
}}
|
||||
>
|
||||
{new Date(s.created_at).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
{delta?.samples && delta.samples.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 100px 100px 100px",
|
||||
gap: 6,
|
||||
fontSize: 12,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<span style={{ color: "#8a8a92" }}>bench</span>
|
||||
<span style={{ color: "#8a8a92", textAlign: "right" }}>before</span>
|
||||
<span style={{ color: "#8a8a92", textAlign: "right" }}>after</span>
|
||||
<span style={{ color: "#8a8a92", textAlign: "right" }}>Δ%</span>
|
||||
{(delta.samples as Array<Record<string, unknown>>).map((row, i) => {
|
||||
const pct = Number(row["delta_pct"] ?? 0);
|
||||
const dir = String(row["direction"] ?? "");
|
||||
const color = dir === "improved" ? "#5fd08a" : "#ff8a7a";
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: mono,
|
||||
fontSize: 11.5,
|
||||
color: "#d7d7db",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{String(row["name"])}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: mono,
|
||||
fontSize: 11.5,
|
||||
color: "#a0a0a8",
|
||||
textAlign: "right",
|
||||
}}
|
||||
>
|
||||
{String(row["before_ns"])} ns
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: mono,
|
||||
fontSize: 11.5,
|
||||
color: "#a0a0a8",
|
||||
textAlign: "right",
|
||||
}}
|
||||
>
|
||||
{String(row["after_ns"])} ns
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: mono,
|
||||
fontSize: 11.5,
|
||||
color,
|
||||
textAlign: "right",
|
||||
}}
|
||||
>
|
||||
{pct > 0 ? "+" : ""}
|
||||
{pct.toFixed(1)}%
|
||||
</span>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user