Skip to content

Commit 874f57b

Browse files
feat: make cargo time incremental by default (#53)
Co-authored-by: Tristan Guichaoua <[email protected]>
1 parent 4c42321 commit 874f57b

15 files changed

+758
-301
lines changed

.cargo/config.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ read = "run --quiet --release -- read"
66

77
solve = "run --quiet --release -- solve"
88
all = "run --quiet --release -- all"
9-
time = "run --quiet --release -- all --release --time"
9+
time = "run --quiet --release -- time"
1010

1111
[env]
1212
AOC_YEAR = "2023"

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,7 @@ data/puzzles/*
2424

2525
# Dhat
2626
dhat-heap.json
27+
28+
# Benchmarks
29+
30+
data/timings.json

Cargo.lock

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ inherits = "release"
1515
debug = 1
1616

1717
[features]
18+
dhat-heap = ["dhat"]
1819
today = ["chrono"]
1920
test_lib = []
20-
dhat-heap = ["dhat"]
2121

2222
[dependencies]
2323
chrono = { version = "0.4.31", optional = true }
24-
pico-args = "0.5.0"
2524
dhat = { version = "0.3.2", optional = true }
25+
pico-args = "0.5.0"
26+
tinyjson = "2"

README.md

+6-4
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,15 @@ cargo all
116116
# Total: 0.20ms
117117
```
118118

119-
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.
119+
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.
120120

121-
#### Update readme benchmarks
121+
### ➡️ Update readme benchmarks
122122

123-
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.
123+
The template can write benchmark times to the README via the `cargo time` command.
124124

125-
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.
125+
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>`.
126+
127+
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.
126128

127129
### ➡️ Run all tests
128130

src/main.rs

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use advent_of_code::template::commands::{all, download, read, scaffold, solve};
1+
use advent_of_code::template::commands::{all, download, read, scaffold, solve, time};
22
use args::{parse, AppArguments};
33

44
#[cfg(feature = "today")]
@@ -32,6 +32,10 @@ mod args {
3232
release: bool,
3333
time: bool,
3434
},
35+
Time {
36+
all: bool,
37+
day: Option<Day>,
38+
},
3539
#[cfg(feature = "today")]
3640
Today,
3741
}
@@ -44,6 +48,14 @@ mod args {
4448
release: args.contains("--release"),
4549
time: args.contains("--time"),
4650
},
51+
Some("time") => {
52+
let all = args.contains("--all");
53+
54+
AppArguments::Time {
55+
all,
56+
day: args.opt_free_from_str()?,
57+
}
58+
}
4759
Some("download") => AppArguments::Download {
4860
day: args.free_from_str()?,
4961
},
@@ -90,6 +102,7 @@ fn main() {
90102
}
91103
Ok(args) => match args {
92104
AppArguments::All { release, time } => all::handle(release, time),
105+
AppArguments::Time { day, all } => time::handle(day, all),
93106
AppArguments::Download { day } => download::handle(day),
94107
AppArguments::Read { day } => read::handle(day),
95108
AppArguments::Scaffold { day, download } => {

src/template/aoc_cli.rs

-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ pub enum AocCommandError {
1111
CommandNotFound,
1212
CommandNotCallable,
1313
BadExitStatus(Output),
14-
IoError,
1514
}
1615

1716
impl Display for AocCommandError {
@@ -22,7 +21,6 @@ impl Display for AocCommandError {
2221
AocCommandError::BadExitStatus(_) => {
2322
write!(f, "aoc-cli exited with a non-zero status.")
2423
}
25-
AocCommandError::IoError => write!(f, "could not write output files to file system."),
2624
}
2725
}
2826
}

src/template/commands/all.rs

+2-251
Original file line numberDiff line numberDiff line change
@@ -1,254 +1,5 @@
1-
use std::io;
2-
3-
use crate::template::{
4-
all_days,
5-
readme_benchmarks::{self, Timings},
6-
Day, ANSI_BOLD, ANSI_ITALIC, ANSI_RESET,
7-
};
1+
use crate::template::{all_days, run_multi::run_multi};
82

93
pub fn handle(is_release: bool, is_timed: bool) {
10-
let mut timings: Vec<Timings> = vec![];
11-
12-
all_days().for_each(|day| {
13-
if day > 1 {
14-
println!();
15-
}
16-
17-
println!("{ANSI_BOLD}Day {day}{ANSI_RESET}");
18-
println!("------");
19-
20-
let output = child_commands::run_solution(day, is_timed, is_release).unwrap();
21-
22-
if output.is_empty() {
23-
println!("Not solved.");
24-
} else {
25-
let val = child_commands::parse_exec_time(&output, day);
26-
timings.push(val);
27-
}
28-
});
29-
30-
if is_timed {
31-
let total_millis = timings.iter().map(|x| x.total_nanos).sum::<f64>() / 1_000_000_f64;
32-
33-
println!("\n{ANSI_BOLD}Total:{ANSI_RESET} {ANSI_ITALIC}{total_millis:.2}ms{ANSI_RESET}");
34-
35-
if is_release {
36-
match readme_benchmarks::update(timings, total_millis) {
37-
Ok(()) => println!("Successfully updated README with benchmarks."),
38-
Err(_) => {
39-
eprintln!("Failed to update readme with benchmarks.");
40-
}
41-
}
42-
}
43-
}
44-
}
45-
46-
#[derive(Debug)]
47-
pub enum Error {
48-
BrokenPipe,
49-
Parser(String),
50-
IO(io::Error),
51-
}
52-
53-
impl From<std::io::Error> for Error {
54-
fn from(e: std::io::Error) -> Self {
55-
Error::IO(e)
56-
}
57-
}
58-
59-
#[must_use]
60-
pub fn get_path_for_bin(day: Day) -> String {
61-
format!("./src/bin/{day}.rs")
62-
}
63-
64-
/// All solutions live in isolated binaries.
65-
/// This module encapsulates interaction with these binaries, both invoking them as well as parsing the timing output.
66-
mod child_commands {
67-
use super::{get_path_for_bin, Error};
68-
use crate::template::Day;
69-
use std::{
70-
io::{BufRead, BufReader},
71-
path::Path,
72-
process::{Command, Stdio},
73-
thread,
74-
};
75-
76-
/// Run the solution bin for a given day
77-
pub fn run_solution(day: Day, is_timed: bool, is_release: bool) -> Result<Vec<String>, Error> {
78-
// skip command invocation for days that have not been scaffolded yet.
79-
if !Path::new(&get_path_for_bin(day)).exists() {
80-
return Ok(vec![]);
81-
}
82-
83-
let day_padded = day.to_string();
84-
let mut args = vec!["run", "--quiet", "--bin", &day_padded];
85-
86-
if is_release {
87-
args.push("--release");
88-
}
89-
90-
if is_timed {
91-
// mirror `--time` flag to child invocations.
92-
args.push("--");
93-
args.push("--time");
94-
}
95-
96-
// spawn child command with piped stdout/stderr.
97-
// forward output to stdout/stderr while grabbing stdout lines.
98-
99-
let mut cmd = Command::new("cargo")
100-
.args(&args)
101-
.stdout(Stdio::piped())
102-
.stderr(Stdio::piped())
103-
.spawn()?;
104-
105-
let stdout = BufReader::new(cmd.stdout.take().ok_or(super::Error::BrokenPipe)?);
106-
let stderr = BufReader::new(cmd.stderr.take().ok_or(super::Error::BrokenPipe)?);
107-
108-
let mut output = vec![];
109-
110-
let thread = thread::spawn(move || {
111-
stderr.lines().for_each(|line| {
112-
eprintln!("{}", line.unwrap());
113-
});
114-
});
115-
116-
for line in stdout.lines() {
117-
let line = line.unwrap();
118-
println!("{line}");
119-
output.push(line);
120-
}
121-
122-
thread.join().unwrap();
123-
cmd.wait()?;
124-
125-
Ok(output)
126-
}
127-
128-
pub fn parse_exec_time(output: &[String], day: Day) -> super::Timings {
129-
let mut timings = super::Timings {
130-
day,
131-
part_1: None,
132-
part_2: None,
133-
total_nanos: 0_f64,
134-
};
135-
136-
output
137-
.iter()
138-
.filter_map(|l| {
139-
if !l.contains(" samples)") {
140-
return None;
141-
}
142-
143-
let Some((timing_str, nanos)) = parse_time(l) else {
144-
eprintln!("Could not parse timings from line: {l}");
145-
return None;
146-
};
147-
148-
let part = l.split(':').next()?;
149-
Some((part, timing_str, nanos))
150-
})
151-
.for_each(|(part, timing_str, nanos)| {
152-
if part.contains("Part 1") {
153-
timings.part_1 = Some(timing_str.into());
154-
} else if part.contains("Part 2") {
155-
timings.part_2 = Some(timing_str.into());
156-
}
157-
158-
timings.total_nanos += nanos;
159-
});
160-
161-
timings
162-
}
163-
164-
fn parse_to_float(s: &str, postfix: &str) -> Option<f64> {
165-
s.split(postfix).next()?.parse().ok()
166-
}
167-
168-
fn parse_time(line: &str) -> Option<(&str, f64)> {
169-
// for possible time formats, see: https://github.com/rust-lang/rust/blob/1.64.0/library/core/src/time.rs#L1176-L1200
170-
let str_timing = line
171-
.split(" samples)")
172-
.next()?
173-
.split('(')
174-
.last()?
175-
.split('@')
176-
.next()?
177-
.trim();
178-
179-
let parsed_timing = match str_timing {
180-
s if s.contains("ns") => s.split("ns").next()?.parse::<f64>().ok(),
181-
s if s.contains("µs") => parse_to_float(s, "µs").map(|x| x * 1000_f64),
182-
s if s.contains("ms") => parse_to_float(s, "ms").map(|x| x * 1_000_000_f64),
183-
s => parse_to_float(s, "s").map(|x| x * 1_000_000_000_f64),
184-
}?;
185-
186-
Some((str_timing, parsed_timing))
187-
}
188-
189-
/// copied from: https://github.com/rust-lang/rust/blob/1.64.0/library/std/src/macros.rs#L328-L333
190-
#[cfg(feature = "test_lib")]
191-
macro_rules! assert_approx_eq {
192-
($a:expr, $b:expr) => {{
193-
let (a, b) = (&$a, &$b);
194-
assert!(
195-
(*a - *b).abs() < 1.0e-6,
196-
"{} is not approximately equal to {}",
197-
*a,
198-
*b
199-
);
200-
}};
201-
}
202-
203-
#[cfg(feature = "test_lib")]
204-
mod tests {
205-
use super::parse_exec_time;
206-
207-
use crate::day;
208-
209-
#[test]
210-
fn test_well_formed() {
211-
let res = parse_exec_time(
212-
&[
213-
"Part 1: 0 (74.13ns @ 100000 samples)".into(),
214-
"Part 2: 10 (74.13ms @ 99999 samples)".into(),
215-
"".into(),
216-
],
217-
day!(1),
218-
);
219-
assert_approx_eq!(res.total_nanos, 74130074.13_f64);
220-
assert_eq!(res.part_1.unwrap(), "74.13ns");
221-
assert_eq!(res.part_2.unwrap(), "74.13ms");
222-
}
223-
224-
#[test]
225-
fn test_patterns_in_input() {
226-
let res = parse_exec_time(
227-
&[
228-
"Part 1: @ @ @ ( ) ms (2s @ 5 samples)".into(),
229-
"Part 2: 10s (100ms @ 1 samples)".into(),
230-
"".into(),
231-
],
232-
day!(1),
233-
);
234-
assert_approx_eq!(res.total_nanos, 2100000000_f64);
235-
assert_eq!(res.part_1.unwrap(), "2s");
236-
assert_eq!(res.part_2.unwrap(), "100ms");
237-
}
238-
239-
#[test]
240-
fn test_missing_parts() {
241-
let res = parse_exec_time(
242-
&[
243-
"Part 1: ✖ ".into(),
244-
"Part 2: ✖ ".into(),
245-
"".into(),
246-
],
247-
day!(1),
248-
);
249-
assert_approx_eq!(res.total_nanos, 0_f64);
250-
assert_eq!(res.part_1.is_none(), true);
251-
assert_eq!(res.part_2.is_none(), true);
252-
}
253-
}
4+
run_multi(all_days().collect(), is_release, is_timed);
2545
}

src/template/commands/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ pub mod download;
33
pub mod read;
44
pub mod scaffold;
55
pub mod solve;
6+
pub mod time;

0 commit comments

Comments
 (0)