Repository: bytecodealliance/wasm-tools
Pull request #2079 (2025-02-28 02:52:46)

[wasm-metadata] print summary table
This PR makes it so that `wasm-tools metadata show` now prints a summary table at the start of the output. Think of this as a table of contents for the rest of the output. My hope is that this will make it easy to get a rough sense of what is contained within a Wasm binary, even as we continue to surface more metadata from the more detailed views. Thanks!

## Example

```text
╭───────────┬──────────────────────────┬──────┬───────┬───────────┬────────────╮
│ KIND      ┆ NAME                     ┆ SIZE ┆ SIZE% ┆ LANGUAGES ┆ PARENT     │
╞═══════════╪══════════════════════════╪══════╪═══════╪═══════════╪════════════╡
│ component ┆ unknown(0)               ┆ 2.8M ┆  100% ┆ -         ┆ <root>     │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤
│ module    ┆ rust_wasi_hello.wasm     ┆ 2.8M ┆  >99% ┆ Rust, C11 ┆ unknown(0) │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤
│ module    ┆ wit-component:adapter:wa ┆ 5.6k ┆   <1% ┆ Rust      ┆ unknown(0) │
│           ┆ si_snapshot_preview1     ┆      ┆       ┆           ┆            │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤
│ module    ┆ wit-component:shim       ┆ 1.0k ┆   <1% ┆ -         ┆ unknown(0) │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤
│ module    ┆ wit-component:fixups     ┆ 232B ┆   <1% ┆ -         ┆ unknown(0) │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤
│ component ┆ unknown(1)               ┆ 306B ┆   <1% ┆ -         ┆ unknown(0) │
╰───────────┴──────────────────────────┴──────┴───────┴───────────┴────────────╯
```


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 3622a534..94ecc4f1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -189,6 +189,12 @@ version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
 
+[[package]]
+name = "bytesize"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce3dbc6ab70a821f3e9c36084d4c1a96f50006fd41697123d3fe0a5f03d5912c"
+
 [[package]]
 name = "cast"
 version = "0.3.0"
