1use core::fmt;
4use core::str::FromStr;
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7
8const KIB: u64 = 1024;
11const MIB: u64 = 1024 * KIB;
12const GIB: u64 = 1024 * MIB;
13
14const MS_PER_S: u64 = 1_000;
18const MS_PER_MIN: u64 = 60 * MS_PER_S;
19const MS_PER_H: u64 = 60 * MS_PER_MIN;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
39pub struct MemSize(u64);
40
41impl MemSize {
42 #[inline]
44 #[must_use]
45 pub const fn from_bytes(bytes: u64) -> Self {
46 Self(bytes)
47 }
48
49 #[inline]
51 #[must_use]
52 pub const fn bytes(self) -> u64 {
53 self.0
54 }
55}
56
57impl FromStr for MemSize {
58 type Err = ParseMemSizeError;
59
60 fn from_str(s: &str) -> Result<Self, Self::Err> {
71 if s.is_empty() {
72 return Err(ParseMemSizeError::Empty);
73 }
74 let (digits, multiplier) = match s.as_bytes()[s.len() - 1] {
75 b'G' => (&s[..s.len() - 1], GIB),
76 b'M' => (&s[..s.len() - 1], MIB),
77 b'K' => (&s[..s.len() - 1], KIB),
78 _ => (s, 1),
79 };
80 if digits.is_empty() {
81 return Err(ParseMemSizeError::MissingDigits);
82 }
83 if !digits.bytes().all(|b| b.is_ascii_digit()) {
84 return Err(ParseMemSizeError::InvalidCharacter);
85 }
86 let value: u64 = digits.parse().map_err(|_| ParseMemSizeError::Overflow)?;
87 value
88 .checked_mul(multiplier)
89 .map(Self)
90 .ok_or(ParseMemSizeError::Overflow)
91 }
92}
93
94impl fmt::Display for MemSize {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 match self.0 {
99 0 => f.write_str("0"),
100 b if b % GIB == 0 => write!(f, "{}G", b / GIB),
101 b if b % MIB == 0 => write!(f, "{}M", b / MIB),
102 b if b % KIB == 0 => write!(f, "{}K", b / KIB),
103 b => write!(f, "{b}"),
104 }
105 }
106}
107
108impl Serialize for MemSize {
109 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
110 serializer.collect_str(self)
111 }
112}
113
114impl<'de> Deserialize<'de> for MemSize {
115 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
116 let s = String::deserialize(deserializer)?;
118 s.parse().map_err(serde::de::Error::custom)
119 }
120}
121
122#[non_exhaustive]
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum ParseMemSizeError {
132 Empty,
134 MissingDigits,
136 InvalidCharacter,
140 Overflow,
142}
143
144impl fmt::Display for ParseMemSizeError {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 f.write_str(match self {
147 Self::Empty => "memory size is empty",
148 Self::MissingDigits => "memory size has a unit suffix but no digits",
149 Self::InvalidCharacter => {
150 "memory size must be ASCII digits with an optional trailing G, M, or K"
151 }
152 Self::Overflow => "memory size in bytes overflows u64",
153 })
154 }
155}
156
157impl core::error::Error for ParseMemSizeError {}
158
159#[cfg(feature = "schema")]
168impl schemars::JsonSchema for MemSize {
169 fn schema_name() -> std::borrow::Cow<'static, str> {
170 "MemSize".into()
171 }
172
173 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
174 schemars::json_schema!({
175 "type": "string",
176 "pattern": r"^\d+(G|M|K)?$",
177 "description": "A byte quantity: digits, optionally suffixed G, M or K (binary units).",
178 })
179 }
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
204pub struct UpDuration(core::time::Duration);
205
206impl UpDuration {
207 #[inline]
209 #[must_use]
210 pub const fn from_millis(ms: u64) -> Self {
211 Self(core::time::Duration::from_millis(ms))
212 }
213
214 #[inline]
216 #[must_use]
217 pub const fn as_duration(self) -> core::time::Duration {
218 self.0
219 }
220
221 #[inline]
223 #[must_use]
224 pub const fn as_millis(self) -> u64 {
225 self.0.as_millis() as u64
230 }
231}
232
233impl FromStr for UpDuration {
234 type Err = ParseUpDurationError;
235
236 fn from_str(s: &str) -> Result<Self, Self::Err> {
249 if s.is_empty() {
250 return Err(ParseUpDurationError::Empty);
251 }
252 let (digits, ms_per_unit) = if let Some(rest) = s.strip_suffix("ms") {
257 (rest, 1)
258 } else {
259 match s.as_bytes()[s.len() - 1] {
260 b'h' => (&s[..s.len() - 1], MS_PER_H),
261 b'm' => (&s[..s.len() - 1], MS_PER_MIN),
262 b's' => (&s[..s.len() - 1], MS_PER_S),
263 _ => (s, 1),
264 }
265 };
266 if digits.is_empty() {
267 return Err(ParseUpDurationError::MissingDigits);
268 }
269 if !digits.bytes().all(|b| b.is_ascii_digit()) {
270 return Err(ParseUpDurationError::InvalidCharacter);
271 }
272 let value: u64 = digits.parse().map_err(|_| ParseUpDurationError::Overflow)?;
273 value
274 .checked_mul(ms_per_unit)
275 .map(Self::from_millis)
276 .ok_or(ParseUpDurationError::Overflow)
277 }
278}
279
280impl fmt::Display for UpDuration {
282 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283 let ms = self.as_millis();
284 match ms {
285 0 => f.write_str("0"),
286 v if v % MS_PER_H == 0 => write!(f, "{}h", v / MS_PER_H),
287 v if v % MS_PER_MIN == 0 => write!(f, "{}m", v / MS_PER_MIN),
288 v if v % MS_PER_S == 0 => write!(f, "{}s", v / MS_PER_S),
289 v => write!(f, "{v}"),
290 }
291 }
292}
293
294impl Serialize for UpDuration {
295 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
296 serializer.collect_str(self)
297 }
298}
299
300impl<'de> Deserialize<'de> for UpDuration {
301 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
302 let s = String::deserialize(deserializer)?;
304 s.parse().map_err(serde::de::Error::custom)
305 }
306}
307
308#[non_exhaustive]
315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
316pub enum ParseUpDurationError {
317 Empty,
319 MissingDigits,
321 InvalidCharacter,
324 Overflow,
326}
327
328impl fmt::Display for ParseUpDurationError {
329 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330 f.write_str(match self {
331 Self::Empty => "duration is empty",
332 Self::MissingDigits => "duration has a unit suffix but no digits",
333 Self::InvalidCharacter => {
334 "duration must be ASCII digits with an optional trailing h, m, s, or ms"
335 }
336 Self::Overflow => "duration in milliseconds overflows u64",
337 })
338 }
339}
340
341impl core::error::Error for ParseUpDurationError {}
342
343#[cfg(feature = "schema")]
347impl schemars::JsonSchema for UpDuration {
348 fn schema_name() -> std::borrow::Cow<'static, str> {
349 "UpDuration".into()
350 }
351
352 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
353 schemars::json_schema!({
354 "type": "string",
355 "pattern": r"^\d+(ms|h|m|s)?$",
356 "description": "A duration: digits, optionally suffixed ms, h, m or s. Plain digits are milliseconds.",
357 })
358 }
359}
360
361#[must_use]
375pub fn cpu_percent(cpu_ms: u64, window: core::time::Duration) -> Option<f32> {
376 if window.is_zero() {
377 return None;
378 }
379 Some((cpu_ms as f64 / window.as_secs_f64() / 10.0) as f32)
383}
384
385#[cfg(test)]
386mod mem_size_tests {
387 use super::*;
388
389 #[test]
390 fn plain_digits_parse_as_bytes() {
391 assert_eq!("123".parse::<MemSize>().unwrap().bytes(), 123);
392 }
393
394 #[test]
395 fn units_are_binary() {
396 assert_eq!("7K".parse::<MemSize>().unwrap().bytes(), 7 * 1024);
397 assert_eq!("512M".parse::<MemSize>().unwrap().bytes(), 512 << 20);
398 assert_eq!("3G".parse::<MemSize>().unwrap().bytes(), 3 << 30);
399 }
400
401 #[test]
402 fn rejects_spec_violations() {
403 use ParseMemSizeError::*;
404 assert_eq!("".parse::<MemSize>(), Err(Empty));
405 assert_eq!("G".parse::<MemSize>(), Err(MissingDigits));
406 assert_eq!("512m".parse::<MemSize>(), Err(InvalidCharacter)); assert_eq!(" 512M".parse::<MemSize>(), Err(InvalidCharacter)); assert_eq!("1.5G".parse::<MemSize>(), Err(InvalidCharacter)); assert_eq!("512MB".parse::<MemSize>(), Err(InvalidCharacter)); assert_eq!("18446744073709551616".parse::<MemSize>(), Err(Overflow));
411 assert_eq!("17179869184G".parse::<MemSize>(), Err(Overflow));
412 }
413
414 #[test]
415 fn display_uses_largest_exact_unit_and_round_trips() {
416 for bytes in [
417 0u64,
418 1,
419 1023,
420 1024,
421 1536,
422 1 << 20,
423 (1 << 30) + 1024,
424 u64::MAX,
425 ] {
426 let size = MemSize::from_bytes(bytes);
427 let reparsed: MemSize = size.to_string().parse().unwrap();
428 assert_eq!(reparsed, size, "display of {bytes} bytes must reparse");
429 }
430 assert_eq!(MemSize::from_bytes(3 << 30).to_string(), "3G");
431 assert_eq!(MemSize::from_bytes(1536).to_string(), "1536");
432 }
433
434 #[test]
435 fn serde_uses_string_form() {
436 let size: MemSize = serde_json::from_str("\"512M\"").unwrap();
437 assert_eq!(size.bytes(), 512 << 20);
438 assert_eq!(serde_json::to_string(&size).unwrap(), "\"512M\"");
439 assert!(serde_json::from_str::<MemSize>("\"512MB\"").is_err());
440 }
441
442 #[cfg(feature = "schema")]
446 #[test]
447 fn the_schema_pattern_agrees_with_from_str() {
448 let schema = serde_json::to_value(schemars::schema_for!(MemSize)).unwrap();
449 let pattern = schema["pattern"].as_str().unwrap();
450 let re = regex::Regex::new(pattern).unwrap();
451 for accepted in ["512M", "1G", "4096", "7K"] {
452 assert!(re.is_match(accepted), "pattern rejects {accepted}");
453 assert!(
454 accepted.parse::<MemSize>().is_ok(),
455 "FromStr rejects {accepted}"
456 );
457 }
458 for rejected in ["512MB", "512m", "1.5G", "", "M", "512T", "1P", "512g"] {
459 assert!(!re.is_match(rejected), "pattern accepts {rejected}");
460 assert!(
461 rejected.parse::<MemSize>().is_err(),
462 "FromStr accepts {rejected}"
463 );
464 }
465 }
466}
467
468#[cfg(test)]
469mod up_duration_tests {
470 use super::*;
471
472 #[test]
473 fn plain_digits_are_milliseconds() {
474 assert_eq!("1600".parse::<UpDuration>().unwrap().as_millis(), 1600);
475 }
476
477 #[test]
478 fn units_seconds_minutes_hours() {
479 assert_eq!("30s".parse::<UpDuration>().unwrap().as_millis(), 30_000);
480 assert_eq!("5m".parse::<UpDuration>().unwrap().as_millis(), 300_000);
481 assert_eq!("2h".parse::<UpDuration>().unwrap().as_millis(), 7_200_000);
482 }
483
484 #[test]
488 fn milliseconds_do_not_alias_minutes() {
489 assert_eq!("500ms".parse::<UpDuration>().unwrap().as_millis(), 500);
490 assert_eq!("5ms".parse::<UpDuration>().unwrap().as_millis(), 5);
491 assert_eq!("5m".parse::<UpDuration>().unwrap().as_millis(), 300_000);
492 assert_eq!("1m".parse::<UpDuration>().unwrap().as_millis(), 60_000);
494 }
495
496 #[test]
497 fn rejects_spec_violations() {
498 use ParseUpDurationError::*;
499 assert_eq!("".parse::<UpDuration>(), Err(Empty));
500 assert_eq!("s".parse::<UpDuration>(), Err(MissingDigits));
501 assert_eq!("ms".parse::<UpDuration>(), Err(MissingDigits));
502 assert_eq!("30S".parse::<UpDuration>(), Err(InvalidCharacter)); assert_eq!("1.5s".parse::<UpDuration>(), Err(InvalidCharacter));
504 assert_eq!("30 s".parse::<UpDuration>(), Err(InvalidCharacter));
505 assert_eq!("30MS".parse::<UpDuration>(), Err(InvalidCharacter)); assert_eq!("99999999999999999999h".parse::<UpDuration>(), Err(Overflow));
508 assert_eq!("9999999999999999h".parse::<UpDuration>(), Err(Overflow));
511 }
512
513 #[test]
514 fn display_round_trips() {
515 for ms in [
516 0u64, 1, 999, 1000, 1600, 30_000, 300_000, 7_200_000, 3_601_000,
517 ] {
518 let d = UpDuration::from_millis(ms);
519 assert_eq!(d.to_string().parse::<UpDuration>().unwrap(), d, "{ms}ms");
520 }
521 assert_eq!(UpDuration::from_millis(30_000).to_string(), "30s");
522 assert_eq!(UpDuration::from_millis(1600).to_string(), "1600");
523 assert_eq!(UpDuration::from_millis(7_200_000).to_string(), "2h");
524 }
525
526 #[test]
527 fn serde_uses_string_form() {
528 let d: UpDuration = serde_json::from_str("\"30s\"").unwrap();
529 assert_eq!(d.as_millis(), 30_000);
530 assert_eq!(serde_json::to_string(&d).unwrap(), "\"30s\"");
531 }
532
533 #[cfg(feature = "schema")]
536 #[test]
537 fn the_schema_pattern_agrees_with_from_str() {
538 let schema = serde_json::to_value(schemars::schema_for!(UpDuration)).unwrap();
539 let pattern = schema["pattern"].as_str().unwrap();
540 let re = regex::Regex::new(pattern).unwrap();
541 for accepted in ["1600", "30s", "5m", "2h", "500ms"] {
542 assert!(re.is_match(accepted), "pattern rejects {accepted}");
543 assert!(
544 accepted.parse::<UpDuration>().is_ok(),
545 "FromStr rejects {accepted}"
546 );
547 }
548 for rejected in ["30S", "1.5s", "30 s", "", "s", "30d", "30w", "30MS"] {
549 assert!(!re.is_match(rejected), "pattern accepts {rejected}");
550 assert!(
551 rejected.parse::<UpDuration>().is_err(),
552 "FromStr accepts {rejected}"
553 );
554 }
555 }
556}
557
558#[cfg(test)]
559mod cpu_percent_tests {
560 use core::time::Duration;
561
562 use super::*;
563
564 #[test]
568 fn a_full_core_for_the_whole_window_is_a_hundred_percent() {
569 assert_eq!(cpu_percent(1000, Duration::from_secs(1)), Some(100.0));
570 assert_eq!(cpu_percent(1000, Duration::from_secs(2)), Some(50.0));
571 }
572
573 #[test]
575 fn a_tree_over_one_core_reports_over_a_hundred() {
576 assert_eq!(cpu_percent(4000, Duration::from_secs(1)), Some(400.0));
577 }
578
579 #[test]
582 fn a_zero_window_has_no_honest_answer() {
583 assert_eq!(cpu_percent(1000, Duration::ZERO), None);
584 }
585}