use std::time::{Duration, SystemTime};
use crossterm::event::KeyCode;
use super::app::{Action, App, Confirm, Effect, Exit, NoteOf, Overlay, Severity, View};
use super::start::{self, Prices, StartPlan};
use super::tasks::{Msg, Task, TaskId, TrainJob};
use super::training::{Detach, Ended, Follow, Job, Listing, RunActivity};
use crate::cli::front::Report;
use crate::events::Event;
use crate::train::TrainMetric;
const REFRESH: Duration = Duration::from_secs(2);
const ABANDONED: &str = "If its pod is still being prepared, it is deleted and the run fails, \
as Ctrl-C does on the command line; once its job is being sent, the \
run is detached instead.";
impl App {
pub(super) fn refresh_runs(&mut self) -> Vec<Effect> {
self.refreshed = self.now;
if self.training.listing.is_some() {
self.training.list_again = true;
return Vec::new();
}
let id = self.task_id();
self.training.listing = Some(id);
vec![Effect::Spawn(id, Task::Runs)]
}
pub(super) fn refresh_when_due(&mut self) -> Vec<Effect> {
let recent = matches!(
self.now.duration_since(self.refreshed),
Ok(since) if since < REFRESH
);
if self.view == View::Training && !recent {
return self.refresh_runs();
}
Vec::new()
}
pub(super) fn listed(&mut self, id: TaskId, listing: Listing) -> Vec<Effect> {
if self.training.listing != Some(id) {
return Vec::new();
}
self.training.listing = None;
self.dirty = true;
for warning in self.training.listed(listing) {
tracing::warn!("{warning}");
}
let mut effects = self.read_selected();
if std::mem::take(&mut self.training.list_again) {
effects.extend(self.refresh_runs());
}
effects
}
fn read_selected(&mut self) -> Vec<Effect> {
match self.training.to_read() {
Some(run) => self.read_series(&run),
None => Vec::new(),
}
}
fn read_series(&mut self, run: &str) -> Vec<Effect> {
let id = self.task_id();
self.training.reading.insert(run.to_string(), id);
vec![Effect::Spawn(id, Task::Series(run.to_string()))]
}
pub(super) fn series_read(
&mut self,
id: TaskId,
run: String,
series: Option<Vec<TrainMetric>>,
) -> Vec<Effect> {
if self.training.reading.get(&run) != Some(&id) {
return Vec::new();
}
self.training.reading.remove(&run);
if let Some(series) = series {
if let Some(ended) = self.training.ended.get_mut(&run) {
ended.healed = true;
}
self.training.series.insert(run, series);
self.dirty = true;
}
Vec::new()
}
pub(super) fn read_failed(&mut self, id: TaskId, error: &str) -> bool {
if self.training.listing == Some(id) {
self.training.listing = None;
self.training.error = Some(format!("cannot list the runs: {error}"));
return true;
}
let before = self.training.reading.len();
self.training.reading.retain(|_, read| *read != id);
before != self.training.reading.len()
}
pub(super) fn on_training_key(&mut self, code: KeyCode) -> Vec<Effect> {
let view = &mut self.training;
match code {
KeyCode::Up | KeyCode::Char('k') => view.selected = view.selected.saturating_sub(1),
KeyCode::Down | KeyCode::Char('j') => {
view.selected = (view.selected + 1).min(view.runs.len().saturating_sub(1));
},
KeyCode::Char('a') => return self.attach_selected(),
KeyCode::Char('c') => {
self.ask_cancel();
return Vec::new();
},
KeyCode::Char('t') => return self.prepare_start(),
_ => return Vec::new(),
}
self.read_selected()
}
fn attach_selected(&mut self) -> Vec<Effect> {
let Some(id) = self
.training
.selected_run()
.map(|row| row.record.id.clone())
else {
return Vec::new();
};
if self.leaving.is_some() {
self.say(Severity::Warn, "refused: quitting; nothing new is followed");
return Vec::new();
}
if self.training.cancelling(&id) {
self.say(Severity::Info, format!("run {id} is being cancelled"));
return Vec::new();
}
if self.training.task_of(&id).is_some() {
self.say(Severity::Info, format!("run {id} is already followed"));
return Vec::new();
}
self.training.series.remove(&id);
self.training.ended.remove(&id);
self.train(Job::Attach, &id)
}
fn start_refused(&mut self) -> bool {
self.refuse_new("one task at a time", "run")
}
fn prepare_start(&mut self) -> Vec<Effect> {
if self.start_refused() {
return Vec::new();
}
if self.prepare.is_some() {
self.say(Severity::Info, "already preparing a run");
return Vec::new();
}
let id = self.task_id();
self.prepare = Some(id);
vec![Effect::Spawn(id, Task::Prepare)]
}
pub(super) fn prepared(&mut self, plan: Result<StartPlan, String>) -> Vec<Effect> {
self.prepare = None;
if self.leaving.is_some() {
return Vec::new();
}
let plan = match plan {
Ok(plan) => plan,
Err(error) => {
self.say(Severity::Warn, format!("refused: {error}"));
return Vec::new();
},
};
if self.overlay.is_some() || self.dataset.input.is_some() {
self.say(
Severity::Info,
"a run is prepared: press t again to start it",
);
return Vec::new();
}
let gpu_types = plan.runpod.as_ref().map(|runpod| runpod.gpu_types.clone());
self.overlay = Some(Overlay::Confirm(Confirm {
title: " Start a training run? ".to_string(),
text: start::text(&plan, None),
yes: "start",
no: "cancel",
action: Action::Start(Box::new(plan)),
}));
self.prices = None;
let Some(gpu_types) = gpu_types else {
return Vec::new();
};
let id = self.task_id();
self.prices = Some(id);
vec![Effect::Spawn(id, Task::Prices(gpu_types))]
}
pub(super) fn priced(&mut self, prices: &Prices) {
self.prices = None;
if let Some(Overlay::Confirm(confirm)) = &mut self.overlay
&& let Action::Start(plan) = &confirm.action
{
confirm.text = start::text(plan, Some(prices));
}
}
pub(super) fn start_run(&mut self, plan: &StartPlan) -> Vec<Effect> {
if self.start_refused() {
return Vec::new();
}
let runpod = plan.runpod.is_some();
let effects = self.train(Job::Start { runpod }, "");
self.say(Severity::Info, format!("starting a run on {}", plan.target));
effects
}
pub(super) fn start_dropped(&mut self, id: TaskId) {
self.training.tasks.remove(&id);
self.exit_notes.push(
"a new training run was not started: the TUI ended first; start it again with \
`overbrainer train` or t"
.to_string(),
);
self.leave_when_idle();
}
fn train(&mut self, job: Job, run_id: &str) -> Vec<Effect> {
let id = self.task_id();
let task = match job {
Job::Start { .. } => TrainJob::Start,
Job::Attach => TrainJob::Attach(run_id.to_string()),
Job::Cancel => TrainJob::Cancel(run_id.to_string()),
};
if !run_id.is_empty() {
self.forget_notes(&NoteOf::Run(run_id.to_string()));
}
self.training.last.retain(|_, run| run != run_id);
self.training.reading.remove(run_id);
self.training.tasks.insert(id, Follow::new(job, run_id));
vec![Effect::Spawn(id, Task::Train(task))]
}
fn ask_cancel(&mut self) {
let Some(row) = self.training.selected_run() else {
return;
};
let (id, target) = (row.record.id.clone(), row.record.target.clone());
if self.training.cancelling(&id) {
self.say(Severity::Info, format!("run {id} is being cancelled"));
return;
}
if self.leaving == Some(Exit::Signal) {
self.say(Severity::Warn, "refused: interrupted, exiting");
return;
}
let activity = self.training.activity(&id);
let starting = self
.training
.task_of(&id)
.filter(|_| activity.abandons())
.map(|(task, _)| (task, matches!(activity, RunActivity::Abandoning { .. })));
if let Some((task, abandoned)) = starting {
if abandoned {
self.say(Severity::Info, format!("run {id} is being abandoned"));
return;
}
let text = format!(
"Run {id} is still starting, so it has no job to cancel yet. Abandon it \
instead? {ABANDONED}"
);
self.overlay = Some(Overlay::Confirm(Confirm {
title: " Abandon a starting run? ".to_string(),
text: vec![text],
yes: "abandon",
no: "keep it",
action: Action::AbandonStart(task),
}));
return;
}
self.overlay = Some(Overlay::Confirm(Confirm {
title: " Cancel a run? ".to_string(),
text: vec![format!(
"Cancel run {id} on {target}? Its job is stopped (its container first), what it \
produced is retrieved best effort, and for Runpod the pod is then ended. This \
cannot be undone."
)],
yes: "cancel the run",
no: "keep it",
action: Action::Cancel(id),
}));
}
pub(super) fn cancel_run(&mut self, run_id: &str) -> Vec<Effect> {
if self.training.cancelling(run_id) {
self.say(Severity::Info, format!("run {run_id} is being cancelled"));
return Vec::new();
}
let Some((task, _)) = self.training.task_of(run_id) else {
return self.train(Job::Cancel, run_id);
};
let Some(follow) = self.training.tasks.get_mut(&task) else {
return Vec::new();
};
follow.cancel_after = true;
let said = if follow.starting() {
format!("run {run_id} is starting: it is cancelled once its job started")
} else {
format!("detaching run {run_id}, then cancelling it")
};
self.say(Severity::Info, said);
self.detach(task)
}
fn detach(&mut self, id: TaskId) -> Vec<Effect> {
let Some(follow) = self.training.tasks.get_mut(&id) else {
return Vec::new();
};
match (follow.detach, follow.watching) {
(Detach::Done, _) => Vec::new(),
(_, true) => {
follow.detach = Detach::Done;
vec![Effect::Cancel(id)]
},
(_, false) => {
follow.detach = Detach::OnStart;
Vec::new()
},
}
}
pub(super) fn on_training_message(&mut self, id: TaskId, message: Msg) -> Vec<Effect> {
if !self.training.tasks.contains_key(&id) {
self.late_training_message(id, message);
return Vec::new();
}
match message {
Msg::Event(_, event) => {
let started = self.training.event(id, event);
let waiting = self
.training
.tasks
.get(&id)
.is_some_and(|follow| follow.detach == Detach::OnStart);
if started && waiting {
return self.detach(id);
}
},
Msg::Lagged(_, skipped) => {
if let Some(follow) = self.training.tasks.get_mut(&id) {
follow.skipped += skipped;
}
},
Msg::Report(_, Report::Line(line)) => {
if let Some(follow) = self.training.tasks.get_mut(&id) {
follow.lines.push(line);
}
},
Msg::Report(_, Report::RunCreated(run_id)) => {
if let Some(follow) = self.training.tasks.get_mut(&id) {
follow.run_id.clone_from(&run_id);
}
self.training.wanted = Some(run_id);
return self.refresh_runs();
},
Msg::EditorExited(_) => {},
}
Vec::new()
}
fn late_training_message(&mut self, id: TaskId, message: Msg) {
let Some(run) = self.training.last.get(&id).cloned() else {
return;
};
let healed = self.training.ended.get(&run).is_some_and(|e| e.healed);
match message {
Msg::Event(_, Event::Metric(metric)) if !healed => {
self.training.series.entry(run).or_default().push(metric);
},
Msg::Lagged(_, skipped) => {
if let Some(ended) = self.training.ended.get_mut(&run) {
ended.skipped += skipped;
}
},
Msg::Report(_, Report::Line(line)) => {
if self.leaving.is_some() {
self.exit_notes.push(line.clone());
}
if let Some(ended) = self.training.ended.get_mut(&run) {
ended.lines.push(line);
}
},
_ => {},
}
}
pub(super) fn trained(&mut self, id: TaskId, result: Result<(), String>) -> Vec<Effect> {
let Some(follow) = self.training.tasks.remove(&id) else {
return Vec::new();
};
let run = follow.run_id.clone();
let name = follow.run();
let error = result.err();
let cancel = follow.cancel_after && self.leaving != Some(Exit::Signal);
if self.leaving.is_some() {
self.exit_notes.extend(follow.lines.iter().cloned());
if !cancel && let Some(error) = &error {
self.note_leaving(error.clone(), NoteOf::Run(run.clone()));
}
if follow.cancel_after && !cancel {
let note = if follow.starting() {
format!(
"run {run} was not cancelled (a signal came during its start); if it \
is running, cancel it with `overbrainer train cancel {run}`"
)
} else {
format!(
"run {run} was not cancelled: interrupted before its cancel started; \
cancel it with `overbrainer train cancel {run}`"
)
};
self.note_leaving(note, NoteOf::Run(run.clone()));
}
}
match &error {
Some(error) if !follow.cancel_after => {
self.say(Severity::Warn, format!("{name}: {error}"));
},
Some(_) => {},
None => self.say(Severity::Info, format!("{name}: done")),
}
if run.is_empty() {
self.leave_when_idle();
return Vec::new();
}
self.training.ended.insert(
run.clone(),
Ended {
lines: follow.lines,
error,
skipped: follow.skipped,
healed: false,
},
);
self.training.last.insert(id, run.clone());
let mut effects = Vec::new();
if self.leaving.is_none() {
effects.extend(self.read_series(&run));
effects.extend(self.refresh_runs());
}
if cancel {
effects.extend(self.train(Job::Cancel, &run));
}
self.leave_when_idle();
effects
}
pub(super) fn training_quit_text(&self) -> Vec<String> {
self.training
.tasks
.values()
.map(|follow| {
let run = &follow.run_id;
let pod = match follow.job {
Job::Start { runpod: true } => " its pod",
_ => "",
};
match follow.job {
Job::Cancel => format!("Run {run}: cancel in progress, quitting waits for it."),
_ if follow.cancel_after => format!(
"Run {run}: cancel pending, it starts once the run is detached; \
quitting waits for it."
),
Job::Start { .. } if follow.starting() => format!(
"{} is starting{pod}: quitting waits until its job has started, then \
leaves it running.",
capitalized(&follow.run())
),
Job::Start { .. } | Job::Attach => format!(
"Run {run} keeps running{}; attach again from here or with \
`overbrainer train attach {run}`.",
self.where_it_runs(run)
),
}
})
.collect()
}
fn where_it_runs(&self, run: &str) -> String {
let Some(row) = self.training.runs.iter().find(|row| row.record.id == run) else {
return String::new();
};
match row
.pod
.as_ref()
.and_then(|pod| pod.pod_id.as_ref().map(|id| (id, pod)))
{
Some((pod_id, pod)) => match pod.cost_per_hour {
Some(rate) => format!(" on pod {pod_id} (${rate:.2}/h)"),
None => format!(" on pod {pod_id}"),
},
None => format!(" on target `{}`", row.record.target),
}
}
pub(super) fn detach_all(&mut self) -> Vec<Effect> {
let following: Vec<TaskId> = self
.training
.tasks
.iter()
.filter(|(_, follow)| follow.job != Job::Cancel)
.map(|(id, _)| *id)
.collect();
following
.into_iter()
.flat_map(|id| self.detach(id))
.collect()
}
pub(super) fn offer_abandon(&mut self) {
if self.leaving != Some(Exit::Quit) {
return;
}
let starting: Vec<(TaskId, String)> = self
.training
.tasks
.iter()
.filter(|(_, follow)| {
follow.job == Job::Start { runpod: true }
&& follow.starting()
&& follow.detach != Detach::Done
})
.map(|(id, follow)| (*id, follow.run()))
.collect();
if starting.is_empty() {
return;
}
let runs: Vec<&str> = starting.iter().map(|(_, run)| run.as_str()).collect();
let text = format!(
"Quitting waits until the job of {} has started, which can take minutes. Abandon \
it instead? {ABANDONED}",
runs.join(", ")
);
self.overlay = Some(Overlay::Confirm(Confirm {
title: " Abandon a starting run? ".to_string(),
text: vec![text],
yes: "abandon",
no: "wait",
action: Action::Abandon(starting.into_iter().map(|(id, _)| id).collect()),
}));
}
pub(super) fn abandon(&mut self, ids: &[TaskId]) -> Vec<Effect> {
let mut effects = Vec::new();
for id in ids {
if let Some(follow) = self.training.tasks.get_mut(id)
&& follow.starting()
&& follow.detach != Detach::Done
{
follow.detach = Detach::Done;
follow.cancel_after = false;
effects.push(Effect::Abandon(*id));
}
}
effects
}
pub(super) fn abandon_start(&mut self, id: TaskId) -> Vec<Effect> {
let Some(follow) = self.training.tasks.get(&id) else {
return Vec::new();
};
let run = follow.run();
if !follow.starting() {
self.say(
Severity::Warn,
format!(
"{run}: its job started, so it was not abandoned and its pod keeps billing; \
press c to cancel it"
),
);
return Vec::new();
}
if follow.detach == Detach::Done {
self.say(Severity::Info, format!("{run} is being abandoned"));
return Vec::new();
}
self.say(Severity::Info, format!("abandoning {run}"));
self.abandon(&[id])
}
pub(super) fn abandon_all(&mut self) -> Vec<Effect> {
self.training
.tasks
.iter_mut()
.filter(|(_, follow)| follow.job != Job::Cancel)
.map(|(id, follow)| {
follow.detach = Detach::Done;
Effect::Abandon(*id)
})
.collect()
}
pub(super) fn keep_following(&mut self) -> (usize, usize) {
let (mut detached, mut abandoned) = (0, 0);
for follow in self.training.tasks.values_mut() {
if follow.detach == Detach::OnStart && !follow.cancel_after {
follow.detach = Detach::No;
}
if follow.detach != Detach::Done || follow.job == Job::Cancel {
continue;
}
if follow.starting() {
abandoned += 1;
} else {
detached += 1;
}
}
(detached, abandoned)
}
pub(super) fn never() -> SystemTime {
SystemTime::UNIX_EPOCH
}
}
fn capitalized(text: &str) -> String {
let mut chars = text.chars();
chars.next().map_or_else(String::new, |first| {
first.to_uppercase().chain(chars).collect()
})
}