Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a twiggy garbage command. Fixes issue #48. #50

Merged
merged 2 commits into from
May 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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