From 24d5a351d508f76a26737272c69149a01c04eebf Mon Sep 17 00:00:00 2001 From: data-pup Date: Tue, 8 May 2018 08:59:14 -0700 Subject: [PATCH 1/2] Add a `twiggy garbage` command. Fixes issue #48. --- Cargo.lock | 1 + README.md | 10 +++ analyze/Cargo.toml | 1 + analyze/analyze.rs | 87 ++++++++++++++++++++++-- opt/definitions.rs | 66 +++++++++++++++++- opt/opt.rs | 17 +++++ twiggy/tests/expectations/garbage | 11 +++ twiggy/tests/expectations/garbage_json | 1 + twiggy/tests/expectations/garbage_top_2 | 4 ++ twiggy/tests/fixtures/garbage.wasm | Bin 0 -> 84 bytes twiggy/tests/tests.rs | 18 +++++ twiggy/twiggy.rs | 1 + 12 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 twiggy/tests/expectations/garbage create mode 100644 twiggy/tests/expectations/garbage_json create mode 100644 twiggy/tests/expectations/garbage_top_2 create mode 100644 twiggy/tests/fixtures/garbage.wasm diff --git a/Cargo.lock b/Cargo.lock index 2561fb16..07888035 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -326,6 +326,7 @@ dependencies = [ name = "twiggy-analyze" version = "0.1.0" dependencies = [ + "petgraph 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", "twiggy-ir 0.1.0", "twiggy-opt 0.1.0", "twiggy-traits 0.1.0", diff --git a/README.md b/README.md index 2f07dfa7..d70f2c4a 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Use `twiggy` to make your binaries slim! - [`twiggy monos`](#twiggy-monos) - [`twiggy dominators`](#twiggy-dominators) - [`twiggy diff`](#twiggy-diff) + - ['twiggy garbage'](#twiggy-garbage) - [🦀 As a Crate](#-as-a-crate) - [🕸 On the Web with WebAssembly](#-on-the-web-with-webassembly) - [🔎 Supported Binary Formats](#-supported-binary-formats) @@ -364,6 +365,15 @@ and new versions of a binary. +145 ┊ >::remove::hc9e5d4284e8233b8 ``` +#### `twiggy garbage` + +The `twiggy garbage` sub-command finds and display code and data that is not +transitively referenced by any exports or public functions. + +``` +AWOO : Place output example here. +``` + ### 🦀 As a Crate `twiggy` is divided into a collection of crates that you can use diff --git a/analyze/Cargo.toml b/analyze/Cargo.toml index f3b2a57d..ef8db122 100644 --- a/analyze/Cargo.toml +++ b/analyze/Cargo.toml @@ -15,3 +15,4 @@ path = "./analyze.rs" twiggy-ir = { version = "0.1.0", path = "../ir" } twiggy-opt = { version = "0.1.0", path = "../opt", default-features = false } twiggy-traits = { version = "0.1.0", path = "../traits" } +petgraph = "0.4.12" diff --git a/analyze/analyze.rs b/analyze/analyze.rs index 716b48ce..4c93615a 100644 --- a/analyze/analyze.rs +++ b/analyze/analyze.rs @@ -3,6 +3,7 @@ #![deny(missing_docs)] #![deny(missing_debug_implementations)] +extern crate petgraph; extern crate twiggy_ir as ir; extern crate twiggy_opt as opt; extern crate twiggy_traits as traits; @@ -723,10 +724,7 @@ impl traits::Emit for Diff { ]); for entry in &self.deltas { - table.add_row(vec![ - format!("{:+}", entry.delta), - entry.name.clone(), - ]); + table.add_row(vec![format!("{:+}", entry.delta), entry.name.clone()]); } write!(dest, "{}", &table)?; @@ -801,3 +799,84 @@ pub fn diff( let diff = Diff { deltas }; Ok(Box::new(diff) as Box) } + +#[derive(Debug)] +struct Garbage { + items: Vec, + opts: opt::Garbage, +} + +impl traits::Emit for Garbage { + fn emit_text(&self, items: &ir::Items, dest: &mut io::Write) -> Result<(), traits::Error> { + let mut table = Table::with_header(vec![ + (Align::Right, "Bytes".to_string()), + (Align::Right, "Size %".to_string()), + (Align::Right, "Item".to_string()), + ]); + + for &id in &self.items { + let item = &items[id]; + let size = item.size(); + let size_percent = (f64::from(size)) / (f64::from(items.size())) * 100.0; + table.add_row(vec![ + size.to_string(), + format!("{:.2}%", size_percent), + item.name().to_string(), + ]); + } + + write!(dest, "{}", &table)?; + Ok(()) + } + + fn emit_json(&self, items: &ir::Items, dest: &mut io::Write) -> Result<(), traits::Error> { + let mut arr = json::array(dest)?; + + for &id in &self.items { + let item = &items[id]; + + let mut obj = arr.object()?; + obj.field("name", item.name())?; + + let size = item.size(); + let size_percent = (f64::from(size)) / (f64::from(items.size())) * 100.0; + obj.field("bytes", size)?; + obj.field("size_percent", size_percent)?; + } + + Ok(()) + } +} + +/// Find items that are not transitively referenced by any exports or public functions. +pub fn garbage( + _items: &ir::Items, + _opts: &opt::Garbage, +) -> Result, traits::Error> { + fn get_reachable_items(_items: &ir::Items) -> BTreeSet { + let mut reachable_items: BTreeSet = BTreeSet::new(); + let mut dfs = petgraph::visit::Dfs::new(_items, _items.meta_root()); + while let Some(id) = dfs.next(&_items) { + reachable_items.insert(id); + } + reachable_items + } + + let reachable_items = get_reachable_items(&_items); + let mut unreachable_items: Vec<_> = _items + .iter() + .filter(|item| !reachable_items.contains(&item.id())) + .collect(); + + unreachable_items.sort_by(|a, b| b.size().cmp(&a.size())); + unreachable_items.truncate(_opts.max_items() as usize); + + let unreachable_items: Vec<_> = unreachable_items.iter().map(|item| item.id()).collect(); + + let garbage_items = Garbage { + items: unreachable_items, + opts: _opts.clone(), + }; + + Ok(Box::new(garbage_items) as Box) +} diff --git a/opt/definitions.rs b/opt/definitions.rs index 18f6eb59..7c1f2514 100644 --- a/opt/definitions.rs +++ b/opt/definitions.rs @@ -33,7 +33,12 @@ pub enum Options { /// Diff the old and new versions of a binary to see what sizes changed. #[structopt(name = "diff")] - Diff(Diff) + Diff(Diff), + + /// Find and display code and data that is not transitively referenced by + /// any exports or public functions. + #[structopt(name = "garbage")] + Garbage(Garbage) } /// List the top code size offenders in a binary. @@ -409,3 +414,62 @@ impl Diff { self.max_items = n; } } + +/// Find and display code and data that is not transitively referenced by any +/// exports or public functions. +#[derive(Clone, Debug)] +#[derive(StructOpt)] +#[wasm_bindgen] +pub struct Garbage { + /// The path to the input binary to size profile. + #[cfg(feature = "cli")] + #[structopt(parse(from_os_str))] + input: path::PathBuf, + + /// The destination to write the output to. Defaults to `stdout`. + #[cfg(feature = "cli")] + #[structopt(short = "o", default_value = "-")] + output_destination: OutputDestination, + + /// The format the output should be written in. + #[cfg(feature = "cli")] + #[structopt(short = "f", long = "format", default_value = "text")] + output_format: traits::OutputFormat, + + /// The maximum number of items to display. + #[structopt(short = "n", default_value = "10")] + max_items: u32, +} + +impl Default for Garbage { + fn default() -> Garbage { + Garbage { + #[cfg(feature = "cli")] + input: Default::default(), + #[cfg(feature = "cli")] + output_destination: Default::default(), + #[cfg(feature = "cli")] + output_format: Default::default(), + + max_items: 10, + } + } +} + +#[wasm_bindgen] +impl Garbage { + /// Construct a new, default `Garbage` + pub fn new() -> Garbage { + Garbage::default() + } + + /// The maximum number of items to display. + pub fn max_items(&self) -> u32 { + self.max_items + } + + /// Set the maximum number of items to display. + pub fn set_max_items(&mut self, max: u32) { + self.max_items = max; + } +} diff --git a/opt/opt.rs b/opt/opt.rs index 89dc4880..be95fab6 100644 --- a/opt/opt.rs +++ b/opt/opt.rs @@ -51,6 +51,7 @@ cfg_if! { Options::Paths(ref paths) => paths.input(), Options::Monos(ref monos) => monos.input(), Options::Diff(ref diff) => diff.input(), + Options::Garbage(ref garbo) => garbo.input(), } } @@ -61,6 +62,7 @@ cfg_if! { Options::Paths(ref paths) => paths.output_destination(), Options::Monos(ref monos) => monos.output_destination(), Options::Diff(ref diff) => diff.output_destination(), + Options::Garbage(ref garbo) => garbo.output_destination(), } } @@ -71,6 +73,7 @@ cfg_if! { Options::Paths(ref paths) => paths.output_format(), Options::Monos(ref monos) => monos.output_format(), Options::Diff(ref diff) => diff.output_format(), + Options::Garbage(ref garbo) => garbo.output_format(), } } } @@ -152,6 +155,20 @@ cfg_if! { } } + impl CommonCliOptions for Garbage { + fn input(&self) -> &path::Path { + &self.input + } + + fn output_destination(&self) -> &OutputDestination { + &self.output_destination + } + + fn output_format(&self) -> traits::OutputFormat { + self.output_format + } + } + /// Where to output results. #[derive(Clone, Debug)] pub enum OutputDestination { diff --git a/twiggy/tests/expectations/garbage b/twiggy/tests/expectations/garbage new file mode 100644 index 00000000..74ecd117 --- /dev/null +++ b/twiggy/tests/expectations/garbage @@ -0,0 +1,11 @@ + Bytes │ Size % │ Item +───────┼────────┼──────── + 11 ┊ 13.10% ┊ code[2] + 8 ┊ 9.52% ┊ code[1] + 7 ┊ 8.33% ┊ type[2] + 5 ┊ 5.95% ┊ type[1] + 5 ┊ 5.95% ┊ code[0] + 4 ┊ 4.76% ┊ type[0] + 1 ┊ 1.19% ┊ func[0] + 1 ┊ 1.19% ┊ func[1] + 1 ┊ 1.19% ┊ func[2] diff --git a/twiggy/tests/expectations/garbage_json b/twiggy/tests/expectations/garbage_json new file mode 100644 index 00000000..591a2bef --- /dev/null +++ b/twiggy/tests/expectations/garbage_json @@ -0,0 +1 @@ +[{"name":"code[2]","bytes":11,"size_percent":13.095238095238097},{"name":"code[1]","bytes":8,"size_percent":9.523809523809524},{"name":"type[2]","bytes":7,"size_percent":8.333333333333332},{"name":"type[1]","bytes":5,"size_percent":5.952380952380952},{"name":"code[0]","bytes":5,"size_percent":5.952380952380952},{"name":"type[0]","bytes":4,"size_percent":4.761904761904762},{"name":"func[0]","bytes":1,"size_percent":1.1904761904761905},{"name":"func[1]","bytes":1,"size_percent":1.1904761904761905},{"name":"func[2]","bytes":1,"size_percent":1.1904761904761905}] diff --git a/twiggy/tests/expectations/garbage_top_2 b/twiggy/tests/expectations/garbage_top_2 new file mode 100644 index 00000000..3cc5fa87 --- /dev/null +++ b/twiggy/tests/expectations/garbage_top_2 @@ -0,0 +1,4 @@ + Bytes │ Size % │ Item +───────┼────────┼──────── + 11 ┊ 13.10% ┊ code[2] + 8 ┊ 9.52% ┊ code[1] diff --git a/twiggy/tests/fixtures/garbage.wasm b/twiggy/tests/fixtures/garbage.wasm new file mode 100644 index 0000000000000000000000000000000000000000..cd2bf9df9e2f77130f4e126c946d6dff6cb38957 GIT binary patch literal 84 zcmW-Vu?@f=3yjpF literal 0 HcmV?d00001 diff --git a/twiggy/tests/tests.rs b/twiggy/tests/tests.rs index b8531fa2..2f8c7d26 100644 --- a/twiggy/tests/tests.rs +++ b/twiggy/tests/tests.rs @@ -233,3 +233,21 @@ test!( "-n", "5" ); + +test!(garbage, "garbage", "./fixtures/garbage.wasm"); + +test!( + garbage_top_2, + "garbage", + "./fixtures/garbage.wasm", + "-n", + "2" +); + +test!( + garbage_json, + "garbage", + "./fixtures/garbage.wasm", + "-f", + "json" +); diff --git a/twiggy/twiggy.rs b/twiggy/twiggy.rs index 6aaa141f..f873102d 100644 --- a/twiggy/twiggy.rs +++ b/twiggy/twiggy.rs @@ -34,6 +34,7 @@ fn run(opts: opt::Options) -> Result<(), traits::Error> { opt::Options::Dominators(ref doms) => analyze::dominators(&mut items, doms)?, opt::Options::Paths(ref paths) => analyze::paths(&mut items, paths)?, opt::Options::Monos(ref monos) => analyze::monos(&mut items, monos)?, + opt::Options::Garbage(ref garbo) => analyze::garbage(&mut items, garbo)?, opt::Options::Diff(ref diff) => { let mut new_items = parser::read_and_parse(diff.new_input())?; analyze::diff(&mut items, &mut new_items, diff)? From 0c0d4c35451742e5a05dbad468e0cc441d99b508 Mon Sep 17 00:00:00 2001 From: data-pup Date: Wed, 9 May 2018 15:52:07 -0700 Subject: [PATCH 2/2] Patching changes requested by @fitzgen. Changes include: * Remove leading underscores in variables in the `garbage` function. * Check in the WAT source for the used for the garbage test fixture. * Compile the test fixture using wat2wasm's `--debug-names` flag. * Add missing example output to `README.md`. * Adjusted `petgraph` dependency to use same version used elsewhere in project. * Update name of item column when printing to stdout. * Remove extraneous `opts` member from the `Garbage` struct. --- README.md | 12 ++++++- analyze/Cargo.toml | 2 +- analyze/analyze.rs | 21 +++++------- twiggy/tests/expectations/garbage | 22 ++++++------ twiggy/tests/expectations/garbage_json | 2 +- twiggy/tests/expectations/garbage_top_2 | 8 ++--- twiggy/tests/fixtures/garbage.wasm | Bin 84 -> 197 bytes twiggy/tests/fixtures/garbage.wat | 43 ++++++++++++++++++++++++ 8 files changed, 79 insertions(+), 31 deletions(-) create mode 100644 twiggy/tests/fixtures/garbage.wat diff --git a/README.md b/README.md index d70f2c4a..c1a630b6 100644 --- a/README.md +++ b/README.md @@ -371,7 +371,17 @@ The `twiggy garbage` sub-command finds and display code and data that is not transitively referenced by any exports or public functions. ``` -AWOO : Place output example here. + Bytes │ Size % │ Garbage Item +───────┼────────┼────────────────────── + 11 ┊ 5.58% ┊ unusedAddThreeNumbers + 8 ┊ 4.06% ┊ unusedAddOne + 7 ┊ 3.55% ┊ type[2] + 5 ┊ 2.54% ┊ type[1] + 5 ┊ 2.54% ┊ unusedChild + 4 ┊ 2.03% ┊ type[0] + 1 ┊ 0.51% ┊ func[0] + 1 ┊ 0.51% ┊ func[1] + 1 ┊ 0.51% ┊ func[2] ``` ### 🦀 As a Crate diff --git a/analyze/Cargo.toml b/analyze/Cargo.toml index ef8db122..c4b5643b 100644 --- a/analyze/Cargo.toml +++ b/analyze/Cargo.toml @@ -15,4 +15,4 @@ path = "./analyze.rs" twiggy-ir = { version = "0.1.0", path = "../ir" } twiggy-opt = { version = "0.1.0", path = "../opt", default-features = false } twiggy-traits = { version = "0.1.0", path = "../traits" } -petgraph = "0.4.12" +petgraph = "0.4.11" diff --git a/analyze/analyze.rs b/analyze/analyze.rs index 4c93615a..c7c8679c 100644 --- a/analyze/analyze.rs +++ b/analyze/analyze.rs @@ -803,7 +803,6 @@ pub fn diff( #[derive(Debug)] struct Garbage { items: Vec, - opts: opt::Garbage, } impl traits::Emit for Garbage { @@ -811,7 +810,7 @@ impl traits::Emit for Garbage { let mut table = Table::with_header(vec![ (Align::Right, "Bytes".to_string()), (Align::Right, "Size %".to_string()), - (Align::Right, "Item".to_string()), + (Align::Left, "Garbage Item".to_string()), ]); for &id in &self.items { @@ -849,33 +848,29 @@ impl traits::Emit for Garbage { } /// Find items that are not transitively referenced by any exports or public functions. -pub fn garbage( - _items: &ir::Items, - _opts: &opt::Garbage, -) -> Result, traits::Error> { - fn get_reachable_items(_items: &ir::Items) -> BTreeSet { +pub fn garbage(items: &ir::Items, opts: &opt::Garbage) -> Result, traits::Error> { + fn get_reachable_items(items: &ir::Items) -> BTreeSet { let mut reachable_items: BTreeSet = BTreeSet::new(); - let mut dfs = petgraph::visit::Dfs::new(_items, _items.meta_root()); - while let Some(id) = dfs.next(&_items) { + let mut dfs = petgraph::visit::Dfs::new(items, items.meta_root()); + while let Some(id) = dfs.next(&items) { reachable_items.insert(id); } reachable_items } - let reachable_items = get_reachable_items(&_items); - let mut unreachable_items: Vec<_> = _items + let reachable_items = get_reachable_items(&items); + let mut unreachable_items: Vec<_> = items .iter() .filter(|item| !reachable_items.contains(&item.id())) .collect(); unreachable_items.sort_by(|a, b| b.size().cmp(&a.size())); - unreachable_items.truncate(_opts.max_items() as usize); + unreachable_items.truncate(opts.max_items() as usize); let unreachable_items: Vec<_> = unreachable_items.iter().map(|item| item.id()).collect(); let garbage_items = Garbage { items: unreachable_items, - opts: _opts.clone(), }; Ok(Box::new(garbage_items) as Box) diff --git a/twiggy/tests/expectations/garbage b/twiggy/tests/expectations/garbage index 74ecd117..38fa4a2d 100644 --- a/twiggy/tests/expectations/garbage +++ b/twiggy/tests/expectations/garbage @@ -1,11 +1,11 @@ - Bytes │ Size % │ Item -───────┼────────┼──────── - 11 ┊ 13.10% ┊ code[2] - 8 ┊ 9.52% ┊ code[1] - 7 ┊ 8.33% ┊ type[2] - 5 ┊ 5.95% ┊ type[1] - 5 ┊ 5.95% ┊ code[0] - 4 ┊ 4.76% ┊ type[0] - 1 ┊ 1.19% ┊ func[0] - 1 ┊ 1.19% ┊ func[1] - 1 ┊ 1.19% ┊ func[2] + Bytes │ Size % │ Garbage Item +───────┼────────┼────────────────────── + 11 ┊ 5.58% ┊ unusedAddThreeNumbers + 8 ┊ 4.06% ┊ unusedAddOne + 7 ┊ 3.55% ┊ type[2] + 5 ┊ 2.54% ┊ type[1] + 5 ┊ 2.54% ┊ unusedChild + 4 ┊ 2.03% ┊ type[0] + 1 ┊ 0.51% ┊ func[0] + 1 ┊ 0.51% ┊ func[1] + 1 ┊ 0.51% ┊ func[2] diff --git a/twiggy/tests/expectations/garbage_json b/twiggy/tests/expectations/garbage_json index 591a2bef..f23df561 100644 --- a/twiggy/tests/expectations/garbage_json +++ b/twiggy/tests/expectations/garbage_json @@ -1 +1 @@ -[{"name":"code[2]","bytes":11,"size_percent":13.095238095238097},{"name":"code[1]","bytes":8,"size_percent":9.523809523809524},{"name":"type[2]","bytes":7,"size_percent":8.333333333333332},{"name":"type[1]","bytes":5,"size_percent":5.952380952380952},{"name":"code[0]","bytes":5,"size_percent":5.952380952380952},{"name":"type[0]","bytes":4,"size_percent":4.761904761904762},{"name":"func[0]","bytes":1,"size_percent":1.1904761904761905},{"name":"func[1]","bytes":1,"size_percent":1.1904761904761905},{"name":"func[2]","bytes":1,"size_percent":1.1904761904761905}] +[{"name":"unusedAddThreeNumbers","bytes":11,"size_percent":5.583756345177665},{"name":"unusedAddOne","bytes":8,"size_percent":4.060913705583756},{"name":"type[2]","bytes":7,"size_percent":3.5532994923857872},{"name":"type[1]","bytes":5,"size_percent":2.5380710659898478},{"name":"unusedChild","bytes":5,"size_percent":2.5380710659898478},{"name":"type[0]","bytes":4,"size_percent":2.030456852791878},{"name":"func[0]","bytes":1,"size_percent":0.5076142131979695},{"name":"func[1]","bytes":1,"size_percent":0.5076142131979695},{"name":"func[2]","bytes":1,"size_percent":0.5076142131979695}] \ No newline at end of file diff --git a/twiggy/tests/expectations/garbage_top_2 b/twiggy/tests/expectations/garbage_top_2 index 3cc5fa87..1478ca10 100644 --- a/twiggy/tests/expectations/garbage_top_2 +++ b/twiggy/tests/expectations/garbage_top_2 @@ -1,4 +1,4 @@ - Bytes │ Size % │ Item -───────┼────────┼──────── - 11 ┊ 13.10% ┊ code[2] - 8 ┊ 9.52% ┊ code[1] + Bytes │ Size % │ Garbage Item +───────┼────────┼────────────────────── + 11 ┊ 5.58% ┊ unusedAddThreeNumbers + 8 ┊ 4.06% ┊ unusedAddOne diff --git a/twiggy/tests/fixtures/garbage.wasm b/twiggy/tests/fixtures/garbage.wasm index cd2bf9df9e2f77130f4e126c946d6dff6cb38957..8b376c6c88e0d3df675f5606f4dc1b11b45264a7 100644 GIT binary patch delta 119 zcmX}fyA1*{3;}SK9xV%~daBzNrUu5url@Km>l3Mhw%VdaSq!y)Rv8TCKV;0XGX| GxA+2=93r{^ delta 5 McmX@g7&0LQ00vS4{{R30 diff --git a/twiggy/tests/fixtures/garbage.wat b/twiggy/tests/fixtures/garbage.wat new file mode 100644 index 00000000..8d938a64 --- /dev/null +++ b/twiggy/tests/fixtures/garbage.wat @@ -0,0 +1,43 @@ +(module + ;; ------------------------------------------------------------------------- + ;; This is a WebAssembly text file that can be compiled in a wasm module to + ;; test the `twiggy garbage` command. This file contains exported functions, + ;; as well as unreachable functions of different sizes. + ;; ------------------------------------------------------------------------- + ;; NOTE: The test cases expect that this module is compiled with debug + ;; names written to the binary file, which affects the size percentages. + ;; Compile this file using the following command: + ;; + ;; wat2wasm --debug-names garbage.wat -o garbage.wasm + ;; ------------------------------------------------------------------------- + + ;; This unused function is called by 'unusedAddOne'. Push 1 onto the stack. + (func $unusedChild (result i32) + i32.const 1) + + ;; This unused function will call `unusedChild`, and return `val + 1`. + (func $unusedAddOne (param $val i32) (result i32) + get_local $val + call $unusedChild + i32.add) + + ;; This unused function adds three numbers, and returns the result. + (func $unusedAddThreeNumbers + (param $first i32) (param $second i32) (param $third i32) (result i32) + get_local $first + get_local $second + i32.add + get_local $third + i32.add + ) + + ;; This function exists to test that reachable items are not shown. + (func $add (param $lhs i32) (param $rhs i32) (result i32) + get_local $lhs + get_local $rhs + i32.add + ) + + ;; Export only the `add` function. + (export "add" (func $add)) +) \ No newline at end of file