hotpath 0.9.0

Simple Rust profiler with memory and async data-flow insights - quickly find and debug performance bottlenecks.
Documentation
use crate::output::{
    shorten_function_name, FunctionsDataJson, FunctionsJson, MetricType, MetricsProvider, Reporter,
};
use colored::*;
use prettytable::{color, Attr, Cell, Row, Table};
use std::collections::HashMap;
use std::time::Duration;

pub(crate) fn get_sorted_measurements(
    metrics_provider: &dyn MetricsProvider<'_>,
) -> Vec<(String, Vec<MetricType>)> {
    let metric_data = metrics_provider.metric_data();

    let mut sorted_entries: Vec<(String, Vec<MetricType>)> = metric_data.into_iter().collect();
    sorted_entries.sort_by(|(name_a, metrics_a), (name_b, metrics_b)| {
        let key_a = metrics_provider.sort_key(metrics_a);
        let key_b = metrics_provider.sort_key(metrics_b);

        key_b
            .partial_cmp(&key_a)
            .unwrap_or(std::cmp::Ordering::Equal)
            .then_with(|| name_a.cmp(name_b))
    });

    sorted_entries
}

pub(crate) fn display_table(metrics_provider: &dyn MetricsProvider<'_>) {
    let use_colors = std::env::var("NO_COLOR").is_err();

    let mut table = Table::new();

    let header_cells: Vec<Cell> = metrics_provider
        .headers()
        .into_iter()
        .map(|header| {
            if use_colors {
                Cell::new(&header)
                    .with_style(Attr::Bold)
                    .with_style(Attr::ForegroundColor(color::CYAN))
            } else {
                Cell::new(&header).with_style(Attr::Bold)
            }
        })
        .collect();

    table.add_row(Row::new(header_cells));

    let sorted_entries = get_sorted_measurements(metrics_provider);

    for (function_name, metrics) in sorted_entries {
        let mut row_cells = Vec::new();

        let short_name = shorten_function_name(&function_name);
        row_cells.push(Cell::new(&short_name));

        for metric in &metrics {
            row_cells.push(Cell::new(&metric.to_string()));
        }

        table.add_row(Row::new(row_cells));
    }

    println!(
        "{} {} - {}",
        "[hotpath]".blue().bold(),
        metrics_provider.profiling_mode(),
        metrics_provider.description()
    );

    let (displayed, total) = metrics_provider.entry_counts();
    if displayed < total {
        println!(
            "{}: {:.2?} ({}/{})",
            metrics_provider.caller_name().yellow().bold(),
            Duration::from_nanos(metrics_provider.total_elapsed()),
            displayed,
            total
        );
    } else {
        println!(
            "{}: {:.2?}",
            metrics_provider.caller_name().yellow().bold(),
            Duration::from_nanos(metrics_provider.total_elapsed()),
        );
    }

    table.printstd();

    if metrics_provider.has_unsupported_async() {
        println!();
        println!(
            "* {} for async methods is currently only available for tokio {} runtime.",
            "alloc profiling".yellow().bold(),
            "current_thread".green().bold()
        );
        println!(
            "  Please use {} to enable it.",
            "#[tokio::main(flavor = \"current_thread\")]".cyan().bold()
        );
    }
}

fn display_no_measurements_message(total_elapsed: Duration, caller_name: &str) {
    let title = format!(
        "\n{} No measurements recorded from {} (Total time: {:.2?})",
        "[hotpath]".blue().bold(),
        caller_name.yellow().bold(),
        total_elapsed
    );
    println!("{title}");
    println!();
    println!(
        "To start measuring performance, add the {} macro to your functions:",
        "#[hotpath::measure]".cyan().bold()
    );
    println!();
    println!(
        "  {}",
        "#[cfg_attr(feature = \"hotpath\", hotpath::measure)]".cyan()
    );
    println!("  {}", "fn your_function() {".dimmed());
    println!("  {}", "    // your code here".dimmed());
    println!("  {}", "}".dimmed());
    println!();
    println!(
        "Or use {} to measure code blocks:",
        "hotpath::measure_block!".cyan().bold()
    );
    println!();
    println!("  {}", "#[cfg(feature = \"hotpath\")]".cyan());
    println!("  {}", "hotpath::measure_block!(\"label\", {".cyan());
    println!("  {}", "    // your code here".dimmed());
    println!("  {}", "});".cyan());
    println!();
}

pub(crate) struct TableReporter;

impl Reporter for TableReporter {
    fn report(
        &self,
        metrics_provider: &dyn MetricsProvider<'_>,
    ) -> Result<(), Box<dyn std::error::Error>> {
        if metrics_provider.metric_data().is_empty() {
            display_no_measurements_message(
                Duration::from_nanos(metrics_provider.total_elapsed()),
                metrics_provider.caller_name(),
            );
            return Ok(());
        }

        display_table(metrics_provider);
        Ok(())
    }
}

pub(crate) struct JsonReporter;

impl Reporter for JsonReporter {
    fn report(
        &self,
        metrics_provider: &dyn MetricsProvider<'_>,
    ) -> Result<(), Box<dyn std::error::Error>> {
        if metrics_provider.metric_data().is_empty() {
            display_no_measurements_message(Duration::ZERO, metrics_provider.caller_name());
            return Ok(());
        }

        let json = FunctionsJson::from(metrics_provider);
        println!("{}", serde_json::to_string(&json).unwrap());
        Ok(())
    }
}

pub(crate) struct JsonPrettyReporter;

impl Reporter for JsonPrettyReporter {
    fn report(
        &self,
        metrics_provider: &dyn MetricsProvider<'_>,
    ) -> Result<(), Box<dyn std::error::Error>> {
        if metrics_provider.metric_data().is_empty() {
            display_no_measurements_message(Duration::ZERO, metrics_provider.caller_name());
            return Ok(());
        }

        let json = FunctionsJson::from(metrics_provider);
        println!("{}", serde_json::to_string_pretty(&json)?);
        Ok(())
    }
}

impl From<&dyn MetricsProvider<'_>> for FunctionsJson {
    fn from(metrics: &dyn MetricsProvider<'_>) -> Self {
        let hotpath_profiling_mode = metrics.profiling_mode();
        let percentiles = metrics.percentiles();

        let sorted_entries = get_sorted_measurements(metrics);
        let data: HashMap<String, Vec<MetricType>> = sorted_entries.into_iter().collect();

        Self {
            hotpath_profiling_mode,
            total_elapsed: metrics.total_elapsed(),
            description: metrics.description(),
            caller_name: metrics.caller_name().to_string(),
            percentiles,
            data: FunctionsDataJson(data),
        }
    }
}