tauri-plugin-profiling 0.1.0

Tauri plugin for CPU profiling with flamegraph generation
Documentation
//! CPU profiling plugin for Tauri v2.
//!
//! Provides sampling-based CPU profiling with flamegraph generation.
//!
//! # Example
//!
//! ```rust,ignore
//! use tauri_plugin_profiling::ProfilingExt;
//!
//! tauri::Builder::default()
//!     .plugin(tauri_plugin_profiling::init())
//!     .setup(|app| {
//!         app.start_cpu_profile()?;
//!         // ... work ...
//!         let result = app.stop_cpu_profile()?;
//!         println!("Flamegraph: {:?}", result.flamegraph_path);
//!         Ok(())
//!     });
//! ```
//!
//! # Platform Support
//!
//! - **macOS/Linux**: Uses pprof-rs with SIGPROF-based sampling
//! - **Windows**: Uses SuspendThread + StackWalk64

mod commands;
mod config;
mod error;
mod profiler;

pub use config::{ProfileResult, ProfilingConfig, StartOptions};
pub use error::{Error, Result};

use commands::ProfilingState;
use tauri::{
    Manager, Runtime,
    plugin::{Builder, TauriPlugin},
};

/// Extension trait for CPU profiling on Tauri app handles and windows.
pub trait ProfilingExt<R: Runtime> {
    /// Start CPU profiling with default options.
    fn start_cpu_profile(&self) -> Result<()>;

    /// Start CPU profiling with custom options.
    fn start_cpu_profile_with_options(&self, options: StartOptions) -> Result<()>;

    /// Stop profiling and generate a flamegraph.
    fn stop_cpu_profile(&self) -> Result<ProfileResult>;

    /// Check if profiling is currently active.
    fn is_profiling(&self) -> Result<bool>;
}

impl<R: Runtime, T: Manager<R>> ProfilingExt<R> for T {
    fn start_cpu_profile(&self) -> Result<()> {
        self.start_cpu_profile_with_options(StartOptions::default())
    }

    fn start_cpu_profile_with_options(&self, options: StartOptions) -> Result<()> {
        let state = self.state::<ProfilingState>();
        let mut session_lock = state.session.lock().map_err(|_| Error::LockPoisoned)?;

        if session_lock.is_some() {
            return Err(Error::AlreadyRunning);
        }

        let frequency = options.frequency.unwrap_or(state.config.frequency);

        // Determine output directory
        let app = self.app_handle();
        let output_dir = state
            .config
            .output_dir
            .clone()
            .or_else(|| app.path().app_data_dir().ok().map(|p| p.join("profiles")))
            .ok_or_else(|| Error::PathResolution("Could not determine output directory".into()))?;

        std::fs::create_dir_all(&output_dir)?;

        let filename_prefix = options
            .filename
            .unwrap_or_else(|| state.config.filename_prefix.clone());
        let output_path = profiler::generate_output_path(&output_dir, &filename_prefix);

        let session = profiler::ProfileSession::start(frequency, output_path)?;
        *session_lock = Some(session);

        Ok(())
    }

    fn stop_cpu_profile(&self) -> Result<ProfileResult> {
        let state = self.state::<ProfilingState>();
        let mut session_lock = state.session.lock().map_err(|_| Error::LockPoisoned)?;

        let session = session_lock.take().ok_or(Error::NotRunning)?;
        session.stop()
    }

    fn is_profiling(&self) -> Result<bool> {
        let state = self.state::<ProfilingState>();
        let session_lock = state.session.lock().map_err(|_| Error::LockPoisoned)?;
        Ok(session_lock.is_some())
    }
}

/// Initialize the plugin with default configuration (100Hz sampling).
pub fn init<R: Runtime>() -> TauriPlugin<R> {
    Builder::new("profiling")
        .setup(|app, _api| {
            app.manage(ProfilingState::new(ProfilingConfig::default()));
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![
            commands::start_cpu_profile,
            commands::stop_cpu_profile,
            commands::is_profiling,
            commands::is_profiling_supported,
        ])
        .build()
}

/// Initialize the plugin with custom configuration.
pub fn init_with_config<R: Runtime>(config: ProfilingConfig) -> TauriPlugin<R> {
    Builder::new("profiling")
        .setup(move |app, _api| {
            app.manage(ProfilingState::new(config.clone()));
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![
            commands::start_cpu_profile,
            commands::stop_cpu_profile,
            commands::is_profiling,
            commands::is_profiling_supported,
        ])
        .build()
}