hotpath 0.15.0

Simple async Rust profiler with memory and data-flow insights - quickly find and debug performance bottlenecks.
Documentation
use std::collections::HashMap;

use crate::json::JsonFunctionEntry;
use crate::json::JsonFunctionsList;
use crate::lib_on::functions::FunctionStatsConfig;
use crate::output::{
    format_bytes, format_count, format_duration, format_percentile_key, ProfilingMode,
};

use super::state::FunctionStats;

pub(crate) fn build_functions_list_alloc(
    stats: &HashMap<u32, FunctionStats>,
    config: &FunctionStatsConfig,
    current_elapsed_ns: u64,
) -> JsonFunctionsList {
    use crate::lib_on::functions::alloc::guard::{AllocMetric, ALLOC_METRIC};

    let exclude_wrapper = *crate::functions::EXCLUDE_WRAPPER;
    let use_count = *ALLOC_METRIC == AllocMetric::Count;

    let bytes_cache: HashMap<u32, u64> = stats
        .iter()
        .filter(|(_, s)| s.has_data)
        .map(|(&id, s)| (id, s.total_bytes()))
        .collect();

    let count_cache: HashMap<u32, u64> = stats
        .iter()
        .filter(|(_, s)| s.has_data)
        .map(|(&id, s)| (id, s.total_count()))
        .collect();

    let primary_cache = if use_count {
        &count_cache
    } else {
        &bytes_cache
    };

    let grand_total: u64 = if *crate::functions::EXCLUDE_WRAPPER {
        stats
            .values()
            .filter(|s| !s.wrapper && s.has_data)
            .map(|s| primary_cache.get(&s.id).copied().unwrap_or(0))
            .sum()
    } else if *super::guard::ALLOC_CUMULATIVE {
        let wrapper_total = stats
            .values()
            .find(|s| s.wrapper && s.has_data)
            .map(|s| primary_cache.get(&s.id).copied().unwrap_or(0));

        wrapper_total.unwrap_or_else(|| {
            stats
                .values()
                .filter(|s| s.has_data)
                .map(|s| primary_cache.get(&s.id).copied().unwrap_or(0))
                .sum()
        })
    } else {
        stats
            .values()
            .filter(|s| s.has_data)
            .map(|s| primary_cache.get(&s.id).copied().unwrap_or(0))
            .sum()
    };

    let mut entries: Vec<_> = stats
        .values()
        .filter(|s| s.has_data && !(exclude_wrapper && s.wrapper))
        .collect();

    entries.sort_by(|a, b| {
        let a_primary = primary_cache.get(&a.id).copied().unwrap_or(0);
        let b_primary = primary_cache.get(&b.id).copied().unwrap_or(0);
        b_primary.cmp(&a_primary).then_with(|| a.name.cmp(b.name))
    });

    let total_count = entries.len();
    let displayed_count = if config.limit > 0 && config.limit < total_count {
        config.limit
    } else {
        total_count
    };

    if config.limit > 0 {
        entries.truncate(config.limit);
    }

    let format_alloc_value = |bytes: u64, count: u64| -> String {
        if use_count {
            format_count(count)
        } else {
            format_bytes(bytes)
        }
    };

    let profiling_mode = match *ALLOC_METRIC {
        AllocMetric::Bytes => ProfilingMode::AllocBytes,
        AllocMetric::Count => ProfilingMode::AllocCount,
    };

    let data: Vec<JsonFunctionEntry> = entries
        .into_iter()
        .map(|s| {
            let entry_bytes = bytes_cache.get(&s.id).copied().unwrap_or(0);
            let entry_count = count_cache.get(&s.id).copied().unwrap_or(0);
            let primary_total = if use_count { entry_count } else { entry_bytes };

            let percentage = if grand_total > 0 {
                (primary_total as f64 / grand_total as f64) * 100.0
            } else {
                0.0
            };

            let (avg, total, percent_total) = if s.is_async {
                ("N/A".to_string(), "N/A".to_string(), "N/A".to_string())
            } else {
                (
                    format_alloc_value(s.avg_bytes(), s.avg_count()),
                    format_alloc_value(entry_bytes, entry_count),
                    format!("{:.2}%", percentage),
                )
            };

            let mut percentiles = HashMap::new();
            for &p in &config.percentiles {
                if s.is_async {
                    percentiles.insert(format_percentile_key(p), "N/A".to_string());
                } else {
                    let bytes_total = s.bytes_total_percentile(p);
                    let count_total = s.count_total_percentile(p);
                    percentiles.insert(
                        format_percentile_key(p),
                        format_alloc_value(bytes_total, count_total),
                    );
                }
            }

            JsonFunctionEntry {
                id: s.id,
                name: s.name.to_string(),
                calls: s.count,
                avg,
                percentiles,
                total,
                percent_total,
            }
        })
        .collect();

    let description = {
        let metric = match *ALLOC_METRIC {
            AllocMetric::Bytes => "bytes",
            AllocMetric::Count => "count",
        };
        if *super::guard::ALLOC_CUMULATIVE {
            format!(
                "Cumulative allocation {} during each function call (including nested calls).",
                metric
            )
        } else {
            format!(
                "Exclusive allocation {} by each function (excluding nested calls).",
                metric
            )
        }
    };

    JsonFunctionsList {
        profiling_mode,
        time_elapsed: format_duration(current_elapsed_ns),
        total_elapsed_ns: current_elapsed_ns,
        total_allocated: match profiling_mode {
            ProfilingMode::AllocBytes => Some(format_bytes(grand_total)),
            ProfilingMode::AllocCount => Some(format_count(grand_total)),
            ProfilingMode::Timing => None,
        },
        description,
        caller_name: config.caller_name.to_string(),
        percentiles: config.percentiles.clone(),
        data,
        displayed_count,
        total_count,
    }
}

