subweight_core/
lib.rs

1//! Parse and compare weight Substrate weight files.
2
3#![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	/// Version of the library. Example: `swc 0.2.0+78a04b2`.
36	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// Uses options since extrinsics can be added or removed and any time.
92#[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// TODO rename
108#[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/// Parameters for modifying the benchmark behaviour.
120#[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	/// Do a 'git pull' after checking out the refname.
132	///
133	/// This ensures that you get the newest commit on a branch.
134	#[clap(long)]
135	pub git_pull: bool,
136
137	/// Use a git hard-reset instead of checkout.
138	#[clap(long)]
139	pub git_force: bool,
140
141	/// Don't access the network.
142	///
143	/// This overrides any other options like `--git-pull`.
144	#[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	/// Minimal magnitude of a relative change to be relevant.
152	#[clap(long, value_name = "PERCENT", default_value = "5")]
153	pub threshold: Percent,
154
155	/// Only include a subset of change-types.
156	#[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	// Parse the old files.
185	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	// Ignore any parsing errors.
190	let olds = if params.ignore_errors {
191		try_parse_files_in_repo(repo, &paths)
192	} else {
193		// TODO use option for repo
194		parse_files_in_repo(repo, &paths)?
195	};
196
197	// Parse the new files.
198	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	// Ignore any parsing errors.
203	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	// try to reset with remote...
271	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	// Ignore any errors and try again without `origin/` prefix.
279	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	// Try resetting without remote.
289	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: {:?}", &regex);
315		let files = glob::glob(&regex).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	/// The constant base weight of the extrinsic.
336	Base,
337
338	/// Try to find the worst case increase. Errors if any component misses a range annotation.
339	ExactWorst,
340	/// Similar to [`Self::ExactWorst`], but guesses if any component misses a range annotation.
341	GuessWorst,
342	/// Set all components to their exact maximum value.
343	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// We call this *Unit* for ease of use but it is actually a *dimension* and a unit.
407#[derive(serde::Deserialize, clap::ValueEnum, PartialEq, Eq, Hash, Clone, Copy, Debug)]
408#[serde(rename_all = "kebab-case")]
409pub enum Dimension {
410	/// Reference time. Alias to `weight` for backwards compatibility.
411	#[serde(alias = "weight")]
412	Time,
413
414	/// Proof-of-validity (PoV) size.
415	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	// TODO try clap ValueEnum
474	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		// OMG this code is stupid... but since READ and WRITE done incur proof size cost, we ignore
503		// them.
504		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		// NOTE: The maximum could be calculated right here, but for now I want the debug assert.
539		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	// Sanity check: They are either
549	// All Increase/Decrease/Unchanged OR
550	// All Added/Removed
551	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		// Just pick the first one
560		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
571// TODO handle case that both have (different) ranges.
572pub(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	// Combine the maximum and minimum of each component with combinatorics.
596	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	// cartesian product of lowest and highest
603	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		// Only one extrinsic has a component range? Good
629		(Some(r), None) | (None, Some(r)) => Ok(match strategy.min_or_max {
630			Min => r.min,
631			Max => r.max,
632		}),
633		// Both extrinsics have the same range? Good
634		(Some(ra), Some(rb)) if ra == rb => Ok(match strategy.min_or_max {
635			Min => ra.min,
636			Max => ra.max,
637		}),
638		// Both extrinsics have different ranges? Bad, use the min/max.
639		(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		// No ranges? Bad, just guess 100.
648		(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	// Split them into their correct dimension.
693	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			// TODO add "skipped" or "ignored" result type.
711			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
748/// Checks some obvious stuff:
749/// - Does not have more than 1000 reads or writes
750pub 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			/*if self.percent > other.percent {
788				Ordering::Greater
789			} else if self.percent == other.percent {
790				Ordering::Equal
791			} else {
792				Ordering::Less
793			}*/
794			((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	// Note: the pallet and extrinsic are already filtered in compare_files.
803	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			//(old, new) if old == new => RelativeChange::Unchanged,
826			(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	/// Formats pico seconds.
861	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}