feat: make cargo time
incremental by default (#53)
Co-authored-by: Tristan Guichaoua <33934311+tguichaoua@users.noreply.github.com>
This commit is contained in:
parent
4c4232139a
commit
874f57b359
15 changed files with 757 additions and 300 deletions
|
@ -6,7 +6,7 @@ read = "run --quiet --release -- read"
|
|||
|
||||
solve = "run --quiet --release -- solve"
|
||||
all = "run --quiet --release -- all"
|
||||
time = "run --quiet --release -- all --release --time"
|
||||
time = "run --quiet --release -- time"
|
||||
|
||||
[env]
|
||||
AOC_YEAR = "2023"
|
||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -24,3 +24,7 @@ data/puzzles/*
|
|||
|
||||
# Dhat
|
||||
dhat-heap.json
|
||||
|
||||
# Benchmarks
|
||||
|
||||
data/timings.json
|
||||
|
|
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -24,6 +24,7 @@ dependencies = [
|
|||
"chrono",
|
||||
"dhat",
|
||||
"pico-args",
|
||||
"tinyjson",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -390,6 +391,12 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820"
|
||||
|
||||
[[package]]
|
||||
name = "tinyjson"
|
||||
version = "2.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ab95735ea2c8fd51154d01e39cf13912a78071c2d89abc49a7ef102a7dd725a"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
|
|
|
@ -15,11 +15,12 @@ inherits = "release"
|
|||
debug = 1
|
||||
|
||||
[features]
|
||||
dhat-heap = ["dhat"]
|
||||
today = ["chrono"]
|
||||
test_lib = []
|
||||
dhat-heap = ["dhat"]
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4.31", optional = true }
|
||||
pico-args = "0.5.0"
|
||||
dhat = { version = "0.3.2", optional = true }
|
||||
pico-args = "0.5.0"
|
||||
tinyjson = "2"
|
||||
|
|
10
README.md
10
README.md
|
@ -116,13 +116,15 @@ cargo all
|
|||
# Total: 0.20ms
|
||||
```
|
||||
|
||||
This runs all solutions sequentially and prints output to the command-line. Same as for the `solve` command, the `--release` flag runs an optimized build.
|
||||
This runs all solutions sequentially and prints output to the command-line. Same as for the `solve` command, the `--release` flag runs an optimized build and the `--time` flag outputs benchmarks.
|
||||
|
||||
#### Update readme benchmarks
|
||||
### ➡️ Update readme benchmarks
|
||||
|
||||
The template can output a table with solution times to your readme. In order to generate a benchmarking table, run `cargo time`. If everything goes well, the command will output "_Successfully updated README with benchmarks._" after the execution finishes and the readme will be updated.
|
||||
The template can write benchmark times to the README via the `cargo time` command.
|
||||
|
||||
Please note that these are not "scientific" benchmarks, understand them as a fun approximation. 😉 Timings, especially in the microseconds range, might change a bit between invocations.
|
||||
By default, this command checks for missing benchmarks, runs those solutions, and then updates the table. If you want to (re-)time all solutions, run `cargo time --all`. If you want to (re-)time one specific solution, run `cargo time <day>`.
|
||||
|
||||
Please note that these are not _scientific_ benchmarks, understand them as a fun approximation. 😉 Timings, especially in the microseconds range, might change a bit between invocations.
|
||||
|
||||
### ➡️ Run all tests
|
||||
|
||||
|
|
15
src/main.rs
15
src/main.rs
|
@ -1,4 +1,4 @@
|
|||
use advent_of_code::template::commands::{all, download, read, scaffold, solve};
|
||||
use advent_of_code::template::commands::{all, download, read, scaffold, solve, time};
|
||||
use args::{parse, AppArguments};
|
||||
|
||||
#[cfg(feature = "today")]
|
||||
|
@ -32,6 +32,10 @@ mod args {
|
|||
release: bool,
|
||||
time: bool,
|
||||
},
|
||||
Time {
|
||||
all: bool,
|
||||
day: Option<Day>,
|
||||
},
|
||||
#[cfg(feature = "today")]
|
||||
Today,
|
||||
}
|
||||
|
@ -44,6 +48,14 @@ mod args {
|
|||
release: args.contains("--release"),
|
||||
time: args.contains("--time"),
|
||||
},
|
||||
Some("time") => {
|
||||
let all = args.contains("--all");
|
||||
|
||||
AppArguments::Time {
|
||||
all,
|
||||
day: args.opt_free_from_str()?,
|
||||
}
|
||||
}
|
||||
Some("download") => AppArguments::Download {
|
||||
day: args.free_from_str()?,
|
||||
},
|
||||
|
@ -90,6 +102,7 @@ fn main() {
|
|||
}
|
||||
Ok(args) => match args {
|
||||
AppArguments::All { release, time } => all::handle(release, time),
|
||||
AppArguments::Time { day, all } => time::handle(day, all),
|
||||
AppArguments::Download { day } => download::handle(day),
|
||||
AppArguments::Read { day } => read::handle(day),
|
||||
AppArguments::Scaffold { day, download } => {
|
||||
|
|
|
@ -11,7 +11,6 @@ pub enum AocCommandError {
|
|||
CommandNotFound,
|
||||
CommandNotCallable,
|
||||
BadExitStatus(Output),
|
||||
IoError,
|
||||
}
|
||||
|
||||
impl Display for AocCommandError {
|
||||
|
@ -22,7 +21,6 @@ impl Display for AocCommandError {
|
|||
AocCommandError::BadExitStatus(_) => {
|
||||
write!(f, "aoc-cli exited with a non-zero status.")
|
||||
}
|
||||
AocCommandError::IoError => write!(f, "could not write output files to file system."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,254 +1,5 @@
|
|||
use std::io;
|
||||
|
||||
use crate::template::{
|
||||
all_days,
|
||||
readme_benchmarks::{self, Timings},
|
||||
Day, ANSI_BOLD, ANSI_ITALIC, ANSI_RESET,
|
||||
};
|
||||
use crate::template::{all_days, run_multi::run_multi};
|
||||
|
||||
pub fn handle(is_release: bool, is_timed: bool) {
|
||||
let mut timings: Vec<Timings> = vec![];
|
||||
|
||||
all_days().for_each(|day| {
|
||||
if day > 1 {
|
||||
println!();
|
||||
}
|
||||
|
||||
println!("{ANSI_BOLD}Day {day}{ANSI_RESET}");
|
||||
println!("------");
|
||||
|
||||
let output = child_commands::run_solution(day, is_timed, is_release).unwrap();
|
||||
|
||||
if output.is_empty() {
|
||||
println!("Not solved.");
|
||||
} else {
|
||||
let val = child_commands::parse_exec_time(&output, day);
|
||||
timings.push(val);
|
||||
}
|
||||
});
|
||||
|
||||
if is_timed {
|
||||
let total_millis = timings.iter().map(|x| x.total_nanos).sum::<f64>() / 1_000_000_f64;
|
||||
|
||||
println!("\n{ANSI_BOLD}Total:{ANSI_RESET} {ANSI_ITALIC}{total_millis:.2}ms{ANSI_RESET}");
|
||||
|
||||
if is_release {
|
||||
match readme_benchmarks::update(timings, total_millis) {
|
||||
Ok(()) => println!("Successfully updated README with benchmarks."),
|
||||
Err(_) => {
|
||||
eprintln!("Failed to update readme with benchmarks.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
BrokenPipe,
|
||||
Parser(String),
|
||||
IO(io::Error),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
Error::IO(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_path_for_bin(day: Day) -> String {
|
||||
format!("./src/bin/{day}.rs")
|
||||
}
|
||||
|
||||
/// All solutions live in isolated binaries.
|
||||
/// This module encapsulates interaction with these binaries, both invoking them as well as parsing the timing output.
|
||||
mod child_commands {
|
||||
use super::{get_path_for_bin, Error};
|
||||
use crate::template::Day;
|
||||
use std::{
|
||||
io::{BufRead, BufReader},
|
||||
path::Path,
|
||||
process::{Command, Stdio},
|
||||
thread,
|
||||
};
|
||||
|
||||
/// Run the solution bin for a given day
|
||||
pub fn run_solution(day: Day, is_timed: bool, is_release: bool) -> Result<Vec<String>, Error> {
|
||||
// skip command invocation for days that have not been scaffolded yet.
|
||||
if !Path::new(&get_path_for_bin(day)).exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let day_padded = day.to_string();
|
||||
let mut args = vec!["run", "--quiet", "--bin", &day_padded];
|
||||
|
||||
if is_release {
|
||||
args.push("--release");
|
||||
}
|
||||
|
||||
if is_timed {
|
||||
// mirror `--time` flag to child invocations.
|
||||
args.push("--");
|
||||
args.push("--time");
|
||||
}
|
||||
|
||||
// spawn child command with piped stdout/stderr.
|
||||
// forward output to stdout/stderr while grabbing stdout lines.
|
||||
|
||||
let mut cmd = Command::new("cargo")
|
||||
.args(&args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let stdout = BufReader::new(cmd.stdout.take().ok_or(super::Error::BrokenPipe)?);
|
||||
let stderr = BufReader::new(cmd.stderr.take().ok_or(super::Error::BrokenPipe)?);
|
||||
|
||||
let mut output = vec![];
|
||||
|
||||
let thread = thread::spawn(move || {
|
||||
stderr.lines().for_each(|line| {
|
||||
eprintln!("{}", line.unwrap());
|
||||
});
|
||||
});
|
||||
|
||||
for line in stdout.lines() {
|
||||
let line = line.unwrap();
|
||||
println!("{line}");
|
||||
output.push(line);
|
||||
}
|
||||
|
||||
thread.join().unwrap();
|
||||
cmd.wait()?;
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub fn parse_exec_time(output: &[String], day: Day) -> super::Timings {
|
||||
let mut timings = super::Timings {
|
||||
day,
|
||||
part_1: None,
|
||||
part_2: None,
|
||||
total_nanos: 0_f64,
|
||||
};
|
||||
|
||||
output
|
||||
.iter()
|
||||
.filter_map(|l| {
|
||||
if !l.contains(" samples)") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some((timing_str, nanos)) = parse_time(l) else {
|
||||
eprintln!("Could not parse timings from line: {l}");
|
||||
return None;
|
||||
};
|
||||
|
||||
let part = l.split(':').next()?;
|
||||
Some((part, timing_str, nanos))
|
||||
})
|
||||
.for_each(|(part, timing_str, nanos)| {
|
||||
if part.contains("Part 1") {
|
||||
timings.part_1 = Some(timing_str.into());
|
||||
} else if part.contains("Part 2") {
|
||||
timings.part_2 = Some(timing_str.into());
|
||||
}
|
||||
|
||||
timings.total_nanos += nanos;
|
||||
});
|
||||
|
||||
timings
|
||||
}
|
||||
|
||||
fn parse_to_float(s: &str, postfix: &str) -> Option<f64> {
|
||||
s.split(postfix).next()?.parse().ok()
|
||||
}
|
||||
|
||||
fn parse_time(line: &str) -> Option<(&str, f64)> {
|
||||
// for possible time formats, see: https://github.com/rust-lang/rust/blob/1.64.0/library/core/src/time.rs#L1176-L1200
|
||||
let str_timing = line
|
||||
.split(" samples)")
|
||||
.next()?
|
||||
.split('(')
|
||||
.last()?
|
||||
.split('@')
|
||||
.next()?
|
||||
.trim();
|
||||
|
||||
let parsed_timing = match str_timing {
|
||||
s if s.contains("ns") => s.split("ns").next()?.parse::<f64>().ok(),
|
||||
s if s.contains("µs") => parse_to_float(s, "µs").map(|x| x * 1000_f64),
|
||||
s if s.contains("ms") => parse_to_float(s, "ms").map(|x| x * 1_000_000_f64),
|
||||
s => parse_to_float(s, "s").map(|x| x * 1_000_000_000_f64),
|
||||
}?;
|
||||
|
||||
Some((str_timing, parsed_timing))
|
||||
}
|
||||
|
||||
/// copied from: https://github.com/rust-lang/rust/blob/1.64.0/library/std/src/macros.rs#L328-L333
|
||||
#[cfg(feature = "test_lib")]
|
||||
macro_rules! assert_approx_eq {
|
||||
($a:expr, $b:expr) => {{
|
||||
let (a, b) = (&$a, &$b);
|
||||
assert!(
|
||||
(*a - *b).abs() < 1.0e-6,
|
||||
"{} is not approximately equal to {}",
|
||||
*a,
|
||||
*b
|
||||
);
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(feature = "test_lib")]
|
||||
mod tests {
|
||||
use super::parse_exec_time;
|
||||
|
||||
use crate::day;
|
||||
|
||||
#[test]
|
||||
fn test_well_formed() {
|
||||
let res = parse_exec_time(
|
||||
&[
|
||||
"Part 1: 0 (74.13ns @ 100000 samples)".into(),
|
||||
"Part 2: 10 (74.13ms @ 99999 samples)".into(),
|
||||
"".into(),
|
||||
],
|
||||
day!(1),
|
||||
);
|
||||
assert_approx_eq!(res.total_nanos, 74130074.13_f64);
|
||||
assert_eq!(res.part_1.unwrap(), "74.13ns");
|
||||
assert_eq!(res.part_2.unwrap(), "74.13ms");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_patterns_in_input() {
|
||||
let res = parse_exec_time(
|
||||
&[
|
||||
"Part 1: @ @ @ ( ) ms (2s @ 5 samples)".into(),
|
||||
"Part 2: 10s (100ms @ 1 samples)".into(),
|
||||
"".into(),
|
||||
],
|
||||
day!(1),
|
||||
);
|
||||
assert_approx_eq!(res.total_nanos, 2100000000_f64);
|
||||
assert_eq!(res.part_1.unwrap(), "2s");
|
||||
assert_eq!(res.part_2.unwrap(), "100ms");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_parts() {
|
||||
let res = parse_exec_time(
|
||||
&[
|
||||
"Part 1: ✖ ".into(),
|
||||
"Part 2: ✖ ".into(),
|
||||
"".into(),
|
||||
],
|
||||
day!(1),
|
||||
);
|
||||
assert_approx_eq!(res.total_nanos, 0_f64);
|
||||
assert_eq!(res.part_1.is_none(), true);
|
||||
assert_eq!(res.part_2.is_none(), true);
|
||||
}
|
||||
}
|
||||
run_multi(all_days().collect(), is_release, is_timed);
|
||||
}
|
||||
|
|
|
@ -3,3 +3,4 @@ pub mod download;
|
|||
pub mod read;
|
||||
pub mod scaffold;
|
||||
pub mod solve;
|
||||
pub mod time;
|
||||
|
|
35
src/template/commands/time.rs
Normal file
35
src/template/commands/time.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
use std::collections::HashSet;
|
||||
|
||||
use crate::template::run_multi::run_multi;
|
||||
use crate::template::timings::Timings;
|
||||
use crate::template::{all_days, readme_benchmarks, Day};
|
||||
|
||||
pub fn handle(day: Option<Day>, recreate_all: bool) {
|
||||
let stored_timings = Timings::read_from_file();
|
||||
|
||||
let days_to_run = day.map(|day| HashSet::from([day])).unwrap_or_else(|| {
|
||||
if recreate_all {
|
||||
all_days().collect()
|
||||
} else {
|
||||
// when the `--all` flag is not set, filter out days that are fully benched.
|
||||
all_days()
|
||||
.filter(|day| !stored_timings.is_day_complete(day))
|
||||
.collect()
|
||||
}
|
||||
});
|
||||
|
||||
let timings = run_multi(days_to_run, true, true).unwrap();
|
||||
|
||||
let merged_timings = stored_timings.merge(&timings);
|
||||
merged_timings.store_file().unwrap();
|
||||
|
||||
println!();
|
||||
match readme_benchmarks::update(merged_timings) {
|
||||
Ok(()) => {
|
||||
println!("Stored updated benchmarks.")
|
||||
}
|
||||
Err(_) => {
|
||||
eprintln!("Failed to store updated benchmarks.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,12 +2,15 @@ use std::{env, fs};
|
|||
|
||||
pub mod aoc_cli;
|
||||
pub mod commands;
|
||||
mod day;
|
||||
pub mod readme_benchmarks;
|
||||
pub mod runner;
|
||||
|
||||
pub use day::*;
|
||||
|
||||
mod day;
|
||||
mod readme_benchmarks;
|
||||
mod run_multi;
|
||||
mod timings;
|
||||
|
||||
pub const ANSI_ITALIC: &str = "\x1b[3m";
|
||||
pub const ANSI_BOLD: &str = "\x1b[1m";
|
||||
pub const ANSI_RESET: &str = "\x1b[0m";
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
/// The approach taken is similar to how `aoc-readme-stars` handles this.
|
||||
use std::{fs, io};
|
||||
|
||||
use crate::template::timings::Timings;
|
||||
use crate::template::Day;
|
||||
|
||||
static MARKER: &str = "<!--- benchmarking table --->";
|
||||
|
@ -18,14 +19,6 @@ impl From<std::io::Error> for Error {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Timings {
|
||||
pub day: Day,
|
||||
pub part_1: Option<String>,
|
||||
pub part_2: Option<String>,
|
||||
pub total_nanos: f64,
|
||||
}
|
||||
|
||||
pub struct TablePosition {
|
||||
pos_start: usize,
|
||||
pos_end: usize,
|
||||
|
@ -58,7 +51,7 @@ fn locate_table(readme: &str) -> Result<TablePosition, Error> {
|
|||
Ok(TablePosition { pos_start, pos_end })
|
||||
}
|
||||
|
||||
fn construct_table(prefix: &str, timings: Vec<Timings>, total_millis: f64) -> String {
|
||||
fn construct_table(prefix: &str, timings: Timings, total_millis: f64) -> String {
|
||||
let header = format!("{prefix} Benchmarks");
|
||||
|
||||
let mut lines: Vec<String> = vec![
|
||||
|
@ -69,7 +62,7 @@ fn construct_table(prefix: &str, timings: Vec<Timings>, total_millis: f64) -> St
|
|||
"| :---: | :---: | :---: |".into(),
|
||||
];
|
||||
|
||||
for timing in timings {
|
||||
for timing in timings.data {
|
||||
let path = get_path_for_bin(timing.day);
|
||||
lines.push(format!(
|
||||
"| [Day {}]({}) | `{}` | `{}` |",
|
||||
|
@ -87,16 +80,17 @@ fn construct_table(prefix: &str, timings: Vec<Timings>, total_millis: f64) -> St
|
|||
lines.join("\n")
|
||||
}
|
||||
|
||||
fn update_content(s: &mut String, timings: Vec<Timings>, total_millis: f64) -> Result<(), Error> {
|
||||
fn update_content(s: &mut String, timings: Timings, total_millis: f64) -> Result<(), Error> {
|
||||
let positions = locate_table(s)?;
|
||||
let table = construct_table("##", timings, total_millis);
|
||||
s.replace_range(positions.pos_start..positions.pos_end, &table);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update(timings: Vec<Timings>, total_millis: f64) -> Result<(), Error> {
|
||||
pub fn update(timings: Timings) -> Result<(), Error> {
|
||||
let path = "README.md";
|
||||
let mut readme = String::from_utf8_lossy(&fs::read(path)?).to_string();
|
||||
let total_millis = timings.total_millis();
|
||||
update_content(&mut readme, timings, total_millis)?;
|
||||
fs::write(path, &readme)?;
|
||||
Ok(())
|
||||
|
@ -104,30 +98,32 @@ pub fn update(timings: Vec<Timings>, total_millis: f64) -> Result<(), Error> {
|
|||
|
||||
#[cfg(feature = "test_lib")]
|
||||
mod tests {
|
||||
use super::{update_content, Timings, MARKER};
|
||||
use crate::day;
|
||||
use super::{update_content, MARKER};
|
||||
use crate::{day, template::timings::Timing, template::timings::Timings};
|
||||
|
||||
fn get_mock_timings() -> Vec<Timings> {
|
||||
vec![
|
||||
Timings {
|
||||
day: day!(1),
|
||||
part_1: Some("10ms".into()),
|
||||
part_2: Some("20ms".into()),
|
||||
total_nanos: 3e+10,
|
||||
},
|
||||
Timings {
|
||||
day: day!(2),
|
||||
part_1: Some("30ms".into()),
|
||||
part_2: Some("40ms".into()),
|
||||
total_nanos: 7e+10,
|
||||
},
|
||||
Timings {
|
||||
day: day!(4),
|
||||
part_1: Some("40ms".into()),
|
||||
part_2: Some("50ms".into()),
|
||||
total_nanos: 9e+10,
|
||||
},
|
||||
]
|
||||
fn get_mock_timings() -> Timings {
|
||||
Timings {
|
||||
data: vec![
|
||||
Timing {
|
||||
day: day!(1),
|
||||
part_1: Some("10ms".into()),
|
||||
part_2: Some("20ms".into()),
|
||||
total_nanos: 3e+10,
|
||||
},
|
||||
Timing {
|
||||
day: day!(2),
|
||||
part_1: Some("30ms".into()),
|
||||
part_2: Some("40ms".into()),
|
||||
total_nanos: 7e+10,
|
||||
},
|
||||
Timing {
|
||||
day: day!(4),
|
||||
part_1: Some("40ms".into()),
|
||||
part_2: Some("50ms".into()),
|
||||
total_nanos: 9e+10,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
255
src/template/run_multi.rs
Normal file
255
src/template/run_multi.rs
Normal file
|
@ -0,0 +1,255 @@
|
|||
use std::{collections::HashSet, io};
|
||||
|
||||
use crate::template::{Day, ANSI_BOLD, ANSI_ITALIC, ANSI_RESET};
|
||||
|
||||
use super::{
|
||||
all_days,
|
||||
timings::{Timing, Timings},
|
||||
};
|
||||
|
||||
pub fn run_multi(days_to_run: HashSet<Day>, is_release: bool, is_timed: bool) -> Option<Timings> {
|
||||
let mut timings: Vec<Timing> = Vec::with_capacity(days_to_run.len());
|
||||
|
||||
all_days().for_each(|day| {
|
||||
if day > 1 {
|
||||
println!();
|
||||
}
|
||||
|
||||
println!("{ANSI_BOLD}Day {day}{ANSI_RESET}");
|
||||
println!("------");
|
||||
|
||||
if !days_to_run.contains(&day) {
|
||||
println!("Skipped.");
|
||||
return;
|
||||
}
|
||||
|
||||
let output = child_commands::run_solution(day, is_timed, is_release).unwrap();
|
||||
|
||||
if output.is_empty() {
|
||||
println!("Not solved.");
|
||||
} else {
|
||||
let val = child_commands::parse_exec_time(&output, day);
|
||||
timings.push(val);
|
||||
}
|
||||
});
|
||||
|
||||
if is_timed {
|
||||
let timings = Timings { data: timings };
|
||||
let total_millis = timings.total_millis();
|
||||
println!(
|
||||
"\n{ANSI_BOLD}Total (Run):{ANSI_RESET} {ANSI_ITALIC}{total_millis:.2}ms{ANSI_RESET}"
|
||||
);
|
||||
Some(timings)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
BrokenPipe,
|
||||
IO(io::Error),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
Error::IO(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_path_for_bin(day: Day) -> String {
|
||||
format!("./src/bin/{day}.rs")
|
||||
}
|
||||
|
||||
/// All solutions live in isolated binaries.
|
||||
/// This module encapsulates interaction with these binaries, both invoking them as well as parsing the timing output.
|
||||
pub mod child_commands {
|
||||
use super::{get_path_for_bin, Error};
|
||||
use crate::template::Day;
|
||||
use std::{
|
||||
io::{BufRead, BufReader},
|
||||
path::Path,
|
||||
process::{Command, Stdio},
|
||||
thread,
|
||||
};
|
||||
|
||||
/// Run the solution bin for a given day
|
||||
pub fn run_solution(day: Day, is_timed: bool, is_release: bool) -> Result<Vec<String>, Error> {
|
||||
// skip command invocation for days that have not been scaffolded yet.
|
||||
if !Path::new(&get_path_for_bin(day)).exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let day_padded = day.to_string();
|
||||
let mut args = vec!["run", "--quiet", "--bin", &day_padded];
|
||||
|
||||
if is_release {
|
||||
args.push("--release");
|
||||
}
|
||||
|
||||
if is_timed {
|
||||
// mirror `--time` flag to child invocations.
|
||||
args.push("--");
|
||||
args.push("--time");
|
||||
}
|
||||
|
||||
// spawn child command with piped stdout/stderr.
|
||||
// forward output to stdout/stderr while grabbing stdout lines.
|
||||
|
||||
let mut cmd = Command::new("cargo")
|
||||
.args(&args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let stdout = BufReader::new(cmd.stdout.take().ok_or(super::Error::BrokenPipe)?);
|
||||
let stderr = BufReader::new(cmd.stderr.take().ok_or(super::Error::BrokenPipe)?);
|
||||
|
||||
let mut output = vec![];
|
||||
|
||||
let thread = thread::spawn(move || {
|
||||
stderr.lines().for_each(|line| {
|
||||
eprintln!("{}", line.unwrap());
|
||||
});
|
||||
});
|
||||
|
||||
for line in stdout.lines() {
|
||||
let line = line.unwrap();
|
||||
println!("{line}");
|
||||
output.push(line);
|
||||
}
|
||||
|
||||
thread.join().unwrap();
|
||||
cmd.wait()?;
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub fn parse_exec_time(output: &[String], day: Day) -> super::Timing {
|
||||
let mut timings = super::Timing {
|
||||
day,
|
||||
part_1: None,
|
||||
part_2: None,
|
||||
total_nanos: 0_f64,
|
||||
};
|
||||
|
||||
output
|
||||
.iter()
|
||||
.filter_map(|l| {
|
||||
if !l.contains(" samples)") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some((timing_str, nanos)) = parse_time(l) else {
|
||||
eprintln!("Could not parse timings from line: {l}");
|
||||
return None;
|
||||
};
|
||||
|
||||
let part = l.split(':').next()?;
|
||||
Some((part, timing_str, nanos))
|
||||
})
|
||||
.for_each(|(part, timing_str, nanos)| {
|
||||
if part.contains("Part 1") {
|
||||
timings.part_1 = Some(timing_str.into());
|
||||
} else if part.contains("Part 2") {
|
||||
timings.part_2 = Some(timing_str.into());
|
||||
}
|
||||
|
||||
timings.total_nanos += nanos;
|
||||
});
|
||||
|
||||
timings
|
||||
}
|
||||
|
||||
fn parse_to_float(s: &str, postfix: &str) -> Option<f64> {
|
||||
s.split(postfix).next()?.parse().ok()
|
||||
}
|
||||
|
||||
fn parse_time(line: &str) -> Option<(&str, f64)> {
|
||||
// for possible time formats, see: https://github.com/rust-lang/rust/blob/1.64.0/library/core/src/time.rs#L1176-L1200
|
||||
let str_timing = line
|
||||
.split(" samples)")
|
||||
.next()?
|
||||
.split('(')
|
||||
.last()?
|
||||
.split('@')
|
||||
.next()?
|
||||
.trim();
|
||||
|
||||
let parsed_timing = match str_timing {
|
||||
s if s.contains("ns") => s.split("ns").next()?.parse::<f64>().ok(),
|
||||
s if s.contains("µs") => parse_to_float(s, "µs").map(|x| x * 1000_f64),
|
||||
s if s.contains("ms") => parse_to_float(s, "ms").map(|x| x * 1_000_000_f64),
|
||||
s => parse_to_float(s, "s").map(|x| x * 1_000_000_000_f64),
|
||||
}?;
|
||||
|
||||
Some((str_timing, parsed_timing))
|
||||
}
|
||||
|
||||
/// copied from: https://github.com/rust-lang/rust/blob/1.64.0/library/std/src/macros.rs#L328-L333
|
||||
#[cfg(feature = "test_lib")]
|
||||
macro_rules! assert_approx_eq {
|
||||
($a:expr, $b:expr) => {{
|
||||
let (a, b) = (&$a, &$b);
|
||||
assert!(
|
||||
(*a - *b).abs() < 1.0e-6,
|
||||
"{} is not approximately equal to {}",
|
||||
*a,
|
||||
*b
|
||||
);
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(feature = "test_lib")]
|
||||
mod tests {
|
||||
use super::parse_exec_time;
|
||||
|
||||
use crate::day;
|
||||
|
||||
#[test]
|
||||
fn parses_execution_times() {
|
||||
let res = parse_exec_time(
|
||||
&[
|
||||
"Part 1: 0 (74.13ns @ 100000 samples)".into(),
|
||||
"Part 2: 10 (74.13ms @ 99999 samples)".into(),
|
||||
"".into(),
|
||||
],
|
||||
day!(1),
|
||||
);
|
||||
assert_approx_eq!(res.total_nanos, 74130074.13_f64);
|
||||
assert_eq!(res.part_1.unwrap(), "74.13ns");
|
||||
assert_eq!(res.part_2.unwrap(), "74.13ms");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_with_patterns_in_input() {
|
||||
let res = parse_exec_time(
|
||||
&[
|
||||
"Part 1: @ @ @ ( ) ms (2s @ 5 samples)".into(),
|
||||
"Part 2: 10s (100ms @ 1 samples)".into(),
|
||||
"".into(),
|
||||
],
|
||||
day!(1),
|
||||
);
|
||||
assert_approx_eq!(res.total_nanos, 2100000000_f64);
|
||||
assert_eq!(res.part_1.unwrap(), "2s");
|
||||
assert_eq!(res.part_2.unwrap(), "100ms");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_missing_parts() {
|
||||
let res = parse_exec_time(
|
||||
&[
|
||||
"Part 1: ✖ ".into(),
|
||||
"Part 2: ✖ ".into(),
|
||||
"".into(),
|
||||
],
|
||||
day!(1),
|
||||
);
|
||||
assert_approx_eq!(res.total_nanos, 0_f64);
|
||||
assert_eq!(res.part_1.is_none(), true);
|
||||
assert_eq!(res.part_2.is_none(), true);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
/// Encapsulates code that interacts with solution functions.
|
||||
use crate::template::{aoc_cli, Day, ANSI_ITALIC, ANSI_RESET};
|
||||
use std::fmt::Display;
|
||||
use std::hint::black_box;
|
||||
use std::io::{stdout, Write};
|
||||
|
@ -7,7 +6,8 @@ use std::process::Output;
|
|||
use std::time::{Duration, Instant};
|
||||
use std::{cmp, env, process};
|
||||
|
||||
use super::ANSI_BOLD;
|
||||
use crate::template::ANSI_BOLD;
|
||||
use crate::template::{aoc_cli, Day, ANSI_ITALIC, ANSI_RESET};
|
||||
|
||||
pub fn run_part<I: Clone, T: Display>(func: impl Fn(I) -> Option<T>, input: I, day: Day, part: u8) {
|
||||
let part_str = format!("Part {part}");
|
||||
|
|
391
src/template/timings.rs
Normal file
391
src/template/timings.rs
Normal file
|
@ -0,0 +1,391 @@
|
|||
use std::{collections::HashMap, fs, io::Error, str::FromStr};
|
||||
use tinyjson::JsonValue;
|
||||
|
||||
use crate::template::Day;
|
||||
|
||||
static TIMINGS_FILE_PATH: &str = "./data/timings.json";
|
||||
|
||||
/// Represents benchmark times for a single day.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Timing {
|
||||
pub day: Day,
|
||||
pub part_1: Option<String>,
|
||||
pub part_2: Option<String>,
|
||||
pub total_nanos: f64,
|
||||
}
|
||||
|
||||
/// Represents benchmark times for a set of days.
|
||||
/// Can be serialized from / to JSON.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Timings {
|
||||
pub data: Vec<Timing>,
|
||||
}
|
||||
|
||||
impl Timings {
|
||||
/// Dehydrate timings to a JSON file.
|
||||
pub fn store_file(&self) -> Result<(), Error> {
|
||||
let json = JsonValue::from(self.clone());
|
||||
let mut file = fs::File::create(TIMINGS_FILE_PATH)?;
|
||||
json.format_to(&mut file)
|
||||
}
|
||||
|
||||
/// Rehydrate timings from a JSON file. If not present, returns empty timings.
|
||||
pub fn read_from_file() -> Self {
|
||||
let s = fs::read_to_string(TIMINGS_FILE_PATH)
|
||||
.map_err(|x| x.to_string())
|
||||
.and_then(Timings::try_from);
|
||||
|
||||
match s {
|
||||
Ok(timings) => timings,
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
Timings::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge two sets of timings, overwriting `self` with `other` if present.
|
||||
pub fn merge(&self, new: &Self) -> Self {
|
||||
let mut data: Vec<Timing> = vec![];
|
||||
|
||||
for timing in &new.data {
|
||||
data.push(timing.clone());
|
||||
}
|
||||
|
||||
for timing in &self.data {
|
||||
if !data.iter().any(|t| t.day == timing.day) {
|
||||
data.push(timing.clone());
|
||||
}
|
||||
}
|
||||
|
||||
data.sort_unstable_by(|a, b| a.day.cmp(&b.day));
|
||||
Timings { data }
|
||||
}
|
||||
|
||||
/// Sum up total duration of timings as millis.
|
||||
pub fn total_millis(&self) -> f64 {
|
||||
self.data.iter().map(|x| x.total_nanos).sum::<f64>() / 1_000_000_f64
|
||||
}
|
||||
|
||||
pub fn is_day_complete(&self, day: &Day) -> bool {
|
||||
self.data
|
||||
.iter()
|
||||
.any(|t| &t.day == day && t.part_1.is_some() && t.part_2.is_some())
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
impl From<Timings> for JsonValue {
|
||||
fn from(value: Timings) -> Self {
|
||||
let mut map: HashMap<String, JsonValue> = HashMap::new();
|
||||
|
||||
map.insert(
|
||||
"data".into(),
|
||||
JsonValue::Array(value.data.iter().map(JsonValue::from).collect()),
|
||||
);
|
||||
|
||||
JsonValue::Object(map)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Timings {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
let json = JsonValue::from_str(&value).or(Err("not valid JSON file."))?;
|
||||
|
||||
let json_data = json
|
||||
.get::<HashMap<String, JsonValue>>()
|
||||
.ok_or("expected JSON document to be an object.")?
|
||||
.get("data")
|
||||
.ok_or("expected JSON document to have key `data`.")?
|
||||
.get::<Vec<JsonValue>>()
|
||||
.ok_or("expected `json.data` to be an array.")?;
|
||||
|
||||
Ok(Timings {
|
||||
data: json_data
|
||||
.iter()
|
||||
.map(Timing::try_from)
|
||||
.collect::<Result<_, _>>()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
impl From<&Timing> for JsonValue {
|
||||
fn from(value: &Timing) -> Self {
|
||||
let mut map: HashMap<String, JsonValue> = HashMap::new();
|
||||
|
||||
map.insert("day".into(), JsonValue::String(value.day.to_string()));
|
||||
map.insert("total_nanos".into(), JsonValue::Number(value.total_nanos));
|
||||
|
||||
let part_1 = value.part_1.clone().map(JsonValue::String);
|
||||
let part_2 = value.part_2.clone().map(JsonValue::String);
|
||||
|
||||
map.insert(
|
||||
"part_1".into(),
|
||||
match part_1 {
|
||||
Some(x) => x,
|
||||
None => JsonValue::Null,
|
||||
},
|
||||
);
|
||||
|
||||
map.insert(
|
||||
"part_2".into(),
|
||||
match part_2 {
|
||||
Some(x) => x,
|
||||
None => JsonValue::Null,
|
||||
},
|
||||
);
|
||||
|
||||
JsonValue::Object(map)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&JsonValue> for Timing {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: &JsonValue) -> Result<Self, Self::Error> {
|
||||
let json = value
|
||||
.get::<HashMap<String, JsonValue>>()
|
||||
.ok_or("Expected timing to be a JSON object.")?;
|
||||
|
||||
let day = json
|
||||
.get("day")
|
||||
.and_then(|v| v.get::<String>())
|
||||
.and_then(|day| Day::from_str(day).ok())
|
||||
.ok_or("Expected timing.day to be a Day struct.")?;
|
||||
|
||||
let part_1 = json
|
||||
.get("part_1")
|
||||
.map(|v| if v.is_null() { None } else { v.get::<String>() })
|
||||
.ok_or("Expected timing.part_1 to be null or string.")?;
|
||||
|
||||
let part_2 = json
|
||||
.get("part_2")
|
||||
.map(|v| if v.is_null() { None } else { v.get::<String>() })
|
||||
.ok_or("Expected timing.part_2 to be null or string.")?;
|
||||
|
||||
let total_nanos = json
|
||||
.get("total_nanos")
|
||||
.and_then(|v| v.get::<f64>().copied())
|
||||
.ok_or("Expected timing.total_nanos to be a number.")?;
|
||||
|
||||
Ok(Timing {
|
||||
day,
|
||||
part_1: part_1.cloned(),
|
||||
part_2: part_2.cloned(),
|
||||
total_nanos,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
#[cfg(feature = "test_lib")]
|
||||
mod tests {
|
||||
use crate::day;
|
||||
|
||||
use super::{Timing, Timings};
|
||||
|
||||
fn get_mock_timings() -> Timings {
|
||||
Timings {
|
||||
data: vec![
|
||||
Timing {
|
||||
day: day!(1),
|
||||
part_1: Some("10ms".into()),
|
||||
part_2: Some("20ms".into()),
|
||||
total_nanos: 3e+10,
|
||||
},
|
||||
Timing {
|
||||
day: day!(2),
|
||||
part_1: Some("30ms".into()),
|
||||
part_2: Some("40ms".into()),
|
||||
total_nanos: 7e+10,
|
||||
},
|
||||
Timing {
|
||||
day: day!(4),
|
||||
part_1: Some("40ms".into()),
|
||||
part_2: None,
|
||||
total_nanos: 4e+10,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
mod deserialization {
|
||||
use crate::{day, template::timings::Timings};
|
||||
|
||||
#[test]
|
||||
fn handles_json_timings() {
|
||||
let json = r#"{ "data": [{ "day": "01", "part_1": "1ms", "part_2": null, "total_nanos": 1000000000 }] }"#.to_string();
|
||||
let timings = Timings::try_from(json).unwrap();
|
||||
assert_eq!(timings.data.len(), 1);
|
||||
let timing = timings.data.first().unwrap();
|
||||
assert_eq!(timing.day, day!(1));
|
||||
assert_eq!(timing.part_1, Some("1ms".to_string()));
|
||||
assert_eq!(timing.part_2, None);
|
||||
assert_eq!(timing.total_nanos, 1_000_000_000_f64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_empty_timings() {
|
||||
let json = r#"{ "data": [] }"#.to_string();
|
||||
let timings = Timings::try_from(json).unwrap();
|
||||
assert_eq!(timings.data.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn panics_for_invalid_json() {
|
||||
let json = r#"{}"#.to_string();
|
||||
Timings::try_from(json).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn panics_for_malformed_timings() {
|
||||
let json = r#"{ "data": [{ "day": "01" }, { "day": "26" }, { "day": "02", "part_2": null, "total_nanos": 0 }] }"#.to_string();
|
||||
Timings::try_from(json).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
mod serialization {
|
||||
use super::get_mock_timings;
|
||||
use std::collections::HashMap;
|
||||
use tinyjson::JsonValue;
|
||||
|
||||
#[test]
|
||||
fn serializes_timings() {
|
||||
let timings = get_mock_timings();
|
||||
let value = JsonValue::try_from(timings).unwrap();
|
||||
assert_eq!(
|
||||
value
|
||||
.get::<HashMap<String, JsonValue>>()
|
||||
.unwrap()
|
||||
.get("data")
|
||||
.unwrap()
|
||||
.get::<Vec<JsonValue>>()
|
||||
.unwrap()
|
||||
.len(),
|
||||
3
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod is_day_complete {
|
||||
use crate::{
|
||||
day,
|
||||
template::timings::{Timing, Timings},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn handles_completed_days() {
|
||||
let timings = Timings {
|
||||
data: vec![Timing {
|
||||
day: day!(1),
|
||||
part_1: Some("1ms".into()),
|
||||
part_2: Some("2ms".into()),
|
||||
total_nanos: 3_000_000_000_f64,
|
||||
}],
|
||||
};
|
||||
|
||||
assert_eq!(timings.is_day_complete(&day!(1)), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_partial_days() {
|
||||
let timings = Timings {
|
||||
data: vec![Timing {
|
||||
day: day!(1),
|
||||
part_1: Some("1ms".into()),
|
||||
part_2: None,
|
||||
total_nanos: 1_000_000_000_f64,
|
||||
}],
|
||||
};
|
||||
|
||||
assert_eq!(timings.is_day_complete(&day!(1)), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_uncompleted_days() {
|
||||
let timings = Timings {
|
||||
data: vec![Timing {
|
||||
day: day!(1),
|
||||
part_1: None,
|
||||
part_2: None,
|
||||
total_nanos: 0.0,
|
||||
}],
|
||||
};
|
||||
|
||||
assert_eq!(timings.is_day_complete(&day!(1)), false);
|
||||
}
|
||||
}
|
||||
|
||||
mod merge {
|
||||
use crate::{
|
||||
day,
|
||||
template::timings::{Timing, Timings},
|
||||
};
|
||||
|
||||
use super::get_mock_timings;
|
||||
|
||||
#[test]
|
||||
fn handles_disjunct_timings() {
|
||||
let timings = get_mock_timings();
|
||||
let other = Timings {
|
||||
data: vec![Timing {
|
||||
day: day!(3),
|
||||
part_1: None,
|
||||
part_2: None,
|
||||
total_nanos: 0_f64,
|
||||
}],
|
||||
};
|
||||
let merged = timings.merge(&other);
|
||||
assert_eq!(merged.data.len(), 4);
|
||||
assert_eq!(merged.data[0].day, day!(1));
|
||||
assert_eq!(merged.data[1].day, day!(2));
|
||||
assert_eq!(merged.data[2].day, day!(3));
|
||||
assert_eq!(merged.data[3].day, day!(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_overlapping_timings() {
|
||||
let timings = get_mock_timings();
|
||||
|
||||
let other = Timings {
|
||||
data: vec![Timing {
|
||||
day: day!(2),
|
||||
part_1: None,
|
||||
part_2: None,
|
||||
total_nanos: 0_f64,
|
||||
}],
|
||||
};
|
||||
let merged = timings.merge(&other);
|
||||
|
||||
assert_eq!(merged.data.len(), 3);
|
||||
assert_eq!(merged.data[0].day, day!(1));
|
||||
assert_eq!(merged.data[1].day, day!(2));
|
||||
assert_eq!(merged.data[1].total_nanos, 0_f64);
|
||||
assert_eq!(merged.data[2].day, day!(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_empty_timings() {
|
||||
let timings = Timings::default();
|
||||
let other = get_mock_timings();
|
||||
let merged = timings.merge(&other);
|
||||
assert_eq!(merged.data.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_empty_other_timings() {
|
||||
let timings = get_mock_timings();
|
||||
let other = Timings::default();
|
||||
let merged = timings.merge(&other);
|
||||
assert_eq!(merged.data.len(), 3);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue