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)]
39 pub(crate) utilization_by_group: HashMap<String, f64>,
40 #[serde(default)]
42 pub(crate) reposition_distance: f64,
43 #[serde(default)]
46 pub(crate) total_moves: u64,
47 pub(crate) total_settled: u64,
49 pub(crate) total_rerouted: u64,
51
52 #[cfg(feature = "energy")]
54 #[serde(default)]
55 pub(crate) total_energy_consumed: f64,
56 #[cfg(feature = "energy")]
58 #[serde(default)]
59 pub(crate) total_energy_regenerated: f64,
60
61 sum_wait_ticks: u64,
64 sum_ride_ticks: u64,
66 boarded_count: u64,
68 delivered_count: u64,
70 delivery_window: VecDeque<u64>,
72 pub(crate) throughput_window_ticks: u64,
74}
75
76impl Metrics {
77 #[must_use]
79 pub fn new() -> Self {
80 Self {
81 throughput_window_ticks: 3600, ..Default::default()
83 }
84 }
85
86 #[must_use]
88 pub const fn with_throughput_window(mut self, window_ticks: u64) -> Self {
89 self.throughput_window_ticks = window_ticks;
90 self
91 }
92
93 #[must_use]
97 pub const fn avg_wait_time(&self) -> f64 {
98 self.avg_wait_time
99 }
100
101 #[must_use]
103 pub const fn avg_ride_time(&self) -> f64 {
104 self.avg_ride_time
105 }
106
107 #[must_use]
109 pub const fn max_wait_time(&self) -> u64 {
110 self.max_wait_time
111 }
112
113 #[must_use]
115 pub const fn throughput(&self) -> u64 {
116 self.throughput
117 }
118
119 #[must_use]
121 pub const fn total_delivered(&self) -> u64 {
122 self.total_delivered
123 }
124
125 #[must_use]
127 pub const fn total_abandoned(&self) -> u64 {
128 self.total_abandoned
129 }
130
131 #[must_use]
133 pub const fn total_spawned(&self) -> u64 {
134 self.total_spawned
135 }
136
137 #[must_use]
139 pub const fn abandonment_rate(&self) -> f64 {
140 self.abandonment_rate
141 }
142
143 #[must_use]
145 pub const fn total_distance(&self) -> f64 {
146 self.total_distance
147 }
148
149 #[must_use]
151 pub const fn utilization_by_group(&self) -> &HashMap<String, f64> {
152 &self.utilization_by_group
153 }
154
155 #[must_use]
157 pub const fn reposition_distance(&self) -> f64 {
158 self.reposition_distance
159 }
160
161 #[must_use]
164 pub const fn total_moves(&self) -> u64 {
165 self.total_moves
166 }
167
168 #[must_use]
170 pub const fn total_settled(&self) -> u64 {
171 self.total_settled
172 }
173
174 #[must_use]
176 pub const fn total_rerouted(&self) -> u64 {
177 self.total_rerouted
178 }
179
180 #[must_use]
182 pub const fn throughput_window_ticks(&self) -> u64 {
183 self.throughput_window_ticks
184 }
185
186 #[cfg(feature = "energy")]
188 #[must_use]
189 pub const fn total_energy_consumed(&self) -> f64 {
190 self.total_energy_consumed
191 }
192
193 #[cfg(feature = "energy")]
195 #[must_use]
196 pub const fn total_energy_regenerated(&self) -> f64 {
197 self.total_energy_regenerated
198 }
199
200 #[cfg(feature = "energy")]
202 #[must_use]
203 pub const fn net_energy(&self) -> f64 {
204 self.total_energy_consumed - self.total_energy_regenerated
205 }
206
207 #[must_use]
211 pub fn avg_utilization(&self) -> f64 {
212 if self.utilization_by_group.is_empty() {
213 return 0.0;
214 }
215 let sum: f64 = self.utilization_by_group.values().sum();
216 sum / self.utilization_by_group.len() as f64
217 }
218
219 pub(crate) const fn record_spawn(&mut self) {
221 self.total_spawned += 1;
222 }
223
224 #[allow(clippy::cast_precision_loss)] pub(crate) fn record_board(&mut self, wait_ticks: u64) {
227 self.boarded_count += 1;
228 self.sum_wait_ticks += wait_ticks;
229 self.avg_wait_time = self.sum_wait_ticks as f64 / self.boarded_count as f64;
230 if wait_ticks > self.max_wait_time {
231 self.max_wait_time = wait_ticks;
232 }
233 }
234
235 #[allow(clippy::cast_precision_loss)] pub(crate) fn record_delivery(&mut self, ride_ticks: u64, tick: u64) {
238 self.delivered_count += 1;
239 self.total_delivered += 1;
240 self.sum_ride_ticks += ride_ticks;
241 self.avg_ride_time = self.sum_ride_ticks as f64 / self.delivered_count as f64;
242 self.delivery_window.push_back(tick);
243 }
244
245 #[allow(clippy::cast_precision_loss)] pub(crate) fn record_abandonment(&mut self) {
248 self.total_abandoned += 1;
249 if self.total_spawned > 0 {
250 self.abandonment_rate = self.total_abandoned as f64 / self.total_spawned as f64;
251 }
252 }
253
254 pub(crate) const fn record_settle(&mut self) {
256 self.total_settled += 1;
257 }
258
259 pub(crate) const fn record_reroute(&mut self) {
261 self.total_rerouted += 1;
262 }
263
264 pub(crate) fn record_distance(&mut self, distance: f64) {
266 self.total_distance += distance;
267 }
268
269 pub(crate) fn record_reposition_distance(&mut self, distance: f64) {
271 self.reposition_distance += distance;
272 }
273
274 #[cfg(feature = "energy")]
276 pub(crate) fn record_energy(&mut self, consumed: f64, regenerated: f64) {
277 self.total_energy_consumed += consumed;
278 self.total_energy_regenerated += regenerated;
279 }
280
281 #[allow(clippy::cast_possible_truncation)] pub(crate) fn update_throughput(&mut self, current_tick: u64) {
284 let cutoff = current_tick.saturating_sub(self.throughput_window_ticks);
285 while self.delivery_window.front().is_some_and(|&t| t <= cutoff) {
287 self.delivery_window.pop_front();
288 }
289 self.throughput = self.delivery_window.len() as u64;
290 }
291}
292
293impl fmt::Display for Metrics {
294 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302 write!(
303 f,
304 "{} delivered, avg wait {:.1}t, {:.0}% util",
305 self.total_delivered,
306 self.avg_wait_time,
307 self.avg_utilization() * 100.0,
308 )
309 }
310}