diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3582a88..f16c5d3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -27,10 +27,16 @@ jobs:
- uses: Swatinem/rust-cache@v2
- name: cargo test
- run: cargo test
+ run: cargo test --locked
- name: cargo clippy
- run: cargo clippy --all-targets -- -D warnings
+ run: cargo clippy --all-targets --locked -- -D warnings
+
+ - name: cargo build (test-doomgram example)
+ run: cargo build --examples --features test-doomgram --locked
+
+ - name: cargo doc
+ run: cargo doc --no-deps --locked
- name: rustfmt
run: ./scripts/fmt --check
@@ -41,6 +47,9 @@ jobs:
- name: RUST_TEST_NAMING checker
run: python3 scripts/check_test_names.py
+ - name: DERIVE_LAYOUT checker
+ run: python3 scripts/check_derives.py
+
msrv:
name: MSRV (1.74)
runs-on: ubuntu-latest
@@ -52,4 +61,6 @@ jobs:
- uses: Swatinem/rust-cache@v2
- name: cargo check (library)
+ # Full `cargo test` needs dev-deps (criterion → clap_lex 2024 edition),
+ # which exceeds MSRV 1.74; the stable job runs the full test suite.
run: cargo check --lib --locked
diff --git a/CHANGES.md b/CHANGES.md
index f887c03..1f3e485 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,6 +1,11 @@
# Diagnosticism.Rust - CHANGES
+## 0.4.0 - 30th June 2026
+
+* added `DoomGram::to_mmm()` and `DoomGram::to_nmmm()` — compact min/mean/max duration summaries using `nanoseconds_to_string()`;
+
+
## 0.3.2 - 28th June 2026
* optimisation of `nanoseconds_to_string()` — uses a custom return type `NanosecondsStr` for highly efficient conversion in vast majority of cases;
@@ -11,7 +16,7 @@
* internal implementation improvements;
-## 0.3.0 - 27th June 2026
+## 0.3.0 - 28th June 2026
* added `nanoseconds_to_string()` — compact human-readable duration formatting (behaviour matches **Diagnosticism.Python** 0.16.0);
diff --git a/Cargo.lock b/Cargo.lock
index d6c08b5..6dd1eac 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -212,7 +212,7 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "diagnosticism"
-version = "0.3.2"
+version = "0.4.0"
dependencies = [
"base-traits",
"criterion",
diff --git a/Cargo.toml b/Cargo.toml
index 1ddb285..9a3c442 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,7 +26,7 @@ name = "diagnosticism"
readme = "README.md"
repository = "https://github.com/synesissoftware/Diagnosticism.Rust"
rust-version = "1.74"
-version = "0.3.2"
+version = "0.4.0"
# ##########################################################
diff --git a/EXAMPLES.md b/EXAMPLES.md
index c717e5b..1ae8a18 100644
--- a/EXAMPLES.md
+++ b/EXAMPLES.md
@@ -3,7 +3,7 @@
|Name|Source & Description|Summary|
|---|---|---|
|**debug_squeezer**|[examples/debug_squeezer.rs](/examples/debug_squeezer.rs)
[examples/debug_squeezer.md](/examples/debug_squeezer.md)|An example using **Diagnosticism.Rust**'s `DebugSqueezer` type to simplify the `Debug` form of a user-defined type.|
-|**doomgram**|[examples/doomgram.rs](/examples/doomgram.rs)
[examples/doomgram.md](/examples/doomgram.md)|An example using **Diagnosticism.Rust**'s `DoomGram` type to represent the performance of some time-consuming operations.|
+|**doomgram**|[examples/doomgram.rs](/examples/doomgram.rs)
[examples/doomgram.md](/examples/doomgram.md)|An example using **Diagnosticism.Rust**'s `DoomGram` type to represent the performance of some time-consuming operations, including `to_strip()`, `to_mmm()`, and `to_nmmm()`.|
|**ellipsis**|[examples/ellipsis.rs](/examples/ellipsis.rs)
[examples/ellipsis.md](/examples/ellipsis.md)|An example using **Diagnosticism.Rust**'s `Ellipsis` type to shorten the `Debug` form of a user-defined type.|
|**password**|[examples/password.rs](/examples/password.rs)
[examples/password.md](/examples/password.md)|An example using **Diagnosticism.Rust**'s `Password` type to secure the `Debug` form of a user-defined type.|
diff --git a/README.md b/README.md
index 0d6e7ea..3722325 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,7 @@ Other facilities (that are not directly related to `Debug`) will be added to the
- [Functions](#functions)
- [Macros](#macros)
- [Structures](#structures)
+ - [Redacting `Debug` output (`Ellipsis` and `Password`)](#redacting-debug-output-ellipsis-and-password)
- [Traits](#traits)
- [Examples](#examples)
- [Example - `DoomGram`](#example---doomgram)
@@ -108,7 +109,7 @@ The following macros are defined at the crate root (e.g. `use diagnosticism::fil
The following structures are re-exported at the crate root (and defined in the [`diagnostics`](https://docs.rs/diagnosticism/latest/diagnosticism/diagnostics/index.html) module):
* `DebugSqueezer` - used to assist with restricting the length of `Debug` forms of fields within a given width. See the example [**examples/debug_squeezer.md**](./examples/debug_squeezer.md);
-* `DoomGram` - a **D**ecimal **O**rder-**O**f-**M**agnitude histo**G**ram structure that records efficiently duration values in the orders of magnitude 1ns+, 10ns+, 100ns+, 1µs+, ..., 10s+, 100s+ and provides a mechanism for displaying this histogram in a simple single 12-character display, which is useful for logging cumulative execution costs of components in long-running performance-sensitive applications. See the example [**examples/doomgram.md**](./examples/doomgram.md);
+* `DoomGram` - a **D**ecimal **O**rder-**O**f-**M**agnitude histo**G**ram structure that records efficiently duration values in the orders of magnitude 1ns+, 10ns+, 100ns+, 1µs+, ..., 10s+, 100s+ and provides a mechanism for displaying this histogram in a simple single 12-character display (`to_strip()`), plus compact min/mean/max duration summaries (`to_mmm()` and `to_nmmm()`), which is useful for logging cumulative execution costs of components in long-running performance-sensitive applications. See the example [**examples/doomgram.md**](./examples/doomgram.md);
* `NanosecondsStr` - compact storage for a formatted duration string; returned by `nanoseconds_to_string()`; typical outputs fit in 15 inline UTF-8 bytes without heap allocation; implements `Display`, `Deref` to `str`, and `AsRef`;
* `Ellipsis` - provides the string `"..."` to be used for fields whose `Debug` forms are not to be expressed in terse (non-`#alternate()`) output. See the example [**examples/ellipsis.md**](./examples/ellipsis.md);
* `Password` - provides strings such as `"********"` to be used for fields that are sensitive and whose `Debug` forms are not to be expressed. See the example [**examples/password.md**](./examples/password.md);
@@ -134,16 +135,19 @@ Examples are provided in the ```examples``` directory, along with a markdown des
### Example - `DoomGram`
-The example program **doomgram** (in **examples** directory, built with feature `test-doomgram`), illustrates use of `DoomGram` to capture the order-of-magnitude histogram of a large number of small random delays. The program source is:
+The example program **doomgram** (in **examples** directory, built with feature `test-doomgram`), illustrates use of `DoomGram` to capture the order-of-magnitude histogram of a large number of small random delays, and to format min/mean/max duration summaries. The program source is:
```Rust
// examples/doomgram.rs : example program illustrating use of `DoomGram`
-use diagnosticism::DoomGram;
+use diagnosticism::{
+ doom_scope,
+ DoomGram,
+};
use rand::{
rngs::StdRng,
- RngCore,
+ Rng,
SeedableRng,
};
@@ -172,19 +176,15 @@ fn main() {
}
}
- let before = Instant::now();
-
- if 0 != i % 2000 {
- thread::sleep(Duration::from_nanos(v as u64));
- } else {
- // no wait, so should be very low ns
-
- thread::sleep(Duration::from_secs(0));
- }
-
- let after = Instant::now();
+ doom_scope(&mut dg, || {
+ if 0 != i % 2000 {
+ thread::sleep(Duration::from_nanos(v as u64));
+ } else {
+ // no wait, so should be very low ns
- dg.push_event_duration(after - before);
+ thread::sleep(Duration::from_secs(0));
+ }
+ });
}
// output results on second run through
@@ -194,7 +194,19 @@ fn main() {
let after = Instant::now();
eprintln!("`#to_strip()` : {strip} (in {:?})", after - before);
- eprintln!("");
+
+ let before = Instant::now();
+ let mmm = dg.to_mmm();
+ let after = Instant::now();
+
+ eprintln!("`#to_mmm()` : {mmm} (in {:?})", after - before);
+
+ let before = Instant::now();
+ let nmmm = dg.to_nmmm();
+ let after = Instant::now();
+
+ eprintln!("`#to_nmmm()` : {nmmm} (in {:?})", after - before);
+ eprintln!();
eprintln!("dg={dg:#?}");
}
@@ -207,6 +219,8 @@ and a typical output is:
```plaintext
`#to_strip()` : _aabdedba___ (in 1.763µs)
+`#to_mmm()` : 59ns-644µs-197.3ms (in 245ns)
+`#to_nmmm()` : 20000:59ns-644µs-197.3ms (in 312ns)
dg=DoomGram {
event_count: 20000,
@@ -233,14 +247,14 @@ dg=DoomGram {
}
```
-showing the exploded `Debug` form of the `DoomGram` instance and its timing strip that, for particular execution, obtains the value `"_aabdedba___"` that indicates that there have been:
+showing the exploded `Debug` form of the `DoomGram` instance, its timing strip, and compact min/mean/max summaries. For this execution, the strip obtains the value `"_aabdedba___"`, which indicates that there have been:
- 0 events in the 1ns+, 1s+, 10s+, 100s+ magnitudes;
- 1-9 events in 10ns+, 100ns+, 100ms+ magnitudes;
- 10-99 events in 1µs+, 10ms+ magnitudes;
- 1000-9999 events in 10µs+, 10ms+ magnitudes;
- 10000-99999 events in the 100µs+ magnitude;
-Naturally, in a live system one would not be employing the exploded `Debug` view, relying only on the terse and efficient timing strip format.
+Naturally, in a live system one would not be employing the exploded `Debug` view, relying instead on the terse and efficient `to_strip()`, `to_mmm()`, and `to_nmmm()` formats.
### Example - `Ellipsis`
diff --git a/benches/doomgram.rs b/benches/doomgram.rs
index 54dd5a5..32b17d1 100644
--- a/benches/doomgram.rs
+++ b/benches/doomgram.rs
@@ -13,15 +13,104 @@ use criterion::{
};
-#[rustfmt::skip]
-mod constants {
+fn doomgram_empty() -> DoomGram {
+ DoomGram::default()
+}
+
+
+fn doomgram_single() -> DoomGram {
+ let mut dg = DoomGram::default();
+
+ dg.push_event_time_ms(13);
+
+ dg
+}
+
+
+fn doomgram_uniform() -> DoomGram {
+ let mut dg = DoomGram::default();
+
+ dg.push_event_time_s(1);
+ dg.push_event_time_s(1);
+ dg.push_event_time_s(1);
+
+ dg
+}
+
+
+fn doomgram_min_mean_max() -> DoomGram {
+ let mut dg = DoomGram::default();
+
+ dg.push_event_time_s(1);
+ dg.push_event_time_s(2);
+
+ dg
+}
+
+
+fn doomgram_uniform_spread() -> DoomGram {
+ let mut dg = DoomGram::default();
+
+ dg.push_event_time_ns(9);
+ dg.push_event_time_ns(80);
+ dg.push_event_time_ns(700);
+ dg.push_event_time_us(6);
+ dg.push_event_time_us(50);
+ dg.push_event_time_us(400);
+ dg.push_event_time_ms(3);
+ dg.push_event_time_ms(20);
+ dg.push_event_time_ms(100);
+ dg.push_event_time_s(9);
+ dg.push_event_time_s(80);
+ dg.push_event_time_s(700);
+
+ dg
+}
+
+fn doomgram_overflowed() -> DoomGram {
+ let mut dg = DoomGram::default();
+
+ dg.push_event_time_us(18446744073709550);
+ dg.push_event_time_us(1);
+ dg.push_event_time_us(0);
+ dg.push_event_time_us(1);
+
+ dg
}
-#[rustfmt::skip]
-mod implementation {
+fn bench_to_mmm(
+ c : &mut Criterion,
+ dg : DoomGram,
+ label : &str,
+) {
+ let id = format!("`DoomGram::to_mmm()` [{label}]");
+ c.bench_function(&id, |b| {
+ b.iter(|| {
+ let s = black_box(black_box(&dg).to_mmm());
+
+ black_box(s)
+ })
+ });
+}
+
+
+fn bench_to_nmmm(
+ c : &mut Criterion,
+ dg : DoomGram,
+ label : &str,
+) {
+ let id = format!("`DoomGram::to_nmmm()` [{label}]");
+
+ c.bench_function(&id, |b| {
+ b.iter(|| {
+ let s = black_box(black_box(&dg).to_nmmm());
+
+ black_box(s)
+ })
+ });
}
@@ -86,6 +175,26 @@ pub fn BENCHMARK_DoomGram_push_s(c : &mut Criterion) {
}
+pub fn BENCHMARK_DoomGram_to_mmm(c : &mut Criterion) {
+ bench_to_mmm(c, doomgram_empty(), "empty");
+ bench_to_mmm(c, doomgram_single(), "single");
+ bench_to_mmm(c, doomgram_uniform(), "uniform");
+ bench_to_mmm(c, doomgram_min_mean_max(), "min-mean-max");
+ bench_to_mmm(c, doomgram_uniform_spread(), "uniform spread");
+ bench_to_mmm(c, doomgram_overflowed(), "overflow");
+}
+
+
+pub fn BENCHMARK_DoomGram_to_nmmm(c : &mut Criterion) {
+ bench_to_nmmm(c, doomgram_empty(), "empty");
+ bench_to_nmmm(c, doomgram_single(), "single");
+ bench_to_nmmm(c, doomgram_uniform(), "uniform");
+ bench_to_nmmm(c, doomgram_min_mean_max(), "min-mean-max");
+ bench_to_nmmm(c, doomgram_uniform_spread(), "uniform spread");
+ bench_to_nmmm(c, doomgram_overflowed(), "overflow");
+}
+
+
criterion_group!(
benches,
// construction
@@ -96,5 +205,8 @@ criterion_group!(
BENCHMARK_DoomGram_push_us,
BENCHMARK_DoomGram_push_ms,
BENCHMARK_DoomGram_push_s,
+ // formatting
+ BENCHMARK_DoomGram_to_mmm,
+ BENCHMARK_DoomGram_to_nmmm,
);
criterion_main!(benches);
diff --git a/examples/doomgram.md b/examples/doomgram.md
index a19d462..0bdd824 100644
--- a/examples/doomgram.md
+++ b/examples/doomgram.md
@@ -2,7 +2,7 @@
## Summary
-An example using **Diagnosticism.Rust**'s `DoomGram` type to represent the performance of some time-consuming operations.
+An example using **Diagnosticism.Rust**'s `DoomGram` type to represent the performance of some time-consuming operations, including the 12-character histogram strip ([`to_strip()`](https://docs.rs/diagnosticism/latest/diagnosticism/struct.DoomGram.html#method.to_strip)) and compact min/mean/max summaries ([`to_mmm()`](https://docs.rs/diagnosticism/latest/diagnosticism/struct.DoomGram.html#method.to_mmm), [`to_nmmm()`](https://docs.rs/diagnosticism/latest/diagnosticism/struct.DoomGram.html#method.to_nmmm)).
## Source
@@ -10,11 +10,14 @@ An example using **Diagnosticism.Rust**'s `DoomGram` type to represent the perfo
```Rust
// examples/doomgram.rs : example program illustrating use of `DoomGram`
-use diagnosticism::DoomGram;
+use diagnosticism::{
+ doom_scope,
+ DoomGram,
+};
use rand::{
rngs::StdRng,
- RngCore,
+ Rng,
SeedableRng,
};
@@ -43,19 +46,15 @@ fn main() {
}
}
- let before = Instant::now();
-
- if 0 != i % 2000 {
- thread::sleep(Duration::from_nanos(v as u64));
- } else {
- // no wait, so should be very low ns
-
- thread::sleep(Duration::from_secs(0));
- }
-
- let after = Instant::now();
+ doom_scope(&mut dg, || {
+ if 0 != i % 2000 {
+ thread::sleep(Duration::from_nanos(v as u64));
+ } else {
+ // no wait, so should be very low ns
- dg.push_event_duration(after - before);
+ thread::sleep(Duration::from_secs(0));
+ }
+ });
}
// output results on second run through
@@ -65,7 +64,19 @@ fn main() {
let after = Instant::now();
eprintln!("`#to_strip()` : {strip} (in {:?})", after - before);
- eprintln!("");
+
+ let before = Instant::now();
+ let mmm = dg.to_mmm();
+ let after = Instant::now();
+
+ eprintln!("`#to_mmm()` : {mmm} (in {:?})", after - before);
+
+ let before = Instant::now();
+ let nmmm = dg.to_nmmm();
+ let after = Instant::now();
+
+ eprintln!("`#to_nmmm()` : {nmmm} (in {:?})", after - before);
+ eprintln!();
eprintln!("dg={dg:#?}");
}
@@ -87,6 +98,8 @@ it gives the output:
```
`#to_strip()` : _aacdeda____ (in 18.765µs)
+`#to_mmm()` : 66ns-593.2µs-13.55ms (in 245ns)
+`#to_nmmm()` : 20000:66ns-593.2µs-13.55ms (in 312ns)
dg=DoomGram {
event_count: 20000,
@@ -114,5 +127,12 @@ dg=DoomGram {
```
+The three formatted lines use complementary views of the same data:
+
+* **`to_strip()`** — 12-character order-of-magnitude histogram (each position counts events in a decade band);
+* **`to_mmm()`** — min, mean, and max event durations as compact strings via [`nanoseconds_to_string()`](https://docs.rs/diagnosticism/latest/diagnosticism/fn.nanoseconds_to_string.html);
+* **`to_nmmm()`** — the same min/mean/max summary prefixed with the event count (`"{count}:{min}-{mean}-{max}"`).
+
+
diff --git a/examples/doomgram.rs b/examples/doomgram.rs
index 856e6a7..cd28a5c 100644
--- a/examples/doomgram.rs
+++ b/examples/doomgram.rs
@@ -7,7 +7,7 @@ use diagnosticism::{
use rand::{
rngs::StdRng,
- RngCore,
+ Rng,
SeedableRng,
};
@@ -54,6 +54,18 @@ fn main() {
let after = Instant::now();
eprintln!("`#to_strip()` : {strip} (in {:?})", after - before);
+
+ let before = Instant::now();
+ let mmm = dg.to_mmm();
+ let after = Instant::now();
+
+ eprintln!("`#to_mmm()` : {mmm} (in {:?})", after - before);
+
+ let before = Instant::now();
+ let nmmm = dg.to_nmmm();
+ let after = Instant::now();
+
+ eprintln!("`#to_nmmm()` : {nmmm} (in {:?})", after - before);
eprintln!();
eprintln!("dg={dg:#?}");
}
diff --git a/scripts/check_derives.py b/scripts/check_derives.py
old mode 100644
new mode 100755
diff --git a/src/diagnostics/doomgram.rs b/src/diagnostics/doomgram.rs
index e122885..707e0f1 100644
--- a/src/diagnostics/doomgram.rs
+++ b/src/diagnostics/doomgram.rs
@@ -314,6 +314,29 @@ impl DoomGram {
self.num_events_ge_100s
}
+ /// Returns min, mean, and max event times as a compact duration string.
+ ///
+ /// Each duration is formatted by [`crate::nanoseconds_to_string`]. When
+ /// [`Self::event_count()`] is zero, returns an empty string. When
+ /// [`Self::has_overflowed()`] is true, returns `"OVERFLOW"`. When there
+ /// is one event, or min and max are equal, returns a single formatted
+ /// duration; otherwise returns `min-mean-max` separated by `-`.
+ ///
+ /// Mean is [`Self::event_time_total_raw()`] divided by
+ /// [`Self::event_count()`].
+ pub fn to_mmm(&self) -> String {
+ self.to_mmm_impl_()
+ }
+
+ /// Like [`Self::to_mmm()`], prefixed with the event count and `:`.
+ ///
+ /// When [`Self::event_count()`] is zero, returns `"0:"`. When
+ /// [`Self::has_overflowed()`] is true, returns
+ /// `":OVERFLOW"`.
+ pub fn to_nmmm(&self) -> String {
+ self.to_nmmm_impl_()
+ }
+
/// Returns a fixed 12-character ASCII strip for the histogram.
///
/// Each position encodes the order-of-magnitude of the event count in
@@ -437,6 +460,78 @@ impl DoomGram {
}
}
+ fn to_mmm_impl_(
+ &self,
+ ) -> String {
+ use super::time_format::nanoseconds_to_string;
+
+ const OVERFLOW : &str = "OVERFLOW";
+
+ let count = self.event_count();
+
+ if 0 == count {
+ return String::new();
+ }
+
+ if self.has_overflowed() {
+ return OVERFLOW.into();
+ }
+
+ let min_ns = self.min_event_time().unwrap() as i64;
+ let max_ns = self.max_event_time().unwrap() as i64;
+
+ let body = if 1 == count || min_ns == max_ns {
+ format!("{}", nanoseconds_to_string(min_ns, ""))
+ } else {
+ let mean_ns = (self.event_time_total_raw() / count as u64) as i64;
+
+ format!(
+ "{}-{}-{}",
+ nanoseconds_to_string(min_ns, ""),
+ nanoseconds_to_string(mean_ns, ""),
+ nanoseconds_to_string(max_ns, ""),
+ )
+ };
+
+ body
+ }
+
+ fn to_nmmm_impl_(
+ &self,
+ ) -> String {
+ use super::time_format::nanoseconds_to_string;
+
+ const OVERFLOW : &str = "OVERFLOW";
+
+ let count = self.event_count();
+
+ if 0 == count {
+ return "0:".into();
+ }
+
+ if self.has_overflowed() {
+ return format!("{count}:{OVERFLOW}");
+ }
+
+ let min_ns = self.min_event_time().unwrap() as i64;
+ let max_ns = self.max_event_time().unwrap() as i64;
+
+ let body = if 1 == count || min_ns == max_ns {
+ format!("{count}:{}", nanoseconds_to_string(min_ns, ""))
+ } else {
+ let mean_ns = (self.event_time_total_raw() / count as u64) as i64;
+
+ format!(
+ "{count}:{}-{}-{}",
+ nanoseconds_to_string(min_ns, ""),
+ nanoseconds_to_string(mean_ns, ""),
+ nanoseconds_to_string(max_ns, ""),
+ )
+ };
+
+ body
+ }
+
fn try_add_ns_to_total_and_update_minmax_and_count_(
&mut self,
time_in_ns : u64,
@@ -890,6 +985,82 @@ mod tests {
assert_eq!("_a_aa___aa_a", dg.to_strip());
}
+
+ #[test]
+ fn TEST_DoomGram_to_mmm_EMPTY() {
+ let dg = DoomGram::default();
+
+ assert_eq!("", dg.to_mmm());
+ assert_eq!("0:", dg.to_nmmm());
+ }
+
+
+ #[test]
+ fn TEST_DoomGram_to_mmm_SINGLE() {
+ let mut dg = DoomGram::default();
+
+ dg.push_event_time_ms(13);
+
+ assert_eq!("13ms", dg.to_mmm());
+ assert_eq!("1:13ms", dg.to_nmmm());
+ }
+
+
+ #[test]
+ fn TEST_DoomGram_to_mmm_UNIFORM() {
+ let mut dg = DoomGram::default();
+
+ dg.push_event_time_s(1);
+ dg.push_event_time_s(1);
+ dg.push_event_time_s(1);
+
+ assert_eq!("1s", dg.to_mmm());
+ assert_eq!("3:1s", dg.to_nmmm());
+ }
+
+
+ #[test]
+ fn TEST_DoomGram_to_mmm_MIN_MEAN_MAX() {
+ let mut dg = DoomGram::default();
+
+ dg.push_event_time_s(1);
+ dg.push_event_time_s(2);
+
+ assert_eq!("1s-1.500s-2s", dg.to_mmm());
+ assert_eq!("2:1s-1.500s-2s", dg.to_nmmm());
+ }
+
+
+ #[test]
+ fn TEST_DoomGram_to_mmm_ZERO_EVENTS_ALL_SAME() {
+ let mut dg = DoomGram::default();
+
+ dg.push_event_time_ns(0);
+ dg.push_event_time_us(0);
+
+ assert_eq!("0s", dg.to_mmm());
+ assert_eq!("2:0s", dg.to_nmmm());
+ }
+
+
+ #[test]
+ fn TEST_DoomGram_to_mmm_OVERFLOW() {
+ let mut dg = DoomGram::default();
+
+ dg.push_event_time_us(18446744073709550);
+ dg.push_event_time_us(1);
+ dg.push_event_time_us(0);
+
+ assert!(!dg.push_event_time_us(1));
+
+ assert!(dg.has_overflowed());
+ assert_eq!(3, dg.event_count());
+
+ assert_eq!("OVERFLOW", dg.to_mmm());
+ assert_eq!("3:OVERFLOW", dg.to_nmmm());
+ }
+
+
#[test]
fn TEST_DoomGram_OVERFLOW_BY_SECONDS() {
diff --git a/src/diagnostics/time_format/mod.rs b/src/diagnostics/time_format/mod.rs
index db5d713..82cdb64 100644
--- a/src/diagnostics/time_format/mod.rs
+++ b/src/diagnostics/time_format/mod.rs
@@ -3,11 +3,10 @@
// NOTE: this work was brought in from **asynkio** via **Diagnosticism.Python**
// 0.16.0
-mod format;
-mod nanoseconds_str;
+use crate::macros::declare_and_publish;
-pub use format::nanoseconds_to_string;
-pub use nanoseconds_str::NanosecondsStr;
+declare_and_publish!(format, nanoseconds_to_string);
+declare_and_publish!(nanoseconds_str, NanosecondsStr);
// ///////////////////////////// end of file //////////////////////////// //
diff --git a/src/diagnostics/time_format/nanoseconds_str.rs b/src/diagnostics/time_format/nanoseconds_str.rs
index 500c41d..17590db 100644
--- a/src/diagnostics/time_format/nanoseconds_str.rs
+++ b/src/diagnostics/time_format/nanoseconds_str.rs
@@ -14,7 +14,7 @@ const INLINE_CAP : usize = 15;
/// Compact storage for a formatted nanosecond duration string.
///
-/// Obtain values from [`nanoseconds_to_string`]. Most outputs fit in
+/// Obtain values from [`crate::nanoseconds_to_string`]. Most outputs fit in
/// [`INLINE_CAP`] UTF-8 bytes and are stored inline without heap
/// allocation. Longer results use a [`String`] variant.
#[derive(Clone)]
diff --git a/src/lib.rs b/src/lib.rs
index 8602aae..a6d8154 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -42,7 +42,8 @@
//! * [`DebugSqueezer`] — restrict the length of
//! [`Debug`](std::fmt::Debug) output for individual fields;
//! * [`DoomGram`] — decimal order-of-magnitude histogram with a compact
-//! 12-character strip for logging;
+//! 12-character strip for logging, plus [`DoomGram::to_mmm`] and
+//! [`DoomGram::to_nmmm`] min/mean/max duration summaries;
//! * [`Ellipsis`] — emit `"..."` for redacted
//! [`Debug`](std::fmt::Debug) fields;
//! * [`Password`] — emit a run of `*` characters for sensitive