@@ -2020,6 +2026,7 @@ dependencies = [
  "anyhow",
  "arbitrary",
  "bitflags",
+ "bytesize",
  "clap",
  "clap_complete",
  "comfy-table",
diff --git a/Cargo.toml b/Cargo.toml
index a2cd32f0..25e21f63 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -74,6 +74,7 @@ ahash = { version = "0.8.11", default-features = false }
 anyhow = "1.0.58"
 arbitrary = "1.1.0"
 bitflags = "2.5.0"
+bytesize = "2.0.0"
 clap = { version = "4.0.0", features = ["derive"] }
 clap_complete = "4.4.7"
 comfy-table = { version = "7.1.3", default-features = false }
@@ -168,6 +169,7 @@ wit-parser = { workspace = true, optional = true, features = ['decoding', 'wat',
 
 # Dependencies of `metadata`
 wasm-metadata = { workspace = true, features = ["clap"], optional = true }
+bytesize = { workspace = true }
 
 # Dependencies of `wit-smith`
 wit-smith = { workspace = true, features = ["clap"], optional = true }
diff --git a/src/bin/wasm-tools/metadata.rs b/src/bin/wasm-tools/metadata.rs
index 7d3546d2..db899347 100644
--- a/src/bin/wasm-tools/metadata.rs
+++ b/src/bin/wasm-tools/metadata.rs
@@ -1,9 +1,10 @@
+use bytesize::ByteSize;
 use std::io::Write;
 
 use anyhow::Result;
 use comfy_table::modifiers::UTF8_ROUND_CORNERS;
 use comfy_table::presets::UTF8_FULL;
-use comfy_table::{ContentArrangement, Table};
+use comfy_table::{CellAlignment, ContentArrangement, Table};
 use termcolor::WriteColor;
 use wasm_metadata::{Metadata, Payload};
 
@@ -54,7 +55,8 @@ impl ShowOpts {
         if self.json {
             write!(output, "{}", serde_json::to_string(&payload)?)?;
         } else {
-            write_table(&payload, &mut output)?;
+            write_summary_table(&payload, &mut output)?;
+            write_details_table(&payload, &mut output)?;
         }
         Ok(())
     }
@@ -89,8 +91,123 @@ impl AddOpts {
     }
 }
 
-/// Write a table containing a wasm binary's metadata to a writer
-fn write_table(payload: &Payload, f: &mut Box<dyn WriteColor>) -> Result<()> {
+/// Write a table containing a summarized overview of a wasm binary's metadata to
+/// a writer.
+fn write_summary_table(payload: &Payload, f: &mut Box<dyn WriteColor>) -> Result<()> {
+    // Prepare a table and get the individual metadata
+    let mut table = Table::new();
+    table
+        .load_preset(UTF8_FULL)
+        .apply_modifier(UTF8_ROUND_CORNERS)
+        .set_content_arrangement(ContentArrangement::Dynamic)
+        .set_width(80)
+        .set_header(vec!["KIND", "NAME", "SIZE", "SIZE%", "LANGUAGES", "PARENT"]);
+
+    table
+        .column_mut(2)
+        .expect("This should be the SIZE column")
+        .set_cell_alignment(CellAlignment::Right);
+
+    table
+        .column_mut(3)
+        .expect("This should be the SIZE% column")
+        .set_cell_alignment(CellAlignment::Right);
+
+    // Get the max value of the `range` field. This is the upper memory bound.
+    fn find_range_max(max: &mut usize, payload: &Payload) {
+        let range = &payload.metadata().range;
+        if range.end > *max {
+            *max = range.end;
+        }
+
+        if let Payload::Component { children, .. } = payload {
+            for child in children {
+                find_range_max(max, child);
+            }
+        }
+    }
+
+    let mut range_max = 0;
+    find_range_max(&mut range_max, payload);
+
+    // Recursively add all children to the table
+    write_summary_table_inner(&payload, "<root>", &mut 0, range_max, f, &mut table)?;
+
+    // Write the table to the writer
+    writeln!(f, "{table}")?;
+
+    Ok(())
+}
+
+// The recursing inner function of `write_summary_table`
+fn write_summary_table_inner(
+    payload: &Payload,
+    parent: &str,
+    unknown_id: &mut u16,
+    range_max: usize,
+    f: &mut Box<dyn WriteColor>,
+    table: &mut Table,
+) -> Result<()> {
+    let Metadata {
+        name,
+        range,
+        producers,
+        ..
+    } = payload.metadata();
+
+    let name = match name.as_deref() {
+        Some(name) => name.to_owned(),
+        None => {
+            let name = format!("unknown({unknown_id})");
+            *unknown_id += 1;
+            name
+        }
+    };
+    let size = ByteSize::b((range.end - range.start) as u64)
+        .display()
+        .si_short()
+        .to_string();
+
+    let usep = match ((range.end - range.start) as f64 / range_max as f64 * 100.0).round() as u8 {
+        // If the item was truly empty, it wouldn't be part of the binary
+        0..=1 => "<1%".to_string(),
+        // We're hedging against the low-ends, this hedges against the high-ends.
+        // Makes sure we don't see a mix of <1% and 100% in the same table, unless
+        // the item is actually 100% of the binary.
+        100 if range.end != range_max => ">99%".to_string(),
+        usep => format!("{}%", usep),
+    };
+    let kind = match payload {
+        Payload::Component { .. } => "component",
+        Payload::Module(_) => "module",
+    };
+    let languages = match producers {
+        Some(producers) => match producers.iter().find(|(name, _)| *name == "language") {
+            Some((_, pairs)) => pairs
+                .iter()
+                .map(|(lang, _)| lang.to_owned())
+                .collect::<Vec<_>>()
+                .join(", "),
+            None => "-".to_string(),
+        },
+        None => "-".to_string(),
+    };
+
+    table.add_row(vec![&kind, &*name, &*size, &usep, &languages, &parent]);
+
+    // Recursively print any children
+    if let Payload::Component { children, .. } = payload {
+        for payload in children {
+            write_summary_table_inner(payload, &name, unknown_id, range_max, f, table)?;
+        }
+    }
+
+    Ok(())
+}
+
+/// Write a table containing a detailed overview of a wasm binary's metadata to
+/// a writer.
+fn write_details_table(payload: &Payload, f: &mut Box<dyn WriteColor>) -> Result<()> {
     // Prepare a table and get the individual metadata
     let mut table = Table::new();
     table
@@ -194,7 +311,7 @@ fn write_table(payload: &Payload, f: &mut Box<dyn WriteColor>) -> Result<()> {
     // Recursively print any children
     if let Payload::Component { children, .. } = payload {
         for payload in children {
-            write_table(payload, f)?;
+            write_details_table(payload, f)?;
         }
     }
 


Test patch:
diff --git a/tests/cli/add-metadata-merge-sections.wat.stdout b/tests/cli/add-metadata-merge-sections.wat.stdout
index b3564981..35f759ab 100644
--- a/tests/cli/add-metadata-merge-sections.wat.stdout
+++ b/tests/cli/add-metadata-merge-sections.wat.stdout
@@ -1,3 +1,8 @@
+╭────────┬────────────┬──────┬───────┬───────────┬────────╮
+│ KIND   ┆ NAME       ┆ SIZE ┆ SIZE% ┆ LANGUAGES ┆ PARENT │
+╞════════╪════════════╪══════╪═══════╪═══════════╪════════╡
+│ module ┆ unknown(0) ┆  54B ┆  100% ┆ foo, bar  ┆ <root> │
+╰────────┴────────────┴──────┴───────┴───────────┴────────╯
 ╭──────────┬───────────╮
 │ KIND     ┆ VALUE     │
 ╞══════════╪═══════════╡
diff --git a/tests/cli/add-metadata-overwrite-name.wat.stdout b/tests/cli/add-metadata-overwrite-name.wat.stdout
index b8a8496f..d3c387cf 100644
--- a/tests/cli/add-metadata-overwrite-name.wat.stdout
+++ b/tests/cli/add-metadata-overwrite-name.wat.stdout
@@ -1,3 +1,8 @@
+╭────────┬──────┬──────┬───────┬───────────┬────────╮
+│ KIND   ┆ NAME ┆ SIZE ┆ SIZE% ┆ LANGUAGES ┆ PARENT │
+╞════════╪══════╪══════╪═══════╪═══════════╪════════╡
+│ module ┆ foo  ┆  21B ┆  100% ┆ -         ┆ <root> │
+╰────────┴──────┴──────┴───────┴───────────┴────────╯
 ╭───────┬───────────╮
 │ KIND  ┆ VALUE     │
 ╞═══════╪═══════════╡
diff --git a/tests/cli/add-metadata.wat.stdout b/tests/cli/add-metadata.wat.stdout
index d78adae2..a8f31911 100644
--- a/tests/cli/add-metadata.wat.stdout
+++ b/tests/cli/add-metadata.wat.stdout
@@ -1,3 +1,8 @@
+╭────────┬──────┬──────┬───────┬───────────┬────────╮
+│ KIND   ┆ NAME ┆ SIZE ┆ SIZE% ┆ LANGUAGES ┆ PARENT │
+╞════════╪══════╪══════╪═══════╪═══════════╪════════╡
+│ module ┆ foo  ┆  84B ┆  100% ┆ bar       ┆ <root> │
+╰────────┴──────┴──────┴───────┴───────────┴────────╯
 ╭──────────────┬────────────╮
 │ KIND         ┆ VALUE      │
 ╞══════════════╪════════════╡
diff --git a/tests/cli/metadata-add-component.wat.stdout b/tests/cli/metadata-add-component.wat.stdout
index dec6045f..d7d28566 100644
--- a/tests/cli/metadata-add-component.wat.stdout
+++ b/tests/cli/metadata-add-component.wat.stdout
@@ -1,3 +1,10 @@
+╭───────────┬────────────┬──────┬───────┬───────────┬────────╮
+│ KIND      ┆ NAME       ┆ SIZE ┆ SIZE% ┆ LANGUAGES ┆ PARENT │
+╞═══════════╪════════════╪══════╪═══════╪═══════════╪════════╡
+│ component ┆ foo        ┆ 101B ┆  100% ┆ foo       ┆ <root> │
+├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
+│ module    ┆ unknown(0) ┆  39B ┆   39% ┆ -         ┆ foo    │
+╰───────────┴────────────┴──────┴───────┴───────────┴────────╯
 ╭──────────┬────────────────────╮
 │ KIND     ┆ VALUE              │
 ╞══════════╪════════════════════╡
diff --git a/tests/cli/metadata-component.wat.stdout b/tests/cli/metadata-component.wat.stdout
index 27421471..7a192fcc 100644
--- a/tests/cli/metadata-component.wat.stdout
+++ b/tests/cli/metadata-component.wat.stdout
@@ -1,3 +1,12 @@
+╭───────────┬───────────────────┬──────┬───────┬───────────┬─────────╮
+│ KIND      ┆ NAME              ┆ SIZE ┆ SIZE% ┆ LANGUAGES ┆ PARENT  │
+╞═══════════╪═══════════════════╪══════╪═══════╪═══════════╪═════════╡
+│ component ┆ my-name           ┆ 136B ┆  100% ┆ -         ┆ <root>  │
+├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
+│ module    ┆ submodule         ┆  27B ┆   20% ┆ -         ┆ my-name │
+├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
+│ module    ┆ another submodule ┆  35B ┆   26% ┆ -         ┆ my-name │
+╰───────────┴───────────────────┴──────┴───────┴───────────┴─────────╯
 ╭───────┬────────────────────────────╮
 │ KIND  ┆ VALUE                      │
 ╞═══════╪════════════════════════════╡
diff --git a/tests/cli/metadata.wat.stdout b/tests/cli/metadata.wat.stdout
index 8cc41d31..90099265 100644
--- a/tests/cli/metadata.wat.stdout
+++ b/tests/cli/metadata.wat.stdout
@@ -1,3 +1,8 @@
+╭────────┬────────────┬──────┬───────┬───────────┬────────╮
+│ KIND   ┆ NAME       ┆ SIZE ┆ SIZE% ┆ LANGUAGES ┆ PARENT │
+╞════════╪════════════╪══════╪═══════╪═══════════╪════════╡
+│ module ┆ unknown(0) ┆   8B ┆  100% ┆ -         ┆ <root> │
+╰────────┴────────────┴──────┴───────┴───────────┴────────╯
 ╭───────┬───────────╮
 │ KIND  ┆ VALUE     │
 ╞═══════╪═══════════╡