pub(crate) fn build_functions_list_timing(
    stats: &HashMap<u32, FunctionStats>,
    config: &FunctionStatsConfig,
    current_elapsed_ns: u64,
) -> JsonFunctionsList {
    let exclude_wrapper = *crate::functions::EXCLUDE_WRAPPER;

    let reference_total = if exclude_wrapper {
        stats
            .values()
            .filter(|s| !s.wrapper && s.has_data)
            .map(|s| s.total_duration_ns)
            .sum::<u64>()
    } else {
        let wrapper_total = stats
            .values()
            .find(|s| s.wrapper)
            .map(|s| s.total_duration_ns);
        wrapper_total.unwrap_or(config.total_elapsed.as_nanos() as u64)
    };

    let mut entries: Vec<_> = stats
        .values()
        .filter(|s| s.has_data && !(exclude_wrapper && s.wrapper))
        .collect();

    entries.sort_by(|a, b| {
        b.total_duration_ns
            .cmp(&a.total_duration_ns)
            .then_with(|| a.name.cmp(b.name))
    });

    let total_count = entries.len();
    let displayed_count = if config.limit > 0 && config.limit < total_count {
        config.limit
    } else {
        total_count
    };

    if config.limit > 0 {
        entries.truncate(config.limit);
    }

    let data: Vec<JsonFunctionEntry> = entries
        .into_iter()
        .map(|s| {
            let percentage = if reference_total > 0 {
                (s.total_duration_ns as f64 / reference_total as f64) * 100.0
            } else {
                0.0
            };

            let mut percentiles = HashMap::new();
            for &p in &config.percentiles {
                let duration_ns = s.duration_percentile(p);
                percentiles.insert(format_percentile_key(p), format_duration(duration_ns));
            }

            JsonFunctionEntry {
                id: s.id,
                name: s.name.to_string(),
                calls: s.count,
                avg: format_duration(s.avg_duration_ns()),
                percentiles,
                total: format_duration(s.total_duration_ns),
                percent_total: format!("{:.2}%", percentage),
            }
        })
        .collect();

    let total_elapsed_ns = config.total_elapsed.as_nanos() as u64;

    JsonFunctionsList {
        profiling_mode: ProfilingMode::Timing,
        time_elapsed: format_duration(total_elapsed_ns),
        total_elapsed_ns: current_elapsed_ns,
        total_allocated: None,
        description: "Function execution time metrics.".to_string(),
        caller_name: config.caller_name.to_string(),
        percentiles: config.percentiles.clone(),
        data,
        displayed_count,
        total_count,
    }
}