- 
  Notifications
 You must be signed in to change notification settings 
- Fork 61
feat: add incremental benchmarks #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
 
  Merged
 
 
 
 
  Merged
 Changes from all commits
 Commits
 
 
 Show all changes
 
 
 7 commits
 
 
 Select commit
 Hold shift + click to select a range
 
 a2b6b6c
 
 feat: add incremental benchmarks
 
 
 fspoettel e2ece20
 
 fix: clippy
 
 
 fspoettel 8f1fb65
 
 refactor: make some template modules private
 
 
 fspoettel 28252e2
 
 refactor: rename tests
 
 
 fspoettel 0043528
 
 fix(review): address review feedback
 
 
 fspoettel c4b176d
 
 fix(review): address review feedback
 
 
 fspoettel 12e397e
 
 fix: clippy
 
 
 fspoettel File filter
Filter by extension
Conversations
 Failed to load comments. 
 
 
 
  Loading
 
 Jump to
 
 Jump to file
 
 
 
 Failed to load files. 
 
 
 
  Loading
 
 Diff view
Diff view
There are no files selected for viewing
 
 
 This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
 Learn more about bidirectional Unicode characters
 
 
 
 
 
 
 
 This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
 Learn more about bidirectional Unicode characters
 
 
 
 
 | Original file line number | Diff line number | Diff line change | 
|---|---|---|
|  | @@ -24,3 +24,7 @@ data/puzzles/* | |
|  | ||
| # Dhat | ||
| dhat-heap.json | ||
|  | ||
| # Benchmarks | ||
|  | ||
| data/timings.json | ||
 
 
 
 7 changes: 7 additions & 0 deletions
 
 
 
 Cargo.lock
 
 
 
 
  
 
 
 
 
 
 Oops, something went wrong.
 
 
 
 
 
 This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
 Learn more about bidirectional Unicode characters
 
 
 
 
 
 
 
 This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
 Learn more about bidirectional Unicode characters
 
 
 
 
 
 
 
 This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
 Learn more about bidirectional Unicode characters
 
 
 
 
 
 
 
 This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
 Learn more about bidirectional Unicode characters
 
 
 
 
 
 
 
 
 253 changes: 2 additions & 251 deletions
 
 
 
 src/template/commands/all.rs
 
 
 
 
  
 
 
 
 
 
 
 This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
 Learn more about bidirectional Unicode characters
 
 
 
 
 | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -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); | ||
| } | 
 
 
 This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
 Learn more about bidirectional Unicode characters
 
 
 
 
 | Original file line number | Diff line number | Diff line change | 
|---|---|---|
|  | @@ -3,3 +3,4 @@ pub mod download; | |
| pub mod read; | ||
| pub mod scaffold; | ||
| pub mod solve; | ||
| pub mod time; | ||
 
 Oops, something went wrong.
 
 
 
 Add this suggestion to a batch that can be applied as a single commit.
 This suggestion is invalid because no changes were made to the code.
 Suggestions cannot be applied while the pull request is closed.
 Suggestions cannot be applied while viewing a subset of changes.
 Only one suggestion per line can be applied in a batch.
 Add this suggestion to a batch that can be applied as a single commit.
 Applying suggestions on deleted lines is not supported.
 You must change the existing code in this line in order to create a valid suggestion.
 Outdated suggestions cannot be applied.
 This suggestion has been applied or marked resolved.
 Suggestions cannot be applied from pending reviews.
 Suggestions cannot be applied on multi-line comments.
 Suggestions cannot be applied while the pull request is queued to merge.
 Suggestion cannot be applied right now. Please check back later.