feat(strata): initial commit
This commit is contained in:
8
Cargo.toml
Normal file
8
Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "strata"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = ["crates-io"]
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.6.0", features = ["derive"] }
|
||||
49
README.md
Normal file
49
README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Strata: A Flat Profiling Data Reformatter
|
||||
|
||||
## Overview
|
||||
Strata is a command-line utility implemented in Rust designed to ingest flat profiling data files—typically containing line-oriented paths with terminal numeric weights—and reconstruct a directed acyclic rooted path graph. This internal representation facilitates robust traversal, aggregation, and the application of visibility filters, allowing researchers and developers to produce more human-readable profile summaries.
|
||||
|
||||
## Installation
|
||||
The program is provided as a standard Cargo project. It may be compiled and installed globally for the current user by invoking the standard Rust toolchain:
|
||||
|
||||
```sh
|
||||
cargo install --path .
|
||||
```
|
||||
|
||||
Upon installation, the `strata` binary will be available within the user's configured `PATH` (typically `~/.cargo/bin`).
|
||||
|
||||
## Usage
|
||||
Strata accepts a raw profile file and supports multiple output reporting modes.
|
||||
|
||||
```sh
|
||||
strata <input_file> --mode <MODE> [OPTIONS]
|
||||
```
|
||||
|
||||
### Supported Modes
|
||||
- `tree`: Emits a hierarchical representation of the retained call structure alongside inclusive percentages.
|
||||
- `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.
|
||||
|
||||
- `--whitelist <namespaces>`: Only the specified namespaces remain visible in the output hierarchy.
|
||||
- `--blacklist <namespaces>`: The specified namespaces are removed from explicit representation, with their weights folded upward.
|
||||
- `--fold <substrings>`: 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.
|
||||
|
||||
## 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.
|
||||
|
||||
### User Directions Provided
|
||||
The generative model was directed initially by a comprehensive requirements document outlining the functional goals, core design principles, parsing strategies, designated modes of output, filtering semantics, and the underlying mathematical graph specification. Subsequent interactive instructions included verbatim:
|
||||
1. "I want to be able to do something like collapse all mfem related tools (things built into mfem down). Im not sure the best way to do this but basically this tool is supposed to help me optimize my code which uses mfem, its useful for me to see where I call mfem but I do not need to see the calls mfem makes internally."
|
||||
2. "okay and now can I do something like cargo install and then use strata anywhere on my computer?"
|
||||
3. "okay now please write a detailed README. prioriatize professionallism and an academic voice. Do not use emojis or hype language. Make it clear that this program was developed with the assistance of generative AI, document all the instructions I gave you in the readme. Note down the pitfalls of generative AI code and comment on how this code was generateed as a test case for myself and users should use with caution."
|
||||
|
||||
### Cautions Regarding Generative AI Software
|
||||
As an artifact of generative artificial intelligence, this codebase is subject to several known pitfalls inherent to large language models in software engineering:
|
||||
- **Latent Edge Cases:** The parser may harbor undocumented assumptions regarding the formatting of the input data (e.g., rigid reliance on semicolon strings or backtick delimiters) which could fail abruptly under slightly heterogeneous logging schemas.
|
||||
- **Architectural Brittle States:** While functionally demonstrative under tested constraints, AI-generated code occasionally lacks the cohesive architectural foresight necessary for long-term maintainability or seamless extensibility.
|
||||
- **Silent Failures:** Generative integrations may adopt error handling strategies that silently discard malformed data or default to unanticipated fallback states without emitting sufficient diagnostic warnings to the operator.
|
||||
|
||||
Consequently, this utility is provided strictly "as-is." Prospective users are urged to exercise due diligence, thoroughly review the source code logic, and apply critical caution when trusting the aggregated profile data generated by this tool in complex production environments.
|
||||
111
src/builder.rs
Normal file
111
src/builder.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use crate::model::{SourceGraph};
|
||||
use crate::parser::{parse_frame, parse_line};
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
|
||||
pub struct SourceGraphBuilder {
|
||||
pub graph: SourceGraph,
|
||||
}
|
||||
|
||||
impl SourceGraphBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
graph: SourceGraph::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads lines from the given reader and populates the graph.
|
||||
pub fn build_from_reader<R: BufRead>(&mut self, reader: R) {
|
||||
for line_result in reader.lines() {
|
||||
if let Ok(line) = line_result {
|
||||
if let Some((raw_frames, weight)) = parse_line(&line) {
|
||||
self.insert_path(&raw_frames, weight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads from a file and builds the graph.
|
||||
pub fn build_from_file<P: AsRef<Path>>(&mut self, path: P) -> std::io::Result<()> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
self.build_from_reader(reader);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn insert_path(&mut self, raw_frames: &[String], weight: u64) {
|
||||
let mut current_id = self.graph.root;
|
||||
|
||||
// Propagate inclusive weight to root
|
||||
if let Some(root_node) = self.graph.get_node_mut(self.graph.root) {
|
||||
root_node.inclusive_weight += weight;
|
||||
}
|
||||
|
||||
for (i, raw_frame) in raw_frames.iter().enumerate() {
|
||||
let next_id = {
|
||||
let current_node = self.graph.get_node(current_id).unwrap();
|
||||
current_node.children.get(raw_frame).copied()
|
||||
};
|
||||
|
||||
let child_id = match next_id {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
let frame = parse_frame(raw_frame);
|
||||
self.graph.add_node(current_id, frame)
|
||||
}
|
||||
};
|
||||
|
||||
// Update weights
|
||||
if let Some(child_node) = self.graph.get_node_mut(child_id) {
|
||||
child_node.inclusive_weight += weight;
|
||||
if i == raw_frames.len() - 1 {
|
||||
child_node.exclusive_weight += weight;
|
||||
}
|
||||
}
|
||||
|
||||
current_id = child_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SourceGraphBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_builder_insert_path() {
|
||||
let mut builder = SourceGraphBuilder::new();
|
||||
builder.insert_path(&["main".to_string(), "func".to_string()], 10);
|
||||
builder.insert_path(&["main".to_string(), "func2".to_string()], 20);
|
||||
|
||||
// Root
|
||||
let root = builder.graph.get_node(builder.graph.root).unwrap();
|
||||
assert_eq!(root.inclusive_weight, 30);
|
||||
assert_eq!(root.exclusive_weight, 0);
|
||||
|
||||
// main
|
||||
let main_id = builder.graph.nodes[0].children["main"];
|
||||
let main_node = builder.graph.get_node(main_id).unwrap();
|
||||
assert_eq!(main_node.inclusive_weight, 30);
|
||||
assert_eq!(main_node.exclusive_weight, 0);
|
||||
|
||||
// func
|
||||
let func_id = main_node.children["func"];
|
||||
let func_node = builder.graph.get_node(func_id).unwrap();
|
||||
assert_eq!(func_node.inclusive_weight, 10);
|
||||
assert_eq!(func_node.exclusive_weight, 10);
|
||||
|
||||
// func2
|
||||
let func2_id = main_node.children["func2"];
|
||||
let func2_node = builder.graph.get_node(func2_id).unwrap();
|
||||
assert_eq!(func2_node.inclusive_weight, 20);
|
||||
assert_eq!(func2_node.exclusive_weight, 20);
|
||||
}
|
||||
}
|
||||
32
src/cli.rs
Normal file
32
src/cli.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use clap::{Parser, ValueEnum};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
pub struct Cli {
|
||||
/// The input raw profile file
|
||||
#[arg(required = true)]
|
||||
pub input: String,
|
||||
|
||||
/// Mode of output.
|
||||
#[arg(short, long, value_enum, default_value_t = ReportMode::Tree)]
|
||||
pub mode: ReportMode,
|
||||
|
||||
/// Comma-separated list of namespaces to whitelist
|
||||
#[arg(short = 'w', long)]
|
||||
pub whitelist: Option<String>,
|
||||
|
||||
/// Comma-separated list of namespaces to blacklist
|
||||
#[arg(short = 'b', long)]
|
||||
pub blacklist: Option<String>,
|
||||
|
||||
/// Comma-separated list of substrings to match for folding subtrees
|
||||
#[arg(short = 'f', long)]
|
||||
pub fold: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug)]
|
||||
pub enum ReportMode {
|
||||
Tree,
|
||||
FlatSymbol,
|
||||
Namespace,
|
||||
}
|
||||
210
src/filter.rs
Normal file
210
src/filter.rs
Normal file
@@ -0,0 +1,210 @@
|
||||
use crate::model::{NodeId, SourceGraph, SourceNode};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
/// Filtering mode for namespaces.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum FilterMode {
|
||||
None,
|
||||
Whitelist(HashSet<String>),
|
||||
Blacklist(HashSet<String>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FilterOptions {
|
||||
pub mode: FilterMode,
|
||||
pub fold: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for FilterOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mode: FilterMode::None,
|
||||
fold: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Node in the report graph.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReportNode {
|
||||
pub display_label: String,
|
||||
pub children: HashMap<String, usize>, // label -> ReportNode index
|
||||
pub inclusive_weight: u64,
|
||||
pub exclusive_weight: u64,
|
||||
}
|
||||
|
||||
impl ReportNode {
|
||||
pub fn new(display_label: String) -> Self {
|
||||
Self {
|
||||
display_label,
|
||||
children: HashMap::new(),
|
||||
inclusive_weight: 0,
|
||||
exclusive_weight: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The report graph containing visible nodes after filtering.
|
||||
#[derive(Debug)]
|
||||
pub struct ReportGraph {
|
||||
pub nodes: Vec<ReportNode>,
|
||||
pub root: usize,
|
||||
}
|
||||
|
||||
impl ReportGraph {
|
||||
pub fn new() -> Self {
|
||||
let root = ReportNode::new("[root]".to_string());
|
||||
Self {
|
||||
nodes: vec![root],
|
||||
root: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_node_mut(&mut self, id: usize) -> Option<&mut ReportNode> {
|
||||
self.nodes.get_mut(id)
|
||||
}
|
||||
|
||||
pub fn get_or_create_child(&mut self, parent_id: usize, label: &str) -> usize {
|
||||
if let Some(parent) = self.nodes.get(parent_id) {
|
||||
if let Some(&child_id) = parent.children.get(label) {
|
||||
return child_id;
|
||||
}
|
||||
}
|
||||
|
||||
let new_id = self.nodes.len();
|
||||
let new_node = ReportNode::new(label.to_string());
|
||||
self.nodes.push(new_node);
|
||||
|
||||
if let Some(parent) = self.nodes.get_mut(parent_id) {
|
||||
parent.children.insert(label.to_string(), new_id);
|
||||
}
|
||||
new_id
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ReportGraph {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines if a node's frame is visible under the given filter.
|
||||
pub fn is_visible(node: &SourceNode, mode: &FilterMode) -> bool {
|
||||
// Root is always visible
|
||||
if node.id.0 == 0 {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Extract namespace or use special marker
|
||||
let namespace = node.frame.namespace.as_deref().unwrap_or("[no_namespace]");
|
||||
|
||||
match mode {
|
||||
FilterMode::None => true,
|
||||
FilterMode::Whitelist(allowed) => allowed.contains(namespace),
|
||||
FilterMode::Blacklist(denied) => !denied.contains(namespace),
|
||||
}
|
||||
}
|
||||
|
||||
/// Traverses the source graph and builds a ReportGraph, collapsing non-visible nodes.
|
||||
pub fn collapse_graph(source: &SourceGraph, options: &FilterOptions) -> ReportGraph {
|
||||
let mut report = ReportGraph::new();
|
||||
|
||||
// Copy root inclusive weight
|
||||
if let Some(src_root) = source.get_node(source.root) {
|
||||
if let Some(rep_root) = report.get_node_mut(report.root) {
|
||||
rep_root.inclusive_weight = src_root.inclusive_weight;
|
||||
rep_root.exclusive_weight = src_root.exclusive_weight; // usually 0
|
||||
}
|
||||
|
||||
for &child_id in src_root.children.values() {
|
||||
traverse_and_collapse(source, child_id, report.root, options, &mut report);
|
||||
}
|
||||
}
|
||||
|
||||
report
|
||||
}
|
||||
|
||||
fn traverse_and_collapse(
|
||||
source: &SourceGraph,
|
||||
src_id: NodeId,
|
||||
mut active_report_parent: usize,
|
||||
options: &FilterOptions,
|
||||
report: &mut ReportGraph,
|
||||
) {
|
||||
let src_node = source.get_node(src_id).unwrap();
|
||||
|
||||
let visible = is_visible(src_node, &options.mode);
|
||||
let should_fold = options.fold.iter().any(|f| src_node.frame.raw.contains(f));
|
||||
|
||||
// If visible, we create/enter the node in the report graph
|
||||
if visible {
|
||||
let label = src_node.frame.raw.clone();
|
||||
let new_report_id = report.get_or_create_child(active_report_parent, &label);
|
||||
|
||||
if let Some(rep_node) = report.get_node_mut(new_report_id) {
|
||||
rep_node.inclusive_weight += src_node.inclusive_weight;
|
||||
|
||||
if should_fold {
|
||||
rep_node.exclusive_weight += src_node.inclusive_weight;
|
||||
} else {
|
||||
rep_node.exclusive_weight += src_node.exclusive_weight;
|
||||
}
|
||||
}
|
||||
|
||||
active_report_parent = new_report_id;
|
||||
} else {
|
||||
if let Some(rep_node) = report.get_node_mut(active_report_parent) {
|
||||
if should_fold {
|
||||
rep_node.exclusive_weight += src_node.inclusive_weight;
|
||||
} else {
|
||||
rep_node.exclusive_weight += src_node.exclusive_weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !should_fold {
|
||||
for &child_id in src_node.children.values() {
|
||||
traverse_and_collapse(source, child_id, active_report_parent, options, report);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::builder::SourceGraphBuilder;
|
||||
|
||||
#[test]
|
||||
fn test_filter_collapse() {
|
||||
let mut builder = SourceGraphBuilder::new();
|
||||
// [root]
|
||||
// +- a`foo (10 + 5 = 15)
|
||||
// +- b`bar (10)
|
||||
builder.insert_path(&["a`foo".to_string(), "b`bar".to_string()], 10);
|
||||
builder.insert_path(&["a`foo".to_string()], 5);
|
||||
|
||||
let source = builder.graph;
|
||||
|
||||
// Mode: Blacklist 'b'
|
||||
let mut blacklist = HashSet::new();
|
||||
blacklist.insert("b".to_string());
|
||||
let mode = FilterMode::Blacklist(blacklist);
|
||||
let options = FilterOptions { mode, fold: vec![] };
|
||||
|
||||
let report = collapse_graph(&source, &options);
|
||||
|
||||
// Root should have 1 child (a`foo)
|
||||
let root = &report.nodes[report.root];
|
||||
assert_eq!(root.inclusive_weight, 15);
|
||||
assert_eq!(root.children.len(), 1);
|
||||
|
||||
let a_foo_id = root.children["a`foo"];
|
||||
let a_foo = &report.nodes[a_foo_id];
|
||||
|
||||
// a`foo should have inclusive 15, and since b`bar is hidden,
|
||||
// its exclusive weight (10) collapses into a`foo, making a`foo exclusive = 5 + 10 = 15.
|
||||
assert_eq!(a_foo.inclusive_weight, 15);
|
||||
assert_eq!(a_foo.exclusive_weight, 15);
|
||||
assert_eq!(a_foo.children.len(), 0);
|
||||
}
|
||||
}
|
||||
6
src/lib.rs
Normal file
6
src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod builder;
|
||||
pub mod filter;
|
||||
pub mod model;
|
||||
pub mod parser;
|
||||
pub mod cli;
|
||||
pub mod report;
|
||||
58
src/main.rs
Normal file
58
src/main.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use clap::Parser;
|
||||
use std::collections::HashSet;
|
||||
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};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args = Cli::parse();
|
||||
|
||||
// 1. Build source graph
|
||||
let mut builder = SourceGraphBuilder::new();
|
||||
builder.build_from_file(&args.input)?;
|
||||
|
||||
// 2. Parse filters
|
||||
let mut filter_mode = FilterMode::None;
|
||||
if let Some(w) = args.whitelist {
|
||||
let set: HashSet<String> = w.split(',').map(|s| s.trim().to_string()).collect();
|
||||
filter_mode = FilterMode::Whitelist(set);
|
||||
} else if let Some(b) = args.blacklist {
|
||||
let set: HashSet<String> = b.split(',').map(|s| s.trim().to_string()).collect();
|
||||
filter_mode = FilterMode::Blacklist(set);
|
||||
}
|
||||
|
||||
// 3. Setup options
|
||||
let fold = args.fold.map(|s| s.split(',').map(|f| f.trim().to_string()).collect()).unwrap_or_default();
|
||||
let options = FilterOptions {
|
||||
mode: filter_mode,
|
||||
fold,
|
||||
};
|
||||
|
||||
// 4. Collapse graph
|
||||
let report_graph = collapse_graph(&builder.graph, &options);
|
||||
|
||||
// 5. Report
|
||||
match args.mode {
|
||||
ReportMode::Tree => {
|
||||
let lines = report_tree(&report_graph);
|
||||
for line in lines {
|
||||
println!("{}", line);
|
||||
}
|
||||
}
|
||||
ReportMode::FlatSymbol => {
|
||||
let entries = report_flat_symbol(&report_graph);
|
||||
for (sym, weight) in entries {
|
||||
println!("{:10} {}", weight, sym);
|
||||
}
|
||||
}
|
||||
ReportMode::Namespace => {
|
||||
let entries = report_namespace(&report_graph);
|
||||
for (ns, weight) in entries {
|
||||
println!("{:10} {}", weight, ns);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
87
src/model.rs
Normal file
87
src/model.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A unique identifier for a node in the call graph.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct NodeId(pub usize);
|
||||
|
||||
/// Represents a single parsed frame in the call stack.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Frame {
|
||||
pub raw: String,
|
||||
pub namespace: Option<String>,
|
||||
pub symbol: Option<String>,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
pub fn new(raw: String, namespace: Option<String>, symbol: Option<String>) -> Self {
|
||||
Self { raw, namespace, symbol }
|
||||
}
|
||||
}
|
||||
|
||||
/// A node in the source property tree (call graph).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SourceNode {
|
||||
pub id: NodeId,
|
||||
pub parent: Option<NodeId>,
|
||||
pub children: HashMap<String, NodeId>,
|
||||
pub frame: Frame,
|
||||
pub inclusive_weight: u64,
|
||||
pub exclusive_weight: u64,
|
||||
}
|
||||
|
||||
impl SourceNode {
|
||||
pub fn new(id: NodeId, parent: Option<NodeId>, frame: Frame) -> Self {
|
||||
Self {
|
||||
id,
|
||||
parent,
|
||||
children: HashMap::new(),
|
||||
frame,
|
||||
inclusive_weight: 0,
|
||||
exclusive_weight: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the canonical internal graph built from the profile lines.
|
||||
#[derive(Debug)]
|
||||
pub struct SourceGraph {
|
||||
pub nodes: Vec<SourceNode>,
|
||||
pub root: NodeId,
|
||||
}
|
||||
|
||||
impl SourceGraph {
|
||||
pub fn new() -> Self {
|
||||
let root_frame = Frame::new("[root]".to_string(), None, None);
|
||||
let root_node = SourceNode::new(NodeId(0), None, root_frame);
|
||||
Self {
|
||||
nodes: vec![root_node],
|
||||
root: NodeId(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_node(&self, id: NodeId) -> Option<&SourceNode> {
|
||||
self.nodes.get(id.0)
|
||||
}
|
||||
|
||||
pub fn get_node_mut(&mut self, id: NodeId) -> Option<&mut SourceNode> {
|
||||
self.nodes.get_mut(id.0)
|
||||
}
|
||||
|
||||
pub fn add_node(&mut self, parent: NodeId, frame: Frame) -> NodeId {
|
||||
let id = NodeId(self.nodes.len());
|
||||
let raw_label = frame.raw.clone();
|
||||
let node = SourceNode::new(id, Some(parent), frame);
|
||||
self.nodes.push(node);
|
||||
|
||||
if let Some(parent_node) = self.get_node_mut(parent) {
|
||||
parent_node.children.insert(raw_label, id);
|
||||
}
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SourceGraph {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
103
src/parser.rs
Normal file
103
src/parser.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use crate::model::Frame;
|
||||
|
||||
/// Parses a single line of profile output into an ordered list of raw frame strings and a terminal weight.
|
||||
pub fn parse_line(line: &str) -> Option<(Vec<String>, u64)> {
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Find the last whitespace boundary
|
||||
let last_space_idx = line.rfind(char::is_whitespace)?;
|
||||
|
||||
let (path_str, weight_str) = line.split_at(last_space_idx);
|
||||
let weight_str = weight_str.trim();
|
||||
|
||||
// If the weight isn't a valid number, fail parsing this line
|
||||
let weight: u64 = weight_str.parse().ok()?;
|
||||
|
||||
let frames: Vec<String> = path_str
|
||||
.split(';')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
|
||||
Some((frames, weight))
|
||||
}
|
||||
|
||||
/// Parses a raw frame string into its namespace and symbol components, if present.
|
||||
pub fn parse_frame(raw: &str) -> Frame {
|
||||
let raw = raw.trim();
|
||||
|
||||
// Look for backtick separator (namespace`symbol)
|
||||
if let Some((ns, sym)) = raw.split_once('`') {
|
||||
return Frame::new(
|
||||
raw.to_string(),
|
||||
Some(ns.trim().to_string()),
|
||||
Some(sym.trim().to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
// If no backtick, try to fall back to C++ style namespace ::
|
||||
if let Some(last_colon) = raw.rfind("::") {
|
||||
// Special case: ignore operator::
|
||||
if !raw[..last_colon].ends_with("operator") {
|
||||
let ns = &raw[..last_colon];
|
||||
let sym = &raw[last_colon + 2..];
|
||||
return Frame::new(
|
||||
raw.to_string(),
|
||||
Some(ns.trim().to_string()),
|
||||
Some(sym.trim().to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: no specific namespace extracted
|
||||
Frame::new(raw.to_string(), None, Some(raw.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_line_simple() {
|
||||
let line = "thread with id 2548062;dyld`start;mapping`main; 6212";
|
||||
let (frames, weight) = parse_line(line).unwrap();
|
||||
assert_eq!(weight, 6212);
|
||||
assert_eq!(
|
||||
frames,
|
||||
vec!["thread with id 2548062", "dyld`start", "mapping`main"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_line_invalid_weight() {
|
||||
let line = "dyld`start;main not_a_weight";
|
||||
assert!(parse_line(line).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_frame_backtick() {
|
||||
let frame = parse_frame("mapping`mfem::GMRESSolver::Mult");
|
||||
assert_eq!(frame.raw, "mapping`mfem::GMRESSolver::Mult");
|
||||
assert_eq!(frame.namespace.as_deref(), Some("mapping"));
|
||||
assert_eq!(frame.symbol.as_deref(), Some("mfem::GMRESSolver::Mult"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_frame_colons() {
|
||||
let frame = parse_frame("mfem::CGSolver::Mult");
|
||||
assert_eq!(frame.raw, "mfem::CGSolver::Mult");
|
||||
assert_eq!(frame.namespace.as_deref(), Some("mfem::CGSolver"));
|
||||
assert_eq!(frame.symbol.as_deref(), Some("Mult"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_frame_fallback() {
|
||||
let frame = parse_frame("thread with id 2548062");
|
||||
assert_eq!(frame.raw, "thread with id 2548062");
|
||||
assert_eq!(frame.namespace, None);
|
||||
assert_eq!(frame.symbol.as_deref(), Some("thread with id 2548062"));
|
||||
}
|
||||
}
|
||||
77
src/report.rs
Normal file
77
src/report.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use crate::filter::ReportGraph;
|
||||
use crate::parser::parse_frame;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Aggregates all visible nodes by their core symbol.
|
||||
pub fn report_flat_symbol(graph: &ReportGraph) -> Vec<(String, u64)> {
|
||||
let mut map: HashMap<String, u64> = HashMap::new();
|
||||
|
||||
// We traverse all nodes. For a flat profile, we sum exclusive weights.
|
||||
// Assuming root has some children. Root itself has no exclusive.
|
||||
for node in &graph.nodes {
|
||||
if node.display_label == "[root]" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let frame = parse_frame(&node.display_label);
|
||||
let symbol = frame.symbol.unwrap_or(node.display_label.clone());
|
||||
|
||||
*map.entry(symbol).or_insert(0) += node.exclusive_weight;
|
||||
}
|
||||
|
||||
let mut result: Vec<_> = map.into_iter().collect();
|
||||
// Sort descending by weight
|
||||
result.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
result
|
||||
}
|
||||
|
||||
/// Aggregates all visible nodes by their namespace.
|
||||
pub fn report_namespace(graph: &ReportGraph) -> Vec<(String, u64)> {
|
||||
let mut map: HashMap<String, u64> = HashMap::new();
|
||||
|
||||
for node in &graph.nodes {
|
||||
if node.display_label == "[root]" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let frame = parse_frame(&node.display_label);
|
||||
let ns = frame.namespace.unwrap_or("[no_namespace]".to_string());
|
||||
|
||||
*map.entry(ns).or_insert(0) += node.exclusive_weight;
|
||||
}
|
||||
|
||||
let mut result: Vec<_> = map.into_iter().collect();
|
||||
// Sort descending by weight
|
||||
result.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
result
|
||||
}
|
||||
|
||||
/// Generates a hierarchical tree report.
|
||||
pub fn report_tree(graph: &ReportGraph) -> Vec<String> {
|
||||
let mut lines = Vec::new();
|
||||
let root = &graph.nodes[graph.root];
|
||||
|
||||
for child_id in root.children.values() {
|
||||
build_tree_lines(graph, *child_id, 0, &mut lines);
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
|
||||
fn build_tree_lines(graph: &ReportGraph, id: usize, depth: usize, lines: &mut Vec<String>) {
|
||||
let node = &graph.nodes[id];
|
||||
let indent = " ".repeat(depth);
|
||||
lines.push(format!("{}{}: {} (inclusive), {} (exclusive)", indent, node.display_label, node.inclusive_weight, node.exclusive_weight));
|
||||
|
||||
// Sort children by inclusive weight for deterministic output
|
||||
let mut children: Vec<_> = node.children.values().copied().collect();
|
||||
children.sort_by(|&a, &b| {
|
||||
let weight_a = graph.nodes[a].inclusive_weight;
|
||||
let weight_b = graph.nodes[b].inclusive_weight;
|
||||
weight_b.cmp(&weight_a)
|
||||
});
|
||||
|
||||
for child_id in children {
|
||||
build_tree_lines(graph, child_id, depth + 1, lines);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user