Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ jobs:
scripts/check-external-schedule-artifacts.sh
cargo test --release -p akita-config --test trusted_schedule_artifact
- name: Regenerate schedule artifacts in temporary storage
# Planning measured about 18 minutes on CI with two workers, and runner
# variance alone has moved this step by about 1.3x. The bound stops a
# runaway search. Growth in search size (states, candidates,
# descriptors) fails the --planner-cost-baseline check instead; slower
# per-candidate pricing is caught only by this timeout.
timeout-minutes: 35
env:
AKITA_SCHEDULE_GEN_JOBS: 2
run: |
Expand All @@ -196,7 +202,8 @@ jobs:
--bin gen_schedule_artifacts -- "$artifact_dir" --row-progress \
--catalog-baseline artifacts/schedule-catalog.tsv \
--require-catalog-baseline-match \
--catalog-report "$RUNNER_TEMP/akita-schedule-baseline-comparison.tsv"
--catalog-report "$RUNNER_TEMP/akita-schedule-baseline-comparison.tsv" \
--planner-cost-baseline artifacts/schedule-planner-cost.tsv
diff -ru artifacts/schedules "$artifact_dir"
- name: Direct modeled SIS cost for every schedule
run: |
Expand Down
116 changes: 116 additions & 0 deletions artifacts/schedule-planner-cost.tsv

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions crates/akita-planner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,25 @@ repository permits catalog-breaking revisions, so baseline/current policy is
reviewed from the checked evidence. Same-head drift remains an automatic
failure under `--check-catalog`.

Planner search size is guarded the same way. `--planner-cost-snapshot <path>`
writes one row per regenerated row with four deterministic search-work
counters: suffix calls, completed states, generated fold candidates, and
descriptor builds. A reused preplanned row reports the work of the search that
produced it. The counts do not depend on timing or worker count. They measure
how large the search is, not how expensive each candidate is to price. A change
that makes edge, terminal, or grinding pricing slower without visiting more
states or building more candidates passes this check; the regeneration step's
CI timeout is the only backstop for that kind of slowdown. The tracked
baseline is `artifacts/schedule-planner-cost.tsv`; CI passes it through
`--planner-cost-baseline <snapshot>`, which fails when a row is added or
removed or when any counter grows by more than a quarter. When a planner change
intentionally moves rows or costs, regenerate the baseline with the complete
family set:

```bash
scripts/generate-schedule-artifacts.sh --planner-cost-snapshot artifacts/schedule-planner-cost.tsv
```

Targeted generation leaves the shared `mod.rs` wiring complete. Before a
planner change is committed, run the unfiltered command above so every tracked
family is regenerated.
Expand Down
103 changes: 101 additions & 2 deletions crates/akita-planner/src/bin/gen_schedule_artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
mod catalog_policy_report;
mod catalog_snapshot;
mod generation_output_path;
mod planner_cost_snapshot;

