profile-inspect 0.1.2

Analyze V8 CPU and heap profiles from Node.js/Chrome DevTools
Documentation
use std::fs::File;
use std::io::BufWriter;
use std::path::PathBuf;

use bpaf::Bpaf;

use profile_inspect::analysis::CpuAnalyzer;
use profile_inspect::classify::FrameClassifier;
use profile_inspect::output::{OutputFormat, get_formatter};
use profile_inspect::parser::CpuProfileParser;

use super::common::{AnalysisOptions, OutputOptions, parse_categories};
use super::common::{analysis_options, output_options};

/// Analyze a CPU profile
#[derive(Debug, Clone, Bpaf)]
#[bpaf(command("cpu"))]
pub struct CpuCommand {
    #[bpaf(external(output_options))]
    pub output_options: OutputOptions,

    /// Filter by category (app, deps, node, v8, native)
    #[bpaf(long)]
    pub filter: Option<String>,

    #[bpaf(external(analysis_options))]
    pub analysis_options: AnalysisOptions,

    /// Focus on functions matching pattern (regex)
    #[bpaf(long)]
    pub focus: Option<String>,

    /// Exclude functions matching pattern (regex)
    #[bpaf(long)]
    pub exclude: Option<String>,

    /// Directory containing source maps
    #[bpaf(long)]
    pub sourcemap_dir: Option<PathBuf>,

    /// Focus analysis on a specific npm package
    #[bpaf(long, argument("NAME"))]
    pub package: Option<String>,

    /// Path to .cpuprofile file
    #[bpaf(positional("INPUT"))]
    pub input: PathBuf,
}

impl CpuCommand {
    pub fn run(self) -> Result<(), Box<dyn std::error::Error>> {
        // Parse profile
        let classifier = FrameClassifier::default();
        let parser = CpuProfileParser::new(classifier);
        let mut profile = parser.parse_file(&self.input)?;

        // Resolve sourcemaps if directory provided
        if let Some(ref dir) = self.sourcemap_dir {
            eprintln!("Resolving sourcemaps from: {}", dir.display());
            let resolved = profile.resolve_sourcemaps(vec![dir.clone()]);
            if resolved > 0 {
                eprintln!("  Resolved {} frames via sourcemaps", resolved);
            } else {
                eprintln!("  No frames resolved (sourcemap not found or no matches)");
            }
        }

        // Build analyzer with options
        let mut analyzer = CpuAnalyzer::new()
            .include_internals(self.analysis_options.include_internals)
            .min_percent(self.analysis_options.min_percent)
            .top_n(self.analysis_options.top);

        // Apply category filter
        if let Some(filter_str) = &self.filter {
            let categories = parse_categories(filter_str);
            analyzer = analyzer.filter_categories(categories);
        }

        // Apply package filter
        if let Some(pkg) = &self.package {
            analyzer = analyzer.filter_package(pkg.clone());
        }

        // Apply focus pattern
        if let Some(pattern) = &self.focus {
            let regex =
                regex::Regex::new(pattern).map_err(|e| format!("Invalid focus pattern: {e}"))?;
            analyzer = analyzer.focus(regex);
        }

        // Apply exclude pattern
        if let Some(pattern) = &self.exclude {
            let regex =
                regex::Regex::new(pattern).map_err(|e| format!("Invalid exclude pattern: {e}"))?;
            analyzer = analyzer.exclude(regex);
        }

        // Run analysis
        let analysis = analyzer.analyze(&profile);

        // Output to each format
        for fmt_str in &self.output_options.formats() {
            let format = OutputFormat::from_str(fmt_str)
                .ok_or_else(|| format!("Unknown format: {fmt_str}"))?;

            let formatter = get_formatter(format);

            // Determine output path
            let output_path = if let Some(dir) = &self.output_options.output {
                std::fs::create_dir_all(dir)?;
                Some(dir.join(format.default_filename()))
            } else {
                None
            };

            // Write output
            if let Some(path) = output_path {
                let file = File::create(&path)?;
                let mut writer = BufWriter::new(file);
                formatter.write_cpu_analysis(&profile, &analysis, &mut writer)?;
                eprintln!("Wrote: {}", path.display());
            } else {
                let stdout = std::io::stdout();
                let mut writer = stdout.lock();
                formatter.write_cpu_analysis(&profile, &analysis, &mut writer)?;
            }
        }

        Ok(())
    }
}