entrenar/monitor/wasm/
dashboard.rs1#[cfg(target_arch = "wasm32")]
4use wasm_bindgen::prelude::*;
5
6use super::collector::WasmMetricsCollector;
7use super::options::WasmDashboardOptions;
8use super::utils::{generate_sparkline, normalize_values};
9
10#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
14#[derive(Debug)]
15pub struct WasmDashboard {
16 options: WasmDashboardOptions,
17 loss_history: Vec<f64>,
18 accuracy_history: Vec<f64>,
19 max_history: usize,
20}
21
22#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
23impl WasmDashboard {
24 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))]
26 pub fn new() -> Self {
27 Self {
28 options: WasmDashboardOptions::new(),
29 loss_history: Vec::new(),
30 accuracy_history: Vec::new(),
31 max_history: 100,
32 }
33 }
34
35 pub fn with_options(options: WasmDashboardOptions) -> Self {
37 Self { options, loss_history: Vec::new(), accuracy_history: Vec::new(), max_history: 100 }
38 }
39
40 #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
42 pub fn max_history(mut self, max: usize) -> Self {
43 self.max_history = max;
44 self
45 }
46
47 #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
49 pub fn update(&mut self, collector: &WasmMetricsCollector) {
50 let loss = collector.loss_mean();
52 let accuracy = collector.accuracy_mean();
53
54 if !loss.is_nan() {
56 self.loss_history.push(loss);
57 if self.loss_history.len() > self.max_history {
58 self.loss_history.remove(0);
59 }
60 }
61
62 if !accuracy.is_nan() {
63 self.accuracy_history.push(accuracy);
64 if self.accuracy_history.len() > self.max_history {
65 self.accuracy_history.remove(0);
66 }
67 }
68 }
69
70 #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
72 pub fn add_loss(&mut self, value: f64) {
73 if !value.is_nan() && !value.is_infinite() {
74 self.loss_history.push(value);
75 if self.loss_history.len() > self.max_history {
76 self.loss_history.remove(0);
77 }
78 }
79 }
80
81 #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
83 pub fn add_accuracy(&mut self, value: f64) {
84 if !value.is_nan() && !value.is_infinite() {
85 self.accuracy_history.push(value);
86 if self.accuracy_history.len() > self.max_history {
87 self.accuracy_history.remove(0);
88 }
89 }
90 }
91
92 #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
94 pub fn loss_history_len(&self) -> usize {
95 self.loss_history.len()
96 }
97
98 #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
100 pub fn accuracy_history_len(&self) -> usize {
101 self.accuracy_history.len()
102 }
103
104 #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
106 pub fn clear(&mut self) {
107 self.loss_history.clear();
108 self.accuracy_history.clear();
109 }
110
111 #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
113 pub fn width(&self) -> u32 {
114 self.options.width
115 }
116
117 #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
119 pub fn height(&self) -> u32 {
120 self.options.height
121 }
122
123 pub fn loss_normalized(&self) -> Vec<f64> {
126 normalize_values(&self.loss_history)
127 }
128
129 pub fn accuracy_normalized(&self) -> Vec<f64> {
132 normalize_values(&self.accuracy_history)
133 }
134
135 pub fn x_coordinates(&self, len: usize) -> Vec<f64> {
137 if len <= 1 {
138 return vec![0.5];
139 }
140 (0..len).map(|i| i as f64 / (len - 1) as f64).collect()
141 }
142
143 #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
145 pub fn loss_sparkline(&self) -> String {
146 generate_sparkline(&self.loss_history, 20)
147 }
148
149 #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
151 pub fn accuracy_sparkline(&self) -> String {
152 generate_sparkline(&self.accuracy_history, 20)
153 }
154
155 #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
157 pub fn state_json(&self) -> String {
158 let state = DashboardState {
159 width: self.options.width,
160 height: self.options.height,
161 loss_history: self.loss_history.clone(),
162 accuracy_history: self.accuracy_history.clone(),
163 loss_color: self.options.loss_color.clone(),
164 accuracy_color: self.options.accuracy_color.clone(),
165 background_color: self.options.background_color.clone(),
166 };
167 serde_json::to_string(&state).unwrap_or_else(|_err| "{}".to_string())
168 }
169}
170
171impl Default for WasmDashboard {
172 fn default() -> Self {
173 Self::new()
174 }
175}
176
177#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
179pub(crate) struct DashboardState {
180 width: u32,
181 height: u32,
182 loss_history: Vec<f64>,
183 accuracy_history: Vec<f64>,
184 loss_color: String,
185 accuracy_color: String,
186 background_color: String,
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_wasm_dashboard_new() {
195 let dashboard = WasmDashboard::new();
196 assert_eq!(dashboard.loss_history_len(), 0);
197 assert_eq!(dashboard.accuracy_history_len(), 0);
198 assert_eq!(dashboard.width(), 800);
199 assert_eq!(dashboard.height(), 400);
200 }
201
202 #[test]
203 fn test_wasm_dashboard_add_loss() {
204 let mut dashboard = WasmDashboard::new();
205 dashboard.add_loss(0.5);
206 dashboard.add_loss(0.3);
207 assert_eq!(dashboard.loss_history_len(), 2);
208 }
209
210 #[test]
211 fn test_wasm_dashboard_add_accuracy() {
212 let mut dashboard = WasmDashboard::new();
213 dashboard.add_accuracy(0.8);
214 dashboard.add_accuracy(0.9);
215 assert_eq!(dashboard.accuracy_history_len(), 2);
216 }
217
218 #[test]
219 fn test_wasm_dashboard_ignores_nan() {
220 let mut dashboard = WasmDashboard::new();
221 dashboard.add_loss(0.5);
222 dashboard.add_loss(f64::NAN);
223 dashboard.add_loss(0.3);
224 assert_eq!(dashboard.loss_history_len(), 2);
225 }
226
227 #[test]
228 fn test_wasm_dashboard_ignores_inf() {
229 let mut dashboard = WasmDashboard::new();
230 dashboard.add_accuracy(0.8);
231 dashboard.add_accuracy(f64::INFINITY);
232 assert_eq!(dashboard.accuracy_history_len(), 1);
233 }
234
235 #[test]
236 fn test_wasm_dashboard_max_history() {
237 let mut dashboard = WasmDashboard::new().max_history(5);
238 for i in 0..10 {
239 dashboard.add_loss(f64::from(i));
240 }
241 assert_eq!(dashboard.loss_history_len(), 5);
242 }
243
244 #[test]
245 fn test_wasm_dashboard_clear() {
246 let mut dashboard = WasmDashboard::new();
247 dashboard.add_loss(0.5);
248 dashboard.add_accuracy(0.8);
249 dashboard.clear();
250 assert_eq!(dashboard.loss_history_len(), 0);
251 assert_eq!(dashboard.accuracy_history_len(), 0);
252 }
253
254 #[test]
255 fn test_wasm_dashboard_update() {
256 let mut collector = WasmMetricsCollector::new();
257 collector.record_loss(0.5);
258 collector.record_accuracy(0.8);
259
260 let mut dashboard = WasmDashboard::new();
261 dashboard.update(&collector);
262
263 assert_eq!(dashboard.loss_history_len(), 1);
264 assert_eq!(dashboard.accuracy_history_len(), 1);
265 }
266
267 #[test]
268 fn test_wasm_dashboard_sparkline() {
269 let mut dashboard = WasmDashboard::new();
270 for i in 0..10 {
271 dashboard.add_loss(f64::from(i) / 10.0);
272 }
273
274 let sparkline = dashboard.loss_sparkline();
275 assert!(!sparkline.is_empty());
276 assert!(sparkline.chars().count() <= 20);
277 }
278
279 #[test]
280 fn test_wasm_dashboard_state_json() {
281 let mut dashboard = WasmDashboard::new();
282 dashboard.add_loss(0.5);
283 dashboard.add_accuracy(0.8);
284
285 let json = dashboard.state_json();
286 assert!(json.contains("width"));
287 assert!(json.contains("loss_history"));
288 assert!(json.contains("accuracy_history"));
289 }
290
291 #[test]
292 fn test_wasm_dashboard_x_coordinates() {
293 let dashboard = WasmDashboard::new();
294
295 let coords = dashboard.x_coordinates(5);
296 assert_eq!(coords.len(), 5);
297 assert!((coords[0] - 0.0).abs() < 1e-6);
298 assert!((coords[4] - 1.0).abs() < 1e-6);
299 }
300
301 #[test]
302 fn test_wasm_dashboard_x_coordinates_single() {
303 let dashboard = WasmDashboard::new();
304 let coords = dashboard.x_coordinates(1);
305 assert_eq!(coords.len(), 1);
306 assert!((coords[0] - 0.5).abs() < 1e-6);
307 }
308
309 #[test]
310 fn test_wasm_dashboard_loss_normalized() {
311 let mut dashboard = WasmDashboard::new();
312 dashboard.add_loss(0.0);
313 dashboard.add_loss(5.0);
314 dashboard.add_loss(10.0);
315
316 let normalized = dashboard.loss_normalized();
317 assert_eq!(normalized.len(), 3);
318 assert!((normalized[0] - 0.0).abs() < 1e-6);
319 assert!((normalized[1] - 0.5).abs() < 1e-6);
320 assert!((normalized[2] - 1.0).abs() < 1e-6);
321 }
322
323 #[test]
324 fn test_wasm_dashboard_with_options() {
325 let opts = WasmDashboardOptions::new().width(1024).height(768);
326 let dashboard = WasmDashboard::with_options(opts);
327 assert_eq!(dashboard.width(), 1024);
328 assert_eq!(dashboard.height(), 768);
329 }
330
331 #[test]
332 fn test_wasm_dashboard_default() {
333 let dashboard = WasmDashboard::default();
334 assert_eq!(dashboard.width(), 800);
335 }
336}
337
338#[cfg(test)]
339mod proptests {
340 use super::*;
341 use proptest::prelude::*;
342
343 proptest! {
344 #[test]
346 fn prop_dashboard_max_history(
347 max in 5usize..50,
348 count in 10usize..200
349 ) {
350 let mut dashboard = WasmDashboard::new().max_history(max);
351
352 for i in 0..count {
353 dashboard.add_loss(i as f64);
354 }
355
356 prop_assert!(dashboard.loss_history_len() <= max);
357 }
358
359 #[test]
361 fn prop_x_coords_span(len in 2usize..100) {
362 let dashboard = WasmDashboard::new();
363 let coords = dashboard.x_coordinates(len);
364
365 prop_assert_eq!(coords.len(), len);
366 prop_assert!((coords[0] - 0.0).abs() < 1e-10);
367 prop_assert!((coords[len - 1] - 1.0).abs() < 1e-10);
368 }
369
370 #[test]
372 fn prop_dashboard_ignores_nan(
373 valid_count in 1usize..20,
374 nan_count in 1usize..10
375 ) {
376 let mut dashboard = WasmDashboard::new();
377
378 for i in 0..valid_count {
379 dashboard.add_loss(i as f64);
380 }
381 for _ in 0..nan_count {
382 dashboard.add_loss(f64::NAN);
383 }
384
385 prop_assert_eq!(dashboard.loss_history_len(), valid_count);
386 }
387
388 #[test]
390 fn prop_json_state_valid(values in prop::collection::vec(0.0f64..100.0, 0..50)) {
391 let mut dashboard = WasmDashboard::new();
392 for v in &values {
393 dashboard.add_loss(*v);
394 }
395
396 let json = dashboard.state_json();
397 let parsed: Result<serde_json::Value, _> = serde_json::from_str(&json);
398 prop_assert!(parsed.is_ok());
399 }
400 }
401}