use catalog_policy_report::catalog_policy_signature;
use generation_output_path::validate_explicit_output_isolation;
Expand Down Expand Up @@ -40,6 +41,8 @@ struct ParsedArgs {
catalog_snapshot: Option<PathBuf>,
catalog_baseline: Option<PathBuf>,
require_catalog_baseline_match: bool,
planner_cost_snapshot: Option<PathBuf>,
planner_cost_baseline: Option<PathBuf>,
row_progress: bool,
family_filter: Option<Vec<String>>,
explicit_rows: ExplicitRows,
Expand All @@ -63,6 +66,7 @@ fn usage() -> &'static str {
--bin gen_schedule_artifacts -- <output-dir> [--check-catalog] \
[--catalog-report <path>] [--catalog-snapshot <path>] \
[--catalog-baseline <snapshot>] [--require-catalog-baseline-match] \
[--planner-cost-snapshot <path>] [--planner-cost-baseline <snapshot>] \
[--row-progress] \
[family_name ...]\n\
positional family names select only those generated families; omit them \
Expand Down Expand Up @@ -136,6 +140,8 @@ fn parse_args_from(raw_args: Vec<String>) -> Result<ParsedArgs, String> {
let mut catalog_snapshot = None;
let mut catalog_baseline = None;
let mut require_catalog_baseline_match = false;
let mut planner_cost_snapshot = None;
let mut planner_cost_baseline = None;
let mut row_progress = false;
let mut family_args = Vec::new();
let mut explicit_rows = ExplicitRows::default();
Expand Down Expand Up @@ -184,6 +190,26 @@ fn parse_args_from(raw_args: Vec<String>) -> Result<ParsedArgs, String> {
require_catalog_baseline_match = true;
i += 1;
}
"--planner-cost-snapshot" => {
let value = raw_args
.get(i + 1)
.ok_or_else(|| "--planner-cost-snapshot requires a path".to_string())?;
if planner_cost_snapshot.is_some() {
return Err("--planner-cost-snapshot may be supplied only once".to_string());
}
planner_cost_snapshot = Some(PathBuf::from(value));
i += 2;
}
"--planner-cost-baseline" => {
let value = raw_args
.get(i + 1)
.ok_or_else(|| "--planner-cost-baseline requires a path".to_string())?;
if planner_cost_baseline.is_some() {
return Err("--planner-cost-baseline may be supplied only once".to_string());
}
planner_cost_baseline = Some(PathBuf::from(value));
i += 2;
}
"--final-group" => {
let value = raw_args
.get(i + 1)
Expand Down Expand Up @@ -235,8 +261,11 @@ fn parse_args_from(raw_args: Vec<String>) -> Result<ParsedArgs, String> {
} else {
Some(family_args)
};
let catalog_rows_requested =
check_catalog || catalog_snapshot.is_some() || catalog_baseline.is_some();
let catalog_rows_requested = check_catalog
|| catalog_snapshot.is_some()
|| catalog_baseline.is_some()
|| planner_cost_snapshot.is_some()
|| planner_cost_baseline.is_some();
if catalog_rows_requested && explicit_rows.final_group.is_some() {
return Err("catalog checks and snapshots require the standard artifact rows".to_string());
}
Expand All @@ -252,13 +281,20 @@ fn parse_args_from(raw_args: Vec<String>) -> Result<ParsedArgs, String> {
if require_catalog_baseline_match && catalog_baseline.is_none() {
return Err("--require-catalog-baseline-match requires --catalog-baseline".to_string());
}
if planner_cost_baseline.is_some() && family_filter.is_some() {
return Err(
"--planner-cost-baseline requires the complete generated family set".to_string(),
);
}
Ok(ParsedArgs {
base_dir,
check_catalog,
catalog_report,
catalog_snapshot,
catalog_baseline,
require_catalog_baseline_match,
planner_cost_snapshot,
planner_cost_baseline,
row_progress,
family_filter,
explicit_rows,
Expand Down Expand Up @@ -485,6 +521,31 @@ fn materialized_snapshot_rows(
.collect()
}

fn planner_cost_rows(
spec: &EmitSpec,
entries: &[akita_planner::emit::MaterializedEntry],
) -> Result<Vec<planner_cost_snapshot::PlannerCostRow>, String> {
entries
.iter()
.map(|entry| {
let key = entry.key();
let cost = entry.planner_cost().ok_or_else(|| {
format!(
"{}: {} has no planner cost",
spec.family_name,
catalog_logical_key(&key)
)
})?;
Ok(planner_cost_snapshot::PlannerCostRow {
family: spec.family_name.to_string(),
logical_key: catalog_logical_key(&key),
lookup_key_digest: catalog_lookup_key_digest(&key),
cost,
})
})
.collect()
}

impl ExplicitRows {
fn has_family(&self, family: &GeneratedFamily) -> bool {
self.final_group
Expand Down Expand Up @@ -679,10 +740,14 @@ fn main() -> Result<(), String> {
};
let mut changed_catalog_rows = 0usize;
let mut current_catalog_rows = Vec::new();
let collect_planner_cost =
args.planner_cost_snapshot.is_some() || args.planner_cost_baseline.is_some();
let mut current_planner_cost_rows = Vec::new();
let outputs = render_schedule_artifact_outputs_with_validation(
&specs,
MaterializationDiagnostics {
row_progress: args.row_progress,
planner_cost: collect_planner_cost,
},
|spec, entries| {
if check_catalog {
Expand All @@ -695,6 +760,9 @@ fn main() -> Result<(), String> {
if collect_catalog_snapshot {
current_catalog_rows.extend(materialized_snapshot_rows(spec, entries)?);
}
if collect_planner_cost {
current_planner_cost_rows.extend(planner_cost_rows(spec, entries)?);
}
Ok(())
},
)?;
Expand Down Expand Up @@ -754,6 +822,37 @@ fn main() -> Result<(), String> {
));
}
}
if let Some(path) = &args.planner_cost_snapshot {
let snapshot = planner_cost_snapshot::write_snapshot(current_planner_cost_rows.clone())?;
fs::write(path, snapshot).map_err(|error| format!("write {}: {error}", path.display()))?;
eprintln!("wrote planner cost snapshot {}", path.display());
}
if let Some(path) = &args.planner_cost_baseline {
let baseline = fs::read_to_string(path)
.map_err(|error| format!("read {}: {error}", path.display()))?;
let comparison = planner_cost_snapshot::compare_snapshots(
planner_cost_snapshot::parse_snapshot(&baseline)?,
current_planner_cost_rows,
)?;
eprint!("{}", comparison.report);
for ((name, baseline_total), current_total) in planner_cost_snapshot::COUNTER_NAMES
.into_iter()
.zip(comparison.baseline_totals)
.zip(comparison.current_totals)
{
eprintln!(
"planner cost total {name}: baseline {baseline_total}, current {current_total}"
);
}
if comparison.failures != 0 {
return Err(format!(
"planner search work differs from {} in {} places; \
regenerate it with --planner-cost-snapshot if the change is intended",
path.display(),
comparison.failures,
));
}
}
let publish_started = args.row_progress.then(Instant::now);
if args.row_progress {
eprintln!(
Expand Down
Loading
Loading