1#![forbid(unsafe_code)]
2
3use std::io::{self, Write};
27use std::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 pub fn inner(&self) -> &W {
66 &self.inner
67 }
68
69 #[inline]
71 pub fn inner_mut(&mut self) -> &mut W {
72 &mut self.inner
73 }
74
75 #[inline]
77 pub fn into_inner(self) -> W {
78 self.inner
79 }
80}
81
82impl<W: Write> Write for CountingWriter<W> {
83 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
84 let n = self.inner.write(buf)?;
85 self.bytes_written += n as u64;
86 Ok(n)
87 }
88
89 fn flush(&mut self) -> io::Result<()> {
90 self.inner.flush()
91 }
92
93 fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
94 self.inner.write_all(buf)?;
95 self.bytes_written += buf.len() as u64;
96 Ok(())
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct PresentStats {
106 pub bytes_emitted: u64,
108 pub cells_changed: usize,
110 pub run_count: usize,
112 pub duration: Duration,
114}
115
116impl PresentStats {
117 #[inline]
119 pub fn new(
120 bytes_emitted: u64,
121 cells_changed: usize,
122 run_count: usize,
123 duration: Duration,
124 ) -> Self {
125 Self {
126 bytes_emitted,
127 cells_changed,
128 run_count,
129 duration,
130 }
131 }
132
133 #[inline]
137 pub fn bytes_per_cell(&self) -> f64 {
138 if self.cells_changed == 0 {
139 0.0
140 } else {
141 self.bytes_emitted as f64 / self.cells_changed as f64
142 }
143 }
144
145 #[inline]
149 pub fn bytes_per_run(&self) -> f64 {
150 if self.run_count == 0 {
151 0.0
152 } else {
153 self.bytes_emitted as f64 / self.run_count as f64
154 }
155 }
156
157 #[inline]
161 pub fn within_budget(&self) -> bool {
162 let budget = expected_max_bytes(self.cells_changed, self.run_count);
163 self.bytes_emitted <= budget
164 }
165
166 #[cfg(feature = "tracing")]
168 pub fn log(&self) {
169 tracing::debug!(
170 bytes = self.bytes_emitted,
171 cells_changed = self.cells_changed,
172 runs = self.run_count,
173 duration_us = self.duration.as_micros() as u64,
174 bytes_per_cell = format!("{:.1}", self.bytes_per_cell()),
175 "Present stats"
176 );
177 }
178
179 #[cfg(not(feature = "tracing"))]
181 pub fn log(&self) {
182 }
184}
185
186impl Default for PresentStats {
187 fn default() -> Self {
188 Self {
189 bytes_emitted: 0,
190 cells_changed: 0,
191 run_count: 0,
192 duration: Duration::ZERO,
193 }
194 }
195}
196
197pub const BYTES_PER_CELL_MAX: u64 = 40;
201
202pub const SYNC_OVERHEAD: u64 = 20;
204
205pub const BYTES_PER_CURSOR_MOVE: u64 = 10;
207
208#[inline]
212pub fn expected_max_bytes(cells_changed: usize, runs: usize) -> u64 {
213 (runs as u64 * BYTES_PER_CURSOR_MOVE)
215 + (cells_changed as u64 * BYTES_PER_CELL_MAX)
216 + SYNC_OVERHEAD
217}
218
219#[derive(Debug)]
223pub struct StatsCollector {
224 start: Instant,
225 cells_changed: usize,
226 run_count: usize,
227}
228
229impl StatsCollector {
230 #[inline]
232 pub fn start(cells_changed: usize, run_count: usize) -> Self {
233 Self {
234 start: Instant::now(),
235 cells_changed,
236 run_count,
237 }
238 }
239
240 #[inline]
242 pub fn finish(self, bytes_emitted: u64) -> PresentStats {
243 PresentStats {
244 bytes_emitted,
245 cells_changed: self.cells_changed,
246 run_count: self.run_count,
247 duration: self.start.elapsed(),
248 }
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
259 fn counting_writer_basic() {
260 let mut buffer = Vec::new();
261 let mut writer = CountingWriter::new(&mut buffer);
262
263 writer.write_all(b"Hello").unwrap();
264 assert_eq!(writer.bytes_written(), 5);
265
266 writer.write_all(b", world!").unwrap();
267 assert_eq!(writer.bytes_written(), 13);
268 }
269
270 #[test]
271 fn counting_writer_reset() {
272 let mut buffer = Vec::new();
273 let mut writer = CountingWriter::new(&mut buffer);
274
275 writer.write_all(b"Hello").unwrap();
276 assert_eq!(writer.bytes_written(), 5);
277
278 writer.reset_counter();
279 assert_eq!(writer.bytes_written(), 0);
280
281 writer.write_all(b"Hi").unwrap();
282 assert_eq!(writer.bytes_written(), 2);
283 }
284
285 #[test]
286 fn counting_writer_write() {
287 let mut buffer = Vec::new();
288 let mut writer = CountingWriter::new(&mut buffer);
289
290 let n = writer.write(b"Hello").unwrap();
292 assert_eq!(n, 5);
293 assert_eq!(writer.bytes_written(), 5);
294 }
295
296 #[test]
297 fn counting_writer_flush() {
298 let mut buffer = Vec::new();
299 let mut writer = CountingWriter::new(&mut buffer);
300
301 writer.write_all(b"test").unwrap();
302 writer.flush().unwrap();
303
304 assert_eq!(writer.bytes_written(), 4);
306 }
307
308 #[test]
309 fn counting_writer_into_inner() {
310 let buffer: Vec<u8> = Vec::new();
311 let writer = CountingWriter::new(buffer);
312 let inner = writer.into_inner();
313 assert!(inner.is_empty());
314 }
315
316 #[test]
317 fn counting_writer_inner_ref() {
318 let mut buffer = Vec::new();
319 let mut writer = CountingWriter::new(&mut buffer);
320 writer.write_all(b"test").unwrap();
321
322 assert_eq!(writer.inner().len(), 4);
323 }
324
325 #[test]
328 fn stats_bytes_per_cell() {
329 let stats = PresentStats::new(100, 10, 2, Duration::from_micros(50));
330 assert!((stats.bytes_per_cell() - 10.0).abs() < f64::EPSILON);
331 }
332
333 #[test]
334 fn stats_bytes_per_cell_zero() {
335 let stats = PresentStats::new(0, 0, 0, Duration::ZERO);
336 assert!((stats.bytes_per_cell() - 0.0).abs() < f64::EPSILON);
337 }
338
339 #[test]
340 fn stats_bytes_per_run() {
341 let stats = PresentStats::new(100, 10, 5, Duration::from_micros(50));
342 assert!((stats.bytes_per_run() - 20.0).abs() < f64::EPSILON);
343 }
344
345 #[test]
346 fn stats_bytes_per_run_zero() {
347 let stats = PresentStats::new(0, 0, 0, Duration::ZERO);
348 assert!((stats.bytes_per_run() - 0.0).abs() < f64::EPSILON);
349 }
350
351 #[test]
352 fn stats_within_budget_pass() {
353 let stats = PresentStats::new(200, 10, 2, Duration::from_micros(50));
356 assert!(stats.within_budget());
357 }
358
359 #[test]
360 fn stats_within_budget_fail() {
361 let stats = PresentStats::new(500, 10, 2, Duration::from_micros(50));
364 assert!(!stats.within_budget());
365 }
366
367 #[test]
368 fn stats_default() {
369 let stats = PresentStats::default();
370 assert_eq!(stats.bytes_emitted, 0);
371 assert_eq!(stats.cells_changed, 0);
372 assert_eq!(stats.run_count, 0);
373 assert_eq!(stats.duration, Duration::ZERO);
374 }
375
376 #[test]
379 fn expected_max_bytes_calculation() {
380 let budget = expected_max_bytes(10, 2);
382 assert_eq!(budget, 440);
384 }
385
386 #[test]
387 fn expected_max_bytes_empty() {
388 let budget = expected_max_bytes(0, 0);
389 assert_eq!(budget, SYNC_OVERHEAD);
391 }
392
393 #[test]
394 fn expected_max_bytes_single_cell() {
395 let budget = expected_max_bytes(1, 1);
396 assert_eq!(budget, 70);
398 }
399
400 #[test]
403 fn stats_collector_basic() {
404 let collector = StatsCollector::start(10, 2);
405 std::thread::sleep(Duration::from_micros(100));
406 let stats = collector.finish(150);
407
408 assert_eq!(stats.cells_changed, 10);
409 assert_eq!(stats.run_count, 2);
410 assert_eq!(stats.bytes_emitted, 150);
411 assert!(stats.duration >= Duration::from_micros(100));
412 }
413
414 #[test]
417 fn full_stats_workflow() {
418 let mut buffer = Vec::new();
419 let mut writer = CountingWriter::new(&mut buffer);
420
421 let collector = StatsCollector::start(5, 1);
423
424 writer.write_all(b"\x1b[1;1H").unwrap(); writer.write_all(b"\x1b[0m").unwrap(); writer.write_all(b"Hello").unwrap(); writer.flush().unwrap();
428
429 let stats = collector.finish(writer.bytes_written());
430
431 assert_eq!(stats.cells_changed, 5);
432 assert_eq!(stats.run_count, 1);
433 assert_eq!(stats.bytes_emitted, 6 + 4 + 5); assert!(stats.within_budget());
435 }
436
437 #[test]
438 fn spinner_update_budget() {
439 let stats = PresentStats::new(35, 1, 1, Duration::from_micros(10));
441 assert!(
442 stats.within_budget(),
443 "Single cell update should be within budget"
444 );
445 assert!(
446 stats.bytes_emitted < 50,
447 "Spinner tick should be < 50 bytes"
448 );
449 }
450
451 #[test]
452 fn status_bar_budget() {
453 let stats = PresentStats::new(2500, 80, 1, Duration::from_micros(100));
455 assert!(
456 stats.within_budget(),
457 "Status bar update should be within budget"
458 );
459 assert!(
460 stats.bytes_emitted < 3500,
461 "Status bar should be < 3500 bytes"
462 );
463 }
464
465 #[test]
466 fn full_redraw_budget() {
467 let stats = PresentStats::new(50000, 1920, 24, Duration::from_micros(1000));
469 assert!(stats.within_budget(), "Full redraw should be within budget");
470 assert!(stats.bytes_emitted < 80000, "Full redraw should be < 80KB");
471 }
472}