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)
}
}
pub fn parse_duration(s: &str) -> Option<u64> {
let s = s.trim();
if let Some(num) = s.strip_suffix(" ns") {
num.trim().parse::<f64>().ok().map(|v| v.round() as u64)
} else if let Some(num) = s.strip_suffix(" µs") {
num.trim()
.parse::<f64>()
.ok()
.map(|v| (v * 1_000.0).round() as u64)
} else if let Some(num) = s.strip_suffix(" ms") {
num.trim()
.parse::<f64>()
.ok()
.map(|v| (v * 1_000_000.0).round() as u64)
} else if let Some(num) = s.strip_suffix(" s") {
num.trim()
.parse::<f64>()
.ok()
.map(|v| (v * 1_000_000_000.0).round() as u64)
} 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),
}
}
}
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") {
num.trim()
.parse::<f64>()
.ok()
.map(|v| (v * 1024.0_f64.powi(4)).round() as u64)
} else if let Some(num) = s.strip_suffix(" GB") {
num.trim()
.parse::<f64>()
.ok()
.map(|v| (v * 1024.0_f64.powi(3)).round() as u64)
} else if let Some(num) = s.strip_suffix(" MB") {
num.trim()
.parse::<f64>()
.ok()
.map(|v| (v * 1024.0_f64.powi(2)).round() as u64)
} else if let Some(num) = s.strip_suffix(" KB") {
num.trim()
.parse::<f64>()
.ok()
.map(|v| (v * 1024.0).round() as u64)
} 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 parse_count(s: &str) -> Option<u64> {
s.trim().parse::<u64>().ok()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProfilingMode {
Timing,
AllocBytes,
AllocCount,
}
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"),
}
}
}
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"),
}
}
}
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),
_ => Err(serde::de::Error::unknown_variant(
&s,
&["timing", "alloc-bytes", "alloc-count"],
)),
}
}
}
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 super::*;
#[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));
}
#[test]
fn test_parse_duration_invalid() {
assert_eq!(parse_duration(""), None);
assert_eq!(parse_duration("invalid"), None);
assert_eq!(parse_duration("abc 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));
}
#[test]
fn test_parse_bytes_invalid() {
assert_eq!(parse_bytes(""), None);
assert_eq!(parse_bytes("invalid"), None);
assert_eq!(parse_bytes("abc KB"), 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");
}
#[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 super::*;
#[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}'"
);
}
}
}