Skip to content

Commit

Permalink
Merge pull request #50 from data-pup/add-garbage-subcommand
Browse files Browse the repository at this point in the history
Add a `twiggy garbage` command. Fixes issue #48.
  • Loading branch information
fitzgen authored May 9, 2018
2 parents 8ff9dec + 0c0d4c3 commit db5e64a
Show file tree
Hide file tree
Showing 13 changed files with 260 additions and 5 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -364,6 +365,25 @@ and new versions of a binary.
+145 ┊ <wee_alloc::neighbors::Neighbors<'a, T>>::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
Expand Down
1 change: 1 addition & 0 deletions analyze/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
82 changes: 78 additions & 4 deletions analyze/analyze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -801,3 +799,79 @@ pub fn diff(
let diff = Diff { deltas };
Ok(Box::new(diff) as Box<traits::Emit>)
}

#[derive(Debug)]
struct Garbage {
items: Vec<ir::Id>,
}

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<Box<traits::Emit>, traits::Error> {
fn get_reachable_items(items: &ir::Items) -> BTreeSet<ir::Id> {
let mut reachable_items: BTreeSet<ir::Id> = 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<traits::Emit>)
}
66 changes: 65 additions & 1 deletion opt/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
}
17 changes: 17 additions & 0 deletions opt/opt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
}

Expand All @@ -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(),
}
}

Expand All @@ -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(),
}
}
}
Expand Down Expand Up @@ -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 {
Expand Down
11 changes: 11 additions & 0 deletions twiggy/tests/expectations/garbage
Original file line number Diff line number Diff line change
@@ -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]
1 change: 1 addition & 0 deletions twiggy/tests/expectations/garbage_json
Original file line number Diff line number Diff line change
@@ -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}]
4 changes: 4 additions & 0 deletions twiggy/tests/expectations/garbage_top_2
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Bytes │ Size % │ Garbage Item
───────┼────────┼──────────────────────
11 ┊ 5.58% ┊ unusedAddThreeNumbers
8 ┊ 4.06% ┊ unusedAddOne
Binary file added twiggy/tests/fixtures/garbage.wasm
Binary file not shown.
43 changes: 43 additions & 0 deletions twiggy/tests/fixtures/garbage.wat
Original file line number Diff line number Diff line change
@@ -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))
)
18 changes: 18 additions & 0 deletions twiggy/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
1 change: 1 addition & 0 deletions twiggy/twiggy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?
Expand Down

0 comments on commit db5e64a

Please sign in to comment.