1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::collections::VecDeque;
6use std::fmt;
7
8#[derive(Debug, Clone, Default, Serialize, Deserialize)]
13#[non_exhaustive]
14pub struct Metrics {
15 pub(crate) avg_wait_time: f64,
18 pub(crate) avg_ride_time: f64,
20 pub(crate) max_wait_time: u64,
22 pub(crate) throughput: u64,
24 pub(crate) total_delivered: u64,
26 pub(crate) total_abandoned: u64,
28 pub(crate) total_spawned: u64,
30 pub(crate) abandonment_rate: f64,
32 pub(crate) total_distance: f64,
34 #[serde(default)]
38 pub(crate) utilization_by_group: HashMap<String, f64>,
39 #[serde(default)]
41 pub(crate) reposition_distance: f64,
42 #[serde(default)]
45 pub(crate) total_moves: u64,
46 pub(crate) total_settled: u64,
48 pub(crate) total_rerouted: u64,
50
51 #[cfg(feature = "energy")]
53 #[serde(default)]
54 pub(crate) total_energy_consumed: f64,
55 #[cfg(feature = "energy")]
57 #[serde(default)]
58 pub(crate) total_energy_regenerated: f64,
59
60 sum_wait_ticks: u64,
63 sum_ride_ticks: u64,
65 boarded_count: u64,
67 delivered_count: u64,
69 delivery_window: VecDeque<u64>,
71 pub(crate) throughput_window_ticks: u64,
73}
74
75impl Metrics {
76 #[must_use]
78 pub fn new() -> Self {
79 Self {
80 throughput_window_ticks: 3600, ..Default::default()
82 }
83 }
84
85 #[must_use]
87 pub const fn with_throughput_window(mut self, window_ticks: u64) -> Self {
88 self.throughput_window_ticks = window_ticks;
89 self
90 }
91
92 #[must_use]
96 pub const fn avg_wait_time(&self) -> f64 {
97 self.avg_wait_time
98 }
99
100 #[must_use]
102 pub const fn avg_ride_time(&self) -> f64 {
103 self.avg_ride_time
104 }
105
106 #[must_use]
108 pub const fn max_wait_time(&self) -> u64 {
109 self.max_wait_time
110 }
111
112 #[must_use]
114 pub const fn throughput(&self) -> u64 {
115 self.throughput
116 }
117
118 #[must_use]
120 pub const fn total_delivered(&self) -> u64 {
121 self.total_delivered
122 }
123
124 #[must_use]
126 pub const fn total_abandoned(&self) -> u64 {
127 self.total_abandoned
128 }
129
130 #[must_use]
132 pub const fn total_spawned(&self) -> u64 {
133 self.total_spawned
134 }
135
136 #[must_use]
138 pub const fn abandonment_rate(&self) -> f64 {
139 self.abandonment_rate
140 }
141
142 #[must_use]
144 pub const fn total_distance(&self) -> f64 {
145 self.total_distance
146 }
147
148 #[must_use]
150 pub const fn utilization_by_group(&self) -> &HashMap<String, f64> {
151 &self.utilization_by_group
152 }
153
154 #[must_use]
156 pub const fn reposition_distance(&self) -> f64 {
157 self.reposition_distance
158 }
159
160 #[must_use]
163 pub const fn total_moves(&self) -> u64 {
164 self.total_moves
165 }
166
167 #[must_use]
169 pub const fn total_settled(&self) -> u64 {
170 self.total_settled
171 }
172
173 #[must_use]
175 pub const fn total_rerouted(&self) -> u64 {
176 self.total_rerouted
177 }
178
179 #[must_use]
181 pub const fn throughput_window_ticks(&self) -> u64 {
182 self.throughput_window_ticks
183 }
184
185 #[cfg(feature = "energy")]
187 #[must_use]
188 pub const fn total_energy_consumed(&self) -> f64 {
189 self.total_energy_consumed
190 }
191
192 #[cfg(feature = "energy")]
194 #[must_use]
195 pub const fn total_energy_regenerated(&self) -> f64 {
196 self.total_energy_regenerated
197 }
198
199 #[cfg(feature = "energy")]
201 #[must_use]
202 pub const fn net_energy(&self) -> f64 {
203 self.total_energy_consumed - self.total_energy_regenerated
204 }
205
206 #[must_use]
210 pub fn avg_utilization(&self) -> f64 {
211 if self.utilization_by_group.is_empty() {
212 return 0.0;
213 }
214 let sum: f64 = self.utilization_by_group.values().sum();
215 sum / self.utilization_by_group.len() as f64
216 }
217
218 pub(crate) const fn record_spawn(&mut self) {
220 self.total_spawned += 1;
221 }
222
223 #[allow(clippy::cast_precision_loss)] pub(crate) fn record_board(&mut self, wait_ticks: u64) {
226 self.boarded_count += 1;
227 self.sum_wait_ticks += wait_ticks;
228 self.avg_wait_time = self.sum_wait_ticks as f64 / self.boarded_count as f64;
229 if wait_ticks > self.max_wait_time {
230 self.max_wait_time = wait_ticks;
231 }
232 }
233
234 #[allow(clippy::cast_precision_loss)] pub(crate) fn record_delivery(&mut self, ride_ticks: u64, tick: u64) {
237 self.delivered_count += 1;
238 self.total_delivered += 1;
239 self.sum_ride_ticks += ride_ticks;
240 self.avg_ride_time = self.sum_ride_ticks as f64 / self.delivered_count as f64;
241 self.delivery_window.push_back(tick);
242 }
243
244 #[allow(clippy::cast_precision_loss)] pub(crate) fn record_abandonment(&mut self) {
247 self.total_abandoned += 1;
248 if self.total_spawned > 0 {
249 self.abandonment_rate = self.total_abandoned as f64 / self.total_spawned as f64;
250 }
251 }
252
253 pub(crate) const fn record_settle(&mut self) {
255 self.total_settled += 1;
256 }
257
258 pub(crate) const fn record_reroute(&mut self) {
260 self.total_rerouted += 1;
261 }
262
263 pub(crate) fn record_distance(&mut self, distance: f64) {
265 self.total_distance += distance;
266 }
267
268 pub(crate) fn record_reposition_distance(&mut self, distance: f64) {
270 self.reposition_distance += distance;
271 }
272
273 #[cfg(feature = "energy")]
275 pub(crate) fn record_energy(&mut self, consumed: f64, regenerated: f64) {
276 self.total_energy_consumed += consumed;
277 self.total_energy_regenerated += regenerated;
278 }
279
280 #[allow(clippy::cast_possible_truncation)] pub(crate) fn update_throughput(&mut self, current_tick: u64) {
283 let cutoff = current_tick.saturating_sub(self.throughput_window_ticks);
284 while self.delivery_window.front().is_some_and(|&t| t <= cutoff) {
286 self.delivery_window.pop_front();
287 }
288 self.throughput = self.delivery_window.len() as u64;
289 }
290}
291
292impl fmt::Display for Metrics {
293 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301 write!(
302 f,
303 "{} delivered, avg wait {:.1}t, {:.0}% util",
304 self.total_delivered,
305 self.avg_wait_time,
306 self.avg_utilization() * 100.0,
307 )
308 }
309}