1use serde::ser::Serializer;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4use std::fs::File;
5use std::io::Write;
6use std::path::PathBuf;
7use std::sync::LazyLock;
8
9const DEFAULT_MAX_LOG_LEN: usize = 1536;
10pub static MAX_LOG_LEN: LazyLock<usize> = LazyLock::new(|| {
11 std::env::var("HOTPATH_MAX_LOG_LEN")
12 .ok()
13 .and_then(|s| s.parse().ok())
14 .unwrap_or(DEFAULT_MAX_LOG_LEN)
15});
16
17const DEFAULT_FUNCTIONS_NAME_DEPTH: usize = 2;
18pub static FUNCTIONS_NAME_DEPTH: LazyLock<usize> = LazyLock::new(|| {
19 std::env::var("HOTPATH_FUNCTIONS_NAME_DEPTH")
20 .ok()
21 .and_then(|s| s.parse().ok())
22 .unwrap_or(DEFAULT_FUNCTIONS_NAME_DEPTH)
23});
24
25#[derive(Default)]
27pub enum OutputDestination {
28 #[default]
29 Stdout,
30 File(PathBuf),
31}
32
33pub fn format_duration(ns: u64) -> String {
35 if ns < 1_000 {
36 format!("{} ns", ns)
37 } else if ns < 1_000_000 {
38 format!("{:.2} µs", ns as f64 / 1_000.0)
39 } else if ns < 1_000_000_000 {
40 format!("{:.2} ms", ns as f64 / 1_000_000.0)
41 } else {
42 format!("{:.2} s", ns as f64 / 1_000_000_000.0)
43 }
44}
45
46pub fn parse_duration(s: &str) -> Option<u64> {
49 let s = s.trim();
50 if let Some(num) = s.strip_suffix(" ns") {
51 num.trim().parse::<f64>().ok().map(|v| v.round() as u64)
52 } else if let Some(num) = s.strip_suffix(" µs") {
53 num.trim()
54 .parse::<f64>()
55 .ok()
56 .map(|v| (v * 1_000.0).round() as u64)
57 } else if let Some(num) = s.strip_suffix(" ms") {
58 num.trim()
59 .parse::<f64>()
60 .ok()
61 .map(|v| (v * 1_000_000.0).round() as u64)
62 } else if let Some(num) = s.strip_suffix(" s") {
63 num.trim()
64 .parse::<f64>()
65 .ok()
66 .map(|v| (v * 1_000_000_000.0).round() as u64)
67 } else {
68 None
69 }
70}
71
72pub fn format_percentile_key(p: f64) -> String {
74 if p.fract() == 0.0 {
75 format!("p{}", p as u64)
76 } else {
77 format!("p{}", p)
78 }
79}
80
81pub fn format_percentile_header(p: f64) -> String {
83 if p.fract() == 0.0 {
84 format!("P{}", p as u64)
85 } else {
86 format!("P{}", p)
87 }
88}
89
90pub fn format_bytes(bytes: u64) -> String {
92 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
93 const THRESHOLD: f64 = 1024.0;
94
95 if bytes == 0 {
96 return "0 B".to_string();
97 }
98
99 let bytes_f = bytes as f64;
100 let unit_index = (bytes_f.log(THRESHOLD).floor() as usize).min(UNITS.len() - 1);
101 let unit_value = bytes_f / THRESHOLD.powi(unit_index as i32);
102
103 if unit_index == 0 {
104 format!("{} {}", bytes, UNITS[unit_index])
105 } else {
106 format!("{:.1} {}", unit_value, UNITS[unit_index])
107 }
108}
109
110pub fn format_rate(rate: Option<f64>) -> String {
112 rate.map_or_else(|| "-".to_string(), |v| format!("{v:.1}"))
113}
114
115pub fn parse_bytes(s: &str) -> Option<u64> {
118 let s = s.trim();
119 if let Some(num) = s.strip_suffix(" TB") {
120 num.trim()
121 .parse::<f64>()
122 .ok()
123 .map(|v| (v * 1024.0_f64.powi(4)).round() as u64)
124 } else if let Some(num) = s.strip_suffix(" GB") {
125 num.trim()
126 .parse::<f64>()
127 .ok()
128 .map(|v| (v * 1024.0_f64.powi(3)).round() as u64)
129 } else if let Some(num) = s.strip_suffix(" MB") {
130 num.trim()
131 .parse::<f64>()
132 .ok()
133 .map(|v| (v * 1024.0_f64.powi(2)).round() as u64)
134 } else if let Some(num) = s.strip_suffix(" KB") {
135 num.trim()
136 .parse::<f64>()
137 .ok()
138 .map(|v| (v * 1024.0).round() as u64)
139 } else if let Some(num) = s.strip_suffix(" B") {
140 num.trim().parse::<u64>().ok()
141 } else {
142 None
143 }
144}
145
146pub fn format_count(count: u64) -> String {
148 count.to_string()
149}
150
151pub fn parse_count(s: &str) -> Option<u64> {
154 s.trim().parse::<u64>().ok()
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159pub enum ProfilingMode {
160 Timing,
162 AllocBytes,
164 AllocCount,
166}
167
168impl fmt::Display for ProfilingMode {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 match self {
171 ProfilingMode::Timing => write!(f, "timing"),
172 ProfilingMode::AllocBytes => write!(f, "alloc-bytes"),
173 ProfilingMode::AllocCount => write!(f, "alloc-count"),
174 }
175 }
176}
177
178#[cfg(feature = "hotpath")]
179static USE_COLORS: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
180
181#[cfg(feature = "hotpath")]
182pub(crate) fn set_use_colors(value: bool) {
183 let _ = USE_COLORS.set(value);
184}
185
186#[cfg(feature = "hotpath")]
187pub(crate) fn use_colors() -> bool {
188 *USE_COLORS.get().unwrap_or(&false)
189}
190
191#[cfg(feature = "hotpath-cpu")]
192pub(crate) fn cyan(text: &str) -> String {
193 if use_colors() {
194 format!("\x1b[1;36m{text}\x1b[0m")
195 } else {
196 text.to_string()
197 }
198}
199
200impl OutputDestination {
201 pub fn writer(&self) -> Result<Box<dyn Write>, std::io::Error> {
207 match self {
208 OutputDestination::Stdout => Ok(Box::new(std::io::stdout())),
209 OutputDestination::File(path) => {
210 if let Some(parent) = path.parent() {
211 std::fs::create_dir_all(parent)?;
212 }
213 Ok(Box::new(File::create(path)?))
214 }
215 }
216 }
217
218 pub fn from_path(path: Option<PathBuf>) -> Self {
224 if let Ok(env_path) = std::env::var("HOTPATH_OUTPUT_PATH") {
225 return OutputDestination::File(resolve_output_path(env_path));
226 }
227
228 match path {
229 Some(p) => OutputDestination::File(p),
230 None => OutputDestination::Stdout,
231 }
232 }
233}
234
235pub(crate) fn resolve_output_path(path: impl AsRef<std::path::Path>) -> PathBuf {
237 let path = path.as_ref();
238 if path.is_absolute() {
239 path.to_path_buf()
240 } else {
241 std::env::current_dir()
242 .map(|cwd| cwd.join(path))
243 .unwrap_or_else(|_| path.to_path_buf())
244 }
245}
246
247impl Serialize for ProfilingMode {
248 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
249 where
250 S: Serializer,
251 {
252 match self {
253 ProfilingMode::Timing => serializer.serialize_str("timing"),
254 ProfilingMode::AllocBytes => serializer.serialize_str("alloc-bytes"),
255 ProfilingMode::AllocCount => serializer.serialize_str("alloc-count"),
256 }
257 }
258}
259
260impl<'de> Deserialize<'de> for ProfilingMode {
261 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
262 where
263 D: serde::Deserializer<'de>,
264 {
265 let s = String::deserialize(deserializer)?;
266 match s.as_str() {
267 "timing" => Ok(ProfilingMode::Timing),
268 "alloc-bytes" => Ok(ProfilingMode::AllocBytes),
269 "alloc-count" => Ok(ProfilingMode::AllocCount),
270 _ => Err(serde::de::Error::unknown_variant(
271 &s,
272 &["timing", "alloc-bytes", "alloc-count"],
273 )),
274 }
275 }
276}
277
278pub fn floor_char_boundary(s: &str, index: usize) -> usize {
279 if index >= s.len() {
280 return s.len();
281 }
282 let mut i = index;
283 while i > 0 && !s.is_char_boundary(i) {
284 i -= 1;
285 }
286 i
287}
288
289pub fn ceil_char_boundary(s: &str, index: usize) -> usize {
290 if index >= s.len() {
291 return s.len();
292 }
293 let mut i = index;
294 while i < s.len() && !s.is_char_boundary(i) {
295 i += 1;
296 }
297 i
298}
299
300#[cfg(feature = "hotpath")]
301struct TruncatingWriter {
302 buf: String,
303 limit: usize,
304 truncated: bool,
305}
306
307#[cfg(feature = "hotpath")]
308impl std::fmt::Write for TruncatingWriter {
309 fn write_str(&mut self, s: &str) -> std::fmt::Result {
310 if self.truncated {
311 return Ok(());
312 }
313
314 let remaining = self.limit.saturating_sub(self.buf.len());
315 if remaining == 0 {
316 if !s.is_empty() {
317 self.truncated = true;
318 }
319 return Ok(());
320 }
321
322 let end = floor_char_boundary(s, s.len().min(remaining));
323
324 if end < s.len() {
325 self.truncated = true;
326 }
327
328 self.buf.push_str(&s[..end]);
329 Ok(())
330 }
331}
332
333#[cfg(feature = "hotpath")]
334#[cfg_attr(feature = "hotpath-meta", hotpath_meta::measure)]
335pub fn format_debug_truncated(value: &impl std::fmt::Debug) -> String {
336 let _suspend = crate::lib_on::SuspendAllocTracking::new();
337 use std::fmt::Write;
338 let limit = MAX_LOG_LEN.saturating_sub(3);
339 let mut writer = TruncatingWriter {
340 buf: String::with_capacity(64),
341 limit,
342 truncated: false,
343 };
344 let _ = write!(writer, "{:?}", value);
345
346 if writer.truncated {
347 writer.buf.push_str("...");
348 }
349
350 writer.buf
351}
352
353pub fn shorten_function_name(function_name: &str) -> String {
354 let depth = *FUNCTIONS_NAME_DEPTH;
355 if depth == 0 {
356 return function_name.to_string();
357 }
358 let parts: Vec<&str> = function_name.split("::").collect();
359 if parts.len() > depth {
360 parts[parts.len() - depth..].join("::")
361 } else {
362 function_name.to_string()
363 }
364}
365
366#[derive(Debug, Clone, Serialize, Deserialize)]
374#[allow(dead_code)]
375pub(crate) struct FunctionLog {
376 pub value: Option<u64>,
378 pub elapsed_nanos: u64,
380 pub alloc_count: Option<u64>,
382 pub tid: Option<u64>,
384 pub result: Option<String>,
386}
387
388#[derive(Debug, Clone)]
390#[allow(dead_code)]
391pub(crate) struct FunctionLogsList {
392 pub function_name: String,
393 pub logs: Vec<FunctionLog>,
394 pub count: usize,
396}
397
398#[cfg(test)]
399mod truncation_tests {
400 use super::*;
401
402 #[test]
403 fn test_format_debug_truncated() {
404 let truncate_point = MAX_LOG_LEN.saturating_sub(3);
405
406 let test_cases: Vec<(&str, String)> = vec![
407 (
408 "japanese at boundary",
409 format!("{}リプライ", "a".repeat(truncate_point - 2)),
410 ),
411 ("emoji", "🦀".repeat(500)),
412 ("chinese", "拥抱中文字符测试".repeat(200)),
413 (
414 "2-byte at boundary",
415 format!("{}ñoño", "a".repeat(truncate_point - 1)),
416 ),
417 ];
418
419 for (name, input) in test_cases {
420 let result = format_debug_truncated(&input);
421 assert!(
422 result.chars().count() > 0,
423 "{}: result should have chars",
424 name
425 );
426 if input.len() > *MAX_LOG_LEN {
427 assert!(
428 result.ends_with("..."),
429 "{}: truncated result should end with '...'",
430 name
431 );
432 }
433 }
434 }
435}
436
437#[cfg(test)]
438mod parse_tests {
439 use super::*;
440
441 #[test]
442 fn test_parse_duration_units() {
443 assert_eq!(parse_duration("123 ns"), Some(123));
444 assert_eq!(parse_duration("0 ns"), Some(0));
445 assert_eq!(parse_duration("1.23 µs"), Some(1230));
446 assert_eq!(parse_duration("1.23 ms"), Some(1230000));
447 assert_eq!(parse_duration("1.23 s"), Some(1230000000));
448 }
449
450 #[test]
451 fn test_parse_duration_invalid() {
452 assert_eq!(parse_duration(""), None);
453 assert_eq!(parse_duration("invalid"), None);
454 assert_eq!(parse_duration("abc ns"), None);
455 }
456
457 #[test]
458 fn test_parse_duration_roundtrip() {
459 for val in [0, 1, 500, 999, 1000, 50_000, 1_230_000, 1_230_000_000] {
460 let formatted = format_duration(val);
461 let parsed = parse_duration(&formatted);
462 assert_eq!(
463 parsed,
464 Some(val),
465 "round-trip failed for {val}: formatted as '{formatted}'"
466 );
467 }
468 }
469
470 #[test]
471 fn test_parse_bytes_units() {
472 assert_eq!(parse_bytes("0 B"), Some(0));
473 assert_eq!(parse_bytes("123 B"), Some(123));
474 assert_eq!(parse_bytes("1.5 KB"), Some(1536));
475 assert_eq!(parse_bytes("1.0 MB"), Some(1048576));
476 assert_eq!(parse_bytes("1.0 GB"), Some(1073741824));
477 assert_eq!(parse_bytes("0.5 TB"), Some(549755813888));
478 }
479
480 #[test]
481 fn test_parse_bytes_invalid() {
482 assert_eq!(parse_bytes(""), None);
483 assert_eq!(parse_bytes("invalid"), None);
484 assert_eq!(parse_bytes("abc KB"), None);
485 }
486
487 #[test]
488 fn test_parse_bytes_roundtrip() {
489 for val in [0, 100, 1023, 1024, 1536, 1048576, 1073741824] {
490 let formatted = format_bytes(val);
491 let parsed = parse_bytes(&formatted);
492 assert_eq!(
493 parsed,
494 Some(val),
495 "round-trip failed for {val}: formatted as '{formatted}'"
496 );
497 }
498 }
499
500 #[test]
501 fn test_format_count() {
502 assert_eq!(format_count(0), "0");
503 assert_eq!(format_count(999), "999");
504 assert_eq!(format_count(1_000), "1000");
505 assert_eq!(format_count(1_000_000), "1000000");
506 }
507
508 #[test]
509 fn test_parse_count_roundtrip() {
510 for val in [0, 1, 500, 999, 1_000, 1_500, 50_000, 1_000_000] {
511 let formatted = format_count(val);
512 let parsed = parse_count(&formatted);
513 assert_eq!(
514 parsed,
515 Some(val),
516 "round-trip failed for {val}: formatted as '{formatted}'"
517 );
518 }
519 }
520}