use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Instant;
use tauri::{AppHandle, Manager, Runtime};
use tauri_plugin_profiling::ProfilingExt;
use tracing::span::Attributes;
use tracing::{Id, Span, Subscriber};
use tracing_subscriber::Layer;
use tracing_subscriber::layer::Context;
use tracing_subscriber::registry::LookupSpan;
pub use tauri_plugin_profiling::{
Error as ProfilingError, ProfileResult, ProfilingConfig, ProfilingExt as ProfilingExtBase,
Result as ProfilingResult, StartOptions, init as init_profiling,
init_with_config as init_profiling_with_config,
};
#[derive(Debug, Clone)]
pub struct SpanEvent {
pub name: String,
pub span_id: u64,
pub parent_id: u64,
pub event_type: SpanEventType,
pub timestamp_us: u64,
pub thread_id: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpanEventType {
Enter,
Exit,
Close,
}
#[derive(Debug)]
pub struct SpanTimingCapture {
events: Mutex<Vec<SpanEvent>>,
start_time: Mutex<Option<Instant>>,
capturing: AtomicBool,
next_id: AtomicU64,
}
impl SpanTimingCapture {
fn new() -> Self {
Self {
events: Mutex::new(Vec::new()),
start_time: Mutex::new(None),
capturing: AtomicBool::new(false),
next_id: AtomicU64::new(1),
}
}
pub fn start_capture(&self) {
if let Ok(mut events) = self.events.lock() {
events.clear();
}
if let Ok(mut start) = self.start_time.lock() {
*start = Some(Instant::now());
}
self.capturing.store(true, Ordering::SeqCst);
}
pub fn stop_capture(&self) -> Vec<SpanEvent> {
self.capturing.store(false, Ordering::SeqCst);
if let Ok(mut events) = self.events.lock() {
std::mem::take(&mut *events)
} else {
Vec::new()
}
}
pub fn is_capturing(&self) -> bool {
self.capturing.load(Ordering::SeqCst)
}
fn record_event(&self, name: String, span_id: u64, parent_id: u64, event_type: SpanEventType) {
if !self.capturing.load(Ordering::SeqCst) {
return;
}
let timestamp_us = if let Ok(start) = self.start_time.lock() {
start.map(|s| s.elapsed().as_micros() as u64).unwrap_or(0)
} else {
0
};
let thread_id = {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
std::thread::current().id().hash(&mut hasher);
hasher.finish()
};
let event = SpanEvent {
name,
span_id,
parent_id,
event_type,
timestamp_us,
thread_id,
};
if let Ok(mut events) = self.events.lock() {
events.push(event);
}
}
fn next_id(&self) -> u64 {
self.next_id.fetch_add(1, Ordering::SeqCst)
}
}
pub struct SpanTimingLayer {
capture: Arc<SpanTimingCapture>,
}
impl SpanTimingLayer {
pub fn new() -> (Self, Arc<SpanTimingCapture>) {
let capture = Arc::new(SpanTimingCapture::new());
(
Self {
capture: capture.clone(),
},
capture,
)
}
}
impl Default for SpanTimingLayer {
fn default() -> Self {
Self::new().0
}
}
struct SpanTimingId(u64);
impl<S> Layer<S> for SpanTimingLayer
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
let internal_id = self.capture.next_id();
if let Some(span) = ctx.span(id) {
span.extensions_mut().insert(SpanTimingId(internal_id));
}
let parent_id = attrs
.parent()
.and_then(|pid| ctx.span(pid))
.and_then(|span| span.extensions().get::<SpanTimingId>().map(|id| id.0))
.or_else(|| {
ctx.lookup_current()
.and_then(|span| span.extensions().get::<SpanTimingId>().map(|id| id.0))
})
.unwrap_or(0);
let name = format!("{}::{}", attrs.metadata().target(), attrs.metadata().name());
self.capture
.record_event(name, internal_id, parent_id, SpanEventType::Enter);
}
fn on_enter(&self, id: &Id, ctx: Context<'_, S>) {
if let Some(span) = ctx.span(id)
&& let Some(timing_id) = span.extensions().get::<SpanTimingId>()
{
let name = format!("{}::{}", span.metadata().target(), span.metadata().name());
self.capture
.record_event(name, timing_id.0, 0, SpanEventType::Enter);
}
}
fn on_exit(&self, id: &Id, ctx: Context<'_, S>) {
if let Some(span) = ctx.span(id)
&& let Some(timing_id) = span.extensions().get::<SpanTimingId>()
{
let name = format!("{}::{}", span.metadata().target(), span.metadata().name());
self.capture
.record_event(name, timing_id.0, 0, SpanEventType::Exit);
}
}
fn on_close(&self, id: Id, ctx: Context<'_, S>) {
if let Some(span) = ctx.span(&id)
&& let Some(timing_id) = span.extensions().get::<SpanTimingId>()
{
let name = format!("{}::{}", span.metadata().target(), span.metadata().name());
self.capture
.record_event(name, timing_id.0, 0, SpanEventType::Close);
}
}
}
#[derive(Debug, Clone)]
pub struct ActiveSpan {
pub name: String,
pub total_time_us: u64,
pub enter_count: u64,
pub percentage: f64,
}
#[derive(Debug)]
pub struct SpanCorrelationReport {
pub profile: ProfileResult,
pub duration_us: u64,
pub active_spans: Vec<ActiveSpan>,
pub events: Vec<SpanEvent>,
}
impl std::fmt::Display for SpanCorrelationReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "=== CPU Profile with Span Correlation ===")?;
writeln!(f, "Duration: {:.2}ms", self.duration_us as f64 / 1000.0)?;
writeln!(f, "Samples: {}", self.profile.sample_count)?;
writeln!(f, "Flamegraph: {}", self.profile.flamegraph_path.display())?;
writeln!(f)?;
writeln!(f, "Active Spans (by time):")?;
writeln!(f, "{:-<60}", "")?;
for span in &self.active_spans {
writeln!(
f,
"{:50} {:>6.1}% ({:.2}ms, {} entries)",
truncate_span_name(&span.name, 50),
span.percentage,
span.total_time_us as f64 / 1000.0,
span.enter_count
)?;
}
if self.active_spans.is_empty() {
writeln!(f, "(no spans recorded during profiling)")?;
}
Ok(())
}
}
fn truncate_span_name(name: &str, max_len: usize) -> String {
if name.len() <= max_len {
name.to_string()
} else {
format!("...{}", &name[name.len() - max_len + 3..])
}
}
fn analyze_span_events(events: &[SpanEvent], duration_us: u64) -> Vec<ActiveSpan> {
use std::collections::HashMap;
#[derive(Default)]
struct SpanStats {
total_time_us: u64,
enter_count: u64,
last_enter_time: Option<u64>,
}
let mut stats: HashMap<String, SpanStats> = HashMap::new();
for event in events {
let entry = stats.entry(event.name.clone()).or_default();
match event.event_type {
SpanEventType::Enter => {
entry.enter_count += 1;
entry.last_enter_time = Some(event.timestamp_us);
}
SpanEventType::Exit | SpanEventType::Close => {
if let Some(enter_time) = entry.last_enter_time.take() {
entry.total_time_us += event.timestamp_us.saturating_sub(enter_time);
}
}
}
}
for entry in stats.values_mut() {
if let Some(enter_time) = entry.last_enter_time.take() {
entry.total_time_us += duration_us.saturating_sub(enter_time);
}
}
let mut active_spans: Vec<_> = stats
.into_iter()
.filter(|(_, s)| s.total_time_us > 0 || s.enter_count > 0)
.map(|(name, s)| ActiveSpan {
name,
total_time_us: s.total_time_us,
enter_count: s.enter_count,
percentage: if duration_us > 0 {
(s.total_time_us as f64 / duration_us as f64) * 100.0
} else {
0.0
},
})
.collect();
active_spans.sort_by_key(|s| std::cmp::Reverse(s.total_time_us));
active_spans
}
struct ProfilingSpanGuard {
span: Mutex<Option<Span>>,
}
pub trait TracedProfilingExt<R: Runtime> {
fn start_cpu_profile_traced(&self) -> ProfilingResult<()>;
fn start_cpu_profile_traced_with_options(&self, options: StartOptions) -> ProfilingResult<()>;
fn stop_cpu_profile_traced(&self) -> ProfilingResult<ProfileResult>;
}
impl<R: Runtime, T: Manager<R>> TracedProfilingExt<R> for T {
fn start_cpu_profile_traced(&self) -> ProfilingResult<()> {
start_traced_impl(self.app_handle(), None)
}
fn start_cpu_profile_traced_with_options(&self, options: StartOptions) -> ProfilingResult<()> {
start_traced_impl(self.app_handle(), Some(options))
}
fn stop_cpu_profile_traced(&self) -> ProfilingResult<ProfileResult> {
let result = self.app_handle().stop_cpu_profile()?;
tracing::info!(
samples = result.sample_count,
duration_ms = result.duration_ms,
flamegraph = %result.flamegraph_path.display(),
"CPU profiling stopped"
);
if let Some(state) = self.app_handle().try_state::<ProfilingSpanGuard>()
&& let Ok(mut guard) = state.span.lock()
&& let Some(span) = guard.take()
{
drop(span);
}
Ok(result)
}
}
fn start_traced_impl<R: Runtime>(
app: &AppHandle<R>,
options: Option<StartOptions>,
) -> ProfilingResult<()> {
let frequency = options.as_ref().and_then(|o| o.frequency).unwrap_or(100);
let span = tracing::info_span!("cpu_profile", frequency = frequency);
tracing::info!(frequency = frequency, "CPU profiling started");
match options {
Some(opts) => app.start_cpu_profile_with_options(opts)?,
None => app.start_cpu_profile()?,
}
match app.try_state::<ProfilingSpanGuard>() {
Some(state) => {
if let Ok(mut guard) = state.span.lock() {
*guard = Some(span);
}
}
None => {
app.manage(ProfilingSpanGuard {
span: Mutex::new(Some(span)),
});
}
}
Ok(())
}
pub trait SpanAwareProfilingExt<R: Runtime> {
fn start_span_aware_profile(&self) -> ProfilingResult<()>;
fn start_span_aware_profile_with_options(&self, options: StartOptions) -> ProfilingResult<()>;
fn stop_span_aware_profile(&self) -> ProfilingResult<SpanCorrelationReport>;
}
impl<R: Runtime, T: Manager<R>> SpanAwareProfilingExt<R> for T {
fn start_span_aware_profile(&self) -> ProfilingResult<()> {
start_span_aware_impl(self.app_handle(), None)
}
fn start_span_aware_profile_with_options(&self, options: StartOptions) -> ProfilingResult<()> {
start_span_aware_impl(self.app_handle(), Some(options))
}
fn stop_span_aware_profile(&self) -> ProfilingResult<SpanCorrelationReport> {
let profile = self.app_handle().stop_cpu_profile()?;
let events = if let Some(capture) = self.app_handle().try_state::<Arc<SpanTimingCapture>>()
{
capture.stop_capture()
} else {
Vec::new()
};
let duration_us = profile.duration_ms * 1000;
let active_spans = analyze_span_events(&events, duration_us);
tracing::info!(
samples = profile.sample_count,
duration_ms = profile.duration_ms,
spans_recorded = events.len(),
active_spans = active_spans.len(),
flamegraph = %profile.flamegraph_path.display(),
"Span-aware CPU profiling stopped"
);
Ok(SpanCorrelationReport {
profile,
duration_us,
active_spans,
events,
})
}
}
fn start_span_aware_impl<R: Runtime>(
app: &AppHandle<R>,
options: Option<StartOptions>,
) -> ProfilingResult<()> {
if let Some(capture) = app.try_state::<Arc<SpanTimingCapture>>() {
capture.start_capture();
} else {
tracing::warn!(
"SpanTimingCapture not found in state - span correlation will be unavailable"
);
}
let frequency = options.as_ref().and_then(|o| o.frequency).unwrap_or(100);
tracing::info!(frequency = frequency, "Span-aware CPU profiling started");
match options {
Some(opts) => app.start_cpu_profile_with_options(opts)?,
None => app.start_cpu_profile()?,
}
Ok(())
}