1#![forbid(unsafe_code)]
2
3use std::io::{self, Write};
27use web_time::{Duration, Instant};
28
29#[derive(Debug)]
34pub struct CountingWriter<W> {
35 inner: W,
37 bytes_written: u64,
39}
40
41impl<W> CountingWriter<W> {
42 #[inline]
44 pub fn new(inner: W) -> Self {
45 Self {
46 inner,
47 bytes_written: 0,
48 }
49 }
50
51 #[inline]
53 pub fn bytes_written(&self) -> u64 {
54 self.bytes_written
55 }
56
57 #[inline]
59 pub fn reset_counter(&mut self) {
60 self.bytes_written = 0;
61 }
62
63 #[inline]
65 #[must_use]
66 pub fn inner(&self) -> &W {
67 &self.inner
68 }
69
70 #[inline]
72 #[must_use]
73 pub fn inner_mut(&mut self) -> &mut W {
74 &mut self.inner
75 }
76
77 #[inline]
79 #[must_use]
80 pub fn into_inner(self) -> W {
81 self.inner
82 }
83}
84
85impl<W: Write> Write for CountingWriter<W> {
86 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
87 let n = self.inner.write(buf)?;
88 self.bytes_written += n as u64;
89 Ok(n)
90 }
91
92 fn flush(&mut self) -> io::Result<()> {
93 self.inner.flush()
94 }
95
96 fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
97 self.inner.write_all(buf)?;
98 self.bytes_written += buf.len() as u64;
99 Ok(())
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct PresentStats {
109 pub bytes_emitted: u64,
111 pub cells_changed: usize,
113 pub run_count: usize,
115 pub duration: Duration,
117}
118
119impl PresentStats {
120 #[inline]
122 pub fn new(
123 bytes_emitted: u64,
124 cells_changed: usize,
125 run_count: usize,
126 duration: Duration,
127 ) -> Self {
128 Self {
129 bytes_emitted,
130 cells_changed,
131 run_count,
132 duration,
133 }
134 }
135
136 #[inline]
140 pub fn bytes_per_cell(&self) -> f64 {
141 if self.cells_changed == 0 {
142 0.0
143 } else {
144 self.bytes_emitted as f64 / self.cells_changed as f64
145 }
146 }
147
148 #[inline]
152 pub fn bytes_per_run(&self) -> f64 {
153 if self.run_count == 0 {
154 0.0
155 } else {
156 self.bytes_emitted as f64 / self.run_count as f64
157 }
158 }
159
160 #[inline]
164 pub fn within_budget(&self) -> bool {
165 let budget = expected_max_bytes(self.cells_changed, self.run_count);
166 self.bytes_emitted <= budget
167 }
168
169 #[cfg(feature = "tracing")]
171 pub fn log(&self) {
172 tracing::debug!(
173 bytes = self.bytes_emitted,
174 cells_changed = self.cells_changed,
175 runs = self.run_count,
176 duration_us = self.duration.as_micros() as u64,
177 bytes_per_cell = format!("{:.1}", self.bytes_per_cell()),
178 "Present stats"
179 );
180 }
181
182 #[cfg(not(feature = "tracing"))]
184 pub fn log(&self) {
185 }
187}
188
189impl Default for PresentStats {
190 fn default() -> Self {
191 Self {
192 bytes_emitted: 0,
193 cells_changed: 0,
194 run_count: 0,
195 duration: Duration::ZERO,
196 }
197 }
198}
199
200pub const BYTES_PER_CELL_MAX: u64 = 40;
204
205pub const SYNC_OVERHEAD: u64 = 20;
207
208pub const BYTES_PER_CURSOR_MOVE: u64 = 10;
210
211#[inline]
215pub fn expected_max_bytes(cells_changed: usize, runs: usize) -> u64 {
216 (runs as u64 * BYTES_PER_CURSOR_MOVE)
218 + (cells_changed as u64 * BYTES_PER_CELL_MAX)
219 + SYNC_OVERHEAD
220}
221
222#[derive(Debug)]
226pub struct StatsCollector {
227 start: Instant,
228 cells_changed: usize,
229 run_count: usize,
230}
231
232impl StatsCollector {
233 #[inline]
235 pub fn start(cells_changed: usize, run_count: usize) -> Self {
236 Self {
237 start: Instant::now(),
238 cells_changed,
239 run_count,
240 }
241 }
242
243 #[inline]
245 pub fn finish(self, bytes_emitted: u64) -> PresentStats {
246 PresentStats {
247 bytes_emitted,
248 cells_changed: self.cells_changed,
249 run_count: self.run_count,
250 duration: self.start.elapsed(),
251 }
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
262 fn counting_writer_basic() {
263 let mut buffer = Vec::new();
264 let mut writer = CountingWriter::new(&mut buffer);
265
266 writer.write_all(b"Hello").unwrap();
267 assert_eq!(writer.bytes_written(), 5);
268
269 writer.write_all(b", world!").unwrap();
270 assert_eq!(writer.bytes_written(), 13);
271 }
272
273 #[test]
274 fn counting_writer_reset() {
275 let mut buffer = Vec::new();
276 let mut writer = CountingWriter::new(&mut buffer);
277
278 writer.write_all(b"Hello").unwrap();
279 assert_eq!(writer.bytes_written(), 5);
280
281 writer.reset_counter();
282 assert_eq!(writer.bytes_written(), 0);
283
284 writer.write_all(b"Hi").unwrap();
285 assert_eq!(writer.bytes_written(), 2);
286 }
287
288 #[test]
289 fn counting_writer_write() {
290 let mut buffer = Vec::new();
291 let mut writer = CountingWriter::new(&mut buffer);
292
293 let n = writer.write(b"Hello").unwrap();
295 assert_eq!(n, 5);
296 assert_eq!(writer.bytes_written(), 5);
297 }
298
299 #[test]
300 fn counting_writer_flush() {
301 let mut buffer = Vec::new();
302 let mut writer = CountingWriter::new(&mut buffer);
303
304 writer.write_all(b"test").unwrap();
305 writer.flush().unwrap();
306
307 assert_eq!(writer.bytes_written(), 4);
309 }
310
311 #[test]
312 fn counting_writer_into_inner() {
313 let buffer: Vec<u8> = Vec::new();
314 let writer = CountingWriter::new(buffer);
315 let inner = writer.into_inner();
316 assert!(inner.is_empty());
317 }
318
319 #[test]
320 fn counting_writer_inner_ref() {
321 let mut buffer = Vec::new();
322 let mut writer = CountingWriter::new(&mut buffer);
323 writer.write_all(b"test").unwrap();
324
325 assert_eq!(writer.inner().len(), 4);
326 }
327
328 #[test]
331 fn stats_bytes_per_cell() {
332 let stats = PresentStats::new(100, 10, 2, Duration::from_micros(50));
333 assert!((stats.bytes_per_cell() - 10.0).abs() < f64::EPSILON);
334 }
335
336 #[test]
337 fn stats_bytes_per_cell_zero() {
338 let stats = PresentStats::new(0, 0, 0, Duration::ZERO);
339 assert!((stats.bytes_per_cell() - 0.0).abs() < f64::EPSILON);
340 }
341
342 #[test]
343 fn stats_bytes_per_run() {
344 let stats = PresentStats::new(100, 10, 5, Duration::from_micros(50));
345 assert!((stats.bytes_per_run() - 20.0).abs() < f64::EPSILON);
346 }
347
348 #[test]
349 fn stats_bytes_per_run_zero() {
350 let stats = PresentStats::new(0, 0, 0, Duration::ZERO);
351 assert!((stats.bytes_per_run() - 0.0).abs() < f64::EPSILON);
352 }
353
354 #[test]
355 fn stats_within_budget_pass() {
356 let stats = PresentStats::new(200, 10, 2, Duration::from_micros(50));
359 assert!(stats.within_budget());
360 }
361
362 #[test]
363 fn stats_within_budget_fail() {
364 let stats = PresentStats::new(500, 10, 2, Duration::from_micros(50));
367 assert!(!stats.within_budget());
368 }
369
370 #[test]
371 fn stats_default() {
372 let stats = PresentStats::default();
373 assert_eq!(stats.bytes_emitted, 0);
374 assert_eq!(stats.cells_changed, 0);
375 assert_eq!(stats.run_count, 0);
376 assert_eq!(stats.duration, Duration::ZERO);
377 }
378
379 #[test]
382 fn expected_max_bytes_calculation() {
383 let budget = expected_max_bytes(10, 2);
385 assert_eq!(budget, 440);
387 }
388
389 #[test]
390 fn expected_max_bytes_empty() {
391 let budget = expected_max_bytes(0, 0);
392 assert_eq!(budget, SYNC_OVERHEAD);
394 }
395
396 #[test]
397 fn expected_max_bytes_single_cell() {
398 let budget = expected_max_bytes(1, 1);
399 assert_eq!(budget, 70);
401 }
402
403 #[test]
406 fn stats_collector_basic() {
407 let collector = StatsCollector::start(10, 2);
408 std::thread::sleep(Duration::from_micros(100));
409 let stats = collector.finish(150);
410
411 assert_eq!(stats.cells_changed, 10);
412 assert_eq!(stats.run_count, 2);
413 assert_eq!(stats.bytes_emitted, 150);
414 assert!(stats.duration >= Duration::from_micros(100));
415 }
416
417 #[test]
420 fn full_stats_workflow() {
421 let mut buffer = Vec::new();
422 let mut writer = CountingWriter::new(&mut buffer);
423
424 let collector = StatsCollector::start(5, 1);
426
427 writer.write_all(b"\x1b[1;1H").unwrap(); writer.write_all(b"\x1b[0m").unwrap(); writer.write_all(b"Hello").unwrap(); writer.flush().unwrap();
431
432 let stats = collector.finish(writer.bytes_written());
433
434 assert_eq!(stats.cells_changed, 5);
435 assert_eq!(stats.run_count, 1);
436 assert_eq!(stats.bytes_emitted, 6 + 4 + 5); assert!(stats.within_budget());
438 }
439
440 #[test]
441 fn spinner_update_budget() {
442 let stats = PresentStats::new(35, 1, 1, Duration::from_micros(10));
444 assert!(
445 stats.within_budget(),
446 "Single cell update should be within budget"
447 );
448 assert!(
449 stats.bytes_emitted < 50,
450 "Spinner tick should be < 50 bytes"
451 );
452 }
453
454 #[test]
455 fn status_bar_budget() {
456 let stats = PresentStats::new(2500, 80, 1, Duration::from_micros(100));
458 assert!(
459 stats.within_budget(),
460 "Status bar update should be within budget"
461 );
462 assert!(
463 stats.bytes_emitted < 3500,
464 "Status bar should be < 3500 bytes"
465 );
466 }
467
468 #[test]
469 fn full_redraw_budget() {
470 let stats = PresentStats::new(50000, 1920, 24, Duration::from_micros(1000));
472 assert!(stats.within_budget(), "Full redraw should be within budget");
473 assert!(stats.bytes_emitted < 80000, "Full redraw should be < 80KB");
474 }
475
476 #[test]
479 fn counting_writer_debug() {
480 let buffer: Vec<u8> = Vec::new();
481 let writer = CountingWriter::new(buffer);
482 let dbg = format!("{:?}", writer);
483 assert!(dbg.contains("CountingWriter"), "Debug: {dbg}");
484 }
485
486 #[test]
487 fn counting_writer_inner_mut() {
488 let mut writer = CountingWriter::new(Vec::<u8>::new());
489 writer.write_all(b"hello").unwrap();
490 writer.inner_mut().push(b'!');
492 assert_eq!(writer.inner(), &b"hello!"[..]);
493 assert_eq!(writer.bytes_written(), 5);
495 }
496
497 #[test]
498 fn counting_writer_empty_write() {
499 let mut buffer = Vec::new();
500 let mut writer = CountingWriter::new(&mut buffer);
501 writer.write_all(b"").unwrap();
502 assert_eq!(writer.bytes_written(), 0);
503 let n = writer.write(b"").unwrap();
504 assert_eq!(n, 0);
505 assert_eq!(writer.bytes_written(), 0);
506 }
507
508 #[test]
509 fn counting_writer_multiple_resets() {
510 let mut buffer = Vec::new();
511 let mut writer = CountingWriter::new(&mut buffer);
512 writer.write_all(b"abc").unwrap();
513 writer.reset_counter();
514 writer.reset_counter();
515 assert_eq!(writer.bytes_written(), 0);
516 writer.write_all(b"de").unwrap();
517 assert_eq!(writer.bytes_written(), 2);
518 }
519
520 #[test]
521 fn counting_writer_accumulates_u64() {
522 let mut buffer = Vec::new();
523 let mut writer = CountingWriter::new(&mut buffer);
524 for _ in 0..1000 {
526 writer.write_all(b"x").unwrap();
527 }
528 assert_eq!(writer.bytes_written(), 1000);
529 }
530
531 #[test]
532 fn counting_writer_multiple_flushes() {
533 let mut buffer = Vec::new();
534 let mut writer = CountingWriter::new(&mut buffer);
535 writer.write_all(b"test").unwrap();
536 writer.flush().unwrap();
537 writer.flush().unwrap();
538 writer.flush().unwrap();
539 assert_eq!(writer.bytes_written(), 4);
540 }
541
542 #[test]
543 fn counting_writer_into_inner_preserves_data() {
544 let mut writer = CountingWriter::new(Vec::<u8>::new());
545 writer.write_all(b"hello world").unwrap();
546 let inner = writer.into_inner();
547 assert_eq!(&inner, b"hello world");
548 }
549
550 #[test]
551 fn counting_writer_initial_state() {
552 let buffer: Vec<u8> = Vec::new();
553 let writer = CountingWriter::new(buffer);
554 assert_eq!(writer.bytes_written(), 0);
555 assert!(writer.inner().is_empty());
556 }
557
558 #[test]
561 fn present_stats_debug_clone_eq() {
562 let a = PresentStats::new(100, 10, 2, Duration::from_micros(50));
563 let dbg = format!("{:?}", a);
564 assert!(dbg.contains("PresentStats"), "Debug: {dbg}");
565 let cloned = a.clone();
566 assert_eq!(a, cloned);
567 let b = PresentStats::new(200, 10, 2, Duration::from_micros(50));
568 assert_ne!(a, b);
569 }
570
571 #[test]
572 fn present_stats_log_noop() {
573 let stats = PresentStats::default();
574 stats.log(); }
576
577 #[test]
578 fn present_stats_large_values() {
579 let stats = PresentStats::new(u64::MAX, usize::MAX, usize::MAX, Duration::MAX);
580 assert_eq!(stats.bytes_emitted, u64::MAX);
581 assert_eq!(stats.cells_changed, usize::MAX);
582 }
583
584 #[test]
585 fn present_stats_bytes_per_cell_fractional() {
586 let stats = PresentStats::new(10, 3, 1, Duration::ZERO);
587 let bpc = stats.bytes_per_cell();
588 assert!((bpc - 3.333333333).abs() < 0.001);
589 }
590
591 #[test]
592 fn present_stats_bytes_per_run_fractional() {
593 let stats = PresentStats::new(10, 5, 3, Duration::ZERO);
594 let bpr = stats.bytes_per_run();
595 assert!((bpr - 3.333333333).abs() < 0.001);
596 }
597
598 #[test]
599 fn present_stats_within_budget_at_exact_boundary() {
600 let budget = expected_max_bytes(10, 2);
602 assert_eq!(budget, 440);
603
604 let at_boundary = PresentStats::new(440, 10, 2, Duration::ZERO);
605 assert!(at_boundary.within_budget());
606
607 let over_boundary = PresentStats::new(441, 10, 2, Duration::ZERO);
608 assert!(!over_boundary.within_budget());
609 }
610
611 #[test]
614 fn constants_values() {
615 assert_eq!(BYTES_PER_CELL_MAX, 40);
616 assert_eq!(SYNC_OVERHEAD, 20);
617 assert_eq!(BYTES_PER_CURSOR_MOVE, 10);
618 }
619
620 #[test]
623 fn expected_max_bytes_many_runs_few_cells() {
624 let budget = expected_max_bytes(1, 100);
626 assert_eq!(budget, 1060);
628 }
629
630 #[test]
631 fn expected_max_bytes_many_cells_one_run() {
632 let budget = expected_max_bytes(1000, 1);
633 assert_eq!(budget, 40030);
635 }
636
637 #[test]
640 fn stats_collector_debug() {
641 let collector = StatsCollector::start(5, 2);
642 let dbg = format!("{:?}", collector);
643 assert!(dbg.contains("StatsCollector"), "Debug: {dbg}");
644 }
645
646 #[test]
647 fn stats_collector_zero_cells_runs() {
648 let collector = StatsCollector::start(0, 0);
649 let stats = collector.finish(0);
650 assert_eq!(stats.cells_changed, 0);
651 assert_eq!(stats.run_count, 0);
652 assert_eq!(stats.bytes_emitted, 0);
653 assert!(stats.within_budget()); }
655
656 #[test]
657 fn stats_collector_immediate_finish() {
658 let collector = StatsCollector::start(1, 1);
659 let stats = collector.finish(50);
660 assert_eq!(stats.bytes_emitted, 50);
661 assert!(stats.duration < Duration::from_millis(100));
663 }
664}