diff --git a/ofborg/Cargo.lock b/ofborg/Cargo.lock index 147a2fb..bc8b3df 100644 --- a/ofborg/Cargo.lock +++ b/ofborg/Cargo.lock @@ -47,6 +47,14 @@ name = "antidote" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "arrayvec" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "autocfg" version = "0.1.1" @@ -399,6 +407,11 @@ dependencies = [ "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "nodrop" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "nom" version = "4.1.1" @@ -407,6 +420,15 @@ dependencies = [ "memchr 2.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "num-format" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "num-integer" version = "0.1.39" @@ -452,6 +474,7 @@ dependencies = [ "lru-cache 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "md5 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "nom 4.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "num-format 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.34 (registry+https://github.com/rust-lang/crates.io-index)", @@ -850,6 +873,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum amq-proto 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "66d79639b71f74c7006c12683cc2ff221615a51a741688fa7798ccd080dc54d3" "checksum amqp 0.1.0 (git+https://github.com/grahamc/rust-amqp.git)" = "" "checksum antidote 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "34fde25430d87a9388dadbe6e34d7f72a462c8b43ac8d309b42b0a8505d7e2a5" +"checksum arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "92c7fb76bc8826a8b33b4ee5bb07a247a81e76764ab4d55e8f73e3a4d8808c71" "checksum autocfg 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4e5f34df7a019573fb8bdc7e24a2bfebe51a2a1d6bfdbaeccedb3c41fc574727" "checksum backtrace 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)" = "b5b493b66e03090ebc4343eb02f94ff944e0cbc9ac6571491d170ba026741eb5" "checksum backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "797c830ac25ccc92a7f8a7b9862bde440715531514594a6154e3d4a54dd769b6" @@ -896,7 +920,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum memchr 2.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "db4c41318937f6e76648f42826b1d9ade5c09cafb5aef7e351240a70f39206e9" "checksum mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0" "checksum native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f74dbadc8b43df7864539cedb7bc91345e532fdd913cfdc23ad94f4d2d40fbc0" +"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945" "checksum nom 4.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9c349f68f25f596b9f44cf0e7c69752a5c633b0550c3ff849518bfba0233774a" +"checksum num-format 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bafe4179722c2894288ee77a9f044f02811c86af699344c498b0840c698a2465" "checksum num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea" "checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" "checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1" diff --git a/ofborg/Cargo.toml b/ofborg/Cargo.toml index c6e1063..f341178 100644 --- a/ofborg/Cargo.toml +++ b/ofborg/Cargo.toml @@ -27,6 +27,7 @@ lru-cache = "0.1.1" nom = "4.0.0-beta3" sys-info = "0.5.6" chrono = "0.4.6" +num-format = "0.4.0" [patch.crates-io] #hubcaps = { path = "../hubcaps" } diff --git a/ofborg/src/lib.rs b/ofborg/src/lib.rs index 5f57650..230feeb 100644 --- a/ofborg/src/lib.rs +++ b/ofborg/src/lib.rs @@ -27,6 +27,7 @@ extern crate hyper; extern crate hyper_native_tls; extern crate lru_cache; extern crate md5; +extern crate num_format; extern crate tempfile; extern crate uuid; diff --git a/ofborg/src/nixstats.rs b/ofborg/src/nixstats.rs index 47182b4..781de5b 100644 --- a/ofborg/src/nixstats.rs +++ b/ofborg/src/nixstats.rs @@ -1,4 +1,6 @@ +use num_format::{Locale, ToFormattedString}; /// Statistics emitted by Nix when NIX_SHOW_STATS=1 +use std::collections::HashMap; #[derive(Deserialize)] pub struct EvaluationStats { @@ -94,14 +96,273 @@ pub struct GarbageCollector { pub total_bytes: u64, } +pub struct EvaluationStatsDiff<'a> { + left: &'a EvaluationStats, + right: &'a EvaluationStats, +} + +impl<'a> EvaluationStatsDiff<'a> { + pub fn compare( + left: &'a EvaluationStats, + right: &'a EvaluationStats, + ) -> EvaluationStatsDiff<'a> { + EvaluationStatsDiff { left, right } + } + + pub fn markdown(&self) -> String { + struct Row { + before: String, + after: String, + diff: String, + diff_pct: String, + } + impl Row { + fn from_u64(left: u64, right: u64) -> Row { + let diff: u64; + let direction: &str; + let diff_pct: String; + + if left > right { + diff = left - right; + direction = "🡖 "; + } else if left < right { + diff = right - left; + direction = "🡕 "; + } else { + diff = 0; + direction = ""; + } + + if diff > 0 { + diff_pct = format!( + "{:.2}%", + ((right as f64) - (left as f64)) / (left as f64) * 100.0 + ); + } else { + diff_pct = String::from(""); + } + + Row { + before: left.to_formatted_string(&Locale::en), + after: right.to_formatted_string(&Locale::en), + diff: format!("{}{}", direction, diff.to_formatted_string(&Locale::en)), + diff_pct: diff_pct, + } + } + + fn from_f32(left: f32, right: f32) -> Row { + let diff: f32; + let direction: &str; + let diff_pct: String; + + if left > right { + diff = left - right; + direction = "🡖 "; + } else if left < right { + diff = right - left; + direction = "🡕 "; + } else { + diff = 0.0; + direction = ""; + } + + if diff > 0.0 { + diff_pct = format!( + "{:.2}%", + ((right as f64) - (left as f64)) / (left as f64) * 100.0 + ); + } else { + diff_pct = String::from(""); + } + + Row { + before: format!("{:.2}", left), + after: format!("{:.2}", right), + diff: format!("{}{:.2}", direction, diff), + diff_pct: diff_pct, + } + } + } + + let mut data: HashMap<&str, Row> = HashMap::new(); + data.insert( + "cpuTime", + Row::from_f32(self.left.cpu_time, self.right.cpu_time), + ); + + data.insert( + "envs-number", + Row::from_u64(self.left.envs.number, self.right.envs.number), + ); + data.insert( + "envs-elements", + Row::from_u64(self.left.envs.elements, self.right.envs.elements), + ); + data.insert( + "envs-bytes", + Row::from_u64(self.left.envs.bytes, self.right.envs.bytes), + ); + + data.insert( + "list-elements", + Row::from_u64(self.left.list.elements, self.right.list.elements), + ); + data.insert( + "list-bytes", + Row::from_u64(self.left.list.bytes, self.right.list.bytes), + ); + data.insert( + "list-concats", + Row::from_u64(self.left.list.concats, self.right.list.concats), + ); + + data.insert( + "values-number", + Row::from_u64(self.left.values.number, self.right.values.number), + ); + data.insert( + "values-bytes", + Row::from_u64(self.left.values.bytes, self.right.values.bytes), + ); + + data.insert( + "symbols-number", + Row::from_u64(self.left.symbols.number, self.right.symbols.number), + ); + data.insert( + "symbols-bytes", + Row::from_u64(self.left.symbols.bytes, self.right.symbols.bytes), + ); + + data.insert( + "sets-number", + Row::from_u64(self.left.sets.number, self.right.sets.number), + ); + data.insert( + "sets-bytes", + Row::from_u64(self.left.sets.bytes, self.right.sets.bytes), + ); + data.insert( + "sets-elements", + Row::from_u64(self.left.sets.elements, self.right.sets.elements), + ); + + data.insert( + "sizes-Env", + Row::from_u64(self.left.sizes.env, self.right.sizes.env), + ); + data.insert( + "sizes-Value", + Row::from_u64(self.left.sizes.value, self.right.sizes.value), + ); + data.insert( + "sizes-Bindings", + Row::from_u64(self.left.sizes.bindings, self.right.sizes.bindings), + ); + data.insert( + "sizes-Attr", + Row::from_u64(self.left.sizes.attr, self.right.sizes.attr), + ); + + data.insert( + "nrOpUpdates", + Row::from_u64(self.left.nr_op_updates, self.right.nr_op_updates), + ); + data.insert( + "nrOpUpdateValuesCopied", + Row::from_u64( + self.left.nr_op_update_values_copied, + self.right.nr_op_update_values_copied, + ), + ); + data.insert( + "nrThunks", + Row::from_u64(self.left.nr_thunks, self.right.nr_thunks), + ); + data.insert( + "nrAvoided", + Row::from_u64(self.left.nr_avoided, self.right.nr_avoided), + ); + data.insert( + "nrLookups", + Row::from_u64(self.left.nr_lookups, self.right.nr_lookups), + ); + data.insert( + "nrPrimOpCalls", + Row::from_u64(self.left.nr_prim_op_calls, self.right.nr_prim_op_calls), + ); + data.insert( + "nrFunctionCalls", + Row::from_u64(self.left.nr_function_calls, self.right.nr_function_calls), + ); + data.insert( + "gc-heapSize", + Row::from_u64(self.left.gc.heap_size, self.right.gc.heap_size), + ); + data.insert( + "gc-totalBytes", + Row::from_u64(self.left.gc.total_bytes, self.right.gc.total_bytes), + ); + + let (keylen, beforelen, afterlen, difflen, diff_pctlen): ( + usize, + usize, + usize, + usize, + usize, + ) = data.iter().fold( + (0, 0, 0, 0, 0), + |(keylen, before, after, diff, diff_pct), (key, row)| { + ( + std::cmp::max(keylen, key.chars().count()), + std::cmp::max(before, row.before.chars().count()), + std::cmp::max(after, row.after.chars().count()), + std::cmp::max(diff, row.diff.chars().count()), + std::cmp::max(diff_pct, row.diff_pct.chars().count()), + ) + }, + ); + + let mut keys = data.keys().cloned().collect::>(); + keys.sort(); + + let rows = keys + .into_iter() + .map(|key| { + let row = data.get(&key).unwrap(); + format!("| {key:beforewidth$} | {after:>afterwidth$} | {diff:diff_pctwidth$} |", + key=format!("**{}**", key), keywidth=(keylen + 4), + before=row.before, beforewidth=beforelen, + after=row.after, afterwidth=afterlen, + diff=row.diff, diffwidth=difflen, + diff_pct=row.diff_pct, diff_pctwidth=diff_pctlen) + }) + .collect::>(); + + let header = format!( +" +|{key:^keywidth$}|{before:^beforewidth$}|{after:^afterwidth$}|{diff:^diffwidth$}|{diff_pct:^diff_pctwidth$}| +|{keydash:-beforewidth$}|{afterdash:->afterwidth$}|{diffdash:-diff_pctwidth$}| +", + key="stat", keywidth=(keylen + 6), + before="before", beforewidth=(beforelen + 2), + after="after", afterwidth=(afterlen + 2), + diff="Δ", diffwidth=(difflen + 2), + diff_pct="Δ%", diff_pctwidth=(diff_pctlen + 2), + keydash=":", beforedash=":", afterdash=":", diffdash=":", diff_pctdash=":" + ); + + format!("{}\n{}", header.trim(), rows.join("\n")) + } +} + #[cfg(test)] mod tests { use super::EvaluationStats; + use super::EvaluationStatsDiff; use serde_json; - #[test] - fn verify_load() { - let load: EvaluationStats = serde_json::from_str( - r#" + + const EXAMPLE: &'static str = r#" { "cpuTime": 135.2, "envs": { @@ -145,9 +406,57 @@ mod tests { "totalBytes": 24191819392 } } -"#, - ) - .unwrap(); +"#; + + const EXAMPLE2: &'static str = r#" +{ + "cpuTime": 132.897, + "envs": { + "number": 124766593, + "elements": 177627124, + "bytes": 3417282480 + }, + "list": { + "elements": 204449868, + "bytes": 1635598944, + "concats": 6988658 + }, + "values": { + "number": 244542804, + "bytes": 5869027296 + }, + "symbols": { + "number": 372917, + "bytes": 16324250 + }, + "sets": { + "number": 27307373, + "bytes": 7133945368, + "elements": 288145266 + }, + "sizes": { + "Env": 16, + "Value": 24, + "Bindings": 8, + "Attr": 24 + }, + "nrOpUpdates": 11881928, + "nrOpUpdateValuesCopied": 208814478, + "nrThunks": 167655588, + "nrAvoided": 170493166, + "nrLookups": 75275349, + "nrPrimOpCalls": 80373629, + "nrFunctionCalls": 109822957, + "gc": { + "heapSize": 11433721856, + "totalBytes": 23468008832 + } +} +"#; + + #[test] + fn verify_load() { + let load: EvaluationStats = serde_json::from_str(EXAMPLE).unwrap(); assert_eq!(load.cpu_time, 135.2); assert_eq!(load.envs.number, 130714125); @@ -184,4 +493,57 @@ mod tests { assert_eq!(load.gc.heap_size, 12104687616); assert_eq!(load.gc.total_bytes, 24191819392); } + + fn diff_text(left: &str, right: &str) { + println!("left:\n{}", left); + println!("right:\n{}", right); + + let lines = left.split("\n").zip(right.split("\n")); + + for (idx, (linea, lineb)) in lines.enumerate() { + assert_eq!(linea, lineb, "Line {}", idx); + } + } + + #[test] + fn markdown() { + let left: EvaluationStats = serde_json::from_str(EXAMPLE).unwrap(); + let right: EvaluationStats = serde_json::from_str(EXAMPLE2).unwrap(); + + diff_text( + &EvaluationStatsDiff::compare(&left, &right).markdown(), + r#" +| stat | before | after | Δ | Δ% | +|:---------------------------|---------------:|---------------:|:--------------|-------:| +| **cpuTime** | 135.20 | 132.90 | 🡖 2.30 | -1.70% | +| **envs-bytes** | 3,563,057,008 | 3,417,282,480 | 🡖 145,774,528 | -4.09% | +| **envs-elements** | 183,953,876 | 177,627,124 | 🡖 6,326,752 | -3.44% | +| **envs-number** | 130,714,125 | 124,766,593 | 🡖 5,947,532 | -4.55% | +| **gc-heapSize** | 12,104,687,616 | 11,433,721,856 | 🡖 670,965,760 | -5.54% | +| **gc-totalBytes** | 24,191,819,392 | 23,468,008,832 | 🡖 723,810,560 | -2.99% | +| **list-bytes** | 1,659,372,128 | 1,635,598,944 | 🡖 23,773,184 | -1.43% | +| **list-concats** | 7,194,150 | 6,988,658 | 🡖 205,492 | -2.86% | +| **list-elements** | 207,421,516 | 204,449,868 | 🡖 2,971,648 | -1.43% | +| **nrAvoided** | 177,840,681 | 170,493,166 | 🡖 7,347,515 | -4.13% | +| **nrFunctionCalls** | 115,193,164 | 109,822,957 | 🡖 5,370,207 | -4.66% | +| **nrLookups** | 75,292,052 | 75,275,349 | 🡖 16,703 | -0.02% | +| **nrOpUpdateValuesCopied** | 208,834,564 | 208,814,478 | 🡖 20,086 | -0.01% | +| **nrOpUpdates** | 11,883,339 | 11,881,928 | 🡖 1,411 | -0.01% | +| **nrPrimOpCalls** | 85,571,252 | 80,373,629 | 🡖 5,197,623 | -6.07% | +| **nrThunks** | 173,325,665 | 167,655,588 | 🡖 5,670,077 | -3.27% | +| **sets-bytes** | 7,134,676,648 | 7,133,945,368 | 🡖 731,280 | -0.01% | +| **sets-elements** | 288,174,680 | 288,145,266 | 🡖 29,414 | -0.01% | +| **sets-number** | 27,310,541 | 27,307,373 | 🡖 3,168 | -0.01% | +| **sizes-Attr** | 24 | 24 | 0 | | +| **sizes-Bindings** | 8 | 8 | 0 | | +| **sizes-Env** | 16 | 16 | 0 | | +| **sizes-Value** | 24 | 24 | 0 | | +| **symbols-bytes** | 16,324,262 | 16,324,250 | 🡖 12 | -0.00% | +| **symbols-number** | 372,918 | 372,917 | 🡖 1 | -0.00% | +| **values-bytes** | 6,250,904,880 | 5,869,027,296 | 🡖 381,877,584 | -6.11% | +| **values-number** | 260,454,370 | 244,542,804 | 🡖 15,911,566 | -6.11% | +"# + .trim_start(), + ); + } } diff --git a/ofborg/src/outpathdiff.rs b/ofborg/src/outpathdiff.rs index e09f91c..cc25128 100644 --- a/ofborg/src/outpathdiff.rs +++ b/ofborg/src/outpathdiff.rs @@ -1,6 +1,6 @@ use crate::nixenv::Error as NixEnvError; use crate::nixenv::HydraNixEnv; -use crate::nixstats::EvaluationStats; +use crate::nixstats::{EvaluationStats, EvaluationStatsDiff}; use ofborg::nix; use std::collections::{HashMap, HashSet}; use std::io::BufRead; @@ -37,6 +37,18 @@ impl OutPathDiff { Ok(()) } + pub fn performance_diff(&self) -> Option { + if let Some((_, ref cur)) = self.current { + if let Some((_, ref orig)) = self.original { + Some(EvaluationStatsDiff::compare(orig, cur)) + } else { + None + } + } else { + None + } + } + pub fn package_diff(&self) -> Option<(Vec, Vec)> { if let Some((ref cur, _)) = self.current { if let Some((ref orig, _)) = self.original {