Repository: bencherdev/bencher (language: rust)

Issue:
`rust-iai-callgrind` fails to collect all metrics
`iai-callgrind` has many tools. By default, it only runs `CALLGRIND`, which reports the following six metrics:

```log
iai::benchmarks::test1
  Instructions:          3102359729|3102359161      (+0.00002%) [+1.00000x]
  L1 Hits:               4435753829|4435875893      (-0.00275%) [-1.00003x]
  L2 Hits:                  1199262|1076439         (+11.4101%) [+1.11410x]
  RAM Hits:                   25535|25449           (+0.33793%) [+1.00338x]
  Total read+write:      4436978626|4436977781      (+0.00002%) [+1.00000x]
  Estimated Cycles:      4442643864|4442148803      (+0.01114%) [+1.00011x]
```

But when the `DHAT` tool is additionally enabled, the following metrics are reported as well (and the output is split into two sections):

```log
iai::benchmarks::test1
  ======= CALLGRIND =======================================================
  Instructions:          3102359729|3102359729      (No change)
  L1 Hits:               4435921602|4435921602      (No change)
  L2 Hits:                  1031503|1031503         (No change)
  RAM Hits:                   25521|25521           (No change)
  Total read+write:      4436978626|4436978626      (No change)
  Estimated Cycles:      4441972352|4441972352      (No change)
  ======= DHAT ============================================================
  Total bytes:             26294939|26294939        (No change)
  Total blocks:             2328086|2328086         (No change)
  At t-gmax bytes:           933718|933718          (No change)
  At t-gmax blocks:           18344|18344           (No change)
  At t-end bytes:                 0|0               (No change)
  At t-end blocks:                0|0               (No change)
  Reads bytes:             47577425|47577425        (No change)
  Writes bytes:            37733810|37733810        (No change)
```

