1#![deny(rustdoc::broken_intra_doc_links)]
4
5use clap::Args;
6use fancy_regex::Regex;
7use git_version::git_version;
8use lazy_static::lazy_static;
9
10use std::{
11 cmp::Ordering,
12 collections::{BTreeSet, HashMap, HashSet},
13 path::{Path, PathBuf},
14 process::Command,
15};
16use syn::{Expr, Item, Type};
17
18pub mod parse;
19pub mod scope;
20pub mod term;
21pub mod testing;
22pub mod traits;
23
24#[cfg(test)]
25mod test;
26
27use parse::pallet::{
28 parse_files_in_repo, try_parse_files_in_repo, ChromaticExtrinsic, ComponentRange,
29 SimpleExtrinsic,
30};
31use scope::SimpleScope;
32use term::SimpleTerm;
33
34lazy_static! {
35 pub static ref VERSION: String = format!("{}+{}", env!("CARGO_PKG_VERSION"), git_version!(args = ["--always"], fallback = "unknown"));
37
38 pub static ref VERSION_DIRTY: bool = {
39 VERSION.clone().contains("dirty")
40 };
41}
42
43pub type PalletName = String;
44pub type ExtrinsicName = String;
45pub type TotalDiff = Vec<ExtrinsicDiff>;
46
47pub type Percent = f64;
48pub const WEIGHT_PER_NANOS: u128 = 1_000;
49
50#[derive(Clone)]
51#[cfg_attr(feature = "bloat", derive(Debug))]
52pub struct ExtrinsicDiff {
53 pub name: ExtrinsicName,
54 pub file: String,
55
56 pub change: TermDiff,
57}
58
59#[derive(Clone)]
60#[cfg_attr(feature = "bloat", derive(Debug))]
61pub enum TermDiff {
62 Changed(TermChange),
63 Warning(TermChange, String),
64 Failed(String),
65}
66
67impl ExtrinsicDiff {
68 pub fn term(&self) -> Option<&TermChange> {
69 match &self.change {
70 TermDiff::Changed(change) => Some(change),
71 TermDiff::Warning(change, _) => Some(change),
72 _ => None,
73 }
74 }
75
76 pub fn error(&self) -> Option<&String> {
77 match &self.change {
78 TermDiff::Failed(err) => Some(err),
79 _ => None,
80 }
81 }
82
83 pub fn warning(&self) -> Option<&String> {
84 match &self.change {
85 TermDiff::Warning(_, warning) => Some(warning),
86 _ => None,
87 }
88 }
89}
90
91#[derive(Clone)]
93#[cfg_attr(feature = "bloat", derive(Debug))]
94pub struct TermChange {
95 pub old: Option<SimpleTerm>,
96 pub old_v: Option<u128>,
97
98 pub new: Option<SimpleTerm>,
99 pub new_v: Option<u128>,
100
101 pub scope: SimpleScope,
102 pub percent: Percent,
103 pub change: RelativeChange,
104 pub method: CompareMethod,
105}
106
107#[derive(
109 Debug, serde::Deserialize, clap::ValueEnum, Clone, Eq, Ord, PartialEq, PartialOrd, Copy,
110)]
111#[serde(rename_all = "kebab-case")]
112pub enum RelativeChange {
113 Unchanged,
114 Added,
115 Removed,
116 Changed,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Args)]
121pub struct CompareParams {
122 #[clap(long, short, value_name = "METHOD", ignore_case = true)]
123 pub method: CompareMethod,
124
125 #[clap(long, short, value_name = "UNIT", ignore_case = true, default_value = "time")]
126 pub unit: Dimension,
127
128 #[clap(long)]
129 pub ignore_errors: bool,
130
131 #[clap(long)]
135 pub git_pull: bool,
136
137 #[clap(long)]
139 pub git_force: bool,
140
141 #[clap(long)]
145 pub offline: bool,
146}
147
148#[derive(Debug, Clone, PartialEq, Args)]
149#[cfg_attr(feature = "bloat", derive(Default))]
150pub struct FilterParams {
151 #[clap(long, value_name = "PERCENT", default_value = "5")]
153 pub threshold: Percent,
154
155 #[clap(long, ignore_case = true, num_args = 0.., value_name = "CHANGE-TYPE")]
157 pub change: Option<Vec<RelativeChange>>,
158
159 #[clap(long, ignore_case = true, value_name = "REGEX")]
160 pub extrinsic: Option<String>,
161
162 #[clap(long, alias("file"), ignore_case = true, value_name = "REGEX")]
163 pub pallet: Option<String>,
164}
165
166impl CompareParams {
167 pub fn should_pull(&self) -> bool {
168 self.git_pull && !self.offline
169 }
170}
171
172pub fn compare_commits(
173 repo: &Path,
174 old: &str,
175 new: &str,
176 params: &CompareParams,
177 filter: &FilterParams,
178 path_pattern: &str,
179 max_files: usize,
180) -> Result<TotalDiff, Box<dyn std::error::Error>> {
181 if path_pattern.contains("..") {
182 return Err("Path pattern cannot contain '..'".into())
183 }
184 if let Err(err) = git_checkout(repo, old, params.should_pull(), params.git_force) {
186 return Err(format!("{:?}", err).into())
187 }
188 let paths = list_files(repo, path_pattern, max_files)?;
189 let olds = if params.ignore_errors {
191 try_parse_files_in_repo(repo, &paths)
192 } else {
193 parse_files_in_repo(repo, &paths)?
195 };
196
197 if let Err(err) = git_checkout(repo, new, params.should_pull(), params.git_force) {
199 return Err(format!("{:?}", err).into())
200 }
201 let paths = list_files(repo, path_pattern, max_files)?;
202 let news = if params.ignore_errors {
204 try_parse_files_in_repo(repo, &paths)
205 } else {
206 parse_files_in_repo(repo, &paths)?
207 };
208
209 compare_files(olds, news, params, filter)
210}
211
212pub fn git_checkout(
213 path: &Path,
214 refname: &str,
215 should_pull: bool,
216 force: bool,
217) -> Result<(), String> {
218 if force {
219 return git_reset(path, refname, should_pull)
220 }
221
222 log::info!("Checking out {}", refname);
223 if should_pull {
224 git_pull(path, refname)?;
225 } else {
226 log::debug!("Not fetching branch {} (should_fetch={})", refname, should_pull);
227 }
228
229 let output = Command::new("git")
230 .arg("checkout")
231 .arg(refname)
232 .current_dir(path)
233 .output()
234 .map_err(|e| format!("Failed to checkout branch: {:?}", e))?;
235
236 if !output.status.success() {
237 return Err(format!(
238 "Failed to checkout branch: {}",
239 String::from_utf8_lossy(&output.stderr),
240 ))
241 }
242
243 Ok(())
244}
245
246pub fn git_pull(path: &Path, refname: &str) -> Result<(), String> {
247 log::info!("Fetching branch {}", refname);
248
249 let output = Command::new("git")
250 .arg("fetch")
251 .arg("origin")
252 .arg(refname)
253 .current_dir(path)
254 .output()
255 .map_err(|e| format!("Failed to fetch branch: {:?}", &e))?;
256
257 if !output.status.success() {
258 return Err(format!("Failed to fetch branch: {}", String::from_utf8_lossy(&output.stderr),))
259 }
260
261 Ok(())
262}
263
264pub fn git_reset(path: &Path, refname: &str, pull: bool) -> Result<(), String> {
265 if pull {
266 git_pull(path, refname)?;
267 } else {
268 log::debug!("Not fetching branch {} (should_fetch={})", refname, pull);
269 }
270 log::info!("Resetting to origin/{}", refname);
272 let output = Command::new("git")
273 .arg("reset")
274 .arg("--hard")
275 .arg(format!("origin/{}", refname))
276 .current_dir(path)
277 .output();
278 match output {
280 Err(err) => log::info!("Failed to reset to origin/{}: {}", refname, err),
281 Ok(output) =>
282 if !output.status.success() {
283 log::warn!("Failed to reset to: origin/{}", String::from_utf8_lossy(&output.stderr))
284 } else {
285 return Ok(())
286 },
287 }
288 log::info!("Fallback: Resetting to {}", refname);
290 let output = Command::new("git")
291 .arg("reset")
292 .arg("--hard")
293 .arg(refname)
294 .current_dir(path)
295 .output()
296 .map_err(|e| format!("Failed to reset branch: {:?}", e))?;
297
298 if !output.status.success() {
299 return Err(format!("Failed to reset branch: {}", String::from_utf8_lossy(&output.stderr)))
300 }
301 Ok(())
302}
303
304fn list_files(
305 base_path: &Path,
306 regex: &str,
307 max_files: usize,
308) -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
309 let regex = regex.split(',');
310
311 let mut paths = Vec::new();
312 for regex in regex {
313 let regex = format!("{}/{}", base_path.display(), regex);
314 log::info!("Listing files matching: {:?}", ®ex);
315 let files = glob::glob(®ex).map_err(|e| format!("Invalid path pattern: {:?}", e))?;
316 let files = files
317 .collect::<Result<Vec<_>, _>>()
318 .map_err(|e| format!("Path pattern error: {:?}", e))?;
319 let files: Vec<_> = files.iter().filter(|f| !f.ends_with("mod.rs")).cloned().collect();
320 paths.extend(files);
321 if paths.len() > max_files {
322 return Err(
323 format!("Found too many files. Found: {}, Max: {}", paths.len(), max_files).into()
324 )
325 }
326 }
327 paths.sort();
328 paths.dedup();
329 Ok(paths)
330}
331
332#[derive(serde::Deserialize, clap::ValueEnum, PartialEq, Eq, Hash, Clone, Copy, Debug)]
333#[serde(rename_all = "kebab-case")]
334pub enum CompareMethod {
335 Base,
337
338 ExactWorst,
340 GuessWorst,
342 ExactAsymptotic,
344 Asymptotic,
345}
346
347impl CompareMethod {
348 pub const fn min(&self) -> ComponentInstanceStrategy {
349 match self {
350 Self::Base | Self::GuessWorst => ComponentInstanceStrategy::guess_min(),
351 Self::ExactWorst => ComponentInstanceStrategy::exact_min(),
352 Self::ExactAsymptotic => ComponentInstanceStrategy::exact_max(),
353 Self::Asymptotic => ComponentInstanceStrategy::guess_max(),
354 }
355 }
356
357 pub const fn max(&self) -> ComponentInstanceStrategy {
358 match self {
359 Self::Base => ComponentInstanceStrategy::guess_min(),
360 Self::GuessWorst => ComponentInstanceStrategy::guess_max(),
361 Self::ExactWorst | Self::ExactAsymptotic => ComponentInstanceStrategy::exact_max(),
362 Self::Asymptotic => ComponentInstanceStrategy::guess_max(),
363 }
364 }
365}
366
367#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
368pub struct ComponentInstanceStrategy {
369 pub exact: bool,
370 pub min_or_max: MinOrMax,
371}
372
373impl ComponentInstanceStrategy {
374 pub const fn exact_min() -> Self {
375 Self { exact: true, min_or_max: MinOrMax::Min }
376 }
377
378 pub const fn exact_max() -> Self {
379 Self { exact: true, min_or_max: MinOrMax::Max }
380 }
381
382 pub const fn guess_min() -> Self {
383 Self { exact: false, min_or_max: MinOrMax::Min }
384 }
385
386 pub const fn guess_max() -> Self {
387 Self { exact: false, min_or_max: MinOrMax::Max }
388 }
389}
390
391#[derive(serde::Deserialize, clap::ValueEnum, PartialEq, Eq, Hash, Clone, Copy, Debug)]
392pub enum MinOrMax {
393 Min,
394 Max,
395}
396
397impl core::fmt::Display for MinOrMax {
398 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
399 match self {
400 MinOrMax::Min => write!(f, "min"),
401 MinOrMax::Max => write!(f, "max"),
402 }
403 }
404}
405
406#[derive(serde::Deserialize, clap::ValueEnum, PartialEq, Eq, Hash, Clone, Copy, Debug)]
408#[serde(rename_all = "kebab-case")]
409pub enum Dimension {
410 #[serde(alias = "weight")]
412 Time,
413
414 Proof,
416}
417
418impl std::str::FromStr for CompareMethod {
419 type Err = String;
420
421 fn from_str(s: &str) -> Result<Self, String> {
422 match s {
423 "base" => Ok(CompareMethod::Base),
424 "guess-worst" => Ok(CompareMethod::GuessWorst),
425 "exact-worst" => Ok(CompareMethod::ExactWorst),
426 "exact-asymptotic" => Ok(CompareMethod::ExactAsymptotic),
427 "asymptotic" => Ok(CompareMethod::Asymptotic),
428 _ => Err(format!("Unknown method: {}", s)),
429 }
430 }
431}
432
433impl CompareMethod {
434 pub fn all() -> Vec<Self> {
435 vec![
436 Self::Base,
437 Self::GuessWorst,
438 Self::ExactWorst,
439 Self::ExactAsymptotic,
440 Self::Asymptotic,
441 ]
442 }
443
444 pub fn variants() -> Vec<&'static str> {
445 vec!["base", "guess-worst", "exact-worst", "exact-asymptotic", "asymptotic"]
446 }
447
448 pub fn reflect() -> Vec<(Self, &'static str)> {
449 Self::all().into_iter().zip(Self::variants()).collect()
450 }
451}
452
453impl std::str::FromStr for Dimension {
454 type Err = String;
455
456 fn from_str(s: &str) -> Result<Self, String> {
457 match s {
458 "time" | "weight" => Ok(Self::Time),
459 "proof" => Ok(Self::Proof),
460 _ => Err(format!("Unknown method: {}", s)),
461 }
462 }
463}
464
465impl FilterParams {
466 pub fn included(&self, change: &RelativeChange) -> bool {
467 self.change.as_ref().map_or(true, |s| s.contains(change))
468 }
469}
470
471impl std::str::FromStr for RelativeChange {
472 type Err = String;
473 fn from_str(s: &str) -> Result<Self, String> {
475 match s {
476 "unchanged" => Ok(Self::Unchanged),
477 "changed" => Ok(Self::Changed),
478 "added" => Ok(Self::Added),
479 "removed" => Ok(Self::Removed),
480 _ => Err(format!("Unknown change: {}", s)),
481 }
482 }
483}
484
485impl RelativeChange {
486 pub fn variants() -> Vec<&'static str> {
487 vec!["unchanged", "changed", "added", "removed"]
488 }
489}
490
491pub fn compare_extrinsics(
492 mut old: Option<SimpleExtrinsic>,
493 mut new: Option<SimpleExtrinsic>,
494 params: &CompareParams,
495) -> Result<TermChange, String> {
496 let mut scope = scope::SimpleScope::empty();
497 if params.unit == Dimension::Time {
498 scope = scope
499 .with_storage_weights(SimpleTerm::Scalar(25_000_000), SimpleTerm::Scalar(100_000_000));
500 } else {
501 scope = scope.with_storage_weights(SimpleTerm::Scalar(0), SimpleTerm::Scalar(0));
502 old = old.map(|mut o| {
505 o.term.substitute("READ", &scalar!(0));
506 o
507 });
508 old = old.map(|mut o| {
509 o.term.substitute("WRITE", &scalar!(0));
510 o
511 });
512 new = new.map(|mut o| {
513 o.term.substitute("READ", &scalar!(0));
514 o
515 });
516 new = new.map(|mut o| {
517 o.term.substitute("WRITE", &scalar!(0));
518 o
519 });
520 }
521 let (new, old) = (new.as_ref(), old.as_ref());
522 let scopes = extend_scoped_components(old, new, params.method, &scope)?;
523 let name = old.map(|o| o.name.clone()).or_else(|| new.map(|n| n.name.clone())).unwrap();
524 let pallet = old.map(|o| o.pallet.clone()).or_else(|| new.map(|n| n.pallet.clone())).unwrap();
525
526 let mut results = Vec::<TermChange>::new();
527
528 for scope in scopes.iter() {
529 if !old.map_or(true, |e| e.term.free_vars(scope).is_empty()) {
530 unreachable!(
531 "Free variable where there should be none: {}::{} {:?}",
532 name,
533 &pallet,
534 old.unwrap().term.free_vars(scope)
535 );
536 }
537 assert!(new.map_or(true, |e| e.term.free_vars(scope).is_empty()));
538 results.push(compare_terms(
540 old.map(|o| &o.term),
541 new.map(|n| &n.term),
542 params.method,
543 scope,
544 )?);
545 }
546 log::trace!(target: "compare", "{}::{} Evaluated {} scopes", pallet, name, scopes.len());
547
548 let all_increase_or_decrease = results
552 .iter()
553 .all(|r| matches!(r.change, RelativeChange::Changed | RelativeChange::Unchanged));
554 let all_added_or_removed = results
555 .iter()
556 .all(|r| matches!(r.change, RelativeChange::Added | RelativeChange::Removed));
557
558 if all_added_or_removed {
559 Ok(results.into_iter().next().unwrap())
561 } else if all_increase_or_decrease {
562 Ok(results.into_iter().max_by(|a, b| a.cmp(b)).unwrap())
563 } else {
564 unreachable!(
565 "Inconclusive: all_increase_or_decrease: {}, all_added_or_removed: {}",
566 all_increase_or_decrease, all_added_or_removed
567 );
568 }
569}
570
571pub(crate) fn extend_scoped_components(
573 a: Option<&SimpleExtrinsic>,
574 b: Option<&SimpleExtrinsic>,
575 method: CompareMethod,
576 scope: &SimpleScope,
577) -> Result<Vec<SimpleScope>, String> {
578 let free_a = a.map(|e| e.term.free_vars(scope)).unwrap_or_default();
579 let free_b = b.map(|e| e.term.free_vars(scope)).unwrap_or_default();
580 let frees = free_a.union(&free_b).cloned().collect::<HashSet<_>>();
581
582 let ra = a.map(|ext| ext.clone().comp_ranges.unwrap_or_default());
583 let rb = b.map(|ext| ext.clone().comp_ranges.unwrap_or_default());
584
585 let (pallet, extrinsic) = a.or(b).map(|e| (e.pallet.clone(), e.name.clone())).unwrap();
586
587 if frees.len() > 16 {
588 return Err(format!(
589 "Too many components to compare: {}::{} has {} components - limit is 16",
590 pallet,
591 extrinsic,
592 frees.len()
593 ))
594 }
595 let (mut lowest, mut highest) = (Vec::new(), Vec::new());
597 for free in frees.iter() {
598 lowest.push(instance_component(free, &ra, &rb, method.min(), &pallet, &extrinsic)?);
599 highest.push(instance_component(free, &ra, &rb, method.max(), &pallet, &extrinsic)?);
600 }
601
602 let mut scopes = BTreeSet::new();
604 for i in 0..(1 << frees.len()) {
605 let mut scope = scope.clone();
606 for (c, component) in frees.iter().enumerate() {
607 let value = if i & (1 << c) == 0 { lowest[c] } else { highest[c] };
608 scope.put_var(component, SimpleTerm::Scalar(value as u128));
609 }
610 if !scope.is_empty() {
611 scopes.insert(scope);
612 }
613 }
614 Ok(scopes.into_iter().collect())
615}
616
617fn instance_component(
618 component: &str,
619 ra: &Option<HashMap<String, ComponentRange>>,
620 rb: &Option<HashMap<String, ComponentRange>>,
621 strategy: ComponentInstanceStrategy,
622 pallet: &str,
623 extrinsic: &str,
624) -> Result<u32, String> {
625 use MinOrMax::*;
626
627 match (ra.as_ref().and_then(|r| r.get(component)), rb.as_ref().and_then(|r| r.get(component))) {
628 (Some(r), None) | (None, Some(r)) => Ok(match strategy.min_or_max {
630 Min => r.min,
631 Max => r.max,
632 }),
633 (Some(ra), Some(rb)) if ra == rb => Ok(match strategy.min_or_max {
635 Min => ra.min,
636 Max => ra.max,
637 }),
638 (Some(ra), Some(rb)) => match (strategy.exact, strategy.min_or_max) {
640 (true, _) => Err(format!(
641 "Component {} of call {}::{} has different ranges in the old and new version - use Guess instead!",
642 component, pallet, extrinsic,
643 )),
644 (false, Min) => Ok(ra.min.min(rb.min)),
645 (false, Max) => Ok(ra.max.max(rb.max)),
646 },
647 (None, None) => match (strategy.exact, strategy.min_or_max) {
649 (false, Min) => Ok(0),
650 (false, Max) => Ok(100),
651 (true, _) => Err(format!(
652 "No range for component {} of call {}::{} - use Guess instead!",
653 component, pallet, extrinsic,
654 )),
655 },
656 }
657}
658
659pub fn compare_terms(
660 old: Option<&SimpleTerm>,
661 new: Option<&SimpleTerm>,
662 method: CompareMethod,
663 scope: &SimpleScope,
664) -> Result<TermChange, String> {
665 let old_v = old.map(|t| t.eval(scope)).transpose()?;
666 let new_v = new.map(|t| t.eval(scope)).transpose()?;
667 let change =
668 if old == new { RelativeChange::Unchanged } else { RelativeChange::new(old_v, new_v) };
669 let p = percent(old_v.unwrap_or_default(), new_v.unwrap_or_default());
670 log::trace!(target: "compare", "Evaluating {:?} vs {:?} ({:?}) [{:?}]", old_v.unwrap_or_default(), new_v.unwrap_or_default(), p, &scope);
671
672 Ok(TermChange {
673 old: old.cloned(),
674 old_v,
675 new: new.cloned(),
676 new_v,
677 change,
678 percent: p,
679 method,
680 scope: scope.clone(),
681 })
682}
683
684pub fn compare_files(
685 olds: Vec<ChromaticExtrinsic>,
686 news: Vec<ChromaticExtrinsic>,
687 params: &CompareParams,
688 filter: &FilterParams,
689) -> Result<TotalDiff, Box<dyn std::error::Error>> {
690 let ext_regex = filter.extrinsic.as_ref().map(|s| Regex::new(s)).transpose()?;
691 let pallet_regex = filter.pallet.as_ref().map(|s| Regex::new(s)).transpose()?;
692 let olds = olds
694 .into_iter()
695 .map(|e| e.map_term(|t| t.simplify(params.unit).expect("Must simplify term")))
696 .collect::<Vec<_>>();
697 let news = news
698 .into_iter()
699 .map(|e| e.map_term(|t| t.simplify(params.unit).expect("Must simplify term")))
700 .collect::<Vec<_>>();
701
702 let mut diff = TotalDiff::new();
703 let old_names = olds.iter().cloned().map(|e| (e.pallet, e.name));
704 let new_names = news.iter().cloned().map(|e| (e.pallet, e.name));
705 let names = old_names.chain(new_names).collect::<std::collections::BTreeSet<_>>();
706 log::trace!("Comparing {} terms", olds.len());
707
708 for (pallet, extrinsic) in names {
709 if !pallet_regex.as_ref().map_or(true, |r| r.is_match(&pallet).unwrap_or_default()) {
710 continue
712 }
713 if !ext_regex.as_ref().map_or(true, |r| r.is_match(&extrinsic).unwrap_or_default()) {
714 continue
715 }
716
717 let new = news.iter().find(|&n| n.name == extrinsic && n.pallet == pallet);
718 let old = olds.iter().find(|&n| n.name == extrinsic && n.pallet == pallet);
719 log::trace!("Comparing {}::{}", pallet, extrinsic);
720
721 let change = match compare_extrinsics(old.cloned(), new.cloned(), params) {
722 Err(err) => {
723 log::warn!("Parsing failed {}: {:?}", &pallet, err);
724 TermDiff::Failed(err)
725 },
726 Ok(change) =>
727 if let Some(ext) = new.or(old) {
728 if let Err(err) = sanity_check_term(&ext.term)
729 .map_err(|e| format!("{}: {}::{}", e, ext.pallet, ext.name))
730 {
731 TermDiff::Warning(change, err)
732 } else {
733 TermDiff::Changed(change)
734 }
735 } else {
736 unreachable!(
737 "We already checked that the extrinsic exists in either old or new"
738 )
739 },
740 };
741
742 diff.push(ExtrinsicDiff { name: extrinsic.clone(), file: pallet.clone(), change });
743 }
744
745 Ok(diff)
746}
747
748pub fn sanity_check_term(term: &SimpleTerm) -> Result<(), String> {
751 let reads = term.find_largest_factor("READ").unwrap_or_default();
752 let writes = term.find_largest_factor("WRITE").unwrap_or_default();
753 let larger = reads.max(writes);
754
755 if larger > 1000 {
756 if reads > writes {
757 Err(format!("Call has {} READs", reads))
758 } else {
759 Err(format!("Call has {} WRITEs", writes))
760 }
761 } else {
762 Ok(())
763 }
764}
765
766pub fn sort_changes(diff: &mut TotalDiff) {
767 diff.sort_by(|a, b| a.change.cmp(&b.change));
768}
769
770impl TermDiff {
771 fn cmp(&self, other: &Self) -> Ordering {
772 match (&self, &other) {
773 (TermDiff::Failed(_), _) => Ordering::Less,
774 (_, TermDiff::Failed(_)) => Ordering::Greater,
775 (TermDiff::Warning(a, _), TermDiff::Changed(b)) => a.cmp(b),
776 (TermDiff::Changed(a), TermDiff::Warning(b, _)) => a.cmp(b),
777 (TermDiff::Warning(a, _), TermDiff::Warning(b, _)) => a.cmp(b),
778 (TermDiff::Changed(a), TermDiff::Changed(b)) => a.cmp(b),
779 }
780 }
781}
782
783impl TermChange {
784 fn cmp(&self, other: &Self) -> Ordering {
785 let ord = self.change.cmp(&other.change);
786 if ord == Ordering::Equal {
787 ((self.percent * 1000.0) as i128).cmp(&((other.percent * 1000.0) as i128))
795 } else {
796 ord
797 }
798 }
799}
800
801pub fn filter_changes(diff: TotalDiff, params: &FilterParams) -> TotalDiff {
802 diff.iter()
804 .filter(|extrinsic| match extrinsic.change {
805 TermDiff::Failed(_) => true,
806 TermDiff::Warning(ref change, ..) | TermDiff::Changed(ref change) => {
807 if !params.included(&change.change) {
808 return false
809 }
810
811 match change.change {
812 RelativeChange::Changed if change.percent.abs() < params.threshold => false,
813 RelativeChange::Unchanged if params.threshold >= 0.000001 => false,
814 _ => true,
815 }
816 },
817 })
818 .cloned()
819 .collect()
820}
821
822impl RelativeChange {
823 pub fn new(old: Option<u128>, new: Option<u128>) -> RelativeChange {
824 match (old, new) {
825 (Some(_), Some(_)) => RelativeChange::Changed,
827 (None, Some(_)) => RelativeChange::Added,
828 (Some(_), None) => RelativeChange::Removed,
829 (None, None) => unreachable!("Either old or new must be set"),
830 }
831 }
832}
833
834pub fn percent(old: u128, new: u128) -> Percent {
835 100.0 * (new as f64 / old as f64) - 100.0
836}
837
838impl Dimension {
839 pub fn fmt_value(&self, v: u128) -> String {
840 match self {
841 Self::Time => Self::fmt_time(v),
842 Self::Proof => Self::fmt_proof(v),
843 }
844 }
845
846 pub fn fmt_scalar(w: u128) -> String {
847 if w >= 1_000_000_000_000 {
848 format!("{:.2}T", w as f64 / 1_000_000_000_000f64)
849 } else if w >= 1_000_000_000 {
850 format!("{:.2}G", w as f64 / 1_000_000_000f64)
851 } else if w >= 1_000_000 {
852 format!("{:.2}M", w as f64 / 1_000_000f64)
853 } else if w >= 1_000 {
854 format!("{:.2}K", w as f64 / 1_000f64)
855 } else {
856 w.to_string()
857 }
858 }
859
860 pub fn fmt_time(t: u128) -> String {
862 if t >= 1_000_000_000_000 {
863 format!("{:.2}s", t as f64 / 1_000_000_000_000f64)
864 } else if t >= 1_000_000_000 {
865 format!("{:.2}ms", t as f64 / 1_000_000_000f64)
866 } else if t >= 1_000_000 {
867 format!("{:.2}us", t as f64 / 1_000_000f64)
868 } else if t >= 1_000 {
869 format!("{:.2}ns", t as f64 / 1_000f64)
870 } else {
871 format!("{:.2}ps", t)
872 }
873 }
874
875 pub fn fmt_proof(b: u128) -> String {
876 const BYTE_PER_KIB: u128 = 1024;
877 const BYTE_PER_MIB: u128 = BYTE_PER_KIB * 1024;
878 const BYTE_PER_GIB: u128 = BYTE_PER_MIB * 1024;
879
880 if b >= BYTE_PER_GIB {
881 format!("{:.2}GiB", b as f64 / BYTE_PER_GIB as f64)
882 } else if b >= BYTE_PER_MIB {
883 format!("{:.2}MiB", b as f64 / BYTE_PER_MIB as f64)
884 } else if b >= BYTE_PER_KIB {
885 format!("{:.2}KiB", b as f64 / BYTE_PER_KIB as f64)
886 } else {
887 format!("{}B", b)
888 }
889 }
890
891 pub fn all() -> Vec<Self> {
892 vec![Self::Time, Self::Proof]
893 }
894
895 pub fn variants() -> Vec<&'static str> {
896 vec!["time", "proof"]
897 }
898
899 pub fn reflect() -> Vec<(Self, &'static str)> {
900 Self::all().into_iter().zip(Self::variants()).collect()
901 }
902}