Skip to content
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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ members = [
]

[workspace.package]
version = "0.1.11"
version = "0.1.12"
edition = "2021"
authors = ["CreativesOnchain"]
license = "MIT"
Expand All @@ -17,7 +17,7 @@ keywords = ["arbitrum", "stylus", "profiling", "gas", "flamegraph"]
categories = ["development-tools::profiling", "wasm"]

[workspace.dependencies]
stylus-trace-core = { version = "0.1.11", path = "crates/stylus-trace-core" }
stylus-trace-core = { version = "0.1.12", path = "crates/stylus-trace-core" }
clap = { version = "4.5", features = ["derive", "env"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Expand Down
29 changes: 17 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ Stylus Trace turns opaque Stylus transaction traces into **interactive flamegrap

## 🚀 Key Features

- **Interactive Flamegraphs**: Visualize execution paths with interactive SVG snapshots.
- **Interactive Web Viewer**: Explore transactions in a high-intensity "Cyber Diagnostics" terminal with real-time symbol search and magnitude-sorted deltas.
- **Optimization Insights**: Get qualitative feedback on loop redundancies, high-cost storage access, and potential caching opportunities.
- **Gas & Ink Analysis**: Seamlessly toggle between standard Gas and high-precision Stylus Ink (10,000x) units.
- **Transaction Dashboards**: Get a "hot path" summary directly in your terminal.
- **Side-by-Side Diffing**: Compare two profiles visually to hunt down regressions or verify optimizations.
- **Automated Artifacts**: Built-in organization for profiles and graphs in a dedicated `artifacts/` folder.
- **Arbitrum Native**: Designed specifically for the Arbitrum Nitro/Stylus execution environment.

Expand Down Expand Up @@ -81,23 +81,20 @@ stylus-trace capture \
--flamegraph <anything.svg> \
--summary

OR

# This will generate output as profile.json but no flamegraph in artifacts directory
stylus-trace capture --tx <TX_HASH> --summary
# Profile and immediately launch the Cyber Viewer
stylus-trace capture --tx <TX_HASH> --view

OR
# this will generate output as profile.json and flamegraph.svg in artifacts directory
stylus-trace capture \
--tx <TX_HASH> \
--flamegraph \
--summary

# Generate output as profile.json and interactive flamegraph
stylus-trace capture --tx <TX_HASH> --flamegraph --summary

```

**What happens?**
- `artifacts/profile.json`: A detailed data structure of your transaction.
- `artifacts/flamegraph.svg`: An interactive SVG you can open in any browser.
- `artifacts/viewer.html`: A self-contained, high-performance web viewer.
- `artifacts/flamegraph.svg`: A classic interactive SVG snapshot.
- **Terminal Output**: A high-level summary of the hottest paths.

---
Expand Down Expand Up @@ -139,6 +136,14 @@ This feature will be enabled automatically once upstream tracer support is avail
| `--summary` | Print human-readable summary to terminal | `true` |
| `--output` | Path to write the diff report JSON | `artifacts/diff/diff_report.json` |
| `--flamegraph` | Path to write visual diff flamegraph SVG | `artifacts/diff/diff.svg` |
| `--view` | Open the interactive comparison viewer | `false` |

### `view`

| Flag | Description | Default |
|------------|---------------------------------------------|---------|
| `--target` | **(Required)** Path to profile JSON to view | - |
| `--baseline` | Optional baseline profile JSON for comparison| - |

### `ci init`
| Flag | Description | Default |
Expand Down
55 changes: 55 additions & 0 deletions bin/stylus-trace-studio/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
use anyhow::{Context, Result};
use clap::{Args, Parser, Subcommand};
use env_logger::Env;
use log::info;
use std::path::PathBuf;

use stylus_trace_core::commands::{
display_schema, display_version, execute_capture, validate_args, validate_profile_file,
CaptureArgs,
};
use stylus_trace_core::flamegraph::FlamegraphConfig;
use stylus_trace_core::output::json::read_profile;
use stylus_trace_core::output::viewer::{generate_viewer, open_browser};

/// Stylus Trace Studio - Performance profiling for Arbitrum Stylus
#[derive(Parser, Debug)]
Expand Down Expand Up @@ -88,11 +91,26 @@ pub enum Commands {
/// Specific HostIO calls increase threshold percentage
#[arg(long = "hostio-threshold")]
hostio_threshold: Option<f64>,

/// Open interactive web viewer
#[arg(long)]
view: bool,
},

/// Compare two transaction profiles and detect regressions
Diff(DiffSubArgs),

/// Open a previously captured profile in the web viewer
View {
/// Transaction hash or path to profile JSON
#[arg(short, long)]
tx: String,

/// RPC endpoint URL (optional, used if fetching new trace)
#[arg(short, long, default_value = "http://localhost:8547")]
rpc: String,
},

/// Validate a profile JSON file
Validate {
/// Path to profile JSON file
Expand Down Expand Up @@ -152,6 +170,10 @@ pub struct DiffSubArgs {
/// Path to write the visual diff flamegraph SVG
#[arg(short = 'f', long, default_missing_value = "diff.svg", num_args = 0..=1)]
pub flamegraph: Option<PathBuf>,

/// Open interactive side-by-side web viewer
#[arg(long)]
pub view: bool,
}

fn main() -> Result<()> {
Expand All @@ -161,6 +183,7 @@ fn main() -> Result<()> {
match cli.command {
Commands::Capture { .. } => handle_capture(cli.command)?,
Commands::Diff(ref args) => handle_diff(args)?,
Commands::View { ref tx, ref rpc } => handle_view(tx, rpc)?,
Commands::Validate { file } => {
validate_profile_file(file).context("Failed to validate profile")?
}
Expand Down Expand Up @@ -251,6 +274,7 @@ fn handle_capture(command: Commands) -> Result<()> {
threshold_percent,
gas_threshold,
hostio_threshold,
view,
} = command
{
// Enforce artifacts/ directory for relative paths
Expand Down Expand Up @@ -287,6 +311,7 @@ fn handle_capture(command: Commands) -> Result<()> {
gas_threshold,
hostio_threshold,
wasm: None,
view,
};

validate_args(&args).context("Invalid capture arguments")?;
Expand Down Expand Up @@ -314,13 +339,43 @@ fn handle_diff(args: &DiffSubArgs) -> Result<()> {
.map(|p| resolve_artifact_path(p.clone(), "diff")),
gas_threshold: args.gas_threshold,
hostio_threshold: args.hostio_threshold,
view: args.view,
};

stylus_trace_core::commands::diff::execute_diff(studio_args)
.context("Diff execution failed")?;
Ok(())
}

/// Handle the view command logic
fn handle_view(tx_or_path: &str, rpc: &str) -> Result<()> {
let path = PathBuf::from(tx_or_path);

// Check if it's an existing JSON file
if path.exists() && path.extension().is_some_and(|ext| ext == "json") {
info!("Opening existing profile: {}", path.display());
let profile = read_profile(&path).context("Failed to read profile JSON")?;
let viewer_path = path.with_extension("html");
generate_viewer(&profile, &viewer_path)?;
open_browser(&viewer_path)?;
} else if tx_or_path.starts_with("0x") && tx_or_path.len() == 66 {
info!("Capturing and viewing transaction: {}", tx_or_path);
let output = resolve_artifact_path(PathBuf::from("profile.json"), "capture");
let args = CaptureArgs {
rpc_url: rpc.to_string(),
transaction_hash: tx_or_path.to_string(),
output_json: output,
view: true,
..Default::default()
};
execute_capture(args).context("Capture and view failed")?;
} else {
anyhow::bail!("Invalid input: provide a path to a .json profile or a 0x transaction hash");
}

Ok(())
}

/// Resolves a path to the artifacts/<category> directory if it's a simple filename
fn resolve_artifact_path(path: PathBuf, category: &str) -> PathBuf {
if path
Expand Down
16 changes: 15 additions & 1 deletion crates/stylus-trace-core/src/commands/capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ pub fn execute_capture(args: CaptureArgs) -> Result<()> {
&args,
&parsed_trace,
&stacks,
hot_paths,
hot_paths.clone(),
mapper.as_ref(),
svg_content,
)?;
Expand Down Expand Up @@ -182,6 +182,20 @@ pub fn execute_capture(args: CaptureArgs) -> Result<()> {
print_transaction_summary(&args, &parsed_trace, &stacks, mapper.as_ref());
}

if args.view {
info!("Generating interactive web viewer...");
let viewer_path = args.output_json.with_extension("html");
let profile = to_profile(
&parsed_trace,
hot_paths,
Some(stacks.to_vec()),
mapper.as_ref(),
);
crate::output::viewer::generate_viewer(&profile, &viewer_path)?;
info!("✓ Viewer generated at: {}", viewer_path.display());
crate::output::viewer::open_browser(&viewer_path)?;
}

info!(
"Capture completed in {:.2}s",
start_time.elapsed().as_secs_f64()
Expand Down
20 changes: 20 additions & 0 deletions crates/stylus-trace-core/src/commands/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::output::json::read_profile;
use crate::parser::schema::Profile;
use anyhow::{Context, Result};
use colored::*;
use log::info;
use std::fs;

/// Execute the diff command
Expand Down Expand Up @@ -123,6 +124,25 @@ pub fn execute_diff(args: DiffArgs) -> Result<()> {
println!("{}", render_terminal_diff(&report));
}

if args.view {
info!("Generating interactive side-by-side diff viewer...");
let viewer_path = args
.output
.clone()
.unwrap_or_else(|| args.target.with_extension("diff.html"))
.with_extension("html");

let report_json = serde_json::to_value(&report)?;
crate::output::viewer::generate_diff_viewer(
&baseline,
&target,
&report_json,
&viewer_path,
)?;
info!("✓ Diff viewer generated at: {}", viewer_path.display());
crate::output::viewer::open_browser(&viewer_path)?;
}

// Step 7: Final Status Exit Code Handling (implicit)
if report.summary.status == "FAILED" {
return Err(anyhow::anyhow!("Regression detected against thresholds"));
Expand Down
8 changes: 8 additions & 0 deletions crates/stylus-trace-core/src/commands/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ pub struct CaptureArgs {

/// Path to WASM binary (optional)
pub wasm: Option<PathBuf>,

/// Open interactive web viewer
pub view: bool,
}

impl Default for CaptureArgs {
Expand All @@ -67,6 +70,7 @@ impl Default for CaptureArgs {
threshold_percent: None,
gas_threshold: None,
hostio_threshold: None,
view: false,
}
}
}
Expand Down Expand Up @@ -125,6 +129,9 @@ pub struct DiffArgs {

/// Path to write the visual diff flamegraph SVG
pub output_svg: Option<PathBuf>,

/// Open interactive web viewer
pub view: bool,
}

impl Default for DiffArgs {
Expand All @@ -139,6 +146,7 @@ impl Default for DiffArgs {
summary: true,
output: None,
output_svg: None,
view: false,
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions crates/stylus-trace-core/src/output/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@

pub mod json;
pub mod svg;
pub mod viewer;

// Re-export main functions
pub use json::{read_profile, write_profile};
pub use svg::write_svg;
pub use viewer::{generate_diff_viewer, generate_viewer, open_browser};

use crate::utils::error::OutputError;
use std::path::Path;
Expand Down
Loading
Loading