hotpath 0.15.0

Simple async Rust profiler with memory and data-flow insights - quickly find and debug performance bottlenecks.
Documentation
//! Value metrics - key-value logging for arbitrary values.

use std::fmt::Debug;
use std::sync::Arc;

use crate::instant::Instant;

use crate::channels::{extract_filename, START_TIME};
#[cfg(feature = "hotpath-mcp")]
use crate::debug::get_sorted_debug_val_entries;
use crate::debug::{
    get_or_create_val_id, init_debug_state, send_debug_event, DebugEvent, ValEntry,
};
use crate::json::{format_time_ago, JsonDebugEntry, JsonDebugLog, JsonDebugValLogs};
use crate::output::format_duration;

fn get_thread_id() -> Option<u64> {
    Some(crate::tid::current_tid())
}

pub struct ValHandle {
    id: u32,
    key: Arc<str>,
    source: &'static str,
}

#[cfg_attr(feature = "hotpath-meta", hotpath_meta::measure_all)]
impl ValHandle {
    #[inline]
    pub fn new(key: impl Into<Arc<str>>, source: &'static str) -> Self {
        init_debug_state();
        let key = key.into();
        let id = get_or_create_val_id(&key);
        Self { id, key, source }
    }

    #[inline]
    pub fn set<T: Debug>(&self, value: &T) {
        let value_str = crate::output::format_debug_truncated(value);
        let timestamp = Instant::now();
        let tid = get_thread_id();

        send_debug_event(DebugEvent::Val {
            id: self.id,
            key: self.key.clone(),
            source: self.source,
            value: value_str,
            timestamp,
            tid,
        });
    }
}

#[cfg(feature = "hotpath-mcp")]
#[cfg_attr(feature = "hotpath-meta", hotpath_meta::measure(log = true))]
pub(crate) fn get_debug_val_entries_json() -> Vec<JsonDebugEntry> {
    get_sorted_debug_val_entries()
        .iter()
        .map(JsonDebugEntry::from)
        .collect()
}

#[cfg_attr(feature = "hotpath-meta", hotpath_meta::measure(log = true))]
pub(crate) fn get_val_logs(id: u32) -> Option<JsonDebugValLogs> {
    let current_elapsed_ns = START_TIME
        .get()
        .map(|t| t.elapsed().as_nanos() as u64)
        .unwrap_or(0);

    crate::debug::get_debug_val_entries_by_id(id)
        .map(|s| JsonDebugValLogs::from_stats(&s, current_elapsed_ns))
}

fn truncate_source_path(source: &str) -> String {
    if let Some(colon_pos) = source.find(':') {
        let path_part = &source[..colon_pos];
        let suffix = &source[colon_pos..];
        format!("{}{}", extract_filename(path_part), suffix)
    } else {
        extract_filename(source)
    }
}

impl From<&ValEntry> for JsonDebugEntry {
    fn from(stats: &ValEntry) -> Self {
        let last_value = stats.logs.back().map(|e| e.value.clone());
        let (source, source_display) = stats
            .logs
            .back()
            .map(|e| (e.source.to_string(), truncate_source_path(e.source)))
            .unwrap_or_else(|| ("<unknown>".to_string(), "<unknown>".to_string()));
        JsonDebugEntry {
            id: stats.id,
            entry_type: crate::json::DebugEntryType::Val,
            source,
            source_display,
            expression: stats.key.to_string(),
            log_count: stats.log_count,
            last_value,
        }
    }
}

#[cfg_attr(feature = "hotpath-meta", hotpath_meta::measure_all)]
impl JsonDebugValLogs {
    pub(crate) fn from_stats(stats: &ValEntry, current_elapsed_ns: u64) -> Self {
        JsonDebugValLogs {
            key: stats.key.to_string(),
            total_logs: stats.log_count,
            logs: stats
                .logs
                .iter()
                .map(|e| JsonDebugLog {
                    index: e.index,
                    timestamp: format_duration(e.timestamp_ns),
                    ago: format_time_ago(current_elapsed_ns.saturating_sub(e.timestamp_ns)),
                    value: e.value.clone(),
                    thread_id: e.tid,
                    source: Some(truncate_source_path(e.source)),
                })
                .collect(),
        }
    }
}