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..c1a630b6 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,25 @@ 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. + +``` + 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 `twiggy` is divided into a collection of crates that you can use diff --git a/analyze/Cargo.toml b/analyze/Cargo.toml index f3b2a57d..c4b5643b 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.11" diff --git a/analyze/analyze.rs b/analyze/analyze.rs index 716b48ce..c7c8679c 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,79 @@ pub fn diff( let diff = Diff { deltas }; Ok(Box::new(diff) as Box) } + +#[derive(Debug)] +struct Garbage { + items: Vec, +} + +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::Left, "Garbage 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, + }; + + 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..38fa4a2d --- /dev/null +++ b/twiggy/tests/expectations/garbage @@ -0,0 +1,11 @@ + 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 new file mode 100644 index 00000000..f23df561 --- /dev/null +++ b/twiggy/tests/expectations/garbage_json @@ -0,0 +1 @@ +[{"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 new file mode 100644 index 00000000..1478ca10 --- /dev/null +++ b/twiggy/tests/expectations/garbage_top_2 @@ -0,0 +1,4 @@ + 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 new file mode 100644 index 00000000..8b376c6c Binary files /dev/null and b/twiggy/tests/fixtures/garbage.wasm differ 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 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)?