use std::collections::HashMap;
use serde_json::Value;
use tracel_experiment::{ExperimentLocation, ExperimentRun, Experiments};
use tracel_runtime::JobScope;
const JOB_NUM_ATTRIBUTE: &str = "tracel.job_num";
pub fn linked(experiments: Experiments) -> Experiments {
match RunningJob::current() {
Some(job) => link(experiments, job, |line| eprintln!("{line}")),
None => experiments,
}
}
struct RunningJob {
num: Option<String>,
input: Value,
}
impl RunningJob {
fn current() -> Option<Self> {
let scope = JobScope::current()?;
Some(Self {
num: scope.num().map(str::to_string),
input: scope.input().clone(),
})
}
}
fn link(
experiments: Experiments,
job: RunningJob,
announce: impl Fn(&str) + Send + Sync + 'static,
) -> Experiments {
let attributes = job
.num
.map(|num| (JOB_NUM_ATTRIBUTE.to_string(), job_num(&num)));
let experiments = match job.input {
Value::Null => experiments,
input => experiments.arguments(input),
};
experiments
.attributes(HashMap::from_iter(attributes))
.on_start(move |run| announce(&recording(run)))
}
fn recording(run: &ExperimentRun) -> String {
match run.location() {
Some(ExperimentLocation::Url(url)) => format!("Recording experiment {}: {url}", run.id()),
Some(ExperimentLocation::Dir(dir)) => format!("Recording offline in {}", dir.display()),
None => format!("Recording experiment {}", run.id()),
}
}
fn job_num(num: &str) -> Value {
match num.parse::<u64>() {
Ok(num) => Value::from(num),
Err(_) => Value::from(num),
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use std::sync::{Arc, Mutex};
use serde_json::json;
use tracel_experiment::ExperimentProvider;
use tracel_experiment::local::LocalExperiments;
use super::*;
type Kept<T> = Arc<Mutex<Vec<T>>>;
fn job(num: Option<&str>, input: Value) -> RunningJob {
RunningJob {
num: num.map(str::to_string),
input,
}
}
fn offline(dir: &Path) -> Experiments {
Experiments::new(Arc::new(LocalExperiments::new(dir)))
}
fn linked_to(experiments: Experiments, job: RunningJob) -> (Experiments, Kept<String>) {
let announced = Arc::new(Mutex::new(Vec::new()));
let experiments = link(experiments, job, {
let announced = announced.clone();
move |line| announced.lock().unwrap().push(line.to_string())
});
(experiments, announced)
}
fn events(dir: &Path, num: u64) -> Vec<Value> {
std::fs::read_to_string(dir.join(format!("train/{num}/events.jsonl")))
.unwrap()
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.collect()
}
#[test]
fn an_experiment_started_in_a_job_belongs_to_it() {
let dir = tempfile::tempdir().unwrap();
let job = job(Some("12"), json!({"epochs": 2}));
let (experiments, announced) = linked_to(offline(dir.path()), job);
experiments.start("train").unwrap().finish().unwrap();
let runs = dir.path().canonicalize().unwrap();
assert!(events(dir.path(), 1).contains(&json!({
"type": "attribute",
"key": "tracel.job_num",
"value": 12
})));
assert!(events(dir.path(), 1).contains(&json!({
"type": "arguments",
"value": {"epochs": 2}
})));
assert_eq!(
*announced.lock().unwrap(),
[format!(
"Recording offline in {}",
runs.join("train/1").display()
)]
);
}
#[test]
fn an_experiment_started_in_a_job_without_a_number_or_input_records_neither() {
let dir = tempfile::tempdir().unwrap();
let job = job(None, Value::Null);
let (experiments, announced) = linked_to(offline(dir.path()), job);
experiments.start("train").unwrap().finish().unwrap();
assert!(
events(dir.path(), 1)
.iter()
.all(|event| event["type"] != "attribute" && event["type"] != "arguments")
);
assert_eq!(announced.lock().unwrap().len(), 1);
}
#[test]
fn outside_a_job_experiments_are_not_linked() {
let dir = tempfile::tempdir().unwrap();
linked(offline(dir.path()))
.start("train")
.unwrap()
.finish()
.unwrap();
assert!(
events(dir.path(), 1)
.iter()
.all(|event| event["type"] != "attribute" && event["type"] != "arguments")
);
}
#[test]
fn a_job_number_is_recorded_as_a_number_when_it_is_one() {
assert_eq!(job_num("42"), json!(42));
assert_eq!(job_num("job-42"), json!("job-42"));
}
#[test]
fn an_experiment_announces_its_page_or_its_directory() {
let dir = tempfile::tempdir().unwrap();
let experiments = LocalExperiments::new(dir.path());
let page = "https://console.tracel.ai/users/me/projects/mnist/experiments/2";
let offline = experiments
.create_experiment("mnist".to_string(), HashMap::new())
.unwrap();
let on_the_console = experiments
.create_experiment("mnist".to_string(), HashMap::new())
.unwrap()
.with_location(ExperimentLocation::Url(page.to_string()));
assert_eq!(
recording(&offline),
format!(
"Recording offline in {}",
dir.path().canonicalize().unwrap().join("mnist/1").display()
)
);
assert_eq!(
recording(&on_the_console),
format!("Recording experiment 2: {page}")
);
}
}