profile-inspect 0.1.3

Analyze V8 CPU and heap profiles from Node.js/Chrome DevTools
Documentation
mod collapsed;
mod json;
mod markdown;
mod speedscope;
mod text;

pub use collapsed::*;
pub use json::*;
pub use markdown::*;
pub use speedscope::*;
pub use text::*;

use std::io::Write;

use thiserror::Error;

use crate::analysis::{CpuAnalysis, HeapAnalysis};
use crate::ir::ProfileIR;

/// Output format errors
#[derive(Debug, Error)]
pub enum OutputError {
    #[error("I/O error: {0}")]
    Io(#[from] std::io::Error),

    #[error("JSON serialization error: {0}")]
    Json(#[from] serde_json::Error),
}

/// Format time in microseconds to human-readable string.
/// - < 1000ms: "X.XX ms"
/// - 1000ms to 60s: "X.XX s"
/// - >= 60s: "X.XX min"
pub fn format_time_us(time_us: u64) -> String {
    let ms = time_us as f64 / 1000.0;
    format_time_ms(ms)
}

/// Format time in milliseconds to human-readable string.
pub fn format_time_ms(ms: f64) -> String {
    if ms < 1000.0 {
        format!("{ms:.2} ms")
    } else if ms < 60_000.0 {
        format!("{:.2} s", ms / 1000.0)
    } else {
        format!("{:.2} min", ms / 60_000.0)
    }
}

/// Available output formats
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputFormat {
    /// Markdown (default, LLM-optimized)
    Markdown,
    /// Plain text with ASCII tables
    Text,
    /// JSON summary for CI
    Json,
    /// Speedscope JSON for visualization
    Speedscope,
    /// Collapsed stacks for flamegraph tools
    Collapsed,
}

impl OutputFormat {
    /// Parse format from string
    pub fn from_str(s: &str) -> Option<Self> {
        match s.to_lowercase().as_str() {
            "markdown" | "md" => Some(Self::Markdown),
            "text" | "txt" => Some(Self::Text),
            "json" => Some(Self::Json),
            "speedscope" => Some(Self::Speedscope),
            "collapsed" | "folded" => Some(Self::Collapsed),
            _ => None,
        }
    }

    /// Get the default file extension for this format
    pub fn extension(&self) -> &'static str {
        match self {
            Self::Markdown => "md",
            Self::Text => "txt",
            Self::Json => "json",
            Self::Speedscope => "speedscope.json",
            Self::Collapsed => "collapsed.txt",
        }
    }

    /// Get the default filename for this format
    pub fn default_filename(&self) -> &'static str {
        match self {
            Self::Markdown => "profile-analysis.md",
            Self::Text => "profile-analysis.txt",
            Self::Json => "profile-analysis.json",
            Self::Speedscope => "profile.speedscope.json",
            Self::Collapsed => "profile.collapsed.txt",
        }
    }
}

/// Trait for output formatters
pub trait Formatter {
    /// Write CPU analysis to output
    fn write_cpu_analysis(
        &self,
        profile: &ProfileIR,
        analysis: &CpuAnalysis,
        writer: &mut dyn Write,
    ) -> Result<(), OutputError>;

    /// Write heap analysis to output
    fn write_heap_analysis(
        &self,
        profile: &ProfileIR,
        analysis: &HeapAnalysis,
        writer: &mut dyn Write,
    ) -> Result<(), OutputError>;
}

/// Get a formatter for the given output format
pub fn get_formatter(format: OutputFormat) -> Box<dyn Formatter> {
    match format {
        OutputFormat::Markdown => Box::new(MarkdownFormatter),
        OutputFormat::Text => Box::new(TextFormatter),
        OutputFormat::Json => Box::new(JsonFormatter),
        OutputFormat::Speedscope => Box::new(SpeedscopeFormatter),
        OutputFormat::Collapsed => Box::new(CollapsedFormatter),
    }
}