diff --git a/analyze/analyze.rs b/analyze/analyze.rs index 13c259cf..14d1f74e 100644 --- a/analyze/analyze.rs +++ b/analyze/analyze.rs @@ -412,7 +412,11 @@ impl traits::Emit for Paths { label.push_str(" "); } if depth > 0 { - label.push_str(" ⬑ "); + if opts.descending() { + label.push_str(" ↳ "); + } else { + label.push_str(" ⬑ "); + } } label.push_str(item.name()); @@ -432,12 +436,21 @@ impl traits::Emit for Paths { ]); seen.insert(id); - for (i, caller) in items.predecessors(id).enumerate() { - if i > 0 { - *paths += 1; + + if opts.descending() { + for callee in items.neighbors(id) { + *paths += 1; // FIXUP: Should this be wrapped in a conditional as above? + recursive_callers(items, seen, table, depth + 1, &mut paths, &opts, callee); + } + } else { + for (i, caller) in items.predecessors(id).enumerate() { + if i > 0 { + *paths += 1; + } + recursive_callers(items, seen, table, depth + 1, &mut paths, &opts, caller); } - recursive_callers(items, seen, table, depth + 1, &mut paths, &opts, caller); } + seen.remove(&id); } @@ -518,20 +531,39 @@ impl traits::Emit for Paths { /// Find all retaining paths for the given items. pub fn paths(items: &mut ir::Items, opts: &opt::Paths) -> Result, traits::Error> { - items.compute_predecessors(); + if !opts.descending() { + items.compute_predecessors(); + } - let mut paths = Paths { - items: Vec::with_capacity(opts.functions().len()), - opts: opts.clone(), + let functions: Vec = match opts.functions().is_empty() { + true => { + if opts.descending() { + let mut roots: Vec<_> = items + .neighbors(items.meta_root()) + .map(|id| &items[id]) + .collect(); + roots.sort_by(|a, b| b.size().cmp(&a.size())); + roots.into_iter().map(|item| item.id()).collect() + } else { + let mut sorted_items: Vec<_> = items + .iter() + .filter(|item| item.id() != items.meta_root()) + .collect(); + sorted_items.sort_by(|a, b| b.size().cmp(&a.size())); + sorted_items.iter().map(|item| item.id()).collect() + } + } + false => opts.functions() + .iter() + .filter_map(|s| items.get_item_by_name(s)) + .map(|item| item.id()) + .collect(), }; - let functions: BTreeSet<_> = opts.functions().iter().map(|s| s.as_str()).collect(); - - for item in items.iter() { - if functions.contains(item.name()) { - paths.items.push(item.id()); - } - } + let paths = Paths { + items: functions, + opts: opts.clone(), + }; Ok(Box::new(paths) as Box) } diff --git a/opt/definitions.rs b/opt/definitions.rs index b7a57410..a0cd3915 100644 --- a/opt/definitions.rs +++ b/opt/definitions.rs @@ -220,6 +220,10 @@ pub struct Paths { /// The maximum number of paths, regardless of depth in the tree, to display. #[structopt(short = "r", default_value = "10")] max_paths: u32, + + /// This direction of the path traversal. + #[structopt(long = "descending")] + descending: bool, } impl Default for Paths { @@ -235,6 +239,7 @@ impl Default for Paths { functions: Default::default(), max_depth: 10, max_paths: 10, + descending: false, } } } @@ -271,6 +276,11 @@ impl Paths { self.max_paths } + /// The direction in which the call paths are traversed. + pub fn descending(&self) -> bool { + self.descending + } + /// Set the maximum depth to print the paths. pub fn set_max_depth(&mut self, max_depth: u32) { self.max_depth = max_depth; @@ -280,6 +290,12 @@ impl Paths { pub fn set_max_paths(&mut self, max_paths: u32) { self.max_paths = max_paths; } + + /// Set the call path traversal direction. + pub fn set_descending(&mut self, descending: bool) { + self.descending = descending; + } + } /// List the generic function monomorphizations that are contributing to diff --git a/twiggy/tests/expectations/paths_test_called_once b/twiggy/tests/expectations/paths_test_called_once new file mode 100644 index 00000000..7e6208ab --- /dev/null +++ b/twiggy/tests/expectations/paths_test_called_once @@ -0,0 +1,7 @@ + Shallow Bytes │ Shallow % │ Retaining Paths +───────────────┼───────────┼──────────────────────────────── + 5 ┊ 3.47% ┊ calledOnce + ┊ ┊ ⬑ func[0] + ┊ ┊ ⬑ woof + ┊ ┊ ⬑ func[3] + ┊ ┊ ⬑ export "woof" diff --git a/twiggy/tests/expectations/paths_test_called_twice b/twiggy/tests/expectations/paths_test_called_twice new file mode 100644 index 00000000..de51b0a6 --- /dev/null +++ b/twiggy/tests/expectations/paths_test_called_twice @@ -0,0 +1,13 @@ + Shallow Bytes │ Shallow % │ Retaining Paths +───────────────┼───────────┼──────────────────────────────────────── + 5 ┊ 3.47% ┊ calledTwice + ┊ ┊ ⬑ func[1] + ┊ ┊ ⬑ bark + ┊ ┊ ⬑ func[2] + ┊ ┊ ⬑ export "bark" + ┊ ┊ ⬑ awoo + ┊ ┊ ⬑ func[4] + ┊ ┊ ⬑ export "awoo" + ┊ ┊ ⬑ woof + ┊ ┊ ⬑ func[3] + ┊ ┊ ⬑ export "woof" diff --git a/twiggy/tests/expectations/paths_test_default_output b/twiggy/tests/expectations/paths_test_default_output new file mode 100644 index 00000000..0f6ce735 --- /dev/null +++ b/twiggy/tests/expectations/paths_test_default_output @@ -0,0 +1,81 @@ + Shallow Bytes │ Shallow % │ Retaining Paths +───────────────┼───────────┼──────────────────────────────────────── + 44 ┊ 30.56% ┊ "function names" subsection + 8 ┊ 5.56% ┊ woof + ┊ ┊ ⬑ func[3] + ┊ ┊ ⬑ export "woof" + 7 ┊ 4.86% ┊ export "awoo" + 7 ┊ 4.86% ┊ export "bark" + 7 ┊ 4.86% ┊ export "woof" + 5 ┊ 3.47% ┊ calledOnce + ┊ ┊ ⬑ func[0] + ┊ ┊ ⬑ woof + ┊ ┊ ⬑ func[3] + ┊ ┊ ⬑ export "woof" + 5 ┊ 3.47% ┊ calledTwice + ┊ ┊ ⬑ func[1] + ┊ ┊ ⬑ bark + ┊ ┊ ⬑ func[2] + ┊ ┊ ⬑ export "bark" + ┊ ┊ ⬑ awoo + ┊ ┊ ⬑ func[4] + ┊ ┊ ⬑ export "awoo" + ┊ ┊ ⬑ woof + ┊ ┊ ⬑ func[3] + ┊ ┊ ⬑ export "woof" + 5 ┊ 3.47% ┊ bark + ┊ ┊ ⬑ func[2] + ┊ ┊ ⬑ export "bark" + ┊ ┊ ⬑ awoo + ┊ ┊ ⬑ func[4] + ┊ ┊ ⬑ export "awoo" + 5 ┊ 3.47% ┊ awoo + ┊ ┊ ⬑ func[4] + ┊ ┊ ⬑ export "awoo" + 4 ┊ 2.78% ┊ type[0] + ┊ ┊ ⬑ func[0] + ┊ ┊ ⬑ woof + ┊ ┊ ⬑ func[3] + ┊ ┊ ⬑ export "woof" + ┊ ┊ ⬑ func[1] + ┊ ┊ ⬑ bark + ┊ ┊ ⬑ func[2] + ┊ ┊ ⬑ export "bark" + ┊ ┊ ⬑ awoo + ┊ ┊ ⬑ func[4] + ┊ ┊ ⬑ export "awoo" + ┊ ┊ ⬑ woof + ┊ ┊ ⬑ func[3] + ┊ ┊ ⬑ export "woof" + ┊ ┊ ⬑ func[2] + ┊ ┊ ⬑ export "bark" + ┊ ┊ ⬑ awoo + ┊ ┊ ⬑ func[4] + ┊ ┊ ⬑ export "awoo" + ┊ ┊ ⬑ func[3] + ┊ ┊ ⬑ export "woof" + ┊ ┊ ⬑ func[4] + ┊ ┊ ⬑ export "awoo" + 1 ┊ 0.69% ┊ func[0] + ┊ ┊ ⬑ woof + ┊ ┊ ⬑ func[3] + ┊ ┊ ⬑ export "woof" + 1 ┊ 0.69% ┊ func[1] + ┊ ┊ ⬑ bark + ┊ ┊ ⬑ func[2] + ┊ ┊ ⬑ export "bark" + ┊ ┊ ⬑ awoo + ┊ ┊ ⬑ func[4] + ┊ ┊ ⬑ export "awoo" + ┊ ┊ ⬑ woof + ┊ ┊ ⬑ func[3] + ┊ ┊ ⬑ export "woof" + 1 ┊ 0.69% ┊ func[2] + ┊ ┊ ⬑ export "bark" + ┊ ┊ ⬑ awoo + ┊ ┊ ⬑ func[4] + ┊ ┊ ⬑ export "awoo" + 1 ┊ 0.69% ┊ func[3] + ┊ ┊ ⬑ export "woof" + 1 ┊ 0.69% ┊ func[4] + ┊ ┊ ⬑ export "awoo" diff --git a/twiggy/tests/expectations/paths_test_default_output_desc b/twiggy/tests/expectations/paths_test_default_output_desc new file mode 100644 index 00000000..8ae2cee1 --- /dev/null +++ b/twiggy/tests/expectations/paths_test_default_output_desc @@ -0,0 +1,30 @@ + Shallow Bytes │ Shallow % │ Retaining Paths +───────────────┼───────────┼────────────────────────────────────── + 44 ┊ 30.56% ┊ "function names" subsection + 7 ┊ 4.86% ┊ export "awoo" + ┊ ┊ ↳ func[4] + ┊ ┊ ↳ type[0] + ┊ ┊ ↳ awoo + ┊ ┊ ↳ func[2] + ┊ ┊ ↳ type[0] + ┊ ┊ ↳ bark + ┊ ┊ ↳ func[1] + ┊ ┊ ↳ type[0] + ┊ ┊ ↳ calledTwice + 7 ┊ 4.86% ┊ export "bark" + ┊ ┊ ↳ func[2] + ┊ ┊ ↳ type[0] + ┊ ┊ ↳ bark + ┊ ┊ ↳ func[1] + ┊ ┊ ↳ type[0] + ┊ ┊ ↳ calledTwice + 7 ┊ 4.86% ┊ export "woof" + ┊ ┊ ↳ func[3] + ┊ ┊ ↳ type[0] + ┊ ┊ ↳ woof + ┊ ┊ ↳ func[0] + ┊ ┊ ↳ type[0] + ┊ ┊ ↳ calledOnce + ┊ ┊ ↳ func[1] + ┊ ┊ ↳ type[0] + ┊ ┊ ↳ calledTwice diff --git a/twiggy/tests/expectations/paths_test_default_output_desc_with_depth b/twiggy/tests/expectations/paths_test_default_output_desc_with_depth new file mode 100644 index 00000000..e3f6076e --- /dev/null +++ b/twiggy/tests/expectations/paths_test_default_output_desc_with_depth @@ -0,0 +1,15 @@ + Shallow Bytes │ Shallow % │ Retaining Paths +───────────────┼───────────┼──────────────────────────── + 44 ┊ 30.56% ┊ "function names" subsection + 7 ┊ 4.86% ┊ export "awoo" + ┊ ┊ ↳ func[4] + ┊ ┊ ↳ type[0] + ┊ ┊ ↳ awoo + 7 ┊ 4.86% ┊ export "bark" + ┊ ┊ ↳ func[2] + ┊ ┊ ↳ type[0] + ┊ ┊ ↳ bark + 7 ┊ 4.86% ┊ export "woof" + ┊ ┊ ↳ func[3] + ┊ ┊ ↳ type[0] + ┊ ┊ ↳ woof diff --git a/twiggy/tests/expectations/paths_json b/twiggy/tests/expectations/paths_wee_alloc_json similarity index 100% rename from twiggy/tests/expectations/paths_json rename to twiggy/tests/expectations/paths_wee_alloc_json diff --git a/twiggy/tests/fixtures/paths_test.wasm b/twiggy/tests/fixtures/paths_test.wasm new file mode 100644 index 00000000..6421c825 Binary files /dev/null and b/twiggy/tests/fixtures/paths_test.wasm differ diff --git a/twiggy/tests/fixtures/paths_test.wat b/twiggy/tests/fixtures/paths_test.wat new file mode 100644 index 00000000..580ec30b --- /dev/null +++ b/twiggy/tests/fixtures/paths_test.wat @@ -0,0 +1,50 @@ +(module + ;; ------------------------------------------------------------------------ + ;; This is a WebAssembly text file that can be compiled in a wasm module to + ;; test the `twiggy paths` command. This intends to provide a non-trivial + ;; structure of call paths for testing purposes. + ;; + ;; The call path is shown in the ascii diagram below with exported + ;; functions enclosed in braces, and unexported functions in quotes. + ;; + ;; [awoo] + ;; | + ;; v + ;; [woof] [bark] + ;; | | | + ;; | -------- | + ;; | | | + ;; v v v + ;; 'calledOnce' 'calledTwice' + ;; ------------------------------------------------------------------------ + ;; 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 paths_test.wat -o paths_test.wasm + ;; ------------------------------------------------------------------------- + + + ;; This function is called once, by 'woof'. + (func $calledOnce (result i32) + i32.const 1) + + ;; This function is called twice, by 'bark' and 'woof'. + (func $calledTwice (result i32) + i32.const 2) + + (func $bark (result i32) + call $calledTwice) + + (func $woof (result i32) + call $calledOnce + call $calledTwice + i32.add) + + (func $awoo (result i32) + call $bark) + + (export "awoo" (func $awoo)) + (export "bark" (func $bark)) + (export "woof" (func $woof)) +) diff --git a/twiggy/tests/tests.rs b/twiggy/tests/tests.rs index 98d6626d..8116c4fa 100644 --- a/twiggy/tests/tests.rs +++ b/twiggy/tests/tests.rs @@ -140,6 +140,42 @@ test!( "hello" ); +test!( + paths_test_called_once, + "paths", + "./fixtures/paths_test.wasm", + "calledOnce" +); + +test!( + paths_test_called_twice, + "paths", + "./fixtures/paths_test.wasm", + "calledTwice" +); + +test!( + paths_test_default_output, + "paths", + "./fixtures/paths_test.wasm" +); + +test!( + paths_test_default_output_desc, + "paths", + "./fixtures/paths_test.wasm", + "--descending" +); + +test!( + paths_test_default_output_desc_with_depth, + "paths", + "./fixtures/paths_test.wasm", + "--descending", + "-d", + "2" +); + test!( paths_wee_alloc, "paths", @@ -162,6 +198,19 @@ test!( "2" ); +test!( + paths_wee_alloc_json, + "paths", + "./fixtures/wee_alloc.wasm", + "wee_alloc::alloc_first_fit::h9a72de3af77ef93f", + "hello", + "goodbye", + "-d", + "3", + "-f", + "json" +); + // This should not fail to open and write `whatever-output.txt`. test!( output_to_file, @@ -192,19 +241,6 @@ test!( "json" ); -test!( - paths_json, - "paths", - "./fixtures/wee_alloc.wasm", - "wee_alloc::alloc_first_fit::h9a72de3af77ef93f", - "hello", - "goodbye", - "-d", - "3", - "-f", - "json" -); - test!( issue_16, "paths",