Currently they are completely ignored by `bencher run`. I think they need to be added to the list [here](https://github.com/bencherdev/bencher/blob/8be4539d865070820aa8dbc6d6ee763db2f6c02c/lib/bencher_adapter/src/adapters/rust/iai_callgrind.rs#L67-L98).

Additionally, the benchmark name is incorrectly picked up as ` ======= CALLGRIND =======================================================`, instead of `iai::benchmarks::test1`, which should be handled when fixing this.

Here is a Rust repro that enables this additional tool:

```rust
#[library_benchmark]
fn test1() {
    black_box(execute_something());
}

library_benchmark_group!(
    name = benchmarks;
    benchmarks = test1
);

main!(
    config = LibraryBenchmarkConfig::default().tool(Tool::new(ValgrindTool::DHAT));
    library_benchmark_groups = benchmarks
);
```

cc @epompeii 


Discussion / hints:
Thank you for pointing this out.
When `iai-callgrind` support was added in https://github.com/bencherdev/bencher/pull/319 I don't think this was under consideration.

I'm open to a PR to add support for this. @OmarTawfik you are welcome to do so. Also tagging @JonasWanke in case he is interested.

Whoever implements this needs to make sure that the ASCII escaping is properly handled: https://github.com/bencherdev/bencher/issues/345


I'll take a stab at it. Thanks for the pointers!

Fix patch:
diff --git a/lib/bencher_adapter/src/adapters/magic.rs b/lib/bencher_adapter/src/adapters/magic.rs
index fa0555051..3482f09b5 100644
--- a/lib/bencher_adapter/src/adapters/magic.rs
+++ b/lib/bencher_adapter/src/adapters/magic.rs
@@ -133,8 +133,9 @@ mod test_magic {
 
     #[test]
     fn test_adapter_magic_rust_iai_callgrind() {
-        let results = convert_file_path::<AdapterMagic>("./tool_output/rust/iai_callgrind/two.txt");
-        test_rust_iai_callgrind::validate_adapter_rust_iai_callgrind(&results);
+        let results =
+            convert_file_path::<AdapterMagic>("./tool_output/rust/iai_callgrind/single-tool.txt");
+        test_rust_iai_callgrind::validate_adapter_rust_iai_callgrind(&results, true, false);
     }
 
     #[test]
diff --git a/lib/bencher_adapter/src/adapters/mod.rs b/lib/bencher_adapter/src/adapters/mod.rs
index daa659ccd..f14a4d7dc 100644
--- a/lib/bencher_adapter/src/adapters/mod.rs
+++ b/lib/bencher_adapter/src/adapters/mod.rs
@@ -25,7 +25,10 @@ fn print_ln(input: &str) -> IResult<&str, ()> {
 #[allow(clippy::panic, clippy::unwrap_used)]
 pub(crate) mod test_util {
     use bencher_json::project::{
-        measure::{LATENCY_SLUG_STR, THROUGHPUT_SLUG_STR},
+        measure::built_in::{
+            generic::{Latency, Throughput},
+            BuiltInMeasure,
+        },
         report::JsonAverage,
     };
     use ordered_float::OrderedFloat;
@@ -70,7 +73,7 @@ pub(crate) mod test_util {
         lower_value: Option<f64>,
         upper_value: Option<f64>,
     ) {
-        validate_metric(metrics, LATENCY_SLUG_STR, value, lower_value, upper_value);
+        validate_metric(metrics, Latency::SLUG_STR, value, lower_value, upper_value);
     }
 
     pub fn validate_throughput(
@@ -81,7 +84,7 @@ pub(crate) mod test_util {
     ) {
         validate_metric(
             metrics,
-            THROUGHPUT_SLUG_STR,
+            Throughput::SLUG_STR,
             value,
             lower_value,
             upper_value,
diff --git a/lib/bencher_adapter/src/adapters/rust/iai.rs b/lib/bencher_adapter/src/adapters/rust/iai.rs
index 30c1f05f6..ac1acb51c 100644
--- a/lib/bencher_adapter/src/adapters/rust/iai.rs
+++ b/lib/bencher_adapter/src/adapters/rust/iai.rs
@@ -1,8 +1,8 @@
 use bencher_json::{
     project::{
-        measure::{
-            ESTIMATED_CYCLES_NAME_STR, INSTRUCTIONS_NAME_STR, L1_ACCESSES_NAME_STR,
-            L2_ACCESSES_NAME_STR, RAM_ACCESSES_NAME_STR,
+        measure::built_in::{
+            iai::{EstimatedCycles, Instructions, L1Accesses, L2Accesses, RamAccesses},
+            BuiltInMeasure,
         },
         report::JsonAverage,
     },
@@ -63,27 +63,27 @@ fn parse_iai_lines(
     #[allow(trivial_casts)]
     let metrics = [
         (
-            INSTRUCTIONS_NAME_STR,
+            Instructions::NAME_STR,
             instructions_line,
             IaiMeasure::Instructions as fn(JsonNewMetric) -> IaiMeasure,
         ),
         (
-            L1_ACCESSES_NAME_STR,
+            L1Accesses::NAME_STR,
             l1_accesses_line,
             IaiMeasure::L1Accesses,
         ),
         (
-            L2_ACCESSES_NAME_STR,
+            L2Accesses::NAME_STR,
             l2_accesses_line,
             IaiMeasure::L2Accesses,
         ),
         (
-            RAM_ACCESSES_NAME_STR,
+            RamAccesses::NAME_STR,
             ram_accesses_line,
             IaiMeasure::RamAccesses,
         ),
         (
-            ESTIMATED_CYCLES_NAME_STR,
+            EstimatedCycles::NAME_STR,
             estimated_cycles_line,
             IaiMeasure::EstimatedCycles,
         ),
@@ -145,9 +145,9 @@ pub(crate) mod test_rust_iai {
         Adaptable, AdapterResults,
     };
     use bencher_json::{
-        project::measure::{
-            ESTIMATED_CYCLES_SLUG_STR, INSTRUCTIONS_NAME_STR, INSTRUCTIONS_SLUG_STR,
-            L1_ACCESSES_SLUG_STR, L2_ACCESSES_SLUG_STR, RAM_ACCESSES_SLUG_STR,
+        project::measure::built_in::{
+            iai::{EstimatedCycles, Instructions, L1Accesses, L2Accesses, RamAccesses},
+            BuiltInMeasure,
         },
         JsonNewMetric,
     };
@@ -174,7 +174,7 @@ pub(crate) mod test_rust_iai {
     #[test]
     fn test_adapter_rust_iai_parse_line() {
         assert_eq!(
-            super::parse_iai_metric("  Instructions:  1234", INSTRUCTIONS_NAME_STR),
+            super::parse_iai_metric("  Instructions:  1234", Instructions::NAME_STR),
             Ok((
                 "",
                 JsonNewMetric {
@@ -186,7 +186,7 @@ pub(crate) mod test_rust_iai {
         );
 
         assert_eq!(
-            super::parse_iai_metric("  Instructions:  1234 (No change)", INSTRUCTIONS_NAME_STR),
+            super::parse_iai_metric("  Instructions:  1234 (No change)", Instructions::NAME_STR),
             Ok((
                 "",
                 JsonNewMetric {
@@ -198,7 +198,7 @@ pub(crate) mod test_rust_iai {
         );
 
         assert_eq!(
-            super::parse_iai_metric("  Instructions:  1234 (+3.14%)", INSTRUCTIONS_NAME_STR),
+            super::parse_iai_metric("  Instructions:  1234 (+3.14%)", Instructions::NAME_STR),
             Ok((
                 "",
                 JsonNewMetric {
@@ -235,22 +235,22 @@ pub(crate) mod test_rust_iai {
         validate_iai(
             metrics,
             [
-                (INSTRUCTIONS_SLUG_STR, 1735.0),
-                (L1_ACCESSES_SLUG_STR, 2364.0),
-                (L2_ACCESSES_SLUG_STR, 1.0),
-                (RAM_ACCESSES_SLUG_STR, 1.0),
-                (ESTIMATED_CYCLES_SLUG_STR, 2404.0),
+                (Instructions::SLUG_STR, 1735.0),
+                (L1Accesses::SLUG_STR, 2364.0),
+                (L2Accesses::SLUG_STR, 1.0),
+                (RamAccesses::SLUG_STR, 1.0),
+                (EstimatedCycles::SLUG_STR, 2404.0),
             ],
         );
         let metrics = results.get("bench_fibonacci_long").unwrap();
         validate_iai(
             metrics,
             [
-                (INSTRUCTIONS_SLUG_STR, 26_214_735.0),
-                (L1_ACCESSES_SLUG_STR, 35_638_623.0),
-                (L2_ACCESSES_SLUG_STR, 2.0),
-                (RAM_ACCESSES_SLUG_STR, 1.0),
-                (ESTIMATED_CYCLES_SLUG_STR, 35_638_668.0),
+                (Instructions::SLUG_STR, 26_214_735.0),
+                (L1Accesses::SLUG_STR, 35_638_623.0),
+                (L2Accesses::SLUG_STR, 2.0),
+                (RamAccesses::SLUG_STR, 1.0),
+                (EstimatedCycles::SLUG_STR, 35_638_668.0),
             ],
         );
     }
@@ -264,22 +264,22 @@ pub(crate) mod test_rust_iai {
         validate_iai(
             metrics,
             [
-                (INSTRUCTIONS_SLUG_STR, 1243.0),
-                (L1_ACCESSES_SLUG_STR, 1580.0),
-                (L2_ACCESSES_SLUG_STR, 1.0),
-                (RAM_ACCESSES_SLUG_STR, 2.0),
-                (ESTIMATED_CYCLES_SLUG_STR, 1655.0),
+                (Instructions::SLUG_STR, 1243.0),
+                (L1Accesses::SLUG_STR, 1580.0),
+                (L2Accesses::SLUG_STR, 1.0),
+                (RamAccesses::SLUG_STR, 2.0),
+                (EstimatedCycles::SLUG_STR, 1655.0),
             ],
         );
         let metrics = results.get("iai_benchmark_long").unwrap();
         validate_iai(
             metrics,
             [
-                (INSTRUCTIONS_SLUG_STR, 18_454_953.0),
-                (L1_ACCESSES_SLUG_STR, 23_447_195.0),
-                (L2_ACCESSES_SLUG_STR, 6.0),
-                (RAM_ACCESSES_SLUG_STR, 2.0),
-                (ESTIMATED_CYCLES_SLUG_STR, 23_447_295.0),
+                (Instructions::SLUG_STR, 18_454_953.0),
+                (L1Accesses::SLUG_STR, 23_447_195.0),
+                (L2Accesses::SLUG_STR, 6.0),
+                (RamAccesses::SLUG_STR, 2.0),
+                (EstimatedCycles::SLUG_STR, 23_447_295.0),
             ],
         );
     }
diff --git a/lib/bencher_adapter/src/adapters/rust/iai_callgrind.rs b/lib/bencher_adapter/src/adapters/rust/iai_callgrind.rs
index eaf418755..c7fe08e2a 100644
--- a/lib/bencher_adapter/src/adapters/rust/iai_callgrind.rs
+++ b/lib/bencher_adapter/src/adapters/rust/iai_callgrind.rs
@@ -1,16 +1,20 @@
 use bencher_json::{
     project::{
-        measure::{ESTIMATED_CYCLES_NAME_STR, INSTRUCTIONS_NAME_STR},
+        measure::built_in::{
+            iai_callgrind::{callgrind_tool, dhat_tool},
+            BuiltInMeasure,
+        },
         report::JsonAverage,
     },
     BenchmarkName, JsonNewMetric,
 };
 use nom::{
     branch::alt,
-    bytes::complete::tag,
+    bytes::complete::{is_a, is_not, tag},
     character::complete::{space0, space1},
-    combinator::{eof, map},
-    sequence::{delimited, tuple},
+    combinator::{map, opt, recognize},
+    multi::{many0, many1},
+    sequence::{delimited, preceded, terminated, tuple},
     IResult,
 };
 
@@ -22,313 +26,337 @@ use crate::{
 
 pub struct AdapterRustIaiCallgrind;
 
-const IAI_CALLGRIND_METRICS_LINE_COUNT: usize = 7;
-const L1_HITS_NAME_STR: &str = "L1 Hits";
-const L2_HITS_NAME_STR: &str = "L2 Hits";
-const RAM_HITS_NAME_STR: &str = "RAM Hits";
-const TOTAL_READ_WRITE_NAME_STR: &str = "Total read+write";
-
 impl Adaptable for AdapterRustIaiCallgrind {
     fn parse(input: &str, settings: Settings) -> Option<AdapterResults> {
         match settings.average {
             None => {},
-            Some(JsonAverage::Mean | JsonAverage::Median) => return None,
-        }
-        // Clean up the input by removing ANSI escape codes.
+            Some(JsonAverage::Mean | JsonAverage::Median) => {
+                return None; // 'iai_callgrind' results are for a single run only.
+            },
+        };
+
+        // Clean up the input by removing ANSI escape codes:
         let input = strip_ansi_escapes::strip_str(input);
 
-        let mut benchmark_metrics = Vec::new();
-        let lines = input.lines().collect::<Vec<_>>();
-        for lines in lines.windows(IAI_CALLGRIND_METRICS_LINE_COUNT) {
-            let Ok(lines) = lines.try_into() else {
-                debug_assert!(
-                    false,
-                    "Windows struct should always be convertible to array of the same size."
-                );
-                continue;
-            };
-            if let Some((benchmark_name, metrics)) = parse_iai_lines(lines) {
-                benchmark_metrics.push((benchmark_name, metrics));
-            }
-        }
+        let benchmarks = match multiple_benchmarks()(&input) {
+            Err(error) => {
+                debug_assert!(false, "Error parsing input:\n{error:#?}");
+                return None;
+            },
+            Ok((remainder, benchmarks)) => {
+                debug_assert_eq!(remainder.len(), 0, "Unparsed trailing input:\n{remainder}");
+                benchmarks
+            },
+        };
 
-        AdapterResults::new_iai_callgrind(benchmark_metrics)
+        AdapterResults::new_iai_callgrind(benchmarks)
     }
 }
 
-fn parse_iai_lines(
-    lines: [&str; IAI_CALLGRIND_METRICS_LINE_COUNT],
-) -> Option<(BenchmarkName, Vec<IaiCallgrindMeasure>)> {
-    let [benchmark_name_line, instructions_line, l1_accesses_line, l2_accesses_line, ram_accesses_line, total_read_write_line, estimated_cycles_line] =
-        lines;
-
-    let name = benchmark_name_line.parse().ok()?;
-    #[allow(trivial_casts)]
-    let metrics = [
-        (
-            INSTRUCTIONS_NAME_STR,
-            instructions_line,
-            IaiCallgrindMeasure::Instructions as fn(JsonNewMetric) -> IaiCallgrindMeasure,
-        ),
-        (
-            L1_HITS_NAME_STR,
-            l1_accesses_line,
-            IaiCallgrindMeasure::L1Accesses,
-        ),
-        (
-            L2_HITS_NAME_STR,
-            l2_accesses_line,
-            IaiCallgrindMeasure::L2Accesses,
-        ),
-        (
-            RAM_HITS_NAME_STR,
-            ram_accesses_line,
-            IaiCallgrindMeasure::RamAccesses,
-        ),
-        (
-            TOTAL_READ_WRITE_NAME_STR,
-            total_read_write_line,
-            IaiCallgrindMeasure::TotalReadWrite,
+fn multiple_benchmarks<'a>(
+) -> impl FnMut(&'a str) -> IResult<&'a str, Vec<(BenchmarkName, Vec<IaiCallgrindMeasure>)>> {
+    map(
+        many0(alt((
+            // Try to parse a single benchmark:
+            single_benchmark(),
+            // Otherwise, parse/ignore unrelated lines:
+            map(terminated(not_line_ending(), opt(line_ending())), |_| None),
+            // Otherwise, parse/ignore empty lines:
+            map(line_ending(), |_| None),
+        ))),
+        // Skip 'None' resulting from empty/unrelated lines:
+        |benchmarks| benchmarks.into_iter().flatten().collect(),
+    )
+}
+
+fn single_benchmark<'a>(
+) -> impl FnMut(&'a str) -> IResult<&'a str, Option<(BenchmarkName, Vec<IaiCallgrindMeasure>)>> {
+    map(
+        tuple((
+            terminated(recognize(not_line_ending()), line_ending()),
+            // Callgrind tool is always enabled:
+            callgrind_tool_measures(),
+            // Add DHAT tool measures if it was enabled:
+            opt(dhat_tool_measures()),
+        )),
+        |(benchmark_name, callgrind_measures, dhat_measures)| {
+            let benchmark_name = benchmark_name.parse().ok()?;
+
+            let mut measures = vec![];
+            measures.extend(callgrind_measures);
+            measures.extend(dhat_measures.into_iter().flatten());
+
+            Some((benchmark_name, measures))
+        },
+    )
+}
+
+fn callgrind_tool_measures<'a>() -> impl FnMut(&'a str) -> IResult<&'a str, Vec<IaiCallgrindMeasure>>
+{
+    map(
+        preceded(
+            opt(tool_name_line("CALLGRIND")),
+            tuple((
+                metric_line(callgrind_tool::Instructions::NAME_STR),
+                metric_line(callgrind_tool::L1Hits::NAME_STR),
+                metric_line(callgrind_tool::L2Hits::NAME_STR),
+                metric_line(callgrind_tool::RamHits::NAME_STR),
+                metric_line(callgrind_tool::TotalReadWrite::NAME_STR),
+                metric_line(callgrind_tool::EstimatedCycles::NAME_STR),
+            )),
         ),
-        (
-            ESTIMATED_CYCLES_NAME_STR,
-            estimated_cycles_line,
-            IaiCallgrindMeasure::EstimatedCycles,
+        |(instructions, l1_hits, l2_hits, ram_hits, total_read_write, estimated_cycles)| {
+            vec![
+                IaiCallgrindMeasure::Instructions(instructions),
+                IaiCallgrindMeasure::L1Hits(l1_hits),
+                IaiCallgrindMeasure::L2Hits(l2_hits),
+                IaiCallgrindMeasure::RamHits(ram_hits),
+                IaiCallgrindMeasure::TotalReadWrite(total_read_write),
+                IaiCallgrindMeasure::EstimatedCycles(estimated_cycles),
+            ]
+        },
+    )
+}
+
+fn dhat_tool_measures<'a>() -> impl FnMut(&'a str) -> IResult<&'a str, Vec<IaiCallgrindMeasure>> {
+    map(
+        preceded(
+            opt(tool_name_line("DHAT")),
+            tuple((
+                metric_line(dhat_tool::TotalBytes::NAME_STR),
+                metric_line(dhat_tool::TotalBlocks::NAME_STR),
+                metric_line(dhat_tool::AtTGmaxBytes::NAME_STR),
+                metric_line(dhat_tool::AtTGmaxBlocks::NAME_STR),
+                metric_line(dhat_tool::AtTEndBytes::NAME_STR),
+                metric_line(dhat_tool::AtTEndBlocks::NAME_STR),
+                metric_line(dhat_tool::ReadsBytes::NAME_STR),
+                metric_line(dhat_tool::WritesBytes::NAME_STR),
+            )),
         ),
-    ]
-    .into_iter()
-    .map(|(measure, input, into_variant)| {
-        parse_iai_callgrind_metric(input, measure)
-            .map(|(_remainder, json_metric)| into_variant(json_metric))
-    })
-    .collect::<Result<Vec<_>, _>>()
-    .ok()?;
-
-    Some((name, metrics))
+        |(
+            total_bytes,
+            total_blocks,
+            at_t_gmax_bytes,
+            at_t_gmax_blocks,
+            at_t_end_bytes,
+            at_t_end_blocks,
+            reads_bytes,
+            writes_bytes,
+        )| {
+            vec![
+                IaiCallgrindMeasure::TotalBytes(total_bytes),
+                IaiCallgrindMeasure::TotalBlocks(total_blocks),
+                IaiCallgrindMeasure::AtTGmaxBytes(at_t_gmax_bytes),
+                IaiCallgrindMeasure::AtTGmaxBlocks(at_t_gmax_blocks),
+                IaiCallgrindMeasure::AtTEndBytes(at_t_end_bytes),
+                IaiCallgrindMeasure::AtTEndBlocks(at_t_end_blocks),
+                IaiCallgrindMeasure::ReadsBytes(reads_bytes),
+                IaiCallgrindMeasure::WritesBytes(writes_bytes),
+            ]
+        },
+    )
+}
+
+fn tool_name_line<'a>(tool_name: &'static str) -> impl FnMut(&'a str) -> IResult<&'a str, &'a str> {
+    delimited(
+        tuple((space0, many1(tag("=")), tag(" "))),
+        tag(tool_name),
+        tuple((tag(" "), many1(tag("=")), line_ending())),
+    )
 }
 
-#[allow(clippy::cast_precision_loss)]
-fn parse_iai_callgrind_metric<'a>(
-    input: &'a str,
-    measure: &'static str,
-) -> IResult<&'a str, JsonNewMetric> {
+fn metric_line<'a>(
+    measure_name: &'static str,
+) -> impl FnMut(&'a str) -> IResult<&'a str, JsonNewMetric> {
     map(
         tuple((
             space0,
-            tag(measure),
+            tag(measure_name),
             tag(":"),
             space1,
+            // the current run value:
             parse_u64,
             tag("|"),
             alt((
-                // No previous run
-                map(tuple((tag("N/A"), space1, tag("(*********)"))), |_| ()),
-                // Comparison to previous run
-                map(
-                    tuple((
-                        parse_u64,
-                        space0,
-                        alt((
-                            map(tag("(No change)"), |_| ()),
-                            map(
-                                tuple((
-                                    delimited(
-                                        tag("("),
-                                        tuple((alt((tag("+"), tag("-"))), parse_f64, tag("%"))),
-                                        tag(")"),
-                                    ),
-                                    space1,
-                                    delimited(
-                                        tag("["),
-                                        tuple((alt((tag("+"), tag("-"))), parse_f64, tag("x"))),
-                                        tag("]"),
-                                    ),
-                                )),
-                                |_| (),
+                // No previous run:
+                recognize(tuple((tag("N/A"), space1, tag("(*********)")))),
+                // Comparison to previous run:
+                recognize(tuple((
+                    parse_u64,
+                    space0,
+                    alt((
+                        recognize(tag("(No change)")),
+                        recognize(tuple((
+                            delimited(
+                                tag("("),
+                                tuple((alt((tag("+"), tag("-"))), parse_f64, tag("%"))),
+                                tag(")"),
                             ),
-                        )),
+                            space1,
+                            delimited(
+                                tag("["),
+                                tuple((alt((tag("+"), tag("-"))), parse_f64, tag("x"))),
+                                tag("]"),
+                            ),
+                        ))),
                     )),
-                    |_| (),
-                ),
+                ))),
             )),
-            eof,
+            line_ending(),
         )),
-        |(_, _, _, _, metric, _, (), _)| JsonNewMetric {
-            value: (metric as f64).into(),
+        |(_, _, _, _, current_value, _, _, _)| JsonNewMetric {
+            #[allow(clippy::cast_precision_loss)]
+            value: (current_value as f64).into(),
             lower_value: None,
             upper_value: None,
         },
-    )(input)
+    )
+}
+
+fn line_ending<'a>() -> impl FnMut(&'a str) -> IResult<&'a str, &'a str> {
+    is_a("\r\n")
+}
+
+fn not_line_ending<'a>() -> impl FnMut(&'a str) -> IResult<&'a str, &'a str> {
+    // Note: `not(line_ending)` doesn't work here, as it won't consume the matched characters
+    is_not("\r\n")
 }
 
 #[cfg(test)]
 pub(crate) mod test_rust_iai_callgrind {
-
-    use crate::{
-        adapters::test_util::convert_file_path, results::adapter_metrics::AdapterMetrics,
-        Adaptable, AdapterResults,
-    };
-    use bencher_json::{
-        project::measure::{
-            ESTIMATED_CYCLES_SLUG_STR, INSTRUCTIONS_NAME_STR, INSTRUCTIONS_SLUG_STR,
-            L1_ACCESSES_SLUG_STR, L2_ACCESSES_SLUG_STR, RAM_ACCESSES_SLUG_STR,
-            TOTAL_ACCESSES_SLUG_STR,
-        },
-        JsonNewMetric,
+    use crate::{adapters::test_util::convert_file_path, AdapterResults};
+    use bencher_json::project::measure::built_in::{
+        iai_callgrind::{callgrind_tool, dhat_tool},
+        BuiltInMeasure,
     };
     use ordered_float::OrderedFloat;
     use pretty_assertions::assert_eq;
 
     use super::AdapterRustIaiCallgrind;
-
-    fn convert_rust_iai_callgrind(suffix: &str) -> AdapterResults {
-        let file_path = format!("./tool_output/rust/iai_callgrind/{suffix}.txt");
-        convert_file_path::<AdapterRustIaiCallgrind>(&file_path)
-    }
-
-    pub fn validate_iai_callgrind(metrics: &AdapterMetrics, results: [(&str, f64); 6]) {
-        assert_eq!(metrics.inner.len(), 6);
-        for (key, value) in results {
-            let metric = metrics.get(key).unwrap();
-            assert_eq!(metric.value, OrderedFloat::from(value));
-            assert_eq!(metric.lower_value, None);
-            assert_eq!(metric.upper_value, None);
-        }
-    }
+    use std::collections::HashMap;
 
     #[test]
-    fn test_adapter_rust_iai_callgrind_parse_line() {
-        assert_eq!(
-            super::parse_iai_callgrind_metric(
-                "  Instructions:                1234|N/A             (*********)",
-                INSTRUCTIONS_NAME_STR
-            ),
-            Ok((
-                "",
-                JsonNewMetric {
-                    value: 1234.0.into(),
-                    upper_value: None,
-                    lower_value: None
-                }
-            ))
+    fn test_iai_adapter_single_tool() {
+        let results = convert_file_path::<AdapterRustIaiCallgrind>(
+            "./tool_output/rust/iai_callgrind/single-tool.txt",
         );
 
-        assert_eq!(
-            super::parse_iai_callgrind_metric(
-                "  Instructions:                1234|1234            (No change)",
-                INSTRUCTIONS_NAME_STR
-            ),
-            Ok((
-                "",
-                JsonNewMetric {
-                    value: 1234.0.into(),
-                    upper_value: None,
-                    lower_value: None
-                }
-            ))
-        );
+        validate_adapter_rust_iai_callgrind(&results, true, false);
+    }
 
-        assert_eq!(
-            super::parse_iai_callgrind_metric(
-                "  Instructions:                1234|1000            (+23.4000%) [+1.23400x]",
-                INSTRUCTIONS_NAME_STR
-            ),
-            Ok((
-                "",
-                JsonNewMetric {
-                    value: 1234.0.into(),
-                    upper_value: None,
-                    lower_value: None
-                }
-            ))
+    #[test]
+    fn test_iai_adapter_multiple_tools() {
+        let results = convert_file_path::<AdapterRustIaiCallgrind>(
+            "./tool_output/rust/iai_callgrind/multiple-tools.txt",
         );
+        validate_adapter_rust_iai_callgrind(&results, true, true);
     }
 
     #[test]
-    fn test_adapter_rust_iai_callgrind_parse_multiple_lines() {
-        let input = "rust_iai_callgrind::bench_fibonacci_group::bench_fibonacci short:10
-  Instructions:                1734|N/A             (*********)
-  L1 Hits:                     2359|N/A             (*********)
-  L2 Hits:                        0|N/A             (*********)
-  RAM Hits:                       3|N/A             (*********)
-  Total read+write:            2362|N/A             (*********)
-  Estimated Cycles:            2464|N/A             (*********)";
-        let output = AdapterRustIaiCallgrind::parse(input, crate::Settings::default());
-        assert!(output.is_some());
+    fn test_iai_adapter_delta() {
+        let results = convert_file_path::<AdapterRustIaiCallgrind>(
+            "./tool_output/rust/iai_callgrind/delta.txt",
+        );
+        validate_adapter_rust_iai_callgrind(&results, true, false);
     }
 
     #[test]
-    fn test_adapter_rust_iai_callgrind() {
-        let results = convert_rust_iai_callgrind("two");
-        validate_adapter_rust_iai_callgrind(&results);
+    fn test_iai_adapter_ansi_escapes_issue_345() {
+        let results = convert_file_path::<AdapterRustIaiCallgrind>(
+            "./tool_output/rust/iai_callgrind/ansi-escapes.txt",
+        );
+        validate_adapter_rust_iai_callgrind(&results, true, false);
     }
 
-    pub fn validate_adapter_rust_iai_callgrind(results: &AdapterResults) {
+    pub fn validate_adapter_rust_iai_callgrind(
+        results: &AdapterResults,
+        callgrind_tool: bool,
+        dhat_tool: bool,
+    ) {
         assert_eq!(results.inner.len(), 2);
 
-        let metrics = results
-            .get("rust_iai_callgrind::bench_fibonacci_group::bench_fibonacci short:10")
-            .unwrap();
-        validate_iai_callgrind(
-            metrics,
-            [
-                (INSTRUCTIONS_SLUG_STR, 1734.0),
-                (L1_ACCESSES_SLUG_STR, 2359.0),
-                (L2_ACCESSES_SLUG_STR, 0.0),
-                (RAM_ACCESSES_SLUG_STR, 3.0),
-                (TOTAL_ACCESSES_SLUG_STR, 2362.0),
-                (ESTIMATED_CYCLES_SLUG_STR, 2464.0),
-            ],
-        );
-        let metrics = results
-            .get("rust_iai_callgrind::bench_fibonacci_group::bench_fibonacci long:30")
-            .unwrap();
-        validate_iai_callgrind(
-            metrics,
-            [
-                (INSTRUCTIONS_SLUG_STR, 26_214_734.0),
-                (L1_ACCESSES_SLUG_STR, 35_638_619.0),
-                (L2_ACCESSES_SLUG_STR, 0.0),
-                (RAM_ACCESSES_SLUG_STR, 3.0),
-                (TOTAL_ACCESSES_SLUG_STR, 35_638_622.0),
-                (ESTIMATED_CYCLES_SLUG_STR, 35_638_724.0),
-            ],
-        );
+        {
+            let mut expected = HashMap::new();
+
+            if callgrind_tool {
+                expected.extend([
+                    (callgrind_tool::Instructions::SLUG_STR, 1_734.0),
+                    (callgrind_tool::L1Hits::SLUG_STR, 2_359.0),
+                    (callgrind_tool::L2Hits::SLUG_STR, 0.0),
+                    (callgrind_tool::RamHits::SLUG_STR, 3.0),
+                    (callgrind_tool::TotalReadWrite::SLUG_STR, 2_362.0),
+                    (callgrind_tool::EstimatedCycles::SLUG_STR, 2_464.0),
+                ]);
+            }
+
+            if dhat_tool {
+                expected.extend([
+                    (dhat_tool::TotalBytes::SLUG_STR, 29_499.0),
+                    (dhat_tool::TotalBlocks::SLUG_STR, 2_806.0),
+                    (dhat_tool::AtTGmaxBytes::SLUG_STR, 378.0),
+                    (dhat_tool::AtTGmaxBlocks::SLUG_STR, 34.0),
+                    (dhat_tool::AtTEndBytes::SLUG_STR, 0.0),
+                    (dhat_tool::AtTEndBlocks::SLUG_STR, 0.0),
+                    (dhat_tool::ReadsBytes::SLUG_STR, 57_725.0),
+                    (dhat_tool::WritesBytes::SLUG_STR, 73_810.0),
+                ]);
+            }
+
+            compare_benchmark(
+                &expected,
+                results,
+                "rust_iai_callgrind::bench_fibonacci_group::bench_fibonacci short:10",
+            );
+        }
+
+        {
+            let mut expected = HashMap::new();
+
+            if callgrind_tool {
+                expected.extend([
+                    (callgrind_tool::Instructions::SLUG_STR, 26_214_734.0),
+                    (callgrind_tool::L1Hits::SLUG_STR, 35_638_619.0),
+                    (callgrind_tool::L2Hits::SLUG_STR, 0.0),
+                    (callgrind_tool::RamHits::SLUG_STR, 3.0),
+                    (callgrind_tool::TotalReadWrite::SLUG_STR, 35_638_622.0),
+                    (callgrind_tool::EstimatedCycles::SLUG_STR, 35_638_724.0),
+                ]);
+            }
+
+            if dhat_tool {
+                expected.extend([
+                    (dhat_tool::TotalBytes::SLUG_STR, 26_294_939.0),
+                    (dhat_tool::TotalBlocks::SLUG_STR, 2_328_086.0),
+                    (dhat_tool::AtTGmaxBytes::SLUG_STR, 933_718.0),
+                    (dhat_tool::AtTGmaxBlocks::SLUG_STR, 18_344.0),
+                    (dhat_tool::AtTEndBytes::SLUG_STR, 0.0),
+                    (dhat_tool::AtTEndBlocks::SLUG_STR, 0.0),
+                    (dhat_tool::ReadsBytes::SLUG_STR, 47_577_425.0),
+                    (dhat_tool::WritesBytes::SLUG_STR, 37_733_810.0),
+                ]);
+            }
+
+            compare_benchmark(
+                &expected,
+                results,
+                "rust_iai_callgrind::bench_fibonacci_group::bench_fibonacci long:30",
+            );
+        }
     }
 
-    #[test]
-    fn test_adapter_rust_iai_callgrind_issue_345() {
-        let contents = "running 0 tests\n\ntest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n\u{001b}[32mrust_iai_callgrind::bench_fibonacci_group::bench_fibonacci\u{001b}[0m \u{001b}[36mshort\u{001b}[0m\u{001b}[36m:\u{001b}[0m\u{001b}[1;34m10\u{001b}[0m\n  Instructions:     \u{001b}[1m           1684\u{001b}[0m|N/A             (\u{001b}[90m*********\u{001b}[0m)\n  L1 Hits:          \u{001b}[1m           2309\u{001b}[0m|N/A             (\u{001b}[90m*********\u{001b}[0m)\n  L2 Hits:          \u{001b}[1m              0\u{001b}[0m|N/A             (\u{001b}[90m*********\u{001b}[0m)\n  RAM Hits:         \u{001b}[1m              3\u{001b}[0m|N/A             (\u{001b}[90m*********\u{001b}[0m)\n  Total read+write: \u{001b}[1m           2312\u{001b}[0m|N/A             (\u{001b}[90m*********\u{001b}[0m)\n  Estimated Cycles: \u{001b}[1m           2414\u{001b}[0m|N/A             (\u{001b}[90m*********\u{001b}[0m)\n\u{001b}[32mrust_iai_callgrind::bench_fibonacci_group::bench_fibonacci\u{001b}[0m \u{001b}[36mlong\u{001b}[0m\u{001b}[36m:\u{001b}[0m\u{001b}[1;34m30\u{001b}[0m\n  Instructions:     \u{001b}[1m       25457719\u{001b}[0m|N/A             (\u{001b}[90m*********\u{001b}[0m)\n  L1 Hits:          \u{001b}[1m       34881604\u{001b}[0m|N/A             (\u{001b}[90m*********\u{001b}[0m)\n  L2 Hits:          \u{001b}[1m              0\u{001b}[0m|N/A             (\u{001b}[90m*********\u{001b}[0m)\n  RAM Hits:         \u{001b}[1m              3\u{001b}[0m|N/A             (\u{001b}[90m*********\u{001b}[0m)\n  Total read+write: \u{001b}[1m       34881607\u{001b}[0m|N/A             (\u{001b}[90m*********\u{001b}[0m)\n  Estimated Cycles: \u{001b}[1m       34881709\u{001b}[0m|N/A             (\u{001b}[90m*********\u{001b}[0m)";
-        let results = AdapterRustIaiCallgrind::parse(contents, crate::Settings::default()).unwrap();
-        assert_eq!(results.inner.len(), 2);
+    fn compare_benchmark(
+        expected: &HashMap<&str, f64>,
+        results: &AdapterResults,
+        benchmark_name: &str,
+    ) {
+        let actual = results.get(benchmark_name).unwrap();
+        assert_eq!(actual.inner.len(), expected.len());
 
-        let metrics = results
-            .get("rust_iai_callgrind::bench_fibonacci_group::bench_fibonacci short:10")
-            .unwrap();
-        validate_iai_callgrind(
-            metrics,
-            [
-                (INSTRUCTIONS_SLUG_STR, 1684.0),
-                (L1_ACCESSES_SLUG_STR, 2309.0),
-                (L2_ACCESSES_SLUG_STR, 0.0),
-                (RAM_ACCESSES_SLUG_STR, 3.0),
-                (TOTAL_ACCESSES_SLUG_STR, 2312.0),
-                (ESTIMATED_CYCLES_SLUG_STR, 2414.0),
-            ],
-        );
-        let metrics = results
-            .get("rust_iai_callgrind::bench_fibonacci_group::bench_fibonacci long:30")
-            .unwrap();
-        validate_iai_callgrind(
-            metrics,
-            [
-                (INSTRUCTIONS_SLUG_STR, 25_457_719.0),
-                (L1_ACCESSES_SLUG_STR, 34_881_604.0),
-                (L2_ACCESSES_SLUG_STR, 0.0),
-                (RAM_ACCESSES_SLUG_STR, 3.0),
-                (TOTAL_ACCESSES_SLUG_STR, 34_881_607.0),
-                (ESTIMATED_CYCLES_SLUG_STR, 34_881_709.0),
-            ],
-        );
+        for (key, value) in expected {
+            let metric = actual.get(key).unwrap();
+            assert_eq!(metric.value, OrderedFloat::from(*value));
+            assert_eq!(metric.lower_value, None);
+            assert_eq!(metric.upper_value, None);
+        }
     }
 }
diff --git a/lib/bencher_adapter/src/adapters/rust/mod.rs b/lib/bencher_adapter/src/adapters/rust/mod.rs
index 11aecfbc7..efe8e8219 100644
--- a/lib/bencher_adapter/src/adapters/rust/mod.rs
+++ b/lib/bencher_adapter/src/adapters/rust/mod.rs
@@ -51,7 +51,8 @@ mod test_rust {
 
     #[test]
     fn test_adapter_rust_iai_callgrind() {
-        let results = convert_file_path::<AdapterRust>("./tool_output/rust/iai_callgrind/two.txt");
-        test_rust_iai_callgrind::validate_adapter_rust_iai_callgrind(&results);
+        let results =
+            convert_file_path::<AdapterRust>("./tool_output/rust/iai_callgrind/single-tool.txt");
+        test_rust_iai_callgrind::validate_adapter_rust_iai_callgrind(&results, true, false);
     }
 }
diff --git a/lib/bencher_adapter/src/results/adapter_results.rs b/lib/bencher_adapter/src/results/adapter_results.rs
index f8d8599d8..b85a04e71 100644
--- a/lib/bencher_adapter/src/results/adapter_results.rs
+++ b/lib/bencher_adapter/src/results/adapter_results.rs
@@ -2,55 +2,16 @@ use std::{collections::HashMap, str::FromStr};
 
 use bencher_json::{
     project::{
-        measure::{
-            ESTIMATED_CYCLES_SLUG_STR, INSTRUCTIONS_SLUG_STR, L1_ACCESSES_SLUG_STR,
-            L2_ACCESSES_SLUG_STR, LATENCY_SLUG_STR, RAM_ACCESSES_SLUG_STR, THROUGHPUT_SLUG_STR,
-            TOTAL_ACCESSES_SLUG_STR,
-        },
+        measure::built_in::{self, BuiltInMeasure},
         metric::Mean,
     },
-    BenchmarkName, JsonNewMetric, NameId,
+    BenchmarkName, JsonNewMetric,
 };
 use literally::hmap;
-use once_cell::sync::Lazy;
 use serde::{Deserialize, Serialize};
 
 use super::{adapter_metrics::AdapterMetrics, CombinedKind};
 
-const MEASURE_SLUG_ERROR: &str = "Failed to parse measure slug.";
-
-#[allow(clippy::expect_used)]
-pub static LATENCY_NAME_ID: Lazy<NameId> =
-    Lazy::new(|| LATENCY_SLUG_STR.parse().expect(MEASURE_SLUG_ERROR));
-
-#[allow(clippy::expect_used)]
-pub static THROUGHPUT_NAME_ID: Lazy<NameId> =
-    Lazy::new(|| THROUGHPUT_SLUG_STR.parse().expect(MEASURE_SLUG_ERROR));
-
-#[allow(clippy::expect_used)]
-pub static INSTRUCTIONS_NAME_ID: Lazy<NameId> =
-    Lazy::new(|| INSTRUCTIONS_SLUG_STR.parse().expect(MEASURE_SLUG_ERROR));
-
-#[allow(clippy::expect_used)]
-pub static L1_ACCESSES_NAME_ID: Lazy<NameId> =
-    Lazy::new(|| L1_ACCESSES_SLUG_STR.parse().expect(MEASURE_SLUG_ERROR));
-
-#[allow(clippy::expect_used)]
-pub static L2_ACCESSES_NAME_ID: Lazy<NameId> =
-    Lazy::new(|| L2_ACCESSES_SLUG_STR.parse().expect(MEASURE_SLUG_ERROR));
-
-#[allow(clippy::expect_used)]
-pub static RAM_ACCESSES_NAME_ID: Lazy<NameId> =
-    Lazy::new(|| RAM_ACCESSES_SLUG_STR.parse().expect(MEASURE_SLUG_ERROR));
-
-#[allow(clippy::expect_used)]
-pub static TOTAL_ACCESSES_NAME_ID: Lazy<NameId> =
-    Lazy::new(|| TOTAL_ACCESSES_SLUG_STR.parse().expect(MEASURE_SLUG_ERROR));
-
-#[allow(clippy::expect_used)]
-pub static ESTIMATED_CYCLES_NAME_ID: Lazy<NameId> =
-    Lazy::new(|| ESTIMATED_CYCLES_SLUG_STR.parse().expect(MEASURE_SLUG_ERROR));
-
 #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
 pub struct AdapterResults {
     #[serde(flatten)]
@@ -82,12 +43,27 @@ pub enum IaiMeasure {
 
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub enum IaiCallgrindMeasure {
+    /*
+     * Callgrind tool:
+     */
     Instructions(JsonNewMetric),
-    L1Accesses(JsonNewMetric),
-    L2Accesses(JsonNewMetric),
-    RamAccesses(JsonNewMetric),
+    L1Hits(JsonNewMetric),
+    L2Hits(JsonNewMetric),
+    RamHits(JsonNewMetric),
     TotalReadWrite(JsonNewMetric),
     EstimatedCycles(JsonNewMetric),
+
+    /*
+     * DHAT tool:
+     */
+    TotalBytes(JsonNewMetric),
+    TotalBlocks(JsonNewMetric),
+    AtTGmaxBytes(JsonNewMetric),
+    AtTGmaxBlocks(JsonNewMetric),
+    AtTEndBytes(JsonNewMetric),
+    AtTEndBlocks(JsonNewMetric),
+    ReadsBytes(JsonNewMetric),
+    WritesBytes(JsonNewMetric),
 }
 
 impl AdapterResults {
@@ -102,12 +78,12 @@ impl AdapterResults {
                 inner: match measure {
                     AdapterMeasure::Latency(json_metric) => {
                         hmap! {
-                            LATENCY_NAME_ID.clone() => json_metric
+                            built_in::generic::Latency::name_id() => json_metric
                         }
                     },
                     AdapterMeasure::Throughput(json_metric) => {
                         hmap! {
-                            THROUGHPUT_NAME_ID.clone() => json_metric
+                            built_in::generic::Throughput::name_id() => json_metric
                         }
                     },
                 },
@@ -153,19 +129,19 @@ impl AdapterResults {
             for metric in metrics {
                 let (resource_id, metric) = match metric {
                     IaiMeasure::Instructions(json_metric) => {
-                        (INSTRUCTIONS_NAME_ID.clone(), json_metric)
+                        (built_in::iai::Instructions::name_id(), json_metric)
                     },
                     IaiMeasure::L1Accesses(json_metric) => {
-                        (L1_ACCESSES_NAME_ID.clone(), json_metric)
+                        (built_in::iai::L1Accesses::name_id(), json_metric)
                     },
                     IaiMeasure::L2Accesses(json_metric) => {
-                        (L2_ACCESSES_NAME_ID.clone(), json_metric)
+                        (built_in::iai::L2Accesses::name_id(), json_metric)
                     },
                     IaiMeasure::RamAccesses(json_metric) => {
-                        (RAM_ACCESSES_NAME_ID.clone(), json_metric)
+                        (built_in::iai::RamAccesses::name_id(), json_metric)
                     },
                     IaiMeasure::EstimatedCycles(json_metric) => {
-                        (ESTIMATED_CYCLES_NAME_ID.clone(), json_metric)
+                        (built_in::iai::EstimatedCycles::name_id(), json_metric)
                     },
                 };
                 metrics_value.inner.insert(resource_id, metric);
@@ -189,24 +165,69 @@ impl AdapterResults {
                 .or_insert_with(AdapterMetrics::default);
             for metric in metrics {
                 let (resource_id, metric) = match metric {
-                    IaiCallgrindMeasure::Instructions(json_metric) => {
-                        (INSTRUCTIONS_NAME_ID.clone(), json_metric)
-                    },
-                    IaiCallgrindMeasure::L1Accesses(json_metric) => {
-                        (L1_ACCESSES_NAME_ID.clone(), json_metric)
-                    },
-                    IaiCallgrindMeasure::L2Accesses(json_metric) => {
-                        (L2_ACCESSES_NAME_ID.clone(), json_metric)
-                    },
-                    IaiCallgrindMeasure::RamAccesses(json_metric) => {
-                        (RAM_ACCESSES_NAME_ID.clone(), json_metric)
-                    },
-                    IaiCallgrindMeasure::TotalReadWrite(json_metric) => {
-                        (TOTAL_ACCESSES_NAME_ID.clone(), json_metric)
-                    },
-                    IaiCallgrindMeasure::EstimatedCycles(json_metric) => {
-                        (ESTIMATED_CYCLES_NAME_ID.clone(), json_metric)
-                    },
+                    /*
+                     * Callgrind tool:
+                     */
+                    IaiCallgrindMeasure::Instructions(json_metric) => (
+                        built_in::iai_callgrind::callgrind_tool::Instructions::name_id(),
+                        json_metric,
+                    ),
+                    IaiCallgrindMeasure::L1Hits(json_metric) => (
+                        built_in::iai_callgrind::callgrind_tool::L1Hits::name_id(),
+                        json_metric,
+                    ),
+                    IaiCallgrindMeasure::L2Hits(json_metric) => (
+                        built_in::iai_callgrind::callgrind_tool::L2Hits::name_id(),
+                        json_metric,
+                    ),
+                    IaiCallgrindMeasure::RamHits(json_metric) => (
+                        built_in::iai_callgrind::callgrind_tool::RamHits::name_id(),
+                        json_metric,
+                    ),
+                    IaiCallgrindMeasure::TotalReadWrite(json_metric) => (
+                        built_in::iai_callgrind::callgrind_tool::TotalReadWrite::name_id(),
+                        json_metric,
+                    ),
+                    IaiCallgrindMeasure::EstimatedCycles(json_metric) => (
+                        built_in::iai_callgrind::callgrind_tool::EstimatedCycles::name_id(),
+                        json_metric,
+                    ),
+
+                    /*
+                     * DHAT tool:
+                     */
+                    IaiCallgrindMeasure::TotalBytes(json_metric) => (
+                        built_in::iai_callgrind::dhat_tool::TotalBytes::name_id(),
+                        json_metric,
+                    ),
+                    IaiCallgrindMeasure::TotalBlocks(json_metric) => (
+                        built_in::iai_callgrind::dhat_tool::TotalBlocks::name_id(),
+                        json_metric,
+                    ),
+                    IaiCallgrindMeasure::AtTGmaxBytes(json_metric) => (
+                        built_in::iai_callgrind::dhat_tool::AtTGmaxBytes::name_id(),
+                        json_metric,
+                    ),
+                    IaiCallgrindMeasure::AtTGmaxBlocks(json_metric) => (
+                        built_in::iai_callgrind::dhat_tool::AtTGmaxBlocks::name_id(),
+                        json_metric,
+                    ),
+                    IaiCallgrindMeasure::AtTEndBytes(json_metric) => (
+                        built_in::iai_callgrind::dhat_tool::AtTEndBytes::name_id(),
+                        json_metric,
+                    ),
+                    IaiCallgrindMeasure::AtTEndBlocks(json_metric) => (
+                        built_in::iai_callgrind::dhat_tool::AtTEndBlocks::name_id(),
+                        json_metric,
+                    ),
+                    IaiCallgrindMeasure::ReadsBytes(json_metric) => (
+                        built_in::iai_callgrind::dhat_tool::ReadsBytes::name_id(),
+                        json_metric,
+                    ),
+                    IaiCallgrindMeasure::WritesBytes(json_metric) => (
+                        built_in::iai_callgrind::dhat_tool::WritesBytes::name_id(),
+                        json_metric,
+                    ),
                 };
                 metrics_value.inner.insert(resource_id, metric);
             }
diff --git a/lib/bencher_adapter/tool_output/rust/iai_callgrind/ansi-escapes.txt b/lib/bencher_adapter/tool_output/rust/iai_callgrind/ansi-escapes.txt
new file mode 100644
index 000000000..bce3c5bc3
--- /dev/null
+++ b/lib/bencher_adapter/tool_output/rust/iai_callgrind/ansi-escapes.txt
@@ -0,0 +1,18 @@
+running 0 tests
+
+test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
+
+[32mrust_iai_callgrind::bench_fibonacci_group::bench_fibonacci[0m [36mshort[0m[36m:[0m[1;34m10[0m
+  Instructions:     [1m           1734[0m|N/A             ([90m*********[0m)
+  L1 Hits:          [1m           2359[0m|N/A             ([90m*********[0m)
+  L2 Hits:          [1m              0[0m|N/A             ([90m*********[0m)
+  RAM Hits:         [1m              3[0m|N/A             ([90m*********[0m)
+  Total read+write: [1m           2362[0m|N/A             ([90m*********[0m)
+  Estimated Cycles: [1m           2464[0m|N/A             ([90m*********[0m)
+[32mrust_iai_callgrind::bench_fibonacci_group::bench_fibonacci[0m [36mlong[0m[36m:[0m[1;34m30[0m
+  Instructions:     [1m       26214734[0m|N/A             ([90m*********[0m)
+  L1 Hits:          [1m       35638619[0m|N/A             ([90m*********[0m)
+  L2 Hits:          [1m              0[0m|N/A             ([90m*********[0m)
+  RAM Hits:         [1m              3[0m|N/A             ([90m*********[0m)
+  Total read+write: [1m       35638622[0m|N/A             ([90m*********[0m)
+  Estimated Cycles: [1m       35638724[0m|N/A             ([90m*********[0m)
diff --git a/lib/bencher_adapter/tool_output/rust/iai_callgrind/change.txt b/lib/bencher_adapter/tool_output/rust/iai_callgrind/change.txt
deleted file mode 100644
index 7cfbee08a..000000000
--- a/lib/bencher_adapter/tool_output/rust/iai_callgrind/change.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-rust_iai_callgrind::bench_fibonacci_group::bench_fibonacci short:10
-  Instructions:                1650|1734            (-4.84429%) [-1.05091x]
-  L1 Hits:                     2275|2359            (-3.56083%) [-1.03692x]
-  L2 Hits:                        0|0               (No change)
-  RAM Hits:                       3|3               (No change)
-  Total read+write:            2278|2362            (-3.55631%) [-1.03687x]
-  Estimated Cycles:            2380|2464            (-3.40909%) [-1.03529x]
-rust_iai_callgrind::bench_fibonacci_group::bench_fibonacci long:30
-  Instructions:            24943490|26214734        (-4.84935%) [-1.05096x]
-  L1 Hits:                 34367375|35638619        (-3.56704%) [-1.03699x]
-  L2 Hits:                        0|0               (No change)
-  RAM Hits:                       3|3               (No change)
-  Total read+write:        34367378|35638622        (-3.56704%) [-1.03699x]
-  Estimated Cycles:        34367480|35638724        (-3.56703%) [-1.03699x]
diff --git a/lib/bencher_adapter/tool_output/rust/iai_callgrind/delta.txt b/lib/bencher_adapter/tool_output/rust/iai_callgrind/delta.txt
new file mode 100644
index 000000000..effda3918
--- /dev/null
+++ b/lib/bencher_adapter/tool_output/rust/iai_callgrind/delta.txt
@@ -0,0 +1,19 @@
+
+running 0 tests
+
+test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
+
+rust_iai_callgrind::bench_fibonacci_group::bench_fibonacci short:10
+  Instructions:                1734|1650            (+5.09090%) [+1.04844x]
+  L1 Hits:                     2359|2275            (+3.69230%) [+1.03560x]
+  L2 Hits:                        0|0               (No change)
+  RAM Hits:                       3|3               (No change)
+  Total read+write:            2362|2278            (+3.68744%) [+1.03556x]
+  Estimated Cycles:            2464|2380            (+3.52941%) [+1.03409x]
+rust_iai_callgrind::bench_fibonacci_group::bench_fibonacci long:30
+  Instructions:            26214734|24943490        (+5.09649%) [+1.04849x]
+  L1 Hits:                 35638619|34367375        (+3.69898%) [+1.03567x]
+  L2 Hits:                        0|0               (No change)
+  RAM Hits:                       3|3               (No change)
+  Total read+write:        35638622|34367378        (+3.69898%) [+1.03567x]
+  Estimated Cycles:        35638724|34367480        (+3.69897%) [+1.03567x]
diff --git a/lib/bencher_adapter/tool_output/rust/iai_callgrind/multiple-tools.txt b/lib/bencher_adapter/tool_output/rust/iai_callgrind/multiple-tools.txt
new file mode 100644
index 000000000..20c7c98d4
--- /dev/null
+++ b/lib/bencher_adapter/tool_output/rust/iai_callgrind/multiple-tools.txt
@@ -0,0 +1,51 @@
+
+running 0 tests
+
+test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
+
+rust_iai_callgrind::bench_fibonacci_group::bench_fibonacci short:10
+  ======= CALLGRIND =======================================================
+  Instructions:                1734|N/A             (*********)
+  L1 Hits:                     2359|N/A             (*********)
+  L2 Hits:                        0|N/A             (*********)
+  RAM Hits:                       3|N/A             (*********)
+  Total read+write:            2362|N/A             (*********)
+  Estimated Cycles:            2464|N/A             (*********)
+  ======= DHAT ============================================================
+  Total bytes:                29499|N/A             (*********)
+  Total blocks:                2806|N/A             (*********)
+  At t-gmax bytes:              378|N/A             (*********)
+  At t-gmax blocks:              34|N/A             (*********)
+  At t-end bytes:                 0|N/A             (*********)
+  At t-end blocks:                0|N/A             (*********)
+  Reads bytes:                57725|N/A             (*********)
+  Writes bytes:               73810|N/A             (*********)
+rust_iai_callgrind::bench_fibonacci_group::bench_fibonacci long:30
+  ======= CALLGRIND =======================================================
+  Instructions:            26214734|N/A             (*********)
+  L1 Hits:                 35638619|N/A             (*********)
+  L2 Hits:                        0|N/A             (*********)
+  RAM Hits:                       3|N/A             (*********)
+  Total read+write:        35638622|N/A             (*********)
+  Estimated Cycles:        35638724|N/A             (*********)
+  ======= DHAT ============================================================
+  Total bytes:             26294939|N/A             (*********)
+  Total blocks:             2328086|N/A             (*********)
+  At t-gmax bytes:           933718|N/A             (*********)
+  At t-gmax blocks:           18344|N/A             (*********)
+  At t-end bytes:                 0|N/A             (*********)
+  At t-end blocks:                0|N/A             (*********)
+  Reads bytes:             47577425|N/A             (*********)
+  Writes bytes:            37733810|N/A             (*********)
+
+
+
+
+
+
+
+
+
+.... Some trailing text that should be ignored ...
+.... Some trailing text that should be ignored ...
+.... Some trailing text that should be ignored ...
diff --git a/lib/bencher_adapter/tool_output/rust/iai_callgrind/two.txt b/lib/bencher_adapter/tool_output/rust/iai_callgrind/single-tool.txt
similarity index 100%
rename from lib/bencher_adapter/tool_output/rust/iai_callgrind/two.txt
rename to lib/bencher_adapter/tool_output/rust/iai_callgrind/single-tool.txt
diff --git a/lib/bencher_json/src/project/measure.rs b/lib/bencher_json/src/project/measure.rs
deleted file mode 100644
index 02714a1c1..000000000
--- a/lib/bencher_json/src/project/measure.rs
+++ /dev/null
@@ -1,254 +0,0 @@
-#![allow(clippy::expect_used)]
-
-use std::fmt;
-
-use bencher_valid::{DateTime, ResourceName, Slug};
-use once_cell::sync::Lazy;
-#[cfg(feature = "schema")]
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-
-use crate::ProjectUuid;
-
-const MEASURE_NAME_ERROR: &str = "Failed to parse measure name.";
-const MEASURE_SLUG_ERROR: &str = "Failed to parse measure slug.";
-const MEASURE_UNITS_ERROR: &str = "Failed to parse measure units.";
-
-pub const MEASURE_UNITS_STR: &str = "Measure (units)";
-pub static MEASURE_UNITS: Lazy<ResourceName> =
-    Lazy::new(|| MEASURE_UNITS_STR.parse().expect(MEASURE_UNITS_ERROR));
-
-pub const LATENCY_NAME_STR: &str = "Latency";
-pub const LATENCY_SLUG_STR: &str = "latency";
-pub const LATENCY_UNITS_STR: &str = "nanoseconds (ns)";
-
-static LATENCY_NAME: Lazy<ResourceName> =
-    Lazy::new(|| LATENCY_NAME_STR.parse().expect(MEASURE_NAME_ERROR));
-static LATENCY_SLUG: Lazy<Slug> = Lazy::new(|| LATENCY_SLUG_STR.parse().expect(MEASURE_SLUG_ERROR));
-static LATENCY_UNITS: Lazy<ResourceName> =
-    Lazy::new(|| LATENCY_UNITS_STR.parse().expect(MEASURE_UNITS_ERROR));
-
-pub const THROUGHPUT_NAME_STR: &str = "Throughput";
-pub const THROUGHPUT_SLUG_STR: &str = "throughput";
-pub const THROUGHPUT_UNITS_STR: &str = "operations / second (ops/s)";
-
-static THROUGHPUT_NAME: Lazy<ResourceName> =
-    Lazy::new(|| THROUGHPUT_NAME_STR.parse().expect(MEASURE_NAME_ERROR));
-static THROUGHPUT_SLUG: Lazy<Slug> =
-    Lazy::new(|| THROUGHPUT_SLUG_STR.parse().expect(MEASURE_SLUG_ERROR));
-static THROUGHPUT_UNITS: Lazy<ResourceName> =
-    Lazy::new(|| THROUGHPUT_UNITS_STR.parse().expect(MEASURE_UNITS_ERROR));
-
-// Iai measures
-
-pub const INSTRUCTIONS_NAME_STR: &str = "Instructions";
-pub const INSTRUCTIONS_SLUG_STR: &str = "instructions";
-pub const INSTRUCTIONS_UNITS_STR: &str = "instructions";
-
-static INSTRUCTIONS_NAME: Lazy<ResourceName> =
-    Lazy::new(|| INSTRUCTIONS_NAME_STR.parse().expect(MEASURE_NAME_ERROR));
-static INSTRUCTIONS_SLUG: Lazy<Slug> =
-    Lazy::new(|| INSTRUCTIONS_SLUG_STR.parse().expect(MEASURE_SLUG_ERROR));
-static INSTRUCTIONS_UNITS: Lazy<ResourceName> =
-    Lazy::new(|| INSTRUCTIONS_UNITS_STR.parse().expect(MEASURE_UNITS_ERROR));
-
-pub const L1_ACCESSES_NAME_STR: &str = "L1 Accesses";
-pub const L1_ACCESSES_SLUG_STR: &str = "l1-accesses";
-pub const L1_ACCESSES_UNITS_STR: &str = "accesses";
-
-static L1_ACCESSES_NAME: Lazy<ResourceName> =
-    Lazy::new(|| L1_ACCESSES_NAME_STR.parse().expect(MEASURE_NAME_ERROR));
-static L1_ACCESSES_SLUG: Lazy<Slug> =
-    Lazy::new(|| L1_ACCESSES_SLUG_STR.parse().expect(MEASURE_SLUG_ERROR));
-static L1_ACCESSES_UNITS: Lazy<ResourceName> =
-    Lazy::new(|| L1_ACCESSES_UNITS_STR.parse().expect(MEASURE_UNITS_ERROR));
-
-pub const L2_ACCESSES_NAME_STR: &str = "L2 Accesses";
-pub const L2_ACCESSES_SLUG_STR: &str = "l2-accesses";
-pub const L2_ACCESSES_UNITS_STR: &str = "accesses";
-
-static L2_ACCESSES_NAME: Lazy<ResourceName> =
-    Lazy::new(|| L2_ACCESSES_NAME_STR.parse().expect(MEASURE_NAME_ERROR));
-static L2_ACCESSES_SLUG: Lazy<Slug> =
-    Lazy::new(|| L2_ACCESSES_SLUG_STR.parse().expect(MEASURE_SLUG_ERROR));
-static L2_ACCESSES_UNITS: Lazy<ResourceName> =
-    Lazy::new(|| L2_ACCESSES_UNITS_STR.parse().expect(MEASURE_UNITS_ERROR));
-
-pub const RAM_ACCESSES_NAME_STR: &str = "RAM Accesses";
-pub const RAM_ACCESSES_SLUG_STR: &str = "ram-accesses";
-pub const RAM_ACCESSES_UNITS_STR: &str = "accesses";
-
-static RAM_ACCESSES_NAME: Lazy<ResourceName> =
-    Lazy::new(|| RAM_ACCESSES_NAME_STR.parse().expect(MEASURE_NAME_ERROR));
-static RAM_ACCESSES_SLUG: Lazy<Slug> =
-    Lazy::new(|| RAM_ACCESSES_SLUG_STR.parse().expect(MEASURE_SLUG_ERROR));
-static RAM_ACCESSES_UNITS: Lazy<ResourceName> =
-    Lazy::new(|| RAM_ACCESSES_UNITS_STR.parse().expect(MEASURE_UNITS_ERROR));
-
-pub const TOTAL_ACCESSES_NAME_STR: &str = "Total Accesses";
-pub const TOTAL_ACCESSES_SLUG_STR: &str = "total-accesses";
-pub const TOTAL_ACCESSES_UNITS_STR: &str = "accesses";
-
-static TOTAL_ACCESSES_NAME: Lazy<ResourceName> =
-    Lazy::new(|| TOTAL_ACCESSES_NAME_STR.parse().expect(MEASURE_NAME_ERROR));
-static TOTAL_ACCESSES_SLUG: Lazy<Slug> =
-    Lazy::new(|| TOTAL_ACCESSES_SLUG_STR.parse().expect(MEASURE_SLUG_ERROR));
-static TOTAL_ACCESSES_UNITS: Lazy<ResourceName> =
-    Lazy::new(|| TOTAL_ACCESSES_SLUG_STR.parse().expect(MEASURE_UNITS_ERROR));
-
-pub const ESTIMATED_CYCLES_NAME_STR: &str = "Estimated Cycles";
-pub const ESTIMATED_CYCLES_SLUG_STR: &str = "estimated-cycles";
-pub const ESTIMATED_CYCLES_UNITS_STR: &str = "estimated cycles";
-
-static ESTIMATED_CYCLES_NAME: Lazy<ResourceName> =
-    Lazy::new(|| ESTIMATED_CYCLES_NAME_STR.parse().expect(MEASURE_NAME_ERROR));
-static ESTIMATED_CYCLES_SLUG: Lazy<Slug> =
-    Lazy::new(|| ESTIMATED_CYCLES_SLUG_STR.parse().expect(MEASURE_SLUG_ERROR));
-static ESTIMATED_CYCLES_UNITS: Lazy<ResourceName> = Lazy::new(|| {
-    ESTIMATED_CYCLES_UNITS_STR
-        .parse()
-        .expect(MEASURE_UNITS_ERROR)
-});
-
-// File size measures
-
-pub const FILE_SIZE_NAME_STR: &str = "File Size";
-pub const FILE_SIZE_SLUG_STR: &str = "file-size";
-pub const FILE_SIZE_UNITS_STR: &str = "bytes (B)";
-
-static FILE_SIZE_NAME: Lazy<ResourceName> =
-    Lazy::new(|| FILE_SIZE_NAME_STR.parse().expect(MEASURE_NAME_ERROR));
-pub static FILE_SIZE_SLUG: Lazy<Slug> =
-    Lazy::new(|| FILE_SIZE_SLUG_STR.parse().expect(MEASURE_SLUG_ERROR));
-static FILE_SIZE_UNITS: Lazy<ResourceName> =
-    Lazy::new(|| FILE_SIZE_UNITS_STR.parse().expect(MEASURE_UNITS_ERROR));
-
-crate::typed_uuid::typed_uuid!(MeasureUuid);
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[cfg_attr(feature = "schema", derive(JsonSchema))]
-pub struct JsonNewMeasure {
-    /// The name of the measure.
-    /// Maximum length is 64 characters.
-    pub name: ResourceName,
-    /// The preferred slug for the measure.
-    /// If not provided, the slug will be generated from the name.
-    /// If the provided or generated slug is already in use, a unique slug will be generated.
-    /// Maximum length is 64 characters.
-    pub slug: Option<Slug>,
-    /// The units of measure.
-    /// Maximum length is 64 characters.
-    pub units: ResourceName,
-}
-
-impl JsonNewMeasure {
-    pub fn latency() -> Self {
-        Self {
-            name: LATENCY_NAME.clone(),
-            slug: Some(LATENCY_SLUG.clone()),
-            units: LATENCY_UNITS.clone(),
-        }
-    }
-
-    pub fn throughput() -> Self {
-        Self {
-            name: THROUGHPUT_NAME.clone(),
-            slug: Some(THROUGHPUT_SLUG.clone()),
-            units: THROUGHPUT_UNITS.clone(),
-        }
-    }
-
-    pub fn instructions() -> Self {
-        Self {
-            name: INSTRUCTIONS_NAME.clone(),
-            slug: Some(INSTRUCTIONS_SLUG.clone()),
-            units: INSTRUCTIONS_UNITS.clone(),
-        }
-    }
-
-    pub fn l1_accesses() -> Self {
-        Self {
-            name: L1_ACCESSES_NAME.clone(),
-            slug: Some(L1_ACCESSES_SLUG.clone()),
-            units: L1_ACCESSES_UNITS.clone(),
-        }
-    }
-
-    pub fn l2_accesses() -> Self {
-        Self {
-            name: L2_ACCESSES_NAME.clone(),
-            slug: Some(L2_ACCESSES_SLUG.clone()),
-            units: L2_ACCESSES_UNITS.clone(),
-        }
-    }
-
-    pub fn ram_accesses() -> Self {
-        Self {
-            name: RAM_ACCESSES_NAME.clone(),
-            slug: Some(RAM_ACCESSES_SLUG.clone()),
-            units: RAM_ACCESSES_UNITS.clone(),
-        }
-    }
-
-    pub fn total_accesses() -> Self {
-        Self {
-            name: TOTAL_ACCESSES_NAME.clone(),
-            slug: Some(TOTAL_ACCESSES_SLUG.clone()),
-            units: TOTAL_ACCESSES_UNITS.clone(),
-        }
-    }
-
-    pub fn estimated_cycles() -> Self {
-        Self {
-            name: ESTIMATED_CYCLES_NAME.clone(),
-            slug: Some(ESTIMATED_CYCLES_SLUG.clone()),
-            units: ESTIMATED_CYCLES_UNITS.clone(),
-        }
-    }
-
-    pub fn file_size() -> Self {
-        Self {
-            name: FILE_SIZE_NAME.clone(),
-            slug: Some(FILE_SIZE_SLUG.clone()),
-            units: FILE_SIZE_UNITS.clone(),
-        }
-    }
-}
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[cfg_attr(feature = "schema", derive(JsonSchema))]
-pub struct JsonMeasures(pub Vec<JsonMeasure>);
-
-crate::from_vec!(JsonMeasures[JsonMeasure]);
-
-#[typeshare::typeshare]
-#[derive(Debug, Clone, Deserialize, Serialize)]
-#[cfg_attr(feature = "schema", derive(JsonSchema))]
-pub struct JsonMeasure {
-    pub uuid: MeasureUuid,
-    pub project: ProjectUuid,
-    pub name: ResourceName,
-    pub slug: Slug,
-    pub units: ResourceName,
-    pub created: DateTime,
-    pub modified: DateTime,
-}
-
-impl fmt::Display for JsonMeasure {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{}: {}", self.name, self.units)
-    }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[cfg_attr(feature = "schema", derive(JsonSchema))]
-pub struct JsonUpdateMeasure {
-    /// The new name of the measure.
-    /// Maximum length is 64 characters.
-    pub name: Option<ResourceName>,
-    /// The preferred new slug for the measure.
-    /// Maximum length is 64 characters.
-    pub slug: Option<Slug>,
-    /// The new units of measure.
-    /// Maximum length is 64 characters.
-    pub units: Option<ResourceName>,
-}
diff --git a/lib/bencher_json/src/project/measure/built_in.rs b/lib/bencher_json/src/project/measure/built_in.rs
new file mode 100644
index 000000000..3e680f9ec
--- /dev/null
+++ b/lib/bencher_json/src/project/measure/built_in.rs
@@ -0,0 +1,132 @@
+use crate::JsonNewMeasure;
+use bencher_valid::NameId;
+
+pub trait BuiltInMeasure {
+    const NAME_STR: &'static str;
+    const SLUG_STR: &'static str;
+    const UNITS_STR: &'static str;
+
+    fn name_id() -> NameId {
+        Self::SLUG_STR
+            .parse()
+            .expect("Failed to parse measure slug.")
+    }
+
+    fn from_name_id(measure_str: &str) -> Option<JsonNewMeasure> {
+        (measure_str == Self::NAME_STR || measure_str == Self::SLUG_STR).then(Self::new_json)
+    }
+
+    fn new_json() -> JsonNewMeasure {
+        JsonNewMeasure {
+            name: Self::NAME_STR
+                .parse()
+                .expect("Failed to parse measure name."),
+            slug: Some(
+                Self::SLUG_STR
+                    .parse()
+                    .expect("Failed to parse measure slug."),
+            ),
+            units: Self::UNITS_STR
+                .parse()
+                .expect("Failed to parse measure units."),
+        }
+    }
+}
+
+macro_rules! create_measure {
+    ($id:ident, $name:literal, $slug:literal, $units:literal) => {
+        pub struct $id;
+
+        impl crate::project::measure::built_in::BuiltInMeasure for $id {
+            const NAME_STR: &'static str = $name;
+            const SLUG_STR: &'static str = $slug;
+            const UNITS_STR: &'static str = $units;
+        }
+    };
+}
+
+pub mod generic {
+    create_measure!(Latency, "Latency", "latency", "nanoseconds (ns)");
+
+    create_measure!(
+        Throughput,
+        "Throughput",
+        "throughput",
+        "operations / second (ops/s)"
+    );
+}
+
+pub mod iai {
+    create_measure!(Instructions, "Instructions", "instructions", "instructions");
+
+    create_measure!(L1Accesses, "L1 Accesses", "l1-accesses", "accesses");
+
+    create_measure!(L2Accesses, "L2 Accesses", "l2-accesses", "accesses");
+
+    create_measure!(RamAccesses, "RAM Accesses", "ram-accesses", "accesses");
+
+    create_measure!(
+        EstimatedCycles,
+        "Estimated Cycles",
+        "estimated-cycles",
+        "cycles"
+    );
+}
+
+pub mod iai_callgrind {
+    pub mod callgrind_tool {
+        create_measure!(Instructions, "Instructions", "instructions", "instructions");
+
+        create_measure!(L1Hits, "L1 Hits", "l1-hits", "hits");
+
+        create_measure!(L2Hits, "L2 Hits", "l2-hits", "hits");
+
+        create_measure!(RamHits, "RAM Hits", "ram-hits", "hits");
+
+        create_measure!(
+            TotalReadWrite,
+            "Total read+write",
+            "total-read-write",
+            "reads/writes"
+        );
+
+        create_measure!(
+            EstimatedCycles,
+            "Estimated Cycles",
+            "estimated-cycles",
+            "cycles"
+        );
+    }
+
+    pub mod dhat_tool {
+        create_measure!(TotalBytes, "Total bytes", "total-bytes", "bytes (B)");
+
+        create_measure!(TotalBlocks, "Total blocks", "total-blocks", "blocks");
+
+        create_measure!(
+            AtTGmaxBytes,
+            "At t-gmax bytes",
+            "at-t-gmax-bytes",
+            "bytes (B)"
+        );
+
+        create_measure!(
+            AtTGmaxBlocks,
+            "At t-gmax blocks",
+            "at-t-gmax-blocks",
+            "blocks"
+        );
+
+        create_measure!(AtTEndBytes, "At t-end bytes", "at-t-end-bytes", "bytes (B)");
+
+        create_measure!(AtTEndBlocks, "At t-end blocks", "at-t-end-blocks", "blocks");
+
+        create_measure!(ReadsBytes, "Reads bytes", "reads-bytes", "bytes (B)");
+
+        create_measure!(WritesBytes, "Writes bytes", "writes-bytes", "bytes (B)");
+    }
+}
+
+pub mod file_size {
+    create_measure!(FileSize, "File Size", "file-size", "bytes (B)");
+}
diff --git a/lib/bencher_json/src/project/measure/mod.rs b/lib/bencher_json/src/project/measure/mod.rs
new file mode 100644
index 000000000..941abf7f8
--- /dev/null
+++ b/lib/bencher_json/src/project/measure/mod.rs
@@ -0,0 +1,77 @@
+#![allow(clippy::expect_used)]
+
+use std::fmt;
+
+use bencher_valid::{DateTime, ResourceName, Slug};
+#[cfg(feature = "schema")]
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+use crate::ProjectUuid;
+
+pub mod built_in;
+
+crate::typed_uuid::typed_uuid!(MeasureUuid);
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[cfg_attr(feature = "schema", derive(JsonSchema))]
+pub struct JsonNewMeasure {
+    /// The name of the measure.
+    /// Maximum length is 64 characters.
+    pub name: ResourceName,
+    /// The preferred slug for the measure.
+    /// If not provided, the slug will be generated from the name.
+    /// If the provided or generated slug is already in use, a unique slug will be generated.
+    /// Maximum length is 64 characters.
+    pub slug: Option<Slug>,
+    /// The units of measure.
+    /// Maximum length is 64 characters.
+    pub units: ResourceName,
+}
+
+impl JsonNewMeasure {
+    pub fn generic_unit() -> ResourceName {
+        "Measure (units)"
+            .parse()
+            .expect("Failed to parse measure units.")
+    }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[cfg_attr(feature = "schema", derive(JsonSchema))]
+pub struct JsonMeasures(pub Vec<JsonMeasure>);
+
+crate::from_vec!(JsonMeasures[JsonMeasure]);
+
+#[typeshare::typeshare]
+#[derive(Debug, Clone, Deserialize, Serialize)]
+#[cfg_attr(feature = "schema", derive(JsonSchema))]
+pub struct JsonMeasure {
+    pub uuid: MeasureUuid,
+    pub project: ProjectUuid,
+    pub name: ResourceName,
+    pub slug: Slug,
+    pub units: ResourceName,
+    pub created: DateTime,
+    pub modified: DateTime,
+}
+
+impl fmt::Display for JsonMeasure {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}: {}", self.name, self.units)
+    }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[cfg_attr(feature = "schema", derive(JsonSchema))]
+pub struct JsonUpdateMeasure {
+    /// The new name of the measure.
+    /// Maximum length is 64 characters.
+    pub name: Option<ResourceName>,
+    /// The preferred new slug for the measure.
+    /// Maximum length is 64 characters.
+    pub slug: Option<Slug>,
+    /// The new units of measure.
+    /// Maximum length is 64 characters.
+    pub units: Option<ResourceName>,
+}
diff --git a/services/api/src/endpoints/organization/projects.rs b/services/api/src/endpoints/organization/projects.rs
index ac48b39cd..e522c72c0 100644
--- a/services/api/src/endpoints/organization/projects.rs
+++ b/services/api/src/endpoints/organization/projects.rs
@@ -1,6 +1,10 @@
 use bencher_json::{
-    project::ProjectRole, DateTime, JsonDirection, JsonNewProject, JsonPagination, JsonProject,
-    JsonProjects, ResourceId, ResourceName,
+    project::{
+        measure::built_in::generic::{Latency, Throughput},
+        ProjectRole,
+    },
+    DateTime, JsonDirection, JsonNewProject, JsonPagination, JsonProject, JsonProjects, ResourceId,
+    ResourceName,
 };
 use bencher_rbac::organization::Permission;
 use diesel::{
@@ -284,7 +288,8 @@ async fn post_inner(
     slog::debug!(log, "Added project testbed: {insert_testbed:?}");
 
     // Add a `latency` measure to the project
-    let insert_measure = InsertMeasure::latency(conn_lock!(context), query_project.id)?;
+    let insert_measure =
+        InsertMeasure::from_measure::<Latency>(conn_lock!(context), query_project.id)?;
     diesel::insert_into(schema::measure::table)
         .values(&insert_measure)
         .execute(conn_lock!(context))
@@ -302,7 +307,8 @@ async fn post_inner(
     slog::debug!(log, "Added project threshold: {threshold_id}");
 
     // Add a `throughput` measure to the project
-    let insert_measure = InsertMeasure::throughput(conn_lock!(context), query_project.id)?;
+    let insert_measure =
+        InsertMeasure::from_measure::<Throughput>(conn_lock!(context), query_project.id)?;
     diesel::insert_into(schema::measure::table)
         .values(&insert_measure)
         .execute(conn_lock!(context))
diff --git a/services/api/src/model/project/measure.rs b/services/api/src/model/project/measure.rs
index 1333813f1..d79302683 100644
--- a/services/api/src/model/project/measure.rs
+++ b/services/api/src/model/project/measure.rs
@@ -1,11 +1,7 @@
 use bencher_json::{
     project::measure::{
-        JsonUpdateMeasure, MeasureUuid, ESTIMATED_CYCLES_NAME_STR, ESTIMATED_CYCLES_SLUG_STR,
-        FILE_SIZE_NAME_STR, FILE_SIZE_SLUG_STR, INSTRUCTIONS_NAME_STR, INSTRUCTIONS_SLUG_STR,
-        L1_ACCESSES_NAME_STR, L1_ACCESSES_SLUG_STR, L2_ACCESSES_NAME_STR, L2_ACCESSES_SLUG_STR,
-        LATENCY_NAME_STR, LATENCY_SLUG_STR, MEASURE_UNITS, RAM_ACCESSES_NAME_STR,
-        RAM_ACCESSES_SLUG_STR, THROUGHPUT_NAME_STR, THROUGHPUT_SLUG_STR, TOTAL_ACCESSES_NAME_STR,
-        TOTAL_ACCESSES_SLUG_STR,
+        built_in::{self, BuiltInMeasure},
+        JsonUpdateMeasure, MeasureUuid,
     },
     DateTime, JsonMeasure, JsonNewMeasure, MeasureNameId, NameIdKind, ResourceName, Slug,
 };
@@ -71,41 +67,59 @@ impl QueryMeasure {
 
         // Dynamically create adapter specific measures
         // Or recreate default measures if they were previously deleted
-        let measure = match measure.as_ref() {
-            // Recreate
-            LATENCY_NAME_STR | LATENCY_SLUG_STR => JsonNewMeasure::latency(),
-            THROUGHPUT_NAME_STR | THROUGHPUT_SLUG_STR => JsonNewMeasure::throughput(),
-            // Adapter specific
-            // Iai
-            INSTRUCTIONS_NAME_STR | INSTRUCTIONS_SLUG_STR => JsonNewMeasure::instructions(),
-            L1_ACCESSES_NAME_STR | L1_ACCESSES_SLUG_STR => JsonNewMeasure::l1_accesses(),
-            L2_ACCESSES_NAME_STR | L2_ACCESSES_SLUG_STR => JsonNewMeasure::l2_accesses(),
-            RAM_ACCESSES_NAME_STR | RAM_ACCESSES_SLUG_STR => JsonNewMeasure::ram_accesses(),
-            TOTAL_ACCESSES_NAME_STR | TOTAL_ACCESSES_SLUG_STR => JsonNewMeasure::total_accesses(),
-            ESTIMATED_CYCLES_NAME_STR | ESTIMATED_CYCLES_SLUG_STR => {
-                JsonNewMeasure::estimated_cycles()
-            },
-            // File size
-            FILE_SIZE_NAME_STR | FILE_SIZE_SLUG_STR => JsonNewMeasure::file_size(),
-            _ => {
-                let Ok(kind) = NameIdKind::<ResourceName>::try_from(measure) else {
-                    return Err(http_error);
-                };
-                match kind {
-                    NameIdKind::Uuid(_) => return Err(http_error),
-                    NameIdKind::Slug(slug) => JsonNewMeasure {
-                        name: slug.clone().into(),
-                        slug: Some(slug),
-                        units: MEASURE_UNITS.clone(),
-                    },
-                    NameIdKind::Name(name) => JsonNewMeasure {
-                        name,
-                        slug: None,
-                        units: MEASURE_UNITS.clone(),
-                    },
-                }
-            },
+        let measure_str = measure.as_ref();
+
+        let measure = if let Some(measure) = built_in::generic::Latency::from_name_id(measure_str)
+            .or_else(|| built_in::generic::Throughput::from_name_id(measure_str))
+            .or_else(|| built_in::iai::Instructions::from_name_id(measure_str))
+            .or_else(|| built_in::iai::L1Accesses::from_name_id(measure_str))
+            .or_else(|| built_in::iai::L2Accesses::from_name_id(measure_str))
+            .or_else(|| built_in::iai::RamAccesses::from_name_id(measure_str))
+            .or_else(|| built_in::iai::EstimatedCycles::from_name_id(measure_str))
+            .or_else(|| {
+                built_in::iai_callgrind::callgrind_tool::Instructions::from_name_id(measure_str)
+            })
+            .or_else(|| built_in::iai_callgrind::callgrind_tool::L1Hits::from_name_id(measure_str))
+            .or_else(|| built_in::iai_callgrind::callgrind_tool::L2Hits::from_name_id(measure_str))
+            .or_else(|| built_in::iai_callgrind::callgrind_tool::RamHits::from_name_id(measure_str))
+            .or_else(|| {
+                built_in::iai_callgrind::callgrind_tool::TotalReadWrite::from_name_id(measure_str)
+            })
+            .or_else(|| {
+                built_in::iai_callgrind::callgrind_tool::EstimatedCycles::from_name_id(measure_str)
+            })
+            .or_else(|| built_in::iai_callgrind::dhat_tool::TotalBytes::from_name_id(measure_str))
+            .or_else(|| built_in::iai_callgrind::dhat_tool::TotalBlocks::from_name_id(measure_str))
+            .or_else(|| built_in::iai_callgrind::dhat_tool::AtTGmaxBytes::from_name_id(measure_str))
+            .or_else(|| {
+                built_in::iai_callgrind::dhat_tool::AtTGmaxBlocks::from_name_id(measure_str)
+            })
+            .or_else(|| built_in::iai_callgrind::dhat_tool::AtTEndBytes::from_name_id(measure_str))
+            .or_else(|| built_in::iai_callgrind::dhat_tool::AtTEndBlocks::from_name_id(measure_str))
+            .or_else(|| built_in::iai_callgrind::dhat_tool::ReadsBytes::from_name_id(measure_str))
+            .or_else(|| built_in::iai_callgrind::dhat_tool::WritesBytes::from_name_id(measure_str))
+            .or_else(|| built_in::file_size::FileSize::from_name_id(measure_str))
+        {
+            measure
+        } else {
+            let Ok(kind) = NameIdKind::<ResourceName>::try_from(measure) else {
+                return Err(http_error);
+            };
+            match kind {
+                NameIdKind::Uuid(_) => return Err(http_error),
+                NameIdKind::Slug(slug) => JsonNewMeasure {
+                    name: slug.clone().into(),
+                    slug: Some(slug),
+                    units: JsonNewMeasure::generic_unit(),
+                },
+                NameIdKind::Name(name) => JsonNewMeasure {
+                    name,
+                    slug: None,
+                    units: JsonNewMeasure::generic_unit(),
+                },
+            }
         };
+
         let insert_measure = InsertMeasure::from_json(conn, project_id, measure)?;
         diesel::insert_into(schema::measure::table)
             .values(&insert_measure)
@@ -157,6 +171,13 @@ pub struct InsertMeasure {
 }
 
 impl InsertMeasure {
+    pub fn from_measure<T: BuiltInMeasure>(
+        conn: &mut DbConnection,
+        project_id: ProjectId,
+    ) -> Result<Self, HttpError> {
+        Self::from_json(conn, project_id, T::new_json())
+    }
+
     pub fn from_json(
         conn: &mut DbConnection,
         project_id: ProjectId,
@@ -175,48 +196,6 @@ impl InsertMeasure {
             modified: timestamp,
         })
     }
-
-    pub fn latency(conn: &mut DbConnection, project_id: ProjectId) -> Result<Self, HttpError> {
-        Self::from_json(conn, project_id, JsonNewMeasure::latency())
-    }
-
-    pub fn throughput(conn: &mut DbConnection, project_id: ProjectId) -> Result<Self, HttpError> {
-        Self::from_json(conn, project_id, JsonNewMeasure::throughput())
-    }
-
-    pub fn instructions(conn: &mut DbConnection, project_id: ProjectId) -> Result<Self, HttpError> {
-        Self::from_json(conn, project_id, JsonNewMeasure::instructions())
-    }
-
-    pub fn l1_accesses(conn: &mut DbConnection, project_id: ProjectId) -> Result<Self, HttpError> {
-        Self::from_json(conn, project_id, JsonNewMeasure::l1_accesses())
-    }
-
-    pub fn l2_accesses(conn: &mut DbConnection, project_id: ProjectId) -> Result<Self, HttpError> {
-        Self::from_json(conn, project_id, JsonNewMeasure::l2_accesses())
-    }
-
-    pub fn ram_accesses(conn: &mut DbConnection, project_id: ProjectId) -> Result<Self, HttpError> {
-        Self::from_json(conn, project_id, JsonNewMeasure::ram_accesses())
-    }
-
-    pub fn total_accesses(
-        conn: &mut DbConnection,
-        project_id: ProjectId,
-    ) -> Result<Self, HttpError> {
-        Self::from_json(conn, project_id, JsonNewMeasure::total_accesses())
-    }
-
-    pub fn estimated_cycles(
-        conn: &mut DbConnection,
-        project_id: ProjectId,
-    ) -> Result<Self, HttpError> {
-        Self::from_json(conn, project_id, JsonNewMeasure::estimated_cycles())
-    }
-
-    pub fn file_size(conn: &mut DbConnection, project_id: ProjectId) -> Result<Self, HttpError> {
-        Self::from_json(conn, project_id, JsonNewMeasure::file_size())
-    }
 }
 
 #[derive(Debug, Clone, diesel::AsChangeset)]
diff --git a/services/cli/src/bencher/sub/mock.rs b/services/cli/src/bencher/sub/mock.rs
index 297ee5b17..027696952 100644
--- a/services/cli/src/bencher/sub/mock.rs
+++ b/services/cli/src/bencher/sub/mock.rs
@@ -1,10 +1,10 @@
 use std::collections::HashMap;
 
-use bencher_adapter::{
-    results::{adapter_metrics::AdapterMetrics, adapter_results::LATENCY_NAME_ID},
-    AdapterResults,
+use bencher_adapter::{results::adapter_metrics::AdapterMetrics, AdapterResults};
+use bencher_json::{
+    project::measure::built_in::{generic::Latency, BuiltInMeasure},
+    JsonNewMetric,
 };
-use bencher_json::JsonNewMetric;
 use literally::hmap;
 use rand::{distributions::Uniform, prelude::Distribution, Rng};
 
@@ -93,7 +93,7 @@ impl Mock {
                     .map_err(MockError::ParseBenchmarkName)?,
                 AdapterMetrics {
                     inner: hmap! {
-                        LATENCY_NAME_ID.clone() => JsonNewMetric {
+                        Latency::name_id() => JsonNewMetric {
                              value: value.into(),
                              lower_value: Some((value - variance).into()),
                              upper_value: Some((value + variance).into()),
diff --git a/services/cli/src/bencher/sub/project/run/runner/file_size.rs b/services/cli/src/bencher/sub/project/run/runner/file_size.rs
index cc54e9c0c..f80c3790c 100644
--- a/services/cli/src/bencher/sub/project/run/runner/file_size.rs
+++ b/services/cli/src/bencher/sub/project/run/runner/file_size.rs
@@ -1,6 +1,9 @@
 use std::fmt;
 
-use bencher_json::{project::measure::FILE_SIZE_SLUG, JsonNewMetric};
+use bencher_json::{
+    project::measure::built_in::{self, BuiltInMeasure},
+    JsonNewMetric,
+};
 use camino::Utf8PathBuf;
 
 use crate::RunError;
@@ -35,7 +38,6 @@ impl FileSize {
                 .unwrap_or(file_path.as_str())
                 .parse()
                 .map_err(RunError::OutputFileName)?;
-            let file_size_slug = FILE_SIZE_SLUG.clone().into();
             #[allow(clippy::cast_precision_loss)]
             let value = (std::fs::metadata(file_path)
                 .map(|m| m.len())
@@ -44,7 +46,7 @@ impl FileSize {
             results.push((
                 file_name,
                 vec![(
-                    file_size_slug,
+                    built_in::file_size::FileSize::name_id(),
                     JsonNewMetric {
                         value,
                         ..Default::default()


Test patch: