From e856554f972c669b4a12bbe87b4d65311d142927 Mon Sep 17 00:00:00 2001 From: Emily Boudreaux Date: Wed, 25 Mar 2026 07:31:14 -0400 Subject: [PATCH] feat(html): added interactive html profiles --- Cargo.toml | 4 +- README.md | 5 +- src/cli.rs | 4 + src/html.rs | 293 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/main.rs | 7 ++ 6 files changed, 311 insertions(+), 3 deletions(-) create mode 100644 src/html.rs diff --git a/Cargo.toml b/Cargo.toml index 7eff1bc..aaf0d14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,10 @@ [package] name = "strata" -version = "0.1.0" +version = "0.1.1" edition = "2024" publish = ["gitea"] [dependencies] clap = { version = "4.6.0", features = ["derive"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" diff --git a/README.md b/README.md index a30e562..b912786 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,13 @@ strata --mode [OPTIONS] - `flat-symbol`: Aggregates the observed weights by the resolved symbol name, irrespective of the call path context, yielding a flat percentage summary. - `namespace`: Aggregates observed weights by namespace exclusively. -### Filtering Options -Strata supports the refinement of the call graph through three primary filtering mechanisms. When nodes are filtered from the reporting view, their weights are correctly collapsed into the nearest visible ancestor to ensure strict weight conservation. +### Filtering and Output Options +Strata supports the refinement of the call graph through three primary filtering mechanisms, as well as an interactive HTML output capability. - `--whitelist `: Only the specified namespaces remain visible in the output hierarchy. - `--blacklist `: The specified namespaces are removed from explicit representation, with their weights folded upward. - `--fold `: Halts traversal at the first node whose literal frame matches the provided substring. The internal operations of that subtree are hidden, and all inclusive weights descending from that node are strictly rolled up into its exclusive weight. +- `--graph `: Generates a self-contained, interactive `D3.js` HTML report. If the mode is set to `tree`, it produces a panning, zooming Node-Link Collapsible Tree visualizer. For `flat-symbol` or `namespace` modes, it generates an interactive bar chart. ## Development Context and Methodology This software was constructed through iterative interactions with a generative artificial intelligence coding assistant. It serves primarily as an experimental test case for the author to evaluate the feasibility of AI-driven software engineering in systems tooling. diff --git a/src/cli.rs b/src/cli.rs index 4f78039..07b87d9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -22,6 +22,10 @@ pub struct Cli { /// Comma-separated list of substrings to match for folding subtrees #[arg(short = 'f', long)] pub fold: Option, + + /// Path to output an interactive HTML graph + #[arg(short = 'g', long)] + pub graph: Option, } #[derive(ValueEnum, Clone, Debug)] diff --git a/src/html.rs b/src/html.rs new file mode 100644 index 0000000..eb5b849 --- /dev/null +++ b/src/html.rs @@ -0,0 +1,293 @@ +use crate::filter::ReportGraph; +use crate::cli::ReportMode; +use serde::Serialize; +use std::fs::File; +use std::io::Write; + +#[derive(Serialize)] +struct TreeNode { + name: String, + inclusive: u64, + exclusive: u64, + children: Vec, +} + +fn build_tree_node(graph: &ReportGraph, id: usize) -> TreeNode { + let node = &graph.nodes[id]; + let mut children = Vec::new(); + for child_id in node.children.values() { + children.push(build_tree_node(graph, *child_id)); + } + + children.sort_by(|a, b| b.inclusive.cmp(&a.inclusive)); + + TreeNode { + name: node.display_label.clone(), + inclusive: node.inclusive_weight, + exclusive: node.exclusive_weight, + children, + } +} + +pub fn generate_html_report(graph: &ReportGraph, mode: &ReportMode, out_path: &str) -> std::io::Result<()> { + let mut f = File::create(out_path)?; + + match mode { + ReportMode::Tree => { + let root_node = build_tree_node(graph, graph.root); + let json_data = serde_json::to_string(&root_node).unwrap(); + + let html = format!(r#" + + + + Strata Call Graph + + + + +
+ + + +"#, json_data); + f.write_all(html.as_bytes())?; + } + ReportMode::FlatSymbol | ReportMode::Namespace => { + let data = match mode { + ReportMode::FlatSymbol => crate::report::report_flat_symbol(graph), + ReportMode::Namespace => crate::report::report_namespace(graph), + _ => unreachable!(), + }; + + let top_data: Vec<_> = data.into_iter().take(150).collect(); + let json_data = serde_json::to_string(&top_data).unwrap(); + let title = match mode { + ReportMode::FlatSymbol => "Flat Symbol Report", + ReportMode::Namespace => "Namespace Report", + _ => "", + }; + + let html = format!(r#" + + + + {1} + + + + +

{1}

+
+
+ + +"#, json_data, title); + + f.write_all(html.as_bytes())?; + } + } + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 23378dd..f22bd29 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,3 +4,4 @@ pub mod model; pub mod parser; pub mod cli; pub mod report; +pub mod html; diff --git a/src/main.rs b/src/main.rs index 3eee340..0eb11ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use strata::builder::SourceGraphBuilder; use strata::cli::{Cli, ReportMode}; use strata::filter::{collapse_graph, FilterMode, FilterOptions}; use strata::report::{report_flat_symbol, report_namespace, report_tree}; +use strata::html::generate_html_report; fn main() -> Result<(), Box> { let args = Cli::parse(); @@ -53,6 +54,12 @@ fn main() -> Result<(), Box> { } } } + + // 5. Build HTML Graph if requested + if let Some(graph_file) = &args.graph { + generate_html_report(&report_graph, &args.mode, graph_file)?; + println!("Interactive graph written to {}", graph_file); + } Ok(()) }