├── .gitignore ├── examples └── lettersorter │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ ├── benches │ └── sort_numbers.rs │ └── src │ └── lib.rs ├── doc ├── sql.png ├── intro-svg.png ├── intro-tbl.png ├── sort-tbl.png ├── intro-history.png ├── intro-viewer-graph.png └── intro-viewer-table.png ├── rsc ├── sql-wasm.wasm ├── README.md ├── dom-doll.js ├── viewer.css ├── viewer.js ├── vis-timeline-graph2d.min.css └── sql-wasm.js ├── src ├── error.rs ├── task_measure.rs ├── task_bench_diff.rs ├── task_history.rs ├── black_box.rs ├── skin.rs ├── lib.rs ├── printer.rs ├── history_graph.rs ├── git_info.rs ├── history_tbl.rs ├── command.rs ├── main_macro.rs ├── task_bench.rs ├── bench.rs ├── report.rs ├── html_viewer.rs ├── glassbench.rs └── db.rs ├── Cargo.toml ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .bacon-locations 2 | /target 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /examples/lettersorter/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /glassbench_*.db 3 | -------------------------------------------------------------------------------- /doc/sql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/glassbench/HEAD/doc/sql.png -------------------------------------------------------------------------------- /doc/intro-svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/glassbench/HEAD/doc/intro-svg.png -------------------------------------------------------------------------------- /doc/intro-tbl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/glassbench/HEAD/doc/intro-tbl.png -------------------------------------------------------------------------------- /doc/sort-tbl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/glassbench/HEAD/doc/sort-tbl.png -------------------------------------------------------------------------------- /rsc/sql-wasm.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/glassbench/HEAD/rsc/sql-wasm.wasm -------------------------------------------------------------------------------- /doc/intro-history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/glassbench/HEAD/doc/intro-history.png -------------------------------------------------------------------------------- /doc/intro-viewer-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/glassbench/HEAD/doc/intro-viewer-graph.png -------------------------------------------------------------------------------- /doc/intro-viewer-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/glassbench/HEAD/doc/intro-viewer-table.png -------------------------------------------------------------------------------- /examples/lettersorter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lettersorter" 3 | version = "0.1.0" 4 | authors = ["Canop "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | 9 | [dev-dependencies] 10 | glassbench = { path = "../.." } 11 | 12 | [[bench]] 13 | name = "sort_numbers" 14 | harness = false 15 | -------------------------------------------------------------------------------- /examples/lettersorter/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | In this directory, run `cargo bench`. 4 | 5 | After that, have a look at the `sort` function in `lib.rs`. Try to optimize it, then check the results by running `cargo bench` again. 6 | 7 | Be careful that changing the benchmark test cases or the load on your system will impact the results. 8 | -------------------------------------------------------------------------------- /rsc/README.md: -------------------------------------------------------------------------------- 1 | 2 | The files of this directory are directly embedded in the temporary HTML file you generate with `--graph`. 3 | 4 | `sql-*` files come from https://github.com/sql-js/sql.js (MIT license) and provide SQLite3 querying capabilities. 5 | 6 | `vis-*` files come from https://visjs.org/ (dual-licensed under Apache-2.0 and MIT) and provide the grapher. 7 | 8 | `dom-doll.js` comes from https://github.com/Canop/dom-doll (MIT license) and is an HTML DOM utility. 9 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | /// glassbench error type 2 | #[derive(thiserror::Error, Debug)] 3 | pub enum GlassBenchError { 4 | #[error("IO error: {0}")] 5 | IO(#[from] std::io::Error), 6 | #[error("iter already called")] 7 | IterAlreadyCalled, 8 | #[error("SQLite error: {0}")] 9 | SQLite(#[from] rusqlite::Error), 10 | #[error("User query not understood")] 11 | ClientError, 12 | #[error("JSON error: {0}")] 13 | Json(#[from] serde_json::Error), 14 | } 15 | -------------------------------------------------------------------------------- /src/task_measure.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | /// The result of the measure of a task: number of iterations 4 | /// and total duration 5 | #[derive(Debug, Clone, Copy)] 6 | pub struct TaskMeasure { 7 | pub iterations: u32, 8 | pub total_duration: Duration, 9 | } 10 | 11 | impl TaskMeasure { 12 | /// compute the only value you're normally interested into: 13 | /// the mean duration 14 | pub fn mean_duration(&self) -> Duration { 15 | self.total_duration / self.iterations 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/task_bench_diff.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// Printable difference between two task measures 4 | pub struct TaskBenchDiff { 5 | pub percents: f64, 6 | } 7 | 8 | impl TaskBenchDiff { 9 | pub fn new(old_mes: TaskMeasure, new_mes: TaskMeasure) -> Self { 10 | let old_ns = old_mes.mean_duration().as_nanos() as f64; 11 | let new_ns = new_mes.mean_duration().as_nanos() as f64; 12 | let diff_ns = new_ns - old_ns; 13 | let percents = 100f64 * diff_ns / old_ns; 14 | Self { percents } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/task_history.rs: -------------------------------------------------------------------------------- 1 | use {crate::*, chrono::prelude::*}; 2 | 3 | /// A measure of a task, with time, commit and tag 4 | #[derive(Debug)] 5 | pub struct TaskRecord { 6 | pub time: DateTime, 7 | pub git_info: Option, 8 | pub tag: Option, 9 | pub measure: TaskMeasure, 10 | } 11 | 12 | /// The history of the measures of ta task as defined 13 | /// by the bench name and task name 14 | #[derive(Debug)] 15 | pub struct TaskHistory { 16 | pub bench_name: String, 17 | pub task_name: String, 18 | pub records: Vec, 19 | } 20 | -------------------------------------------------------------------------------- /src/black_box.rs: -------------------------------------------------------------------------------- 1 | /// tell the compiler not to optimize away the given 2 | /// argument (which is expected to be the function call 3 | /// you want to benchmark). 4 | /// 5 | /// This should use core::hint::bench_black_box 6 | /// but it's not yet stabilized, see 7 | /// https://github.com/rust-lang/rust/issues/64102 8 | /// 9 | /// In the meantime, it uses the same implementation 10 | /// than Criterion. 11 | pub fn pretend_used(t: T) -> T { 12 | unsafe { 13 | let ret = std::ptr::read_volatile(&t); 14 | std::mem::forget(t); 15 | ret 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/skin.rs: -------------------------------------------------------------------------------- 1 | use termimad::{ 2 | crossterm::style::{Attribute::*, Color::*}, 3 | minimad::Alignment, 4 | MadSkin, 5 | }; 6 | 7 | /// crate the skin used for terminal display using Termimad 8 | pub fn make_skin(color: bool) -> MadSkin { 9 | if color { 10 | make_color_skin() 11 | } else { 12 | make_no_color_skin() 13 | } 14 | } 15 | 16 | fn make_color_skin() -> MadSkin { 17 | let mut skin = MadSkin::default(); 18 | //skin.paragraph.set_fg(AnsiValue(153)); 19 | skin.headers[0].align = Alignment::Left; 20 | skin.set_headers_fg(AnsiValue(153)); 21 | skin.strikeout.remove_attr(CrossedOut); 22 | skin.strikeout.set_fg(AnsiValue(9)); 23 | skin.italic.remove_attr(Italic); 24 | skin.italic.set_fg(AnsiValue(70)); 25 | skin 26 | } 27 | 28 | fn make_no_color_skin() -> MadSkin { 29 | MadSkin::no_style() 30 | } 31 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "glassbench" 3 | version = "0.4.3" 4 | authors = ["dystroy "] 5 | repository = "https://github.com/Canop/glassbench" 6 | description = "rust benchmark with memory" 7 | edition = "2021" 8 | keywords = ["benchmark"] 9 | license = "MIT" 10 | categories = ["development-tools", "development-tools::profiling"] 11 | 12 | [dependencies] 13 | base64 = "0.13" 14 | chrono = { version = "0.4", features = ["serde"] } 15 | csv2svg = "0.2.3" 16 | git2 = { version="0.19", default-features=false } 17 | lazy_static = "1.4" 18 | open = "1.1" 19 | rusqlite = { version = "0.31", features = ["bundled"] } 20 | serde = { version = "1.0", features = ["derive"] } 21 | serde_json = "1.0" 22 | tempfile = "3.1" 23 | termimad = { version = "0.30", default-features = false, features = ["special-renders"] } 24 | thiserror = "1.0" 25 | 26 | [patch.crates-io] 27 | # crokey = { path = "../crokey" } 28 | # termimad = { path = "../termimad" } 29 | 30 | -------------------------------------------------------------------------------- /examples/lettersorter/benches/sort_numbers.rs: -------------------------------------------------------------------------------- 1 | use { 2 | lettersorter::sort, 3 | glassbench::*, 4 | }; 5 | 6 | static SMALL_NUMBERS: &[&str] = &[ 7 | "0.123456789", 8 | "42", 9 | "-6", 10 | "π/2", 11 | "e²", 12 | ]; 13 | 14 | static BIG_NUMBERS: &[&str] = &[ 15 | "424568", 16 | "45865452*44574*778141*78999", 17 | "same but even bigger", 18 | "42!", 19 | "infinite", 20 | ]; 21 | 22 | fn bench_number_sorting(bench: &mut Bench) { 23 | bench.task("small numbers", |task| { 24 | task.iter(|| { 25 | for n in SMALL_NUMBERS { 26 | pretend_used(sort(n)); 27 | } 28 | }); 29 | }); 30 | bench.task("big numbers", |task| { 31 | task.iter(|| { 32 | for n in BIG_NUMBERS { 33 | pretend_used(sort(n)); 34 | } 35 | }); 36 | }); 37 | } 38 | 39 | glassbench!( 40 | "Number Sorting", 41 | bench_number_sorting, 42 | ); 43 | 44 | -------------------------------------------------------------------------------- /examples/lettersorter/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | /// sort the letters in the string 3 | pub fn sort(s: &str) -> String { 4 | // let's build a string 5 | let mut s = s.to_string(); 6 | // let's add a 0 because strings should be zero terminated, right ? 7 | s.push_str("0"); 8 | // now make a vector: it's easier to sort 9 | let mut chars: Vec = s.chars().collect(); 10 | // sort in place (so it's faaast!) 11 | chars.sort(); 12 | // make the string to return 13 | let mut s: String = chars.iter().collect(); 14 | // wait, I've been told there should not be a zero, where is it ? 15 | let zero_idx = s.find('0'); 16 | // remove it before somebody notices it 17 | s.remove(zero_idx.unwrap()); 18 | // well, it's done 19 | s 20 | } 21 | 22 | 23 | #[cfg(test)] 24 | mod tests { 25 | use crate::*; 26 | #[test] 27 | fn it_works() { 28 | assert_eq!(sort("bac").as_str(), "abc"); 29 | assert_eq!(sort("π/2").as_str(), "/2π"); 30 | assert_eq!(sort("52145729034508").as_str(), "00122344555789"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | 3 | A micro-benchmarking crate with memory. 4 | 5 | See [usage and example in README](https://github.com/Canop/glassbench#usage). 6 | 7 | In a standard setup you'll only use 8 | 9 | * the [glassbench!] macro which let you title your bench and add functions defining tasks 10 | * the [Bench] struct, as argument of your global bench function, with its [Bench::task] function to define a task 11 | * the [TaskBench] struct that you receive as argument when defining a task. You'll call 12 | [TaskBench::iter] with the callback to benchmark 13 | * [pretend_used] as an opaque sinkhole, which can receive the values you produce in your tests and 14 | prevent the optimizer to remove their construction 15 | 16 | */ 17 | 18 | mod bench; 19 | mod black_box; 20 | mod command; 21 | mod db; 22 | mod error; 23 | mod git_info; 24 | mod history_graph; 25 | mod history_tbl; 26 | mod html_viewer; 27 | mod main_macro; 28 | mod printer; 29 | mod report; 30 | mod skin; 31 | mod task_bench; 32 | mod task_bench_diff; 33 | mod task_history; 34 | mod task_measure; 35 | 36 | pub use { 37 | bench::*, black_box::*, command::*, db::*, error::*, git_info::*, history_graph::*, 38 | history_tbl::*, html_viewer::*, main_macro::*, printer::*, report::*, task_bench::*, 39 | task_bench_diff::*, task_history::*, task_measure::*, 40 | }; 41 | -------------------------------------------------------------------------------- /src/printer.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::*, 3 | termimad::{ 4 | crossterm::tty::IsTty, 5 | minimad::{OwningTemplateExpander, TextTemplate}, 6 | terminal_size, FmtText, MadSkin, 7 | }, 8 | }; 9 | 10 | /// A small helper to print using markdown templates 11 | pub struct Printer { 12 | pub skin: MadSkin, 13 | pub terminal_width: usize, 14 | } 15 | 16 | impl Printer { 17 | /// create a new printer 18 | /// 19 | /// The skin will be without style and color if the 20 | /// output is piped. 21 | pub fn new() -> Self { 22 | let terminal_width = terminal_size().0 as usize; 23 | let color = !is_output_piped(); 24 | let skin = skin::make_skin(color); 25 | Self { 26 | skin, 27 | terminal_width, 28 | } 29 | } 30 | pub fn print(&self, expander: OwningTemplateExpander, template: &str) { 31 | let template = TextTemplate::from(template); 32 | let text = expander.expand(&template); 33 | let fmt_text = FmtText::from_text(&self.skin, text, Some(self.terminal_width)); 34 | print!("{}", fmt_text); 35 | } 36 | } 37 | 38 | impl Default for Printer { 39 | fn default() -> Self { 40 | Self::new() 41 | } 42 | } 43 | 44 | fn is_output_piped() -> bool { 45 | !std::io::stdout().is_tty() 46 | } 47 | -------------------------------------------------------------------------------- /src/history_graph.rs: -------------------------------------------------------------------------------- 1 | use {crate::*, csv2svg::*}; 2 | 3 | /// A temporary structure for graphing a history 4 | pub struct HistoryGraph<'b> { 5 | history: &'b TaskHistory, 6 | } 7 | 8 | impl<'b> HistoryGraph<'b> { 9 | pub fn new(history: &'b TaskHistory) -> Self { 10 | Self { history } 11 | } 12 | 13 | /// Open the history as a SVG graph in the browser (hopefully) 14 | pub fn open_in_browser(&self) -> Result<(), GlassBenchError> { 15 | let h = &self.history; 16 | let mut times = Vec::new(); 17 | let mut durations = Vec::new(); 18 | for record in &h.records { 19 | times.push(record.time); 20 | // here we fairlessly convert u128 to i64 21 | let value = record.measure.mean_duration().as_nanos() as i64; 22 | durations.push(Some(value)); 23 | } 24 | let name = format!("{} / {} (ns)", &h.bench_name, &h.task_name); 25 | let tbl = Tbl::from_seqs(vec![ 26 | Seq::from_increasing_times("time".to_string(), times).unwrap(), 27 | Seq::from_integers(name, durations).unwrap(), 28 | ]) 29 | .unwrap(); 30 | let graph = Graph::new(tbl); 31 | let svg = graph.build_svg(); 32 | let (mut w, path) = temp_file()?; 33 | csv2svg::write_embedded(&mut w, &svg).unwrap(); 34 | open::that(path)?; 35 | Ok(()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ### v0.4.3 - 2024-08-15 3 | - update git2 to 0.19 4 | - update rusqlite to 0.31 5 | 6 | 7 | ### v0.4.2 - 2024-08-12 8 | - update termimad to 0.30 9 | 10 | 11 | ### v0.4.1 - 2024-01-29 12 | - update termimad to 0.29 13 | 14 | 15 | ### v0.4.0 - 2024-01-18 16 | - cross-project dependency versions harmonization, termimad is now 0.28 17 | 18 | 19 | ### v0.3.6 - 2023/10/14 20 | - cross-project dependency versions harmonization to ease vetting 21 | 22 | 23 | ### v0.3.4 - 2022-12-10 24 | - fix a compilation problem (import ambiguity) - Thanks @orhun 25 | 26 | 27 | ### v0.3.3 - 2022-06-07 28 | - upgrade dependencies to fix compilation problems 29 | 30 | 31 | ### v0.3.1 - 2021-03-12 32 | - upgrade git2 dependency due to compilation problem with a minor 0.13 version 33 | 34 | 35 | ### v0.3.0 - 2021-04-02 36 | - new grapher, generated on `--graph` 37 | 38 | 39 | ### v0.2.2 - 2021-03-28 40 | - documentation improvements 41 | 42 | 43 | ### v0.2.1 - 2021-03-28 44 | - better documentation 45 | 46 | 47 | ### v0.2.0 - 2021-03-27 48 | - storage now based on sqlite3 49 | - `--tag` and `--history` 50 | 51 | 52 | ### v0.1.0 - 2021-03-25 53 | - first public release 54 | -------------------------------------------------------------------------------- /src/git_info.rs: -------------------------------------------------------------------------------- 1 | use git2::Repository; 2 | 3 | /// Git related information regarding the execution context 4 | /// 5 | /// Right now it just contains the id of the head commit. 6 | #[derive(Debug, Clone)] 7 | pub struct GitInfo { 8 | pub commit_id: String, 9 | } 10 | 11 | impl GitInfo { 12 | /// Read the current git state (if any) 13 | pub fn read() -> Option { 14 | std::env::current_dir() 15 | .ok() 16 | .and_then(|dir| Repository::discover(dir).ok()) 17 | .and_then(|repo| { 18 | repo.head().ok().and_then(|head| { 19 | //println!("head: {:?} {:#?} ", &head.name(), &head.kind()); 20 | head.peel_to_commit().ok().map(|commit| GitInfo { 21 | commit_id: commit.id().to_string(), 22 | }) 23 | }) 24 | }) 25 | } 26 | 27 | /// Build a readable abstract of the diff of two [GitInfo] 28 | pub(crate) fn diff(old_gi: &Option, new_gi: &Option) -> String { 29 | match (old_gi, new_gi) { 30 | (Some(old_gi), Some(new_gi)) => { 31 | if old_gi.commit_id == new_gi.commit_id { 32 | "(same commit)".to_string() 33 | } else { 34 | format!( 35 | "(last commit: {})", 36 | // I'm sure there's a less stupid way to print the first 8 chars 37 | old_gi.commit_id.chars().take(8).collect::(), 38 | ) 39 | } 40 | } 41 | _ => "".to_string(), 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /rsc/dom-doll.js: -------------------------------------------------------------------------------- 1 | // dom-doll by dystroy 2 | // https://github.com/Canop/dom-doll 3 | 4 | function $() { 5 | const parser = /^<([^ .#>]+)?(?:#([^ .#>]+))?(?:\.([^ #>]+))?(?:[^>]*>(.+))?$/ 6 | var nodes 7 | for (let arg of arguments) { 8 | if (typeof arg == "string") { 9 | let parents = nodes || [document] 10 | nodes = [] 11 | for (let parent of parents) { 12 | let creator = arg.match(parser) 13 | if (creator) { 14 | let e = document.createElement(creator[1] || "div") 15 | if (creator[2]) e.id = creator[2] 16 | if (creator[3]) e.className = creator[3].replaceAll('.', ' ') 17 | if (creator[4]) e.textContent = creator[4] 18 | if (parent != document) { 19 | parent.appendChild(e) 20 | } 21 | nodes.push(e) 22 | } else { 23 | for (let child of parent.querySelectorAll(arg)) { 24 | nodes.push(child) 25 | } 26 | } 27 | } 28 | } else if (typeof arg == "function") { 29 | if (nodes) nodes.forEach(arg) 30 | else nodes = arg() 31 | } else if (arg instanceof Element) { 32 | if (nodes) nodes[0].appendChild(arg) 33 | else nodes = [arg] 34 | } else if (Array.isArray(arg)) { 35 | for (let e of arg) { 36 | nodes[0].appendChild(e) 37 | } 38 | } else if (typeof arg == "object") { 39 | for (let e of nodes) { 40 | for (let attr in arg) { 41 | let val = arg[attr] 42 | if (typeof val == "function") { 43 | e.addEventListener(attr, val) 44 | } else if (["textContent", "innerHTML"].includes(attr)) { 45 | e[attr] = val 46 | } else { 47 | e.setAttribute(attr, val) 48 | } 49 | } 50 | } 51 | } 52 | } 53 | return (nodes && nodes.length==1) ? nodes[0] : nodes 54 | } 55 | 56 | const $$ = document.querySelectorAll.bind(document) 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/history_tbl.rs: -------------------------------------------------------------------------------- 1 | use {crate::*, termimad::minimad::OwningTemplateExpander}; 2 | 3 | static MD: &str = r#" 4 | ## History of ${bench-name} / ${task-name} 5 | |:-:|:-:|:-:|:-: 6 | |**time**|**commit**|**tag**|**mean duration** 7 | |:-|-:|-:|-: 8 | ${records 9 | |${time}|${commit}|${tag}|**${mean-duration}** 10 | } 11 | |-: 12 | "#; 13 | 14 | /// A temporary structure for printing as table a 15 | /// history in standard output 16 | pub struct HistoryTbl<'b> { 17 | history: &'b TaskHistory, 18 | } 19 | 20 | impl<'b> HistoryTbl<'b> { 21 | pub fn new(history: &'b TaskHistory) -> Self { 22 | Self { history } 23 | } 24 | 25 | /// Print the history to the console 26 | pub fn print(&self, printer: &Printer) { 27 | let h = &self.history; 28 | let mut expander = OwningTemplateExpander::new(); 29 | expander 30 | .set("bench-name", &h.bench_name) 31 | .set("task-name", &h.task_name); 32 | for record in &h.records { 33 | let sub = expander.sub("records"); 34 | sub.set("time", record.time) 35 | .set( 36 | "commit", 37 | if let Some(gi) = record.git_info.as_ref() { 38 | gi.commit_id.chars().take(8).collect::() 39 | } else { 40 | " ".to_string() 41 | }, 42 | ) 43 | .set( 44 | "tag", 45 | if let Some(tag) = record.tag.as_ref() { 46 | tag.to_string() 47 | } else { 48 | " ".to_string() 49 | }, 50 | ) 51 | .set( 52 | "mean-duration", 53 | format!("{:?}", record.measure.mean_duration()), 54 | ); 55 | } 56 | printer.print(expander, MD); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/command.rs: -------------------------------------------------------------------------------- 1 | /// What the user asked at the cli 2 | #[derive(Debug, Clone)] 3 | pub struct Command { 4 | pub benches: Vec, 5 | pub graph: Option, 6 | pub history: Option, 7 | pub tag: Option, 8 | pub no_save: bool, 9 | pub verbose: bool, 10 | } 11 | 12 | impl Command { 13 | /// read std::env::args 14 | pub fn read() -> Self { 15 | let mut args = std::env::args().skip(1); // it's the path to the compiled bench in target 16 | let mut benches = Vec::new(); 17 | let mut graph = None; 18 | let mut history = None; 19 | let mut tag = None; 20 | let mut before_sep = true; 21 | let mut no_save = false; 22 | let mut verbose = false; 23 | while let Some(arg) = args.next() { 24 | if arg == "--" { 25 | before_sep = false; 26 | } else if before_sep { 27 | if !arg.starts_with("--") { 28 | benches.push(arg); 29 | } 30 | } else { 31 | match arg.as_str() { 32 | "--no-save" => { 33 | no_save = true; 34 | } 35 | "--verbose" => { 36 | verbose = true; 37 | } 38 | "--graph" => { 39 | if let Some(val) = args.next() { 40 | graph = Some(val); 41 | } 42 | } 43 | "--history" => { 44 | if let Some(val) = args.next() { 45 | history = Some(val); 46 | } 47 | } 48 | "--tag" => { 49 | if let Some(val) = args.next() { 50 | tag = Some(val); 51 | } 52 | } 53 | "--bench" => { 54 | // that's how the command given by cargo bench always ends 55 | } 56 | _ => { 57 | println!("ignored bench argument: {:?}", arg); 58 | } 59 | } 60 | } 61 | } 62 | Self { 63 | benches, 64 | graph, 65 | history, 66 | tag, 67 | no_save, 68 | verbose, 69 | } 70 | } 71 | 72 | /// tell whether this specific bench should be included 73 | pub fn include_bench(&self, name: &str) -> bool { 74 | self.benches.is_empty() || self.benches.iter().any(|g| g == name) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main_macro.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// Generates a benchmark with a consistent id 4 | /// (using the benchmark file title), calling 5 | /// the benchmarking functions given in argument. 6 | /// 7 | /// ```no-test 8 | /// glassbench!( 9 | /// "Sortings", 10 | /// bench_number_sorting, 11 | /// bench_alpha_sorting, 12 | /// ); 13 | /// ``` 14 | /// 15 | /// This generates the whole main function. 16 | /// If you want to set the bench name yourself 17 | /// (not recommanded), or change the way the launch 18 | /// arguments are used, you can write the main 19 | /// yourself and call [create_bench] and [after_bench] 20 | /// instead of using this macro. 21 | #[macro_export] 22 | macro_rules! glassbench { 23 | ( 24 | $title: literal, 25 | $( $fun: path, )+ 26 | ) => { 27 | pub fn main() { 28 | use glassbench::*; 29 | let name = env!("CARGO_CRATE_NAME"); 30 | let cmd = Command::read(); 31 | if cmd.include_bench(&name) { 32 | let mut bench = create_bench(name, $title, &cmd); 33 | $( 34 | $fun(&mut bench); 35 | )+ 36 | if let Err(e) = after_bench(&mut bench, &cmd) { 37 | eprintln!("{}", e); 38 | } 39 | } else { 40 | println!("skipping bench {:?}", &name); 41 | } 42 | } 43 | } 44 | } 45 | 46 | /// Create a bench with a user defined name (instead of 47 | /// the file name) and command (instead of the one read in 48 | /// arguments) 49 | /// 50 | /// Unless you have special reasons, you should not 51 | /// use this function but the [glassbench!] function. 52 | pub fn create_bench(name: S1, title: S2, cmd: &Command) -> Bench 53 | where 54 | S1: Into, 55 | S2: Into, 56 | { 57 | let mut bench = Bench::new(name, title); 58 | bench.tag = cmd.tag.clone(); 59 | bench 60 | } 61 | 62 | /// Print the tabular report for the executed benchmark 63 | /// then graph, list history, and or save according to 64 | /// command 65 | /// 66 | /// Unless you have special reasons, you should not 67 | /// use this function but the [glassbench!] function. 68 | pub fn after_bench(bench: &mut Bench, cmd: &Command) -> Result<(), GlassBenchError> { 69 | let printer = Printer::new(); 70 | let mut db = Db::open()?; 71 | let previous = db.last_bench_named(&bench.name)?; 72 | let report = Report::new(bench, &previous); 73 | report.print(&printer); 74 | let mut no_save = cmd.no_save; 75 | if let Some(graph_arg) = cmd.graph.as_ref() { 76 | let task_name = bench.task_name_from_arg(graph_arg); 77 | let viewer = HtmlViewer::new(&bench.name, task_name); 78 | viewer.open_in_browser()?; 79 | no_save = true; 80 | } 81 | if let Some(tbl_arg) = cmd.history.as_ref() { 82 | let history = bench.task_history(&mut db, tbl_arg)?; 83 | let tbl = HistoryTbl::new(&history); 84 | tbl.print(&printer); 85 | no_save = true; 86 | } 87 | if !no_save { 88 | db.save_bench(bench)?; 89 | } 90 | Ok(()) 91 | } 92 | -------------------------------------------------------------------------------- /src/task_bench.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::*, 3 | std::{ 4 | convert::TryInto, 5 | time::{Duration, Instant}, 6 | }, 7 | }; 8 | 9 | /// Number of iterations to do before everything else 10 | pub const WARMUP_ITERATIONS: usize = 2; 11 | 12 | /// Number of iterations to do, after warmup, to estimate the total number 13 | /// of iterations to do 14 | /// 15 | /// (regarding real benchmark, it can be considered as part of benchmark) 16 | pub const ESTIMATE_ITERATIONS: u32 = 5; 17 | 18 | /// How long we'd like the measures of a task to go. Will be divided by 19 | /// the duration of a task in the estimate phase to decide how many 20 | /// iterations we'll do for measures 21 | pub const OPTIMAL_DURATION_NS: u128 = Duration::from_secs(2).as_nanos(); 22 | 23 | /// The absolute minimal number of iterations we don't want to go below 24 | /// for benchmarking (to minimize random dispersion) 25 | pub const MINIMAL_ITERATIONS: u32 = 50; 26 | 27 | /// Benching of one task 28 | #[derive(Debug, Clone)] 29 | pub struct TaskBench { 30 | pub name: String, 31 | pub measure: Option, 32 | } 33 | 34 | impl TaskBench { 35 | pub(crate) fn new(name: String) -> Self { 36 | Self { 37 | name, 38 | measure: None, 39 | } 40 | } 41 | 42 | pub(crate) fn diff_with(&self, old_bench: &Bench) -> Option { 43 | old_bench 44 | .tasks 45 | .iter() 46 | .find(|tb| tb.name == self.name) 47 | .and_then(|old_tb| old_tb.measure) 48 | .and_then(|old_mes| { 49 | self.measure 50 | .map(|new_mes| TaskBenchDiff::new(old_mes, new_mes)) 51 | }) 52 | } 53 | 54 | /// Call the function to measure 55 | /// 56 | /// There will be an initial warmup, after which 57 | /// the function will be called enought times to 58 | /// get a reliable estimation of its duration. 59 | pub fn iter(&mut self, mut measured: M) 60 | where 61 | M: FnMut() -> R, 62 | { 63 | if self.measure.is_some() { 64 | eprintln!("bench already used - please fix your benchmark"); 65 | return; 66 | } 67 | // just a warmup 68 | for _ in 0..WARMUP_ITERATIONS { 69 | measured(); 70 | } 71 | // first estimation, to compute the number of iterations later 72 | let start = Instant::now(); 73 | for _ in 0..ESTIMATE_ITERATIONS { 74 | measured(); 75 | } 76 | let estimate_ns = start.elapsed().as_nanos(); 77 | let iterations = ((OPTIMAL_DURATION_NS * ESTIMATE_ITERATIONS as u128) / estimate_ns) 78 | .try_into() 79 | .unwrap_or(MINIMAL_ITERATIONS) 80 | .max(MINIMAL_ITERATIONS); 81 | // now we do the real measure 82 | let start = Instant::now(); 83 | for _ in 0..iterations { 84 | measured(); 85 | } 86 | let total_duration = start.elapsed(); 87 | self.measure = Some(TaskMeasure { 88 | iterations, 89 | total_duration, 90 | }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/bench.rs: -------------------------------------------------------------------------------- 1 | use {crate::*, chrono::prelude::*}; 2 | 3 | /// A whole benchmark 4 | /// 5 | /// You normally create it with the `glassbench!` 6 | /// macro which will manage table rendering, saving 7 | /// and graphing if required by arguments. 8 | #[derive(Debug, Clone)] 9 | pub struct Bench { 10 | pub time: DateTime, 11 | pub name: String, 12 | pub title: String, 13 | pub git_info: Option, 14 | pub tag: Option, 15 | pub tasks: Vec, 16 | } 17 | 18 | impl Bench { 19 | /// Create a benchmark with a specific name and title 20 | /// 21 | /// You normally create don't use this function but the `glassbench!` 22 | /// macro which will fetch the id in the name of the executed benchmark. 23 | pub fn new(name: S1, title: S2) -> Self 24 | where 25 | S1: Into, 26 | S2: Into, 27 | { 28 | Self { 29 | time: Utc::now(), 30 | name: name.into(), 31 | title: title.into(), 32 | tasks: Vec::new(), 33 | tag: None, 34 | git_info: GitInfo::read(), 35 | } 36 | } 37 | 38 | /// Specify a task to benchmark 39 | /// 40 | /// Example: 41 | /// 42 | /// ``` 43 | /// # use glassbench::*; 44 | /// # struct BigComputer {} 45 | /// # impl BigComputer { 46 | /// # pub fn new() -> Self { 47 | /// # Self {} 48 | /// # } 49 | /// # pub fn answer(&self, q: usize) -> usize { 50 | /// # q + 2 51 | /// # } 52 | /// # } 53 | /// # let mut bench = Bench::new("doc", "Doc Example"); 54 | /// bench.task("answer 42", |task| { 55 | /// let computer = BigComputer::new(); 56 | /// let question = 42; 57 | /// task.iter(|| { 58 | /// pretend_used(computer.answer(question)); 59 | /// }); 60 | /// }); 61 | /// ``` 62 | pub fn task, F>(&mut self, name: S, mut f: F) 63 | where 64 | F: FnMut(&mut TaskBench), 65 | { 66 | let mut b = TaskBench::new(name.into()); 67 | f(&mut b); 68 | self.tasks.push(b); 69 | } 70 | 71 | /// Warning: this API is considered unstable 72 | pub fn task_name_from_arg(&self, arg: &str) -> Option<&str> { 73 | arg.parse::() 74 | .ok() 75 | .and_then(|num| { 76 | if num == 0 { 77 | eprintln!("history argument 0 not yet implemented"); 78 | None 79 | } else { 80 | self.tasks.get(num - 1) 81 | } 82 | }) 83 | .map(|task| task.name.as_str()) 84 | } 85 | 86 | /// load the history of a task from DB 87 | /// 88 | /// You don't have to call this yourself if you use the [glassbench!] macro. 89 | pub fn task_history(&self, db: &mut Db, tbl_arg: &str) -> Result { 90 | if let Some(task_name) = self.task_name_from_arg(tbl_arg) { 91 | db.task_history(&self.name, task_name) 92 | } else { 93 | Err(GlassBenchError::ClientError) 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/report.rs: -------------------------------------------------------------------------------- 1 | use {crate::*, termimad::minimad::OwningTemplateExpander}; 2 | 3 | static MD: &str = r#" 4 | # ${bench-title} 5 | ${comparison 6 | comparison with ${previous-date} ${git-diff} 7 | } 8 | |-:|:-:|:-:|:-:|:-: 9 | |#|**task**|**iterations**|**total duration**|**mean duration**|**change** 10 | |-:|:-|-:|-:|-:|-: 11 | ${tasks 12 | |**${task-num}**|${task-name}|${iterations}|${total-duration}|**${mean-duration}**|${change} 13 | } 14 | |-: 15 | "#; 16 | 17 | /// A temporary structure to print the result of a benchmark to the console 18 | pub struct Report<'b> { 19 | bench: &'b Bench, 20 | previous: &'b Option, 21 | } 22 | 23 | impl<'b> Report<'b> { 24 | pub fn new(bench: &'b Bench, previous: &'b Option) -> Self { 25 | Self { bench, previous } 26 | } 27 | 28 | /// Print the report to the console 29 | /// 30 | /// You don't have to call this yourself if you use 31 | /// the `glassbench!` macro. 32 | #[allow(clippy::collapsible_else_if)] 33 | pub fn print(&self, printer: &Printer) { 34 | let mut expander = OwningTemplateExpander::new(); 35 | expander 36 | .set("bench-title", &self.bench.title) 37 | .set("bench-name", &self.bench.name); 38 | if let Some(previous) = self.previous.as_ref() { 39 | expander 40 | .sub("comparison") 41 | .set("previous-date", previous.time) 42 | .set( 43 | "git-diff", 44 | GitInfo::diff(&previous.git_info, &self.bench.git_info), 45 | ); 46 | } 47 | for (idx, task) in self.bench.tasks.iter().enumerate() { 48 | if let Some(mes) = &task.measure { 49 | let sub = expander.sub("tasks"); 50 | sub.set("task-num", idx + 1) 51 | .set("task-name", &task.name) 52 | .set("iterations", mes.iterations) 53 | .set("total-duration", format!("{:?}", &mes.total_duration)) 54 | .set("mean-duration", format!("{:?}", mes.mean_duration())); 55 | let diff = self 56 | .previous 57 | .as_ref() 58 | .and_then(|obench| task.diff_with(obench)); 59 | if let Some(diff) = diff { 60 | sub.set_md( 61 | "change", 62 | if diff.percents < 0.0 { 63 | if diff.percents < -2.0 { 64 | format!("*{:.2}%*", diff.percents) 65 | } else { 66 | format!("{:.2}%", diff.percents) 67 | } 68 | } else { 69 | if diff.percents > 2.0 { 70 | format!("~~+{:.2}%~~", diff.percents) 71 | } else { 72 | format!("+{:.2}%", diff.percents) 73 | } 74 | }, 75 | ); 76 | } else { 77 | sub.set("change", " "); 78 | } 79 | } 80 | } 81 | printer.print(expander, MD); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/html_viewer.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::*, 3 | serde::Serialize, 4 | std::{ 5 | fs::File, 6 | io::{self, Read}, 7 | path::PathBuf, 8 | }, 9 | }; 10 | 11 | pub const DOLL_JS: &str = include_str!("../rsc/dom-doll.js"); 12 | pub const VIS_JS: &str = include_str!("../rsc/vis-timeline-graph2d.min.js"); 13 | pub const VIS_CSS: &str = include_str!("../rsc/vis-timeline-graph2d.min.css"); 14 | pub const SQL_JS: &str = include_str!("../rsc/sql-wasm.js"); 15 | pub const SQL_WASM: &[u8] = include_bytes!("../rsc/sql-wasm.wasm"); 16 | pub const VIEWER_JS: &str = include_str!("../rsc/viewer.js"); 17 | pub const VIEWER_CSS: &str = include_str!("../rsc/viewer.css"); 18 | 19 | /// configuration sent to the html page in JSON 20 | #[derive(Debug, Serialize)] 21 | struct Conf<'b> { 22 | bench_name: &'b str, 23 | task_name: Option<&'b str>, 24 | gb_version: String, 25 | } 26 | 27 | /// The builder of the HTML standalone viewer 28 | pub struct HtmlViewer<'b> { 29 | conf: Conf<'b>, 30 | } 31 | 32 | impl<'b> HtmlViewer<'b> { 33 | pub fn new(bench_name: &'b str, task_name: Option<&'b str>) -> Self { 34 | Self { 35 | conf: Conf { 36 | bench_name, 37 | task_name, 38 | gb_version: env!("CARGO_PKG_VERSION").to_string(), 39 | }, 40 | } 41 | } 42 | 43 | pub fn open_in_browser(&self) -> Result<(), GlassBenchError> { 44 | let (mut w, path) = make_temp_file()?; 45 | self.write_html(&mut w)?; 46 | open::that(path)?; 47 | Ok(()) 48 | } 49 | 50 | pub fn write_html(&self, mut w: W) -> Result<(), GlassBenchError> { 51 | writeln!(w, "")?; 52 | writeln!(w, "")?; 53 | writeln!(w, "")?; 54 | writeln!(w, "")?; 55 | writeln!(w, "", VIEWER_CSS)?; 56 | writeln!(w, "", VIS_CSS)?; 57 | writeln!(w, "", VIS_JS)?; 58 | writeln!(w, "", SQL_JS)?; 59 | writeln!(w, "", DOLL_JS)?; 60 | writeln!(w, "", VIEWER_JS)?; 61 | write_db(&mut w)?; 62 | writeln!(w, "")?; 63 | writeln!(w, "")?; 64 | writeln!(w, "")?; 73 | writeln!(w, "")?; 74 | writeln!(w, "")?; 75 | Ok(()) 76 | } 77 | } 78 | 79 | pub fn make_temp_file() -> io::Result<(File, PathBuf)> { 80 | tempfile::Builder::new() 81 | .prefix("glassbench-") 82 | .suffix(".html") 83 | .rand_bytes(12) 84 | .tempfile()? 85 | .keep() 86 | .map_err(|_| io::Error::new(io::ErrorKind::Other, "temp file can't be kept")) 87 | } 88 | 89 | pub fn write_db(mut w: W) -> Result<(), GlassBenchError> { 90 | let mut file = File::open(Db::path()?)?; 91 | let mut bytes = Vec::new(); 92 | file.read_to_end(&mut bytes)?; 93 | writeln!( 94 | w, 95 | r#""#, 96 | base64::encode(&bytes), 97 | )?; 98 | Ok(()) 99 | } 100 | -------------------------------------------------------------------------------- /src/glassbench.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::*, 3 | serde::{Serialize, Deserialize}, 4 | }; 5 | 6 | 7 | /// A whole benchmark. 8 | /// 9 | /// You normally create it with the `glassbench!` 10 | /// macro which will manage table rendering, saving 11 | /// and graphing if required by arguments. 12 | #[derive(Debug, Clone, Serialize, Deserialize)] 13 | pub struct GlassBench { 14 | pub id: String, 15 | pub git_info: Option, 16 | pub name: String, 17 | pub tasks: Vec, 18 | } 19 | 20 | impl GlassBench { 21 | 22 | /// Create a benchmark with a specific id 23 | /// and name. 24 | /// 25 | /// You normally create it with the `glassbench!` 26 | /// macro which will fetch the id in the name of 27 | /// the executed benchmark. 28 | pub fn new(id: S1, name: S2) -> Self 29 | where 30 | S1: Into, 31 | S2: Into, 32 | { 33 | Self { 34 | id: id.into(), 35 | name: name.into(), 36 | tasks: Vec::new(), 37 | git_info: GitInfo::read(), 38 | } 39 | } 40 | 41 | /// Specify a task to benchmark. 42 | pub fn task, F>(&mut self, name: S, mut f: F) 43 | where 44 | F: FnMut(&mut TaskBench), 45 | { 46 | let mut b = TaskBench::new(name.into()); 47 | f(&mut b); 48 | self.tasks.push(b); 49 | } 50 | 51 | /// print the report to the console. 52 | /// 53 | /// You don't have to call this yourself if you use 54 | /// the `glassbench!` macro. 55 | pub fn print_report(&self) { 56 | let previous = match DatedGlassbench::last(&self.id) { 57 | Err(e) => { 58 | eprintln!("failed loading previous report: {}", e); 59 | None 60 | } 61 | Ok(previous) => { 62 | previous 63 | } 64 | }; 65 | let report = Report::new(&self, &previous); 66 | report.print(); 67 | } 68 | 69 | /// graph some history. 70 | /// 71 | /// You don't have to call this yourself if you use 72 | /// the `glassbench!` macro. 73 | pub fn graph(&self, graph_arg: &str) { 74 | if let Ok(num) = graph_arg.parse::() { 75 | if num == 0 { 76 | eprintln!("graph argument 0 not yet implemented"); 77 | } else if let Some(task) = self.tasks.get(num-1) { 78 | match History::of_task(&self.id, &task.name) { 79 | Ok(history) if history.is_graphable() => { 80 | match history.open_graph() { 81 | Err(e) => { 82 | eprintln!("Error opening history graph: {}", e); 83 | } 84 | _ => { 85 | println!("History graph open in your browser"); 86 | } 87 | } 88 | } 89 | Err(e) => { 90 | eprintln!("reading task history failed: {}", e); 91 | } 92 | _ => { 93 | eprintln!("not enough points in history"); 94 | } 95 | } 96 | } else { 97 | eprintln!("no task with number {} found", num); 98 | } 99 | } else { 100 | eprintln!("graph argument not understood: {:?}", graph_arg); 101 | } 102 | } 103 | 104 | /// save the measurements into the .glassbench directory 105 | /// 106 | /// You don't have to call this yourself if you use 107 | /// the `glassbench!` macro. 108 | pub fn save(self) { 109 | let dgb = DatedGlassbench::new(self); 110 | dgb.save(); 111 | } 112 | 113 | } 114 | 115 | 116 | -------------------------------------------------------------------------------- /rsc/viewer.css: -------------------------------------------------------------------------------- 1 | html, body, div { 2 | margin: 0; 3 | padding: 0; 4 | min-height: 0; 5 | } 6 | html, body { 7 | height: 100%; 8 | } 9 | body { 10 | display: flex; 11 | flex-direction: column; 12 | justify-content: stretch; 13 | background: #64798e; 14 | font-family: sans-serif; 15 | font-size: 12px; 16 | } 17 | #infos { 18 | position: fixed; 19 | top: 20px; 20 | right: 20px; 21 | } 22 | #infos a { 23 | color: #eef; 24 | font-weight: bold; 25 | text-decoration: none; 26 | } 27 | #selectors { 28 | display: flex; 29 | flex: 0 0 auto; 30 | flex-direction: column; 31 | align-items: flex-start; 32 | margin: 10px 5px; 33 | z-index: 5; 34 | width: 200px; 35 | } 36 | .selector-wrapper { 37 | flex: 0 0 auto; 38 | display: flex; 39 | flex-direction: row; 40 | align-items: center; 41 | justify-content: stretch; 42 | margin-bottom: 2px; 43 | } 44 | .selector { 45 | flex: 0 0 auto; 46 | display: flex; 47 | flex-direction: row; 48 | padding: 4px; 49 | background: #ced3dc; 50 | box-shadow: 2px 2px 3px rgba(0, 0, 0, .5); 51 | align-items: center; 52 | justify-content: stretch; 53 | } 54 | .adder { 55 | flex: 0 0 auto; 56 | flex-direction: row; 57 | padding: 4px; 58 | background: #ced3dc; 59 | box-shadow: 2px 2px 3px rgba(0, 0, 0, .5); 60 | align-items: center; 61 | justify-content: stretch; 62 | margin-left: 2px; 63 | display: none; 64 | } 65 | .selector-wrapper:first-child:last-child .remover { 66 | visibility: hidden; 67 | } 68 | .selector-wrapper:last-child .adder { 69 | display: flex; 70 | } 71 | label { 72 | margin: 0 4px; 73 | } 74 | select { 75 | margin: 0 2px; 76 | } 77 | 78 | #view { 79 | display: flex; 80 | flex: 1 1 auto; 81 | flex-direction: column; 82 | justify-content: stretch; 83 | margin: 5px; 84 | } 85 | .tabs { 86 | display: flex; 87 | flex: 0 0 auto; 88 | flex-direction: row; 89 | } 90 | .tab { 91 | background: red; 92 | cursor: pointer; 93 | padding: 4px 10px; 94 | margin-right: 5px; 95 | background: #ced3dc; 96 | opacity: .6; 97 | border-radius: 3px 15px 0 0; 98 | } 99 | .tab.selected { 100 | cursor: inherit; 101 | opacity: 1; 102 | } 103 | 104 | .pages { 105 | display: flex; 106 | flex: 1 1 auto; 107 | flex-direction: column; 108 | justify-content: stretch; 109 | } 110 | .page { 111 | display: none; 112 | } 113 | .page.selected { 114 | background: green; 115 | display: flex; 116 | flex: 1 1 auto; 117 | min-height: 200px; 118 | background: #ced3dc; 119 | justify-content: stretch; 120 | } 121 | 122 | #vis { 123 | margin-top: 20px; 124 | background: white; 125 | flex: 1 1 auto; 126 | } 127 | 128 | .selector .tag { 129 | width: 100px; 130 | margin-right: 4px; 131 | } 132 | .selector .legend-icon { 133 | width: 20px; 134 | height: 6px; 135 | margin-right: 4px; 136 | } 137 | .selector-wrapper:nth-child(1) .legend-icon { 138 | background: #4f81bd; 139 | } 140 | .selector-wrapper:nth-child(2) .legend-icon { 141 | background: #f79646; 142 | } 143 | .selector-wrapper:nth-child(3) .legend-icon { 144 | background: #8c51cf; 145 | } 146 | .selector-wrapper:nth-child(4) .legend-icon { 147 | background: #75c841; 148 | } 149 | .selector-wrapper:nth-child(5) .legend-icon { 150 | background: #37d8e6; 151 | } 152 | .selector-wrapper:nth-child(6) .legend-icon { 153 | background: #042662; 154 | } 155 | .selector-wrapper:nth-child(7) .legend-icon { 156 | background: #00ff26; 157 | } 158 | .selector-wrapper:nth-child(8) .legend-icon { 159 | background: #ff00ff; 160 | } 161 | .selector-wrapper:nth-child(9) .legend-icon { 162 | background: #8f3938; 163 | } 164 | 165 | #Table .table-wrapper { 166 | margin-top: 10px; 167 | flex-direction: column; 168 | flex: 1 1 auto; 169 | overflow-y: auto; 170 | } 171 | #Table table { 172 | background: white; 173 | flex-direction: column; 174 | flex: 1 1 auto; 175 | border-collapse: collapse; 176 | margin-left: 2px; 177 | } 178 | #Table th { 179 | padding: 0.5rem; 180 | text-align: center; 181 | background: #ced3dc; 182 | } 183 | #Table td { 184 | border: 1px solid #999; 185 | padding: 0.4rem; 186 | text-align: left; 187 | } 188 | #Table table tbody tr:nth-child(even) { 189 | background-color: #eef; 190 | } 191 | #Table table tbody tr { 192 | border-left-width: 6px; 193 | border-left-color: red; 194 | border-left-style: solid; 195 | } 196 | #Table table tbody tr.group_0 { 197 | border-left-color: #4f81bd; 198 | } 199 | #Table table tbody tr.group_0 { 200 | border-left-color: #4f81bd; 201 | } 202 | #Table table tbody tr.group_1 { 203 | border-left-color: #f79646; 204 | } 205 | #Table table tbody tr.group_2 { 206 | border-left-color: #8c51cf; 207 | } 208 | #Table table tbody tr.group_3 { 209 | border-left-color: #75c841; 210 | } 211 | #Table table tbody tr.group_4 { 212 | border-left-color: #ff0100; 213 | } 214 | #Table table tbody tr.group_5 { 215 | border-left-color: #37d8e6; 216 | } 217 | #Table table tbody tr.group_6 { 218 | border-left-color: #042662; 219 | } 220 | #Table table tbody tr.group_7 { 221 | border-left-color: #00ff26; 222 | } 223 | #Table table tbody tr.group_8 { 224 | border-left-color: #ff00ff; 225 | } 226 | #Table table tbody tr.group_9 { 227 | border-left-color: #8f3938; 228 | } 229 | td.duration_ns, td.duration_str { 230 | text-align: right; 231 | } 232 | -------------------------------------------------------------------------------- /src/db.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::*, 3 | chrono::prelude::*, 4 | rusqlite::{params, Connection, OptionalExtension, Row}, 5 | std::{path::PathBuf, time::Duration}, 6 | }; 7 | 8 | /// version of the schema 9 | pub const VERSION: &str = "1"; 10 | 11 | fn create_tables(con: &Connection) -> Result<(), GlassBenchError> { 12 | con.execute( 13 | "CREATE TABLE IF NOT EXISTS bench ( 14 | id INTEGER PRIMARY KEY, 15 | time INTEGER NOT NULL, 16 | name TEXT NOT NULL, 17 | title TEXT NOT NULL, 18 | tag TEXT, 19 | commit_id TEXT 20 | )", 21 | params![], 22 | )?; 23 | con.execute( 24 | "CREATE TABLE IF NOT EXISTS task ( 25 | bench INTEGER NOT NULL, 26 | name TEXT NOT NULL, 27 | iterations INTEGER NOT NULL, 28 | total_duration_ns INTEGER NOT NULL, 29 | mean_duration_ns INTEGER NOT NULL, 30 | FOREIGN KEY(bench) REFERENCES bench(id), 31 | PRIMARY KEY(bench, name) 32 | )", 33 | params![], 34 | )?; 35 | Ok(()) 36 | } 37 | 38 | /// Storage interface for Glassbench, wrapping a SQLite connection 39 | /// 40 | /// All durations are stored as nanoseconds in i64: 41 | /// If the duration of a task exceeds a few centuries it can 42 | /// be assumed benchmarking it isn't really necessary. 43 | pub struct Db { 44 | pub con: Connection, 45 | } 46 | 47 | impl Db { 48 | /// return the name of the glassbench database file 49 | pub fn path() -> Result { 50 | Ok(std::env::current_dir()?.join(format!("glassbench_v{}.db", VERSION))) 51 | } 52 | 53 | /// Create a new instance of DB, creating the sqlite file and 54 | /// the tables if necessary 55 | pub fn open() -> Result { 56 | let con = Connection::open(Self::path()?)?; 57 | create_tables(&con)?; 58 | Ok(Db { con }) 59 | } 60 | 61 | /// Save a bench, with included tasks if any. Return the id of the bench 62 | pub fn save_bench(&mut self, bench: &Bench) -> Result { 63 | self.con.execute( 64 | "INSERT INTO bench (time, name, title, tag, commit_id) VALUES (?1, ?2, ?3, ?4, ?5)", 65 | params![ 66 | bench.time.timestamp(), 67 | &bench.name, 68 | &bench.title, 69 | &bench.tag, 70 | bench.git_info.as_ref().map(|gi| &gi.commit_id), 71 | ], 72 | )?; 73 | let bench_id = self.con.last_insert_rowid(); 74 | let mut ps = self.con.prepare( 75 | "INSERT INTO task 76 | (bench, name, iterations, total_duration_ns, mean_duration_ns) 77 | VALUES (?1, ?2, ?3, ?4, ?5)", 78 | )?; 79 | let tasks = bench 80 | .tasks 81 | .iter() 82 | .filter_map(|t| t.measure.as_ref().map(|mes| (&t.name, mes))); 83 | for (name, mes) in tasks { 84 | ps.execute(params![ 85 | bench_id, 86 | name, 87 | mes.iterations, 88 | mes.total_duration.as_nanos() as i64, 89 | mes.mean_duration().as_nanos() as i64, 90 | ])?; 91 | } 92 | Ok(bench_id) 93 | } 94 | 95 | /// Load the last bench having this name 96 | pub fn last_bench_named(&mut self, name: &str) -> Result, GlassBenchError> { 97 | match self 98 | .con 99 | .query_row( 100 | "SELECT id, time, name, title, tag, commit_id 101 | fROM bench WHERE name=?1 ORDER BY id DESC LIMIT 1", 102 | params![name], 103 | parse_bench, 104 | ) 105 | .optional()? 106 | { 107 | Some((bench_id, mut bench)) => { 108 | let mut ps = self.con.prepare( 109 | "SELECT name, iterations, total_duration_ns FROM task WHERE bench=?1", 110 | )?; 111 | let iter = ps.query_map(params![bench_id], parse_task)?; 112 | for task in iter { 113 | bench.tasks.push(task?); 114 | } 115 | Ok(Some(bench)) 116 | } 117 | None => { 118 | // no bench found 119 | Ok(None) 120 | } 121 | } 122 | } 123 | 124 | /// Load a [TaskHistory] with all measure for a bench name and task name 125 | pub fn task_history( 126 | &mut self, 127 | bench_name: &str, 128 | task_name: &str, 129 | ) -> Result { 130 | let mut history = TaskHistory { 131 | bench_name: bench_name.into(), 132 | task_name: task_name.into(), 133 | records: Vec::new(), 134 | }; 135 | let mut ps = self.con.prepare( 136 | "SELECT 137 | bench.time, bench.tag, bench.commit_id, 138 | task.iterations, task.total_duration_ns 139 | FROM task JOIN bench ON task.bench=bench.id 140 | WHERE bench.name=?1 AND task.name=?2 141 | ORDER BY bench.time", 142 | )?; 143 | let iter = ps.query_map(params![bench_name, task_name], parse_task_record)?; 144 | for record in iter { 145 | history.records.push(record?); 146 | } 147 | Ok(history) 148 | } 149 | } 150 | 151 | /// parse a bench from a row assuming this order: 152 | /// id, time, name, title, tag, commit_id 153 | fn parse_bench(row: &Row<'_>) -> Result<(i64, Bench), rusqlite::Error> { 154 | let bench_id: i64 = row.get(0)?; 155 | let commit_id: Option = row.get(5)?; 156 | let bench = Bench { 157 | time: Utc.timestamp(row.get(1)?, 0), 158 | name: row.get(2)?, 159 | title: row.get(3)?, 160 | tag: row.get(4)?, 161 | git_info: commit_id.map(|commit_id| GitInfo { commit_id }), 162 | tasks: Vec::new(), 163 | }; 164 | Ok((bench_id, bench)) 165 | } 166 | 167 | /// parse a task_bench from a row assuming this order: 168 | /// name, iterations, total_duration_ns 169 | fn parse_task(row: &Row<'_>) -> Result { 170 | let nanos: i64 = row.get(2)?; 171 | Ok(TaskBench { 172 | name: row.get(0)?, 173 | measure: Some(TaskMeasure { 174 | iterations: row.get(1)?, 175 | total_duration: Duration::from_nanos(nanos as u64), 176 | }), 177 | }) 178 | } 179 | 180 | /// Parse a task_record from a row assuming those columns: 181 | /// bench.time, bench.tag, bench.commit_id, 182 | /// task.iterations, task.total_duration_ns 183 | fn parse_task_record(row: &Row<'_>) -> Result { 184 | let commit_id: Option = row.get(2)?; 185 | let nanos: i64 = row.get(4)?; 186 | Ok(TaskRecord { 187 | time: Utc.timestamp(row.get(0)?, 0), 188 | git_info: commit_id.map(|commit_id| GitInfo { commit_id }), 189 | tag: row.get(1)?, 190 | measure: TaskMeasure { 191 | iterations: row.get(3)?, 192 | total_duration: Duration::from_nanos(nanos as u64), 193 | }, 194 | }) 195 | } 196 | -------------------------------------------------------------------------------- /rsc/viewer.js: -------------------------------------------------------------------------------- 1 | 2 | async function main(sql_conf){ 3 | let db_bytes = base64ToUint8Array(db64) 4 | const SQL = await initSqlJs(sql_conf) 5 | const db = new SQL.Database(db_bytes) 6 | let benches = db.exec("SELECT * FROM bench") 7 | window.gb = wrap(db) 8 | create_gui() 9 | make_selector(gb_conf.bench_name, gb_conf.task_name) 10 | } 11 | 12 | function create_gui() { 13 | $("body", 14 | $("<#infos", 15 | $(`Glassbench ${gb_conf.gb_version}`, { 16 | href: "https://github.com/Canop/glassbench", 17 | target: "_blank", 18 | }), 19 | ), 20 | $("<#selectors"), 21 | $("<#view", 22 | $("<.tabs"), 23 | $("<.pages"), 24 | ) 25 | ) 26 | function unselect() { 27 | $("#view .tabs .tab, #view .pages .page", e => { 28 | e.classList.remove("selected") 29 | }) 30 | } 31 | ;["Table", "Graph"].forEach(name => { 32 | unselect() 33 | let page = $("<.page.selected", { id: name }) 34 | let tab = $(" { 37 | unselect() 38 | tab.classList.add("selected") 39 | page.classList.add("selected") 40 | } 41 | }) 42 | $("#view .tabs", tab) 43 | $("#view .pages", page) 44 | }) 45 | $("#Table", 46 | $("<.table-wrapper", 47 | $("dataset"), 50 | $("bench id"), 51 | $("commit id"), 52 | $("date"), 53 | $("task name"), 54 | $("mean dur."), 55 | $("mean (ns)"), 56 | $("tag"), 57 | )), 58 | $(" ({ 114 | bench_id: row[0], 115 | commit_id: row[3], 116 | date: row[1] * 1000, 117 | duration_ns: row[5], 118 | tag: row[2], 119 | duration_str: fmt_nanos(row[5]) 120 | })) 121 | } 122 | 123 | function update_table(view_data) { 124 | let tbody = $("#tbody") 125 | while (tbody.firstChild) tbody.removeChild(tbody.lastChild) 126 | for (let g of view_data) { 127 | for (let row of g.rows) { 128 | $(tbody, $(`${g.group_id + 1}`), // counting from 1 130 | $("row.date)) max_date = row.date 156 | items.push({ 157 | x: row.date, 158 | y: row.duration_ns, 159 | group: g.group_id, 160 | label: { content: row.duration_str } 161 | }) 162 | } 163 | } 164 | var options = { 165 | start: min_date - (max_date-min_date)/10, 166 | end: max_date + (max_date-min_date)/10, 167 | shaded: true, 168 | } 169 | window.graph = new vis.Graph2d($("#vis"), items, groups, options) 170 | graph.on('click', function (properties) { 171 | console.log("click", properties) 172 | }) 173 | } 174 | 175 | function make_selector(bench_name, task_name) { 176 | let bench_name_select = $(" $(`