use serde::ser::Serializer;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::sync::LazyLock;
const DEFAULT_MAX_LOG_LEN: usize = 1536;
pub static MAX_LOG_LEN: LazyLock<usize> = LazyLock::new(|| {
std::env::var("HOTPATH_MAX_LOG_LEN")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(DEFAULT_MAX_LOG_LEN)
});
const DEFAULT_FUNCTIONS_NAME_DEPTH: usize = 2;
pub(crate) static FUNCTIONS_NAME_DEPTH: LazyLock<usize> = LazyLock::new(|| {
std::env::var("HOTPATH_FUNCTIONS_NAME_DEPTH")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(DEFAULT_FUNCTIONS_NAME_DEPTH)
});
pub fn format_duration(ns: u64) -> String {
if ns < 1_000 {
format!("{} ns", ns)
} else if ns < 1_000_000 {
format!("{:.2} µs", ns as f64 / 1_000.0)
} else if ns < 1_000_000_000 {
format!("{:.2} ms", ns as f64 / 1_000_000.0)
} else {
format!("{:.2} s", ns as f64 / 1_000_000_000.0)
}
}
#[cfg(feature = "hotpath")]
pub(crate) fn format_duration_exact(ns: u64) -> String {
if ns < 1_000 {
format!("{} ns", ns)
} else if ns < 1_000_000 {
format!("{:.3} µs", ns as f64 / 1_000.0)
} else if ns < 1_000_000_000 {
format!("{:.6} ms", ns as f64 / 1_000_000.0)
} else {
format!("{:.9} s", ns as f64 / 1_000_000_000.0)
}
}
fn scaled(num: &str, scale: f64) -> Option<u64> {
let v: f64 = num.trim().parse().ok()?;
let scaled = v * scale;
(v >= 0.0 && scaled.is_finite() && scaled < u64::MAX as f64).then(|| scaled.round() as u64)
}
pub fn parse_duration(s: &str) -> Option<u64> {
let s = s.trim();
if let Some(num) = s.strip_suffix(" ns") {
scaled(num, 1.0)
} else if let Some(num) = s.strip_suffix(" µs").or_else(|| s.strip_suffix(" us")) {
scaled(num, 1_000.0)
} else if let Some(num) = s.strip_suffix(" ms") {
scaled(num, 1_000_000.0)
} else if let Some(num) = s.strip_suffix(" s") {
scaled(num, 1_000_000_000.0)
} else {
None
}
}
pub fn format_percentile_key(p: f64) -> String {
if p.fract() == 0.0 {
format!("p{}", p as u64)
} else {
format!("p{}", p)
}
}
pub fn format_percentile_header(p: f64) -> String {
if p.fract() == 0.0 {
format!("P{}", p as u64)
} else {
format!("P{}", p)
}
}
pub fn format_bytes(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
const THRESHOLD: f64 = 1024.0;
if bytes == 0 {
return "0 B".to_string();
}
let bytes_f = bytes as f64;
let unit_index = (bytes_f.log(THRESHOLD).floor() as usize).min(UNITS.len() - 1);
let unit_value = bytes_f / THRESHOLD.powi(unit_index as i32);
if unit_index == 0 {
format!("{} {}", bytes, UNITS[unit_index])
} else {
format!("{:.1} {}", unit_value, UNITS[unit_index])
}
}
#[cfg(feature = "hotpath")]
pub(crate) fn format_bytes_exact(bytes: u64) -> String {
format!("{} B", bytes)
}
#[cfg(feature = "hotpath")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Precision {
Display,
Exact,
}
#[cfg(feature = "hotpath")]
impl Precision {
pub(crate) fn for_cloud(cloud: bool) -> Self {
if cloud {
Precision::Exact
} else {
Precision::Display
}
}
pub(crate) fn duration(self, ns: u64) -> String {
match self {
Precision::Display => format_duration(ns),
Precision::Exact => format_duration_exact(ns),
}
}
pub(crate) fn bytes(self, bytes: u64) -> String {
match self {
Precision::Display => format_bytes(bytes),
Precision::Exact => format_bytes_exact(bytes),
}
}
#[cfg(feature = "threads")]
pub(crate) fn bytes_signed(self, bytes: i64) -> String {
let sign = if bytes < 0 { "-" } else { "" };
format!("{sign}{}", self.bytes(bytes.unsigned_abs()))
}
}
pub fn format_rate(rate: Option<f64>) -> String {
rate.map_or_else(|| "-".to_string(), |v| format!("{v:.1}"))
}
pub fn parse_bytes(s: &str) -> Option<u64> {
let s = s.trim();
if let Some(num) = s.strip_suffix(" TB") {
scaled(num, 1024.0_f64.powi(4))
} else if let Some(num) = s.strip_suffix(" GB") {
scaled(num, 1024.0_f64.powi(3))
} else if let Some(num) = s.strip_suffix(" MB") {
scaled(num, 1024.0_f64.powi(2))
} else if let Some(num) = s.strip_suffix(" KB") {
scaled(num, 1024.0)
} else if let Some(num) = s.strip_suffix(" B") {
num.trim().parse::<u64>().ok()
} else {
None
}
}
pub fn format_count(count: u64) -> String {
count.to_string()
}
pub fn format_count_display(count: u64) -> String {
group_thousands(count)
}
pub fn parse_count(s: &str) -> Option<u64> {
s.trim().parse::<u64>().ok()
}
pub fn format_icount(hundredths: u64) -> String {
with_icount_fraction((hundredths / 100).to_string(), hundredths)
}
pub fn format_icount_display(hundredths: u64) -> String {
if hundredths >= 100_000 {
let whole = hundredths / 100 + u64::from(hundredths % 100 >= 50);
return group_thousands(whole);
}
with_icount_fraction(group_thousands(hundredths / 100), hundredths)
}
fn group_thousands(value: u64) -> String {
let digits = value.to_string();
let mut grouped = String::with_capacity(digits.len() + digits.len() / 3);
for (i, digit) in digits.chars().enumerate() {
if i > 0 && (digits.len() - i).is_multiple_of(3) {
grouped.push(',');
}
grouped.push(digit);
}
grouped
}
fn with_icount_fraction(whole: String, hundredths: u64) -> String {
match hundredths % 100 {
0 => whole,
rest => format!("{whole}.{rest:02}"),
}
}
pub fn parse_icount(s: &str) -> Option<u64> {
let s = s.trim();
let (whole, frac) = s.split_once('.').unwrap_or((s, ""));
let digits = |p: &str| p.bytes().all(|b| b.is_ascii_digit());
if whole.is_empty() || s.ends_with('.') || !digits(whole) || !digits(frac) || frac.len() > 2 {
return None;
}
let hundredths = match frac.len() {
0 => 0,
1 => frac.parse::<u64>().ok()? * 10,
_ => frac.parse::<u64>().ok()?,
};
whole
.parse::<u64>()
.ok()?
.checked_mul(100)?
.checked_add(hundredths)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProfilingMode {
Timing,
AllocBytes,
AllocCount,
Icount,
}
impl ProfilingMode {
pub fn display_value(self, s: &str) -> String {
match self {
ProfilingMode::AllocCount => {
parse_count(s).map_or_else(|| s.to_string(), format_count_display)
}
ProfilingMode::Icount => {
parse_icount(s).map_or_else(|| s.to_string(), format_icount_display)
}
ProfilingMode::Timing | ProfilingMode::AllocBytes => s.to_string(),
}
}
}
impl fmt::Display for ProfilingMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ProfilingMode::Timing => write!(f, "timing"),
ProfilingMode::AllocBytes => write!(f, "alloc-bytes"),
ProfilingMode::AllocCount => write!(f, "alloc-count"),
ProfilingMode::Icount => write!(f, "icount"),
}
}
}
impl Serialize for ProfilingMode {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
ProfilingMode::Timing => serializer.serialize_str("timing"),
ProfilingMode::AllocBytes => serializer.serialize_str("alloc-bytes"),
ProfilingMode::AllocCount => serializer.serialize_str("alloc-count"),
ProfilingMode::Icount => serializer.serialize_str("icount"),
}
}
}
impl<'de> Deserialize<'de> for ProfilingMode {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
"timing" => Ok(ProfilingMode::Timing),
"alloc-bytes" => Ok(ProfilingMode::AllocBytes),
"alloc-count" => Ok(ProfilingMode::AllocCount),
"icount" => Ok(ProfilingMode::Icount),
_ => Err(serde::de::Error::unknown_variant(
&s,
&["timing", "alloc-bytes", "alloc-count", "icount"],
)),
}
}
}
pub fn floor_char_boundary(s: &str, index: usize) -> usize {
if index >= s.len() {
return s.len();
}
let mut i = index;
while i > 0 && !s.is_char_boundary(i) {
i -= 1;
}
i
}
pub fn ceil_char_boundary(s: &str, index: usize) -> usize {
if index >= s.len() {
return s.len();
}
let mut i = index;
while i < s.len() && !s.is_char_boundary(i) {
i += 1;
}
i
}
pub fn shorten_function_name(function_name: &str) -> String {
let depth = *FUNCTIONS_NAME_DEPTH;
if depth == 0 {
return function_name.to_string();
}
let parts: Vec<&str> = function_name.split("::").collect();
if parts.len() > depth {
parts[parts.len() - depth..].join("::")
} else {
function_name.to_string()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)]
pub(crate) struct FunctionLog {
pub value: Option<u64>,
pub elapsed_nanos: u64,
pub alloc_count: Option<u64>,
pub tid: Option<u64>,
pub result: Option<String>,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub(crate) struct FunctionLogsList {
pub function_name: String,
pub logs: Vec<FunctionLog>,
pub count: usize,
}
#[cfg(test)]
mod parse_tests {
use crate::output::*;
#[test]
fn test_parse_duration_units() {
assert_eq!(parse_duration("123 ns"), Some(123));
assert_eq!(parse_duration("0 ns"), Some(0));
assert_eq!(parse_duration("1.23 µs"), Some(1230));
assert_eq!(parse_duration("1.23 ms"), Some(1230000));
assert_eq!(parse_duration("1.23 s"), Some(1230000000));
assert_eq!(parse_duration("-0 ns"), Some(0));
assert_eq!(parse_duration("1e3 ns"), Some(1000));
assert_eq!(parse_duration("250 us"), parse_duration("250 µs"));
}
#[test]
fn test_parse_duration_invalid() {
assert_eq!(parse_duration(""), None);
assert_eq!(parse_duration("invalid"), None);
assert_eq!(parse_duration("abc ns"), None);
assert_eq!(parse_duration("-5 ms"), None);
assert_eq!(parse_duration("inf ms"), None);
assert_eq!(parse_duration("-inf ns"), None);
assert_eq!(parse_duration("NaN s"), None);
assert_eq!(parse_duration("1e400 ms"), None);
assert_eq!(parse_duration("1e300 s"), None);
assert_eq!(parse_duration("2e19 ns"), None);
}
#[test]
fn test_parse_duration_roundtrip() {
for val in [0, 1, 500, 999, 1000, 50_000, 1_230_000, 1_230_000_000] {
let formatted = format_duration(val);
let parsed = parse_duration(&formatted);
assert_eq!(
parsed,
Some(val),
"round-trip failed for {val}: formatted as '{formatted}'"
);
}
}
#[test]
fn test_parse_bytes_units() {
assert_eq!(parse_bytes("0 B"), Some(0));
assert_eq!(parse_bytes("123 B"), Some(123));
assert_eq!(parse_bytes("1.5 KB"), Some(1536));
assert_eq!(parse_bytes("1.0 MB"), Some(1048576));
assert_eq!(parse_bytes("1.0 GB"), Some(1073741824));
assert_eq!(parse_bytes("0.5 TB"), Some(549755813888));
assert_eq!(parse_bytes("0.0 KB"), Some(0));
}
#[test]
fn test_parse_bytes_invalid() {
assert_eq!(parse_bytes(""), None);
assert_eq!(parse_bytes("invalid"), None);
assert_eq!(parse_bytes("abc KB"), None);
assert_eq!(parse_bytes("-1.5 KB"), None);
assert_eq!(parse_bytes("-1 B"), None);
assert_eq!(parse_bytes("NaN KB"), None);
assert_eq!(parse_bytes("inf MB"), None);
assert_eq!(parse_bytes("1e300 TB"), None);
}
#[test]
fn test_parse_bytes_roundtrip() {
for val in [0, 100, 1023, 1024, 1536, 1048576, 1073741824] {
let formatted = format_bytes(val);
let parsed = parse_bytes(&formatted);
assert_eq!(
parsed,
Some(val),
"round-trip failed for {val}: formatted as '{formatted}'"
);
}
}
#[test]
fn test_format_count() {
assert_eq!(format_count(0), "0");
assert_eq!(format_count(999), "999");
assert_eq!(format_count(1_000), "1000");
assert_eq!(format_count(1_000_000), "1000000");
for (count, shown) in [
(0, "0"),
(999, "999"),
(1_000, "1,000"),
(1_234_567, "1,234,567"),
(u64::MAX, "18,446,744,073,709,551,615"),
] {
assert_eq!(format_count_display(count), shown);
}
assert_eq!(parse_count("1,000"), None);
}
#[test]
fn test_icount_format_and_parse() {
for (hundredths, raw, shown) in [
(0, "0", "0"),
(3305, "33.05", "33.05"),
(3300, "33", "33"),
(99_999, "999.99", "999.99"),
(100_000, "1000", "1,000"),
(100_049, "1000.49", "1,000"),
(100_050, "1000.50", "1,001"),
(10_748_162_425, "107481624.25", "107,481,624"),
(u64::MAX, "184467440737095516.15", "184,467,440,737,095,516"),
(
u64::MAX - 15,
"184467440737095516",
"184,467,440,737,095,516",
),
] {
assert_eq!(format_icount(hundredths), raw);
assert_eq!(format_icount_display(hundredths), shown);
assert_eq!(parse_icount(raw), Some(hundredths), "{raw:?}");
}
for (input, expected) in [
("33.5", Some(3350)),
("33.00", Some(3300)),
("184467440737095516.16", None),
("1,000", None),
("33.", None),
(".5", None),
("33.123", None),
("-1", None),
("", None),
] {
assert_eq!(parse_icount(input), expected, "{input:?}");
}
}
#[test]
fn test_parse_count_roundtrip() {
for val in [0, 1, 500, 999, 1_000, 1_500, 50_000, 1_000_000] {
let formatted = format_count(val);
let parsed = parse_count(&formatted);
assert_eq!(
parsed,
Some(val),
"round-trip failed for {val}: formatted as '{formatted}'"
);
}
}
}
#[cfg(all(test, feature = "hotpath"))]
mod precision_tests {
use crate::output::*;
#[test]
fn test_format_duration_exact_roundtrip() {
for val in [
0,
1,
999,
1_000,
1_001,
999_999,
1_000_000,
1_004_999,
1_234_567,
999_999_999,
1_000_000_000,
1_000_000_001,
123_456_789_012,
1_000_000_000_000,
] {
let formatted = format_duration_exact(val);
assert_eq!(
parse_duration(&formatted),
Some(val),
"round-trip failed for {val}: formatted as '{formatted}'"
);
}
assert_eq!(format_duration_exact(999), "999 ns");
assert_eq!(format_duration_exact(1_001), "1.001 µs");
assert_eq!(format_duration_exact(1_004_999), "1.004999 ms");
assert_eq!(format_duration_exact(1_000_000_001), "1.000000001 s");
}
#[test]
fn test_precision_selects_formatter() {
assert_eq!(Precision::for_cloud(false), Precision::Display);
assert_eq!(Precision::for_cloud(true), Precision::Exact);
assert_eq!(Precision::Display.duration(1_004_999), "1.00 ms");
assert_eq!(Precision::Exact.duration(1_004_999), "1.004999 ms");
assert_eq!(Precision::Display.bytes(1_075), "1.0 KB");
assert_eq!(Precision::Exact.bytes(1_075), "1075 B");
}
#[test]
fn test_format_bytes_exact_roundtrip() {
for val in [0, 1, 1_023, 1_024, 1_075, 65_229, 1_048_576, u64::MAX] {
let formatted = format_bytes_exact(val);
assert_eq!(
parse_bytes(&formatted),
Some(val),
"round-trip failed for {val}: formatted as '{formatted}'"
);
}
}
}