use std::collections::{BTreeMap, BTreeSet};
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use henad_compute::entry::ModelEntry;
use henad_compute::gpu::GpuContext;
use henad_core::explore::measure::MeasurePlan;
use henad_core::explore::outcome::RunOutcome;
use henad_core::explore::plan::Plan;
use henad_core::explore::spec::SweepSpec;
use henad_core::export::csv::parse_records;
use henad_models::example_models;
use henad_compute::fault::install_panic_hook;
use crate::exec::{ActiveRun, BatchEnd, Concurrency, ExecutionLayout, Executor, RunRequest, RunSink, SweepControl};
use crate::handle::SweepOutput;
use crate::output::manifest::{Manifest, RecordedBuild};
use crate::output::runs_csv::{OUTCOME_COLUMNS, TIMING_COLUMNS};
use crate::output::{MANIFEST_FILE, RUNS_FILE, SERIES_FILE, SUMMARY_FILE};
use crate::probe::ProbeReport;
use crate::progress::{NoProgress, Progress, ProgressEvent};
use crate::sweep::{ExploreError, Provenance, SweepOptions, SweepReport, SweepWarning, plan_spec, run_spec};
use crate::testing::{TestDeviceRequest, headless_test_device};
pub fn headless_device() -> Option<GpuContext> {
headless_test_device(&TestDeviceRequest::raised(example_models().gpu_needs()))
}
pub fn baseline_device() -> Option<GpuContext> {
headless_test_device(&TestDeviceRequest::baseline())
}
pub fn entry(id: &str, gpu: Option<&GpuContext>) -> ModelEntry {
example_models()
.lookup(id, gpu)
.cloned()
.unwrap_or_else(|error| panic!("{error}"))
}
pub struct ScratchDir {
path: PathBuf,
}
impl ScratchDir {
pub fn new(name: &str) -> Self {
static NEXT: AtomicU64 = AtomicU64::new(0);
let index = NEXT.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!("henad-explore-{name}-{}-{index}", std::process::id()));
remove(&path);
Self { path }
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl Drop for ScratchDir {
fn drop(&mut self) {
remove(&self.path);
}
}
fn remove(path: &Path) {
if path.exists() {
std::fs::remove_dir_all(path).expect("the scratch directory can be removed");
}
}
pub fn other_engine() -> RecordedBuild {
let mut other = RecordedBuild::engine();
other.commit = "0ther000".to_owned();
other.dirty = Some(false);
other.source_hash = Some(other.source_hash.unwrap_or_default() ^ 1);
other
}
pub fn provenance() -> Provenance {
Provenance::new(henad_core::build_info!(), vec!["henad-explore-tests".to_owned()])
}
pub fn sweep(
entry: &ModelEntry,
gpu: Option<&GpuContext>,
spec: &SweepSpec,
output_dir: &Path,
concurrency: Concurrency,
) -> SweepReport {
let mut options = sweep_options(false);
options.concurrency = concurrency;
sweep_with(entry, gpu, spec, output_dir, &options, &mut NoProgress).expect("the sweep runs")
}
pub fn sweep_with(
entry: &ModelEntry,
gpu: Option<&GpuContext>,
spec: &SweepSpec,
output_dir: &Path,
options: &SweepOptions,
progress: &mut dyn Progress,
) -> Result<SweepReport, ExploreError> {
install_panic_hook();
let output = SweepOutput::Directory(output_dir.to_owned());
run_spec(entry, gpu, spec, output, options, progress).map(|record| record.report)
}
pub fn dry_run(
entry: &ModelEntry,
spec: &SweepSpec,
folder: Option<&Path>,
options: &SweepOptions,
progress: &mut dyn Progress,
) -> Result<SweepReport, ExploreError> {
plan_spec(entry, None, spec, folder, options, progress)
}
pub fn sweep_options(resume: bool) -> SweepOptions {
let mut options = SweepOptions::new(provenance());
options.resume = resume;
options
}
#[derive(Debug, Default)]
pub struct Recorder {
pub committed: Vec<u64>,
pub warnings: Vec<SweepWarning>,
}
impl Progress for Recorder {
fn report(&mut self, event: &ProgressEvent<'_>) {
match event {
ProgressEvent::RunCommitted(outcome) => self.committed.push(outcome.run.run_id),
ProgressEvent::Warned(warning) => self.warnings.push((*warning).clone()),
ProgressEvent::Planned(_)
| ProgressEvent::Progressed(_)
| ProgressEvent::SearchBatchTold(_)
| ProgressEvent::Ended(_) => {}
}
}
}
#[derive(Debug)]
pub struct SecondResume<'a> {
entry: &'a ModelEntry,
spec: &'a SweepSpec,
dir: &'a Path,
pub result: Option<Result<SweepReport, ExploreError>>,
}
impl<'a> SecondResume<'a> {
pub fn new(entry: &'a ModelEntry, spec: &'a SweepSpec, dir: &'a Path) -> Self {
Self {
entry,
spec,
dir,
result: None,
}
}
}
impl Progress for SecondResume<'_> {
fn report(&mut self, event: &ProgressEvent<'_>) {
if matches!(event, ProgressEvent::Planned(_)) && self.result.is_none() {
let options = sweep_options(true);
self.result = Some(sweep_with(
self.entry,
None,
self.spec,
self.dir,
&options,
&mut NoProgress,
));
}
}
}
pub fn manifest(dir: &Path) -> Manifest {
Manifest::read(&dir.join(MANIFEST_FILE)).expect("the manifest reads back")
}
pub fn rewrite_manifest(dir: &Path, change: impl FnOnce(&mut Manifest)) {
let mut recorded = manifest(dir);
change(&mut recorded);
let bytes = serde_json::to_vec_pretty(&recorded).expect("the manifest writes as JSON");
std::fs::write(dir.join(MANIFEST_FILE), bytes).expect("the manifest is written");
}
#[derive(Debug, PartialEq, Eq)]
pub struct OutputTables {
runs_text: String,
series_text: String,
summary_text: String,
pub runs: Vec<Vec<String>>,
pub series: Vec<Vec<String>>,
pub summary: Vec<Vec<String>>,
}
impl OutputTables {
pub fn read(dir: &Path) -> Self {
let read = |file: &str| std::fs::read_to_string(dir.join(file)).expect("the table is written");
let records = |text: &str| parse_records(text).expect("the table is valid CSV");
let (runs_text, series_text, summary_text) = (read(RUNS_FILE), read(SERIES_FILE), read(SUMMARY_FILE));
let runs = without_timing(records(&runs_text));
let timing = timing_positions(&runs[0]);
Self {
runs_text: with_fields_emptied(&runs_text, &timing),
series: records(&series_text),
summary: records(&summary_text),
runs,
series_text,
summary_text,
}
}
pub fn run_column(&self, name: &str) -> Vec<&str> {
column(&self.runs, name)
}
pub fn series_of(&self, run_id: u64, name: &str) -> Vec<(u64, &str)> {
let index = self.series[0]
.iter()
.position(|header| header == name)
.unwrap_or_else(|| panic!("series.csv has no column '{name}'"));
let run_id = run_id.to_string();
self.series[1..]
.iter()
.filter(|record| record[0] == run_id)
.map(|record| (record[1].parse().expect("a tick"), record[index].as_str()))
.collect()
}
pub fn summary_column(&self, name: &str) -> Vec<&str> {
column(&self.summary, name)
}
}
fn column<'t>(table: &'t [Vec<String>], name: &str) -> Vec<&'t str> {
let index = table[0]
.iter()
.position(|header| header == name)
.unwrap_or_else(|| panic!("the table has no column '{name}'"));
table[1..].iter().map(|record| record[index].as_str()).collect()
}
pub fn without_timing(mut runs: Vec<Vec<String>>) -> Vec<Vec<String>> {
let timing = timing_positions(&runs[0]);
for record in &mut runs[1..] {
for &column in &timing {
record[column].clear();
}
}
runs
}
pub fn without_clocks(mut manifest: Manifest) -> Manifest {
manifest.timestamps.started_unix_ms = 0;
manifest.timestamps.started.clear();
manifest.timestamps.finished_unix_ms = None;
manifest.timestamps.finished = None;
for session in &mut manifest.sessions {
session.started.clear();
}
manifest
}
fn timing_positions(header: &[String]) -> Vec<usize> {
let outcomes_start = header
.windows(OUTCOME_COLUMNS.len())
.rposition(|window| window.iter().zip(OUTCOME_COLUMNS).all(|(name, column)| name == column))
.expect("runs.csv has the outcome columns");
TIMING_COLUMNS
.iter()
.map(|timing| {
let offset = OUTCOME_COLUMNS
.iter()
.position(|column| column == timing)
.expect("a timing column is an outcome column");
outcomes_start + offset
})
.collect()
}
fn with_fields_emptied(text: &str, positions: &[usize]) -> String {
let mut kept = String::with_capacity(text.len());
let (mut in_header, mut position, mut quoted) = (true, 0, false);
for character in text.chars() {
let separator = !quoted && matches!(character, ',' | '\n');
if character == '"' {
quoted = !quoted;
}
if separator || in_header || !positions.contains(&position) {
kept.push(character);
}
if character == ',' && separator {
position += 1;
} else if separator {
in_header = false;
position = 0;
}
}
kept
}
pub struct CommitLimit {
control: SweepControl,
limit: usize,
committed: usize,
}
impl CommitLimit {
pub fn new(control: SweepControl, limit: usize) -> Self {
Self {
control,
limit,
committed: 0,
}
}
}
impl Progress for CommitLimit {
fn report(&mut self, event: &ProgressEvent<'_>) {
if let ProgressEvent::RunCommitted(_) = event {
self.committed += 1;
if self.committed == self.limit {
self.control.abort();
}
}
}
}
pub const HOLD_WINDOW: Duration = Duration::from_millis(500);
pub fn ticks_seen(active_runs: impl Fn() -> Vec<ActiveRun>) -> BTreeMap<u64, BTreeSet<u64>> {
let mut seen: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
let deadline = Instant::now() + HOLD_WINDOW;
while Instant::now() < deadline {
for active in active_runs() {
seen.entry(active.run.run_id).or_default().insert(active.tick);
}
std::thread::sleep(Duration::from_millis(5));
}
seen
}
pub fn planned(entry: &ModelEntry, gpu: Option<&GpuContext>, spec: &SweepSpec) -> (Plan, Arc<MeasurePlan>) {
let plan = spec.plan(&entry.schema()).expect("a valid spec");
let probe = ProbeReport::for_plan(entry, gpu, &plan).expect("the probe builds");
let measure =
MeasurePlan::new(plan.run_settings(), plan.measure_settings(), probe.columns).expect("the columns bind");
(plan, Arc::new(measure))
}
pub fn run_plan(
entry: &ModelEntry,
gpu: Option<&GpuContext>,
plan: &Plan,
measure: &Arc<MeasurePlan>,
layout: ExecutionLayout,
sink: &mut dyn RunSink,
) {
let executor =
Executor::new(entry, gpu, Arc::clone(measure), layout, SweepControl::new()).expect("the executor builds");
let requests: Vec<RunRequest<'_>> = plan.runs().map(|run| RunRequest::planned(plan, run)).collect();
let end = executor.run_batch(&requests, sink).expect("the batch runs");
assert_eq!(end, BatchEnd::Complete);
}
#[derive(Debug, Default)]
pub struct Collected(pub Vec<RunOutcome>);
impl RunSink for Collected {
fn commit(&mut self, outcome: RunOutcome) -> io::Result<()> {
self.0.push(outcome);
Ok(())
}
}
pub fn lanes(cpu_lanes: usize, threads_per_lane: usize) -> ExecutionLayout {
ExecutionLayout {
cpu_lanes,
threads_per_lane,
gpu_tracks: 0,
}
}
pub fn tracks(count: usize) -> ExecutionLayout {
ExecutionLayout {
cpu_lanes: 0,
threads_per_lane: 0,
gpu_tracks: count,
}
}
pub const ONE_TRACK: ExecutionLayout = ExecutionLayout {
cpu_lanes: 0,
threads_per_lane: 0,
gpu_tracks: 1,
};
#[test]
fn only_the_timing_fields_of_the_outcome_columns_are_emptied() {
let header = concat!(
"run_id,build_ms,",
"status,stop_reason,ticks,population,build_ms,wall_ms,steps_per_s,",
"Infected:max,note\n"
);
let record = "0,7,ok,steps,30,256,1.5,2.25,13333,\"4, or \"\"5\"\"\",\"line\r\nbreak\"\r\n";
let text = format!("{header}{record}");
let header_fields = &parse_records(&text).expect("valid CSV")[0];
assert_eq!(
timing_positions(header_fields),
[6, 7, 8],
"the parameter named build_ms is kept"
);
assert_eq!(
with_fields_emptied(&text, &[6, 7, 8]),
format!("{header}0,7,ok,steps,30,256,,,,\"4, or \"\"5\"\"\",\"line\r\nbreak\"\r\n"),
"every other byte is kept, quotes and line endings included"
);
}