Skip to main content

trueno_db/experiment/
metric_record.rs

1//! Metric Record - time-series metrics for runs
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6/// Metric Record represents a single metric data point.
7///
8/// Designed for time-series storage, where metrics are ordered by step
9/// and can be efficiently queried by `run_id` and key.
10///
11/// ## Time-Series Optimization
12///
13/// Metrics are stored with:
14/// - `run_id` + `key` as the partition key for efficient filtering
15/// - `step` as the sort key for time-series ordering
16/// - `timestamp` for wall-clock time correlation
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
18pub struct MetricRecord {
19    run_id: String,
20    key: String,
21    step: u64,
22    value: f64,
23    timestamp: DateTime<Utc>,
24}
25
26impl MetricRecord {
27    /// Create a new metric record.
28    ///
29    /// # Arguments
30    ///
31    /// * `run_id` - ID of the parent run
32    /// * `key` - Metric name/key (e.g., "loss", "accuracy")
33    /// * `step` - Training step or epoch number
34    /// * `value` - Metric value
35    ///
36    /// # Returns
37    ///
38    /// A new `MetricRecord` with the current timestamp.
39    #[must_use]
40    pub fn new(run_id: impl Into<String>, key: impl Into<String>, step: u64, value: f64) -> Self {
41        Self { run_id: run_id.into(), key: key.into(), step, value, timestamp: Utc::now() }
42    }
43
44    /// Create a builder for constructing a metric record with optional fields.
45    #[must_use]
46    pub fn builder(
47        run_id: impl Into<String>,
48        key: impl Into<String>,
49        step: u64,
50        value: f64,
51    ) -> MetricRecordBuilder {
52        MetricRecordBuilder::new(run_id, key, step, value)
53    }
54
55    /// Get the run ID.
56    #[must_use]
57    pub fn run_id(&self) -> &str {
58        &self.run_id
59    }
60
61    /// Get the metric key/name.
62    #[must_use]
63    pub fn key(&self) -> &str {
64        &self.key
65    }
66
67    /// Get the step/epoch number.
68    #[must_use]
69    pub const fn step(&self) -> u64 {
70        self.step
71    }
72
73    /// Get the metric value.
74    #[must_use]
75    pub const fn value(&self) -> f64 {
76        self.value
77    }
78
79    /// Get the timestamp when the metric was recorded.
80    #[must_use]
81    pub const fn timestamp(&self) -> DateTime<Utc> {
82        self.timestamp
83    }
84}
85
86/// Builder for `MetricRecord`.
87#[derive(Debug)]
88pub struct MetricRecordBuilder {
89    run_id: String,
90    key: String,
91    step: u64,
92    value: f64,
93    timestamp: DateTime<Utc>,
94}
95
96impl MetricRecordBuilder {
97    /// Create a new builder with required fields.
98    #[must_use]
99    pub fn new(run_id: impl Into<String>, key: impl Into<String>, step: u64, value: f64) -> Self {
100        Self { run_id: run_id.into(), key: key.into(), step, value, timestamp: Utc::now() }
101    }
102
103    /// Set a custom timestamp.
104    #[must_use]
105    pub const fn timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
106        self.timestamp = timestamp;
107        self
108    }
109
110    /// Build the `MetricRecord`.
111    #[must_use]
112    pub fn build(self) -> MetricRecord {
113        MetricRecord {
114            run_id: self.run_id,
115            key: self.key,
116            step: self.step,
117            value: self.value,
118            timestamp: self.timestamp,
119        }
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_metric_record_new() {
129        let metric = MetricRecord::new("run-1", "loss", 0, 0.5);
130        assert_eq!(metric.run_id(), "run-1");
131        assert_eq!(metric.key(), "loss");
132        assert_eq!(metric.step(), 0);
133        assert!((metric.value() - 0.5).abs() < f64::EPSILON);
134    }
135
136    #[test]
137    fn test_metric_record_ordering() {
138        let m1 = MetricRecord::new("run-1", "loss", 0, 1.0);
139        let m2 = MetricRecord::new("run-1", "loss", 1, 0.9);
140        assert!(m1.step() < m2.step());
141    }
142}