diff --git a/analyze/analyze.rs b/analyze/analyze.rs index bdebef4d..e73f96a6 100644 --- a/analyze/analyze.rs +++ b/analyze/analyze.rs @@ -3,6 +3,7 @@ #![deny(missing_docs)] #![deny(missing_debug_implementations)] +extern crate serde; #[macro_use] extern crate serde_derive; extern crate csv; @@ -14,8 +15,9 @@ extern crate twiggy_traits as traits; mod json; +use serde::ser::SerializeStruct; use std::cmp; -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::fmt; use std::io; @@ -1119,6 +1121,18 @@ impl Ord for DiffEntry { } } +impl serde::Serialize for DiffEntry { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut state = serializer.serialize_struct("DiffEntry", 2)?; + state.serialize_field("DeltaBytes", &format!("{:+}", self.delta))?; + state.serialize_field("Item", &self.name)?; + state.end() + } +} + impl traits::Emit for Diff { #[cfg(feature = "emit_text")] fn emit_text( @@ -1131,9 +1145,10 @@ impl traits::Emit for Diff { (Align::Left, "Item".to_string()), ]); - for entry in &self.deltas { - table.add_row(vec![format!("{:+}", entry.delta), entry.name.clone()]); - } + self.deltas + .iter() + .map(|entry| vec![format!("{:+}", entry.delta), entry.name.clone()]) + .for_each(|row| table.add_row(row)); write!(dest, "{}", &table)?; Ok(()) @@ -1147,7 +1162,7 @@ impl traits::Emit for Diff { ) -> Result<(), traits::Error> { let mut arr = json::array(dest)?; - for entry in &self.deltas { + for entry in self.deltas.iter() { let mut obj = arr.object()?; obj.field("delta_bytes", entry.delta as f64)?; obj.field("name", entry.name.as_str())?; @@ -1157,8 +1172,15 @@ impl traits::Emit for Diff { } #[cfg(feature = "emit_csv")] - fn emit_csv(&self, _items: &ir::Items, _dest: &mut io::Write) -> Result<(), traits::Error> { - unimplemented!(); + fn emit_csv(&self, _items: &ir::Items, dest: &mut io::Write) -> Result<(), traits::Error> { + let mut wtr = csv::Writer::from_writer(dest); + + for entry in self.deltas.iter() { + wtr.serialize(entry)?; + wtr.flush()?; + } + + Ok(()) } } @@ -1168,48 +1190,92 @@ pub fn diff( new_items: &mut ir::Items, opts: &opt::Diff, ) -> Result, traits::Error> { - let old_items_by_name: BTreeMap<&str, &ir::Item> = - old_items.iter().map(|item| (item.name(), item)).collect(); - let new_items_by_name: BTreeMap<&str, &ir::Item> = - new_items.iter().map(|item| (item.name(), item)).collect(); - - let mut deltas = vec![]; - - for (name, old_item) in &old_items_by_name { - match new_items_by_name.get(name) { - None => deltas.push(DiffEntry { - name: name.to_string(), - delta: -(old_item.size() as i64), - }), - Some(new_item) => { - let delta = new_item.size() as i64 - old_item.size() as i64; - if delta != 0 { - deltas.push(DiffEntry { - name: name.to_string(), - delta, - }); - } - } - } + let max_items = opts.max_items() as usize; + + // Given a set of items, create a HashMap of the items' names and sizes. + fn get_names_and_sizes(items: &ir::Items) -> HashMap<&str, i64> { + items + .iter() + .map(|item| (item.name(), item.size() as i64)) + .collect() } - for (name, new_item) in &new_items_by_name { - if !old_items_by_name.contains_key(name) { - deltas.push(DiffEntry { - name: name.to_string(), - delta: new_item.size() as i64, - }); + // Collect the names and sizes of the items in the old and new collections. + let old_sizes = get_names_and_sizes(old_items); + let new_sizes = get_names_and_sizes(new_items); + + // Given an item name, create a `DiffEntry` object representing the + // change in size, or an error if the name could not be found in + // either of the item collections. + let get_item_delta = |name: String| -> Result { + let old_size = old_sizes.get::(&name); + let new_size = new_sizes.get::(&name); + let delta: i64 = match (old_size, new_size) { + (Some(old_size), Some(new_size)) => new_size - old_size, + (Some(old_size), None) => -old_size, + (None, Some(new_size)) => *new_size, + (None, None) => { + return Err(traits::Error::with_msg(format!( + "Could not find item with name `{}`", + name + ))) + } + }; + Ok(DiffEntry { name, delta }) + }; + + // Given a result returned by `get_item_delta`, return false if the result + // represents an unchanged item. Ignore errors, these are handled separately. + let unchanged_items_filter = |res: &Result| -> bool { + if let Ok(DiffEntry { name: _, delta: 0 }) = res { + false + } else { + true } - } + }; - deltas.push(DiffEntry { - name: "".to_string(), - delta: new_items.size() as i64 - old_items.size() as i64, - }); + // Create a set of item names from the new and old item collections. + let names = old_sizes + .keys() + .chain(new_sizes.keys()) + .map(|k| k.to_string()) + .collect::>(); + // Iterate through the set of item names, and use the closure above to map + // each item into a `DiffEntry` object. Then, sort the collection. + let mut deltas = names + .into_iter() + .map(get_item_delta) + .filter(unchanged_items_filter) + .collect::, traits::Error>>()?; deltas.sort(); - deltas.truncate(opts.max_items() as usize); + // Create an entry to summarize the diff rows that will be truncated. + let (rem_cnt, rem_delta): (u32, i64) = deltas + .iter() + .skip(max_items) + .fold((0, 0), |(cnt, rem_delta), DiffEntry { name: _, delta }| { + (cnt + 1, rem_delta + delta) + }); + let remaining = DiffEntry { + name: format!("... and {} more.", rem_cnt), + delta: rem_delta, + }; + + // Create a `DiffEntry` representing the net change, and total row count. + let total = DiffEntry { + name: format!("Σ [{} Total Rows]", deltas.len()), + delta: new_items.size() as i64 - old_items.size() as i64, + }; + + // Now that the 'remaining' and 'total' summary entries have been created, + // truncate the vector of deltas before we box up the result, and push + // the remaining and total rows to the deltas vector. + deltas.truncate(max_items); + deltas.push(remaining); + deltas.push(total); + + // Return the results so that they can be emitted. let diff = Diff { deltas }; Ok(Box::new(diff) as Box) } diff --git a/twiggy/tests/expectations/diff_wee_alloc b/twiggy/tests/expectations/diff_wee_alloc index 2fd25b23..18af4c54 100644 --- a/twiggy/tests/expectations/diff_wee_alloc +++ b/twiggy/tests/expectations/diff_wee_alloc @@ -1,6 +1,5 @@ Delta Bytes │ Item ─────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── - -1476 ┊ -1034 ┊ data[3] -593 ┊ "function names" subsection +395 ┊ wee_alloc::alloc_first_fit::he2a4ddf96981c0ce @@ -20,3 +19,6 @@ -8 ┊ type[4] -6 ┊ ::min_cell_size::hc7cee2a550987099 +6 ┊ alloc::alloc::oom::h45ae3f22a516fb04 + -5 ┊ as wee_alloc::AllocPolicy>::min_cell_size::h6f746be886573355 + -21 ┊ ... and 13 more. + -1476 ┊ Σ [33 Total Rows] diff --git a/twiggy/tests/expectations/diff_wee_alloc_csv b/twiggy/tests/expectations/diff_wee_alloc_csv new file mode 100644 index 00000000..b05459a7 --- /dev/null +++ b/twiggy/tests/expectations/diff_wee_alloc_csv @@ -0,0 +1,23 @@ +DeltaBytes,Item +-1034,data[3] +-593,"""function names"" subsection" ++395,wee_alloc::alloc_first_fit::he2a4ddf96981c0ce ++243,goodbye +-225,wee_alloc::alloc_first_fit::h9a72de3af77ef93f +-152,wee_alloc::alloc_with_refill::hb32c1bbce9ebda8e ++145,">::remove::hc9e5d4284e8233b8" +-136, as wee_alloc::AllocPolicy>::new_cell_for_free_list::h3987e3054b8224e6 +-76,::new_cell_for_free_list::h8f071b7bce0301ba +-25,data[1] +-25,data[2] ++15,hello ++15,import env::rust_oom ++12,custom section 'linking' +-12,elem[0] ++8,global[0] +-8,type[4] +-6,::min_cell_size::hc7cee2a550987099 ++6,alloc::alloc::oom::h45ae3f22a516fb04 +-5, as wee_alloc::AllocPolicy>::min_cell_size::h6f746be886573355 +-21,... and 13 more. +-1476,Σ [33 Total Rows] diff --git a/twiggy/tests/expectations/diff_wee_alloc_csv_top_5 b/twiggy/tests/expectations/diff_wee_alloc_csv_top_5 new file mode 100644 index 00000000..ce2f8012 --- /dev/null +++ b/twiggy/tests/expectations/diff_wee_alloc_csv_top_5 @@ -0,0 +1,8 @@ +DeltaBytes,Item +-1034,data[3] +-593,"""function names"" subsection" ++395,wee_alloc::alloc_first_fit::he2a4ddf96981c0ce ++243,goodbye +-225,wee_alloc::alloc_first_fit::h9a72de3af77ef93f +-265,... and 28 more. +-1476,Σ [33 Total Rows] diff --git a/twiggy/tests/expectations/diff_wee_alloc_json b/twiggy/tests/expectations/diff_wee_alloc_json index 8c2db1cc..9458e820 100644 --- a/twiggy/tests/expectations/diff_wee_alloc_json +++ b/twiggy/tests/expectations/diff_wee_alloc_json @@ -1 +1 @@ -[{"delta_bytes":-1476,"name":""},{"delta_bytes":-1034,"name":"data[3]"},{"delta_bytes":-593,"name":"\"function names\" subsection"},{"delta_bytes":395,"name":"wee_alloc::alloc_first_fit::he2a4ddf96981c0ce"},{"delta_bytes":243,"name":"goodbye"}] +[{"delta_bytes":-1034,"name":"data[3]"},{"delta_bytes":-593,"name":"\"function names\" subsection"},{"delta_bytes":395,"name":"wee_alloc::alloc_first_fit::he2a4ddf96981c0ce"},{"delta_bytes":243,"name":"goodbye"},{"delta_bytes":-225,"name":"wee_alloc::alloc_first_fit::h9a72de3af77ef93f"},{"delta_bytes":-152,"name":"wee_alloc::alloc_with_refill::hb32c1bbce9ebda8e"},{"delta_bytes":145,"name":">::remove::hc9e5d4284e8233b8"},{"delta_bytes":-136,"name":" as wee_alloc::AllocPolicy>::new_cell_for_free_list::h3987e3054b8224e6"},{"delta_bytes":-76,"name":"::new_cell_for_free_list::h8f071b7bce0301ba"},{"delta_bytes":-25,"name":"data[1]"},{"delta_bytes":-25,"name":"data[2]"},{"delta_bytes":15,"name":"hello"},{"delta_bytes":15,"name":"import env::rust_oom"},{"delta_bytes":12,"name":"custom section 'linking'"},{"delta_bytes":-12,"name":"elem[0]"},{"delta_bytes":8,"name":"global[0]"},{"delta_bytes":-8,"name":"type[4]"},{"delta_bytes":-6,"name":"::min_cell_size::hc7cee2a550987099"},{"delta_bytes":6,"name":"alloc::alloc::oom::h45ae3f22a516fb04"},{"delta_bytes":-5,"name":" as wee_alloc::AllocPolicy>::min_cell_size::h6f746be886573355"},{"delta_bytes":-21,"name":"... and 13 more."},{"delta_bytes":-1476,"name":"Σ [33 Total Rows]"}] diff --git a/twiggy/tests/expectations/diff_wee_alloc_json_top_5 b/twiggy/tests/expectations/diff_wee_alloc_json_top_5 new file mode 100644 index 00000000..4fe469c1 --- /dev/null +++ b/twiggy/tests/expectations/diff_wee_alloc_json_top_5 @@ -0,0 +1 @@ +[{"delta_bytes":-1034,"name":"data[3]"},{"delta_bytes":-593,"name":"\"function names\" subsection"},{"delta_bytes":395,"name":"wee_alloc::alloc_first_fit::he2a4ddf96981c0ce"},{"delta_bytes":243,"name":"goodbye"},{"delta_bytes":-225,"name":"wee_alloc::alloc_first_fit::h9a72de3af77ef93f"},{"delta_bytes":-265,"name":"... and 28 more."},{"delta_bytes":-1476,"name":"Σ [33 Total Rows]"}] diff --git a/twiggy/tests/expectations/diff_wee_alloc_top_5 b/twiggy/tests/expectations/diff_wee_alloc_top_5 new file mode 100644 index 00000000..e37a151b --- /dev/null +++ b/twiggy/tests/expectations/diff_wee_alloc_top_5 @@ -0,0 +1,9 @@ + Delta Bytes │ Item +─────────────┼────────────────────────────────────────────── + -1034 ┊ data[3] + -593 ┊ "function names" subsection + +395 ┊ wee_alloc::alloc_first_fit::he2a4ddf96981c0ce + +243 ┊ goodbye + -225 ┊ wee_alloc::alloc_first_fit::h9a72de3af77ef93f + -265 ┊ ... and 28 more. + -1476 ┊ Σ [33 Total Rows] diff --git a/twiggy/tests/tests.rs b/twiggy/tests/tests.rs index 17826d65..63d75638 100644 --- a/twiggy/tests/tests.rs +++ b/twiggy/tests/tests.rs @@ -352,17 +352,55 @@ test!( "./fixtures/wee_alloc.2.wasm" ); +test!( + diff_wee_alloc_top_5, + "diff", + "./fixtures/wee_alloc.wasm", + "./fixtures/wee_alloc.2.wasm", + "-n", + "5" +); + test!( diff_wee_alloc_json, "diff", "./fixtures/wee_alloc.wasm", "./fixtures/wee_alloc.2.wasm", "-f", + "json" +); + +test!( + diff_wee_alloc_json_top_5, + "diff", + "./fixtures/wee_alloc.wasm", + "./fixtures/wee_alloc.2.wasm", + "-f", "json", "-n", "5" ); +test!( + diff_wee_alloc_csv, + "diff", + "./fixtures/wee_alloc.wasm", + "./fixtures/wee_alloc.2.wasm", + "-f", + "csv" +); + +test!( + diff_wee_alloc_csv_top_5, + "diff", + "./fixtures/wee_alloc.wasm", + "./fixtures/wee_alloc.2.wasm", + "-f", + "csv", + "-n", + "5" +); + test!(garbage, "garbage", "./fixtures/garbage.wasm"); test!(