1use std::sync::Arc;
2use std::thread::JoinHandle;
3use std::time::{SystemTime, UNIX_EPOCH};
4
5use lasso::ThreadedRodeo;
6
7use photon_batch::{BatchError, BatchStats};
8use photon_core::types::id::RunId;
9use photon_core::types::metric::{Metric, MetricKey, RawPoint, Step};
10use photon_uplink::{UplinkStats, UplinkThreadError};
11use photon_wal::Wal;
12use tokio::sync::oneshot;
13
14use crate::accumulator::Accumulator;
15use crate::error::{FinishError, LogError};
16
17#[derive(Clone, Debug)]
18pub struct RunStats {
19 pub points: u64,
20 pub points_dropped: u64,
21 pub batches: u64,
22 pub bytes_compressed: u64,
23 pub bytes_uncompressed: u64,
24 pub batches_sent: u64,
25 pub batches_acked: u64,
26 pub batches_rejected: u64,
27}
28
29pub(crate) struct UplinkHandle {
30 pub shutdown_tx: Option<oneshot::Sender<()>>,
31 pub handle: JoinHandle<Result<UplinkStats, UplinkThreadError>>,
32}
33
34pub struct Run {
35 id: RunId,
36 accumulator: Accumulator<RawPoint>,
37 interner: Arc<ThreadedRodeo>,
38 batch_handle: JoinHandle<Result<BatchStats, BatchError>>,
39 uplink_handle: Option<UplinkHandle>,
40 wal: Arc<dyn Wal>,
41 points_logged: u64,
42 last_step: Vec<Option<Step>>,
43}
44
45impl Run {
46 pub(crate) fn new(
47 id: RunId,
48 accumulator: Accumulator<RawPoint>,
49 interner: Arc<ThreadedRodeo>,
50 batch_handle: JoinHandle<Result<BatchStats, BatchError>>,
51 uplink_handle: Option<UplinkHandle>,
52 wal: Arc<dyn Wal>,
53 ) -> Self {
54 Self {
55 id,
56 accumulator,
57 interner,
58 batch_handle,
59 uplink_handle,
60 wal,
61 points_logged: 0,
62 last_step: Vec::new(),
63 }
64 }
65
66 pub fn log(&mut self, key: &str, value: f64, step: u64) -> Result<(), LogError> {
71 Metric::new(key)?;
72 let spur = self.interner.get_or_intern(key);
73 let metric_key = MetricKey::new(lasso::Key::into_usize(spur));
74 let step = Step::new(step);
75
76 let idx = metric_key.index();
77 if idx >= self.last_step.len() {
78 self.last_step.resize(idx + 1, None);
79 }
80 if let Some(last) = self.last_step[idx]
81 && step <= last
82 {
83 return Err(LogError::StepNotMonotonic {
84 key: key.to_owned(),
85 step: step.as_u64(),
86 last: last.as_u64(),
87 });
88 }
89 self.last_step[idx] = Some(step);
90
91 let now = SystemTime::now()
92 .duration_since(UNIX_EPOCH)
93 .unwrap_or_default()
94 .as_nanos() as u64;
95
96 self.accumulator.push(RawPoint {
97 key: metric_key,
98 value,
99 step,
100 timestamp_ns: now,
101 });
102
103 self.points_logged += 1;
104 Ok(())
105 }
106
107 pub fn points_logged(&self) -> u64 {
108 self.points_logged
109 }
110
111 pub fn points_dropped(&self) -> u64 {
112 self.accumulator.points_dropped()
113 }
114
115 pub fn id(&self) -> RunId {
116 self.id
117 }
118
119 pub fn finish(mut self) -> Result<RunStats, FinishError> {
121 let points_logged = self.points_logged;
122 let points_dropped = self.accumulator.points_dropped();
123
124 drop(std::mem::replace(&mut self.accumulator, {
126 let (acc, _rx) = Accumulator::new(1);
127 acc
128 }));
129
130 let batch_stats = self
131 .batch_handle
132 .join()
133 .map_err(|_| FinishError::Panicked)?
134 .map_err(FinishError::Batch)?;
135
136 let (batches_sent, batches_acked, batches_rejected) = match self.uplink_handle {
137 Some(mut ctx) => {
138 drop(ctx.shutdown_tx.take());
139
140 let uplink_stats = ctx
141 .handle
142 .join()
143 .map_err(|_| FinishError::Panicked)?
144 .map_err(FinishError::Uplink)?;
145
146 (
147 uplink_stats.batches_sent,
148 uplink_stats.batches_acked,
149 uplink_stats.rejections_received,
150 )
151 }
152 None => (0, 0, 0),
153 };
154
155 if batches_sent == 0 || batches_acked >= batches_sent {
156 let _ = self.wal.close();
157 }
158
159 Ok(RunStats {
160 points: points_logged,
161 points_dropped,
162 batches: batch_stats.batches_created,
163 bytes_compressed: batch_stats.bytes_compressed,
164 bytes_uncompressed: batch_stats.bytes_uncompressed,
165 batches_sent,
166 batches_acked,
167 batches_rejected,
168 })
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use crate::Run;
175 use crate::error::LogError;
176
177 fn new_run() -> Run {
178 Run::builder().start().expect("start should succeed")
179 }
180
181 #[test]
182 fn test_monotonic_steps_accepted() {
183 let mut run = new_run();
184 run.log("train/loss", 0.5, 1).unwrap();
185 run.log("train/loss", 0.4, 2).unwrap();
186 run.log("train/loss", 0.3, 3).unwrap();
187 }
188
189 #[test]
190 fn test_duplicate_step_rejected() {
191 let mut run = new_run();
192 run.log("train/loss", 0.5, 1).unwrap();
193 let err = run.log("train/loss", 0.4, 1).unwrap_err();
194 assert!(matches!(err, LogError::StepNotMonotonic { .. }));
195 }
196
197 #[test]
198 fn test_decreasing_step_rejected() {
199 let mut run = new_run();
200 run.log("train/loss", 0.5, 5).unwrap();
201 let err = run.log("train/loss", 0.4, 3).unwrap_err();
202 assert!(matches!(
203 err,
204 LogError::StepNotMonotonic {
205 step: 3,
206 last: 5,
207 ..
208 }
209 ));
210 }
211
212 #[test]
213 fn test_different_metrics_independent() {
214 let mut run = new_run();
215 run.log("train/loss", 0.5, 1).unwrap();
216 run.log("eval/loss", 0.6, 1).unwrap(); run.log("train/loss", 0.4, 2).unwrap();
218 run.log("eval/loss", 0.5, 2).unwrap();
219 }
220
221 #[test]
222 fn test_large_step_gap_accepted() {
223 let mut run = new_run();
224 run.log("train/loss", 0.5, 1).unwrap();
225 run.log("train/loss", 0.4, 1000).unwrap(); }
227}