Skip to main content

intercept_bounce/filter/
stats.rs

1// This module defines the StatsCollector struct and related types
2// used by the logger thread to accumulate and report statistics.
3use crate::filter::{FILTER_MAP_SIZE, NUM_KEY_STATES};
4
5use crate::filter::keynames::{get_key_name, get_value_name};
6use crate::logger::EventInfo;
7use crate::util;
8use serde::Serialize;
9use std::collections::VecDeque;
10use std::io::Write;
11use std::time::Duration;
12
13// Define histogram bucket boundaries in milliseconds.
14// These represent the *upper bounds* of the buckets.
15// Example: [1, 2, 4] means buckets are <1ms, 1-2ms, 2-4ms, >=4ms.
16pub const HISTOGRAM_BUCKET_BOUNDARIES_MS: &[u64] = &[1, 2, 4, 8, 16, 32, 64, 128];
17pub const NUM_HISTOGRAM_BUCKETS: usize = HISTOGRAM_BUCKET_BOUNDARIES_MS.len() + 1;
18
19pub const MAX_BOUNCE_TIMING_SAMPLES: usize = 512;
20pub const MAX_NEAR_MISS_TIMING_SAMPLES: usize = 512;
21
22#[derive(Debug, Clone)]
23pub struct TimingSamples {
24    data: VecDeque<u64>,
25    capacity: usize,
26}
27
28impl TimingSamples {
29    pub fn with_capacity(capacity: usize) -> Self {
30        let data = VecDeque::new();
31        Self { data, capacity }
32    }
33
34    pub fn push(&mut self, value: u64) {
35        if self.capacity == 0 {
36            return;
37        }
38        if self.data.len() == self.capacity {
39            self.data.pop_front();
40        }
41        self.data.push_back(value);
42    }
43
44    pub fn is_empty(&self) -> bool {
45        self.data.is_empty()
46    }
47
48    pub fn len(&self) -> usize {
49        self.data.len()
50    }
51
52    pub fn to_vec(&self) -> Vec<u64> {
53        self.data.iter().copied().collect()
54    }
55}
56
57impl Default for TimingSamples {
58    fn default() -> Self {
59        Self::with_capacity(MAX_BOUNCE_TIMING_SAMPLES)
60    }
61}
62
63#[derive(Debug, Clone, Default)]
64pub struct TimingSummary {
65    count: u64,
66    sum_us: u128,
67    min_us: Option<u64>,
68    max_us: Option<u64>,
69}
70
71impl TimingSummary {
72    pub fn record(&mut self, value: u64) {
73        self.count = self.count.saturating_add(1);
74        self.sum_us = self.sum_us.saturating_add(value as u128);
75        self.min_us = Some(match self.min_us {
76            Some(current) => current.min(value),
77            None => value,
78        });
79        self.max_us = Some(match self.max_us {
80            Some(current) => current.max(value),
81            None => value,
82        });
83    }
84
85    pub fn count(&self) -> u64 {
86        self.count
87    }
88
89    pub fn min_us(&self) -> Option<u64> {
90        self.min_us
91    }
92
93    pub fn max_us(&self) -> Option<u64> {
94        self.max_us
95    }
96
97    pub fn average_us(&self) -> Option<u64> {
98        if self.count == 0 {
99            return None;
100        }
101        let avg = self.sum_us / u128::from(self.count);
102        Some(avg.min(u128::from(u64::MAX)) as u64)
103    }
104}
105
106/// Represents a histogram of timing values.
107#[derive(Debug, Serialize, Clone)]
108pub struct TimingHistogram {
109    // Counts per bucket. Index 0 is for values < boundary[0], index N is for values >= boundary[N-1].
110    pub buckets: [u64; NUM_HISTOGRAM_BUCKETS],
111    // Total count of events recorded in this histogram.
112    pub count: u64,
113    // Sum of all timings recorded (in microseconds) for calculating average.
114    pub sum_us: u64,
115    // Optional: Store min/max directly if needed, otherwise calculate from raw data if kept.
116    // pub min_us: u64,
117    // pub max_us: u64,
118}
119
120impl Default for TimingHistogram {
121    fn default() -> Self {
122        Self {
123            buckets: [0; NUM_HISTOGRAM_BUCKETS],
124            count: 0,
125            sum_us: 0,
126        }
127    }
128}
129
130impl TimingHistogram {
131    /// Records a timing value (in microseconds) into the correct bucket.
132    #[inline]
133    pub fn record(&mut self, timing_us: u64) {
134        let timing_ms = timing_us / 1000; // Convert to ms for bucket comparison
135        let mut bucket_index = NUM_HISTOGRAM_BUCKETS - 1; // Default to the last bucket (>= last boundary)
136
137        for (i, &boundary_ms) in HISTOGRAM_BUCKET_BOUNDARIES_MS.iter().enumerate() {
138            if timing_ms < boundary_ms {
139                bucket_index = i;
140                break;
141            }
142        }
143
144        self.buckets[bucket_index] += 1;
145        self.count += 1;
146        self.sum_us = self.sum_us.saturating_add(timing_us); // Use saturating_add
147                                                             // Optional: Update min/max
148                                                             // self.min_us = self.min_us.min(timing_us);
149                                                             // self.max_us = self.max_us.max(timing_us);
150    }
151
152    /// Calculates the average timing in microseconds. Returns 0 if count is 0.
153    pub fn average_us(&self) -> u64 {
154        if self.count > 0 {
155            self.sum_us / self.count
156        } else {
157            0
158        }
159    }
160
161    // Add methods like get_buckets(), get_count() if needed externally.
162}
163
164/// Metadata included in JSON statistics output, providing context.
165#[derive(Serialize, Clone, Debug)]
166pub struct Meta {
167    pub debounce_time_us: u64,
168    pub near_miss_threshold_us: u64,
169    pub log_all_events: bool,
170    pub log_bounces: bool,
171    pub log_interval_us: u64,
172}
173
174/// Statistics for a specific key value state (press/release/repeat).
175/// Holds the count of dropped events and the timing differences for those drops.
176#[derive(Debug, Clone)]
177pub struct KeyValueStats {
178    /// Total events processed (passed + dropped) for this specific key state.
179    pub total_processed: u64,
180    /// Count of events that passed the filter for this specific key state.
181    pub passed_count: u64,
182    /// Count of events that were dropped (bounced) for this specific key state.
183    pub dropped_count: u64,
184    /// Histogram of bounce timings for this specific key state.
185    pub bounce_histogram: TimingHistogram,
186    /// Aggregated statistics for bounce timings.
187    pub bounce_summary: TimingSummary,
188    /// Sampled bounce timings retained for debugging/JSON output.
189    pub bounce_samples: TimingSamples,
190}
191
192impl Default for KeyValueStats {
193    fn default() -> Self {
194        Self {
195            total_processed: 0,
196            passed_count: 0,
197            dropped_count: 0,
198            bounce_histogram: TimingHistogram::default(),
199            bounce_summary: TimingSummary::default(),
200            bounce_samples: TimingSamples::with_capacity(MAX_BOUNCE_TIMING_SAMPLES),
201        }
202    }
203}
204
205impl KeyValueStats {
206    /// Records a bounce timing, updating summary, histogram, and sampled values.
207    #[inline]
208    pub fn record_bounce_timing(&mut self, value: u64) {
209        self.bounce_summary.record(value);
210        self.bounce_histogram.record(value);
211        self.bounce_samples.push(value);
212    }
213}
214
215/// Statistics for passed events that were near misses for a specific key value state.
216#[derive(Debug, Clone)]
217pub struct NearMissStats {
218    /// Aggregated statistics for near-miss timings.
219    pub summary: TimingSummary,
220    /// Histogram of near-miss timings.
221    pub histogram: TimingHistogram,
222    /// Sampled near-miss timings retained for debugging/JSON output.
223    pub samples: TimingSamples,
224}
225
226impl Default for NearMissStats {
227    fn default() -> Self {
228        Self {
229            summary: TimingSummary::default(),
230            histogram: TimingHistogram::default(),
231            samples: TimingSamples::with_capacity(MAX_NEAR_MISS_TIMING_SAMPLES),
232        }
233    }
234}
235
236impl NearMissStats {
237    /// Records a near-miss timing, updating summary, histogram, and sampled values.
238    #[inline]
239    pub fn record_timing(&mut self, value: u64) {
240        self.summary.record(value);
241        self.histogram.record(value);
242        self.samples.push(value);
243    }
244}
245
246/// Aggregated statistics for a specific key code, containing stats for each value state.
247#[derive(Debug, Clone, Default)]
248pub struct KeyStats {
249    pub press: KeyValueStats,
250    pub release: KeyValueStats,
251    pub repeat: KeyValueStats,
252}
253
254/// Structure for serializing per-key drop statistics in JSON.
255#[derive(Serialize, Debug)]
256struct PerKeyStatsJson {
257    key_code: u16,
258    key_name: &'static str,
259    total_processed: u64,
260    total_dropped: u64,
261    drop_percentage: f64,
262    stats: KeyStatsJson, // Detailed stats for each state
263}
264
265/// Structure for serializing detailed key value stats in JSON.
266#[derive(Serialize, Debug)]
267struct KeyValueStatsJson {
268    total_processed: u64,
269    passed_count: u64,
270    dropped_count: u64,
271    drop_rate: f64,
272    timings_us: Vec<u64>, // Sampled timings
273    bounce_histogram: TimingHistogramJson,
274    #[serde(skip_serializing_if = "Option::is_none")]
275    min_us: Option<u64>,
276    #[serde(skip_serializing_if = "Option::is_none")]
277    max_us: Option<u64>,
278    #[serde(skip_serializing_if = "Option::is_none")]
279    avg_us: Option<u64>,
280}
281
282/// Structure for serializing detailed key stats in JSON.
283#[derive(Serialize, Debug)]
284struct KeyStatsJson {
285    press: KeyValueStatsJson,
286    release: KeyValueStatsJson,
287    repeat: KeyValueStatsJson, // Keep repeat for structure consistency
288}
289
290/// Structure for serializing histogram data in JSON.
291#[derive(Serialize, Debug)]
292struct TimingHistogramJson {
293    buckets: Vec<HistogramBucketJson>,
294    count: u64,
295    avg_us: u64,
296    // min_us: u64, // Optional
297    // max_us: u64, // Optional
298}
299
300/// Structure for serializing a single histogram bucket in JSON.
301#[derive(Serialize, Debug)]
302struct HistogramBucketJson {
303    min_ms: u64,
304    max_ms: Option<u64>, // None for the last bucket (>= max boundary)
305    count: u64,
306}
307
308/// Structure for serializing near-miss statistics in JSON.
309#[derive(Serialize, Debug)]
310struct NearMissStatsJson {
311    key_code: u16,
312    key_value: i32,
313    key_name: &'static str,
314    value_name: &'static str,
315    count: usize,
316    timings_us: Vec<u64>, // Sampled timings
317    near_miss_histogram: TimingHistogramJson,
318    #[serde(skip_serializing_if = "Option::is_none")]
319    min_us: Option<u64>,
320    #[serde(skip_serializing_if = "Option::is_none")]
321    max_us: Option<u64>,
322    #[serde(skip_serializing_if = "Option::is_none")]
323    avg_us: Option<u64>,
324}
325
326/// Top-level statistics collector. Owned and managed by the logger thread.
327/// Accumulates counts, drop timings, and near-miss timings for all processed events.
328#[derive(Debug, Clone)]
329pub struct StatsCollector {
330    /// Total count of key events processed (passed or dropped).
331    pub key_events_processed: u64,
332    /// Total count of key events that passed the filter.
333    pub key_events_passed: u64,
334    /// Total count of key events dropped by the filter.
335    pub key_events_dropped: u64,
336    /// Holds aggregated drop stats per key code. Uses a fixed-size array for O(1) lookup.
337    pub per_key_stats: Vec<KeyStats>,
338    /// Holds near-miss stats per key code and value. Indexed by `keycode * 3 + value`.
339    pub per_key_near_miss_stats: Vec<NearMissStats>,
340    /// Overall histogram for all bounce timings. Aggregated before reporting.
341    pub overall_bounce_histogram: TimingHistogram,
342    /// Overall histogram for all near_miss timings. Aggregated before reporting.
343    pub overall_near_miss_histogram: TimingHistogram,
344}
345
346// Implement Default to allow std::mem::take in logger.
347impl Default for StatsCollector {
348    fn default() -> Self {
349        StatsCollector::with_capacity()
350    }
351}
352
353impl StatsCollector {
354    /// Creates a new StatsCollector with pre-allocated storage.
355    #[must_use]
356    pub fn with_capacity() -> Self {
357        // Allocate the arrays on the heap using Box::new
358        let per_key_stats = vec![KeyStats::default(); FILTER_MAP_SIZE];
359        let per_key_near_miss_stats =
360            vec![NearMissStats::default(); FILTER_MAP_SIZE * NUM_KEY_STATES];
361
362        StatsCollector {
363            key_events_processed: 0,
364            key_events_passed: 0,
365            key_events_dropped: 0,
366            per_key_stats,
367            per_key_near_miss_stats,
368            overall_bounce_histogram: TimingHistogram::default(),
369            overall_near_miss_histogram: TimingHistogram::default(),
370        }
371    }
372
373    /// Updates statistics based on information about a processed event,
374    /// using the provided configuration.
375    /// This is the central method for stats accumulation, called by the logger thread.
376    pub fn record_event_info_with_config(
377        &mut self,
378        info: &EventInfo,
379        config: &crate::config::Config,
380    ) {
381        use crate::event::is_key_event;
382
383        // Only process EV_KEY events for these statistics.
384        if !is_key_event(&info.event) {
385            return;
386        }
387
388        self.key_events_processed += 1;
389
390        // Get mutable access to the specific KeyValueStats for this event, if valid
391        let key_code_idx = info.event.code as usize;
392        let key_value_idx = info.event.value as usize;
393
394        // Check bounds before accessing arrays
395        if key_code_idx >= FILTER_MAP_SIZE || key_value_idx >= NUM_KEY_STATES {
396            // Out of bounds - ignore for stats accumulation
397            return;
398        }
399
400        let value_stats = match info.event.value {
401            1 => &mut self.per_key_stats[key_code_idx].press,
402            0 => &mut self.per_key_stats[key_code_idx].release,
403            _ => &mut self.per_key_stats[key_code_idx].repeat,
404        };
405
406        // Increment total processed count
407        value_stats.total_processed += 1;
408
409        // Handle bounce/pass logic
410        if info.is_bounce {
411            self.key_events_dropped += 1;
412            // Increment drop count and record timing
413            value_stats.dropped_count += 1; // Increment drop count for this state
414            if let Some(diff) = info.diff_us {
415                value_stats.record_bounce_timing(diff); // Record aggregate + histogram
416            }
417        } else {
418            // Event passed the filter.
419            self.key_events_passed += 1;
420            // Increment passed count
421            value_stats.passed_count += 1;
422
423            // Check for near-miss on passed events
424            if let Some(last_us) = info.last_passed_us {
425                if let Some(diff) = info.event_us.checked_sub(last_us) {
426                    // Check if the difference is within the near-miss window (debounce_time <= diff <= threshold)
427                    // The filter ensures diff >= debounce_time for passed events.
428                    // Here, we check against the near_miss threshold.
429                    if diff <= config.near_miss_threshold_us() {
430                        // Calculate the flat index for the per_key_near_miss_stats array.
431                        let idx = key_code_idx * NUM_KEY_STATES + key_value_idx;
432                        // Bounds check is already done at the start of the function
433                        self.per_key_near_miss_stats[idx].record_timing(diff); // Record aggregate + histogram
434                    }
435                }
436            }
437        }
438    }
439
440    /// Aggregates per-key histograms into the overall histograms.
441    /// Should be called before generating reports.
442    pub fn aggregate_histograms(&mut self) {
443        // Reset overall histograms (important if called multiple times, e.g., periodic)
444        self.overall_bounce_histogram = TimingHistogram::default();
445        self.overall_near_miss_histogram = TimingHistogram::default();
446
447        for key_stats in self.per_key_stats.iter() {
448            // Aggregate bounce histograms
449            Self::accumulate_histogram(
450                &mut self.overall_bounce_histogram,
451                &key_stats.press.bounce_histogram,
452            );
453            Self::accumulate_histogram(
454                &mut self.overall_bounce_histogram,
455                &key_stats.release.bounce_histogram,
456            );
457            // Ignore repeat histogram for bounces (repeat events are not debounced)
458        }
459
460        for near_miss_stats in self.per_key_near_miss_stats.iter() {
461            // Aggregate near_miss histograms
462            Self::accumulate_histogram(
463                &mut self.overall_near_miss_histogram,
464                &near_miss_stats.histogram,
465            );
466        }
467    }
468
469    /// Helper to add counts from a source histogram to a destination histogram.
470    #[inline]
471    fn accumulate_histogram(dest: &mut TimingHistogram, source: &TimingHistogram) {
472        if source.count > 0 {
473            dest.count += source.count;
474            dest.sum_us = dest.sum_us.saturating_add(source.sum_us);
475            for i in 0..NUM_HISTOGRAM_BUCKETS {
476                dest.buckets[i] += source.buckets[i];
477            }
478            // Optional: Update overall min/max if stored directly
479            // dest.min_us = dest.min_us.min(source.min_us);
480            // dest.max_us = dest.max_us.max(source.max_us);
481        }
482    }
483
484    /// Formats a `TimingHistogram` into a human-readable string representation.
485    fn format_histogram_human(histogram: &TimingHistogram) -> String {
486        if histogram.count == 0 {
487            return "No data".to_string();
488        }
489
490        let mut output = String::new();
491        let total_count = histogram.count;
492
493        // Determine max bucket count for scaling the bar
494        let max_bucket_count = histogram.buckets.iter().copied().max().unwrap_or(0);
495        let bar_scale = if max_bucket_count > 0 {
496            50.0 / max_bucket_count as f64
497        } else {
498            0.0
499        }; // Max bar width 50 chars
500
501        for i in 0..NUM_HISTOGRAM_BUCKETS {
502            let bucket_count = histogram.buckets[i];
503            let percentage = if total_count > 0 {
504                (bucket_count as f64 / total_count as f64) * 100.0
505            } else {
506                0.0
507            };
508
509            let label = if i == 0 {
510                format!("< {}ms", HISTOGRAM_BUCKET_BOUNDARIES_MS[0])
511            } else if i == NUM_HISTOGRAM_BUCKETS - 1 {
512                format!(
513                    ">= {}ms",
514                    HISTOGRAM_BUCKET_BOUNDARIES_MS[NUM_HISTOGRAM_BUCKETS - 2]
515                )
516            } else {
517                format!(
518                    "{}-{}ms",
519                    HISTOGRAM_BUCKET_BOUNDARIES_MS[i - 1],
520                    HISTOGRAM_BUCKET_BOUNDARIES_MS[i]
521                )
522            };
523
524            let bar_width = (bucket_count as f64 * bar_scale).round() as usize;
525            let bar = "#".repeat(bar_width);
526
527            output.push_str(&format!(
528                "  {label:<10}: {bucket_count:<5} ({percentage:>5.1}%) [{bar}]\n"
529            ));
530        }
531
532        let avg_us = histogram.average_us();
533        output.push_str(&format!(
534            "  Total: {}, Avg: {}\n",
535            total_count,
536            util::format_us(avg_us)
537        ));
538
539        output
540    }
541
542    /// Formats human-readable statistics summary and writes it to the provided writer.
543    /// Returns an io::Result to handle potential write errors.
544    pub fn format_stats_human_readable(
545        &mut self, // Needs to be mutable to aggregate histograms
546        config: &crate::config::Config,
547        report_type: &str,
548        mut writer: impl Write, // Accept a generic writer
549    ) -> std::io::Result<()> {
550        // Aggregate histograms before reporting
551        self.aggregate_histograms();
552
553        writeln!(writer, "\n--- Overall Statistics ({report_type}) ---")?;
554        writeln!(
555            writer,
556            "Key Events Processed: {}",
557            self.key_events_processed
558        )?;
559        writeln!(writer, "Key Events Passed:   {}", self.key_events_passed)?;
560        writeln!(writer, "Key Events Dropped:  {}", self.key_events_dropped)?;
561        let percentage = if self.key_events_processed > 0 {
562            (self.key_events_dropped as f64 / self.key_events_processed as f64) * 100.0
563        } else {
564            0.0
565        };
566        writeln!(writer, "Percentage Dropped:  {percentage:.2}%")?;
567
568        // Overall Bounce Histogram
569        writeln!(writer, "\n--- Overall Bounce Timing Histogram ---")?;
570        write!(
571            writer,
572            "{}",
573            Self::format_histogram_human(&self.overall_bounce_histogram)
574        )?;
575
576        // Overall Near-Miss Histogram
577        writeln!(
578            writer,
579            "\n--- Overall Near-Miss Timing Histogram (Passed within {}) ---",
580            util::format_duration(config.near_miss_threshold())
581        )?;
582        write!(
583            writer,
584            "{}",
585            Self::format_histogram_human(&self.overall_near_miss_histogram)
586        )?;
587
588        let mut any_drops = false;
589        for key_code in 0..self.per_key_stats.len() {
590            let stats = &self.per_key_stats[key_code];
591            let total_drops_for_key = stats.press.dropped_count
592                + stats.release.dropped_count
593                + stats.repeat.dropped_count;
594
595            if total_drops_for_key > 0
596                || stats.press.total_processed > 0
597                || stats.release.total_processed > 0
598                || stats.repeat.total_processed > 0
599            {
600                // Only print key if it had any activity (passed or dropped)
601                if !any_drops {
602                    writeln!(writer, "\n--- Dropped Event Statistics Per Key ---")?;
603                    writeln!(writer, "Format: Key [Name] (Code):")?;
604                    writeln!(
605                        writer,
606                        "  State (Value): Processed: <count>, Passed: <count>, Dropped: <count> (<rate>%) (Bounce Time: Min / Avg / Max)"
607                    )?;
608                    any_drops = true;
609                }
610
611                let key_name = get_key_name(key_code as u16);
612                writeln!(writer, "\nKey [{key_name}] ({key_code}):")?;
613                // Calculate total processed for this key
614                let total_processed_for_key = stats.press.total_processed
615                    + stats.release.total_processed
616                    + stats.repeat.total_processed;
617                // Calculate total passed for this key
618                let total_passed_for_key = stats.press.passed_count
619                    + stats.release.passed_count
620                    + stats.repeat.passed_count;
621                // Calculate overall drop percentage for this key
622                let key_drop_percentage = if total_processed_for_key > 0 {
623                    // Base percentage on total processed
624                    (total_drops_for_key as f64 / total_processed_for_key as f64) * 100.0
625                } else {
626                    0.0
627                };
628                writeln!(
629                    writer, // Updated summary line format
630                    "  Total Processed: {total_processed_for_key}, Passed: {total_passed_for_key}, Dropped: {total_drops_for_key} ({key_drop_percentage:.2}%)"
631                )?;
632
633                // Use a closure that captures writer mutably
634                let mut print_value_stats = |value_name: &str,
635                                             value_code: i32,
636                                             value_stats: &KeyValueStats|
637                 -> std::io::Result<()> {
638                    if value_stats.total_processed > 0 {
639                        // Calculate drop rate for this specific state
640                        let drop_rate = if value_stats.total_processed > 0 {
641                            (value_stats.dropped_count as f64 / value_stats.total_processed as f64)
642                                * 100.0
643                        } else {
644                            0.0
645                        };
646                        write!(
647                            writer,
648                            "  {:<7} ({}): Processed: {}, Passed: {}, Dropped: {} ({:.2}%)",
649                            value_name,
650                            value_code,
651                            value_stats.total_processed,
652                            value_stats.passed_count,
653                            value_stats.dropped_count,
654                            drop_rate
655                        )?;
656                        if let Some(min) = value_stats.bounce_summary.min_us() {
657                            let max = value_stats.bounce_summary.max_us().unwrap_or(min);
658                            let avg = value_stats.bounce_summary.average_us().unwrap_or(min);
659                            writeln!(
660                                writer,
661                                " (Bounce Time: {} / {} / {})",
662                                util::format_us(min),
663                                util::format_us(avg),
664                                util::format_us(max)
665                            )?;
666                        } else {
667                            writeln!(writer)?;
668                        }
669                    }
670                    Ok(())
671                };
672
673                print_value_stats("Press", 1, &stats.press)?;
674                print_value_stats("Release", 0, &stats.release)?;
675                print_value_stats("Repeat", 2, &stats.repeat)?; // Include repeat stats line if processed
676            }
677        }
678        if !any_drops {
679            writeln!(writer, "\n--- No key events dropped ---")?;
680        }
681
682        let mut any_near_miss = false;
683        for idx in 0..self.per_key_near_miss_stats.len() {
684            let near_miss_stats = &self.per_key_near_miss_stats[idx];
685            if near_miss_stats.summary.count() > 0 {
686                if !any_near_miss {
687                    writeln!(
688                        writer,
689                        "\n--- Passed Event Near-Miss Statistics (Passed within {}) ---",
690                        util::format_duration(config.near_miss_threshold())
691                    )?;
692                    writeln!(
693                        writer,
694                        "Format: Key [Name] (Code, Value): Count (Near-Miss Time: Min / Avg / Max)"
695                    )?;
696                    any_near_miss = true;
697                }
698
699                let key_code = (idx / NUM_KEY_STATES) as u16;
700                let key_value = (idx % NUM_KEY_STATES) as i32;
701                let key_name = get_key_name(key_code);
702
703                let min = near_miss_stats.summary.min_us().unwrap_or(0);
704                let max = near_miss_stats.summary.max_us().unwrap_or(min);
705                let avg = near_miss_stats.summary.average_us().unwrap_or(min);
706                let count = near_miss_stats.summary.count();
707
708                writeln!(
709                    writer,
710                    "  Key [{}] ({}, {}): {} (Near-Miss Time: {} / {} / {})",
711                    key_name,
712                    key_code,
713                    key_value,
714                    count,
715                    util::format_us(min),
716                    util::format_us(avg),
717                    util::format_us(max)
718                )?;
719            }
720        }
721        if !any_near_miss {
722            writeln!(
723                writer,
724                "\n--- No near-miss events recorded (< {}) ---",
725                util::format_duration(config.near_miss_threshold())
726            )?;
727        }
728
729        writeln!(
730            writer,
731            "----------------------------------------------------------"
732        )?;
733        Ok(()) // Return Ok(()) at the end of the function
734    }
735
736    /// Prints human-readable statistics summary to stderr by calling format_stats_human_readable.
737    pub fn print_stats_to_stderr(&mut self, config: &crate::config::Config, report_type: &str) {
738        // Ignore potential write errors when writing to stderr, as there's not much we can do.
739        let _ =
740            self.format_stats_human_readable(config, report_type, &mut std::io::stderr().lock());
741    }
742
743    /// Helper to create JSON representation of a TimingHistogram.
744    fn create_histogram_json(histogram: &TimingHistogram) -> TimingHistogramJson {
745        let mut buckets_json = Vec::with_capacity(NUM_HISTOGRAM_BUCKETS);
746        for i in 0..NUM_HISTOGRAM_BUCKETS {
747            let min_ms = if i == 0 {
748                0
749            } else {
750                HISTOGRAM_BUCKET_BOUNDARIES_MS[i - 1]
751            };
752            let max_ms = if i == NUM_HISTOGRAM_BUCKETS - 1 {
753                None
754            } else {
755                Some(HISTOGRAM_BUCKET_BOUNDARIES_MS[i])
756            };
757            buckets_json.push(HistogramBucketJson {
758                min_ms,
759                max_ms,
760                count: histogram.buckets[i],
761            });
762        }
763        TimingHistogramJson {
764            buckets: buckets_json,
765            count: histogram.count,
766            avg_us: histogram.average_us(),
767            // min_us: histogram.min_us, // Optional
768            // max_us: histogram.max_us, // Optional
769        }
770    }
771
772    /// Prints statistics in JSON format to the given writer.
773    /// Includes runtime provided externally (calculated in main thread).
774    pub fn print_stats_json(
775        &mut self,
776        config: &crate::config::Config,
777        runtime_us: Option<u64>,
778        report_type: &str,
779        mut writer: impl Write,
780    ) {
781        // Aggregate histograms before reporting
782        self.aggregate_histograms();
783
784        // --- Prepare Per-Key Drop Stats for JSON ---
785        let mut per_key_stats_json_vec = Vec::new();
786        for (key_code_usize, stats) in self.per_key_stats.iter().enumerate() {
787            let total_processed_for_key = stats.press.total_processed
788                + stats.release.total_processed
789                + stats.repeat.total_processed;
790            let total_dropped_for_key = stats.press.dropped_count
791                + stats.release.dropped_count
792                + stats.repeat.dropped_count;
793
794            if total_processed_for_key > 0 {
795                // Include keys with any activity (passed or dropped)
796                let key_code = key_code_usize as u16;
797                let key_name = get_key_name(key_code);
798                let drop_percentage = if total_processed_for_key > 0 {
799                    (total_dropped_for_key as f64 / total_processed_for_key as f64) * 100.0
800                } else {
801                    0.0
802                };
803
804                // Helper closure to create KeyValueStatsJson
805                let create_kv_stats_json = |kv_stats: &KeyValueStats| -> KeyValueStatsJson {
806                    let drop_rate = if kv_stats.total_processed > 0 {
807                        (kv_stats.dropped_count as f64 / kv_stats.total_processed as f64) * 100.0
808                    } else {
809                        0.0
810                    };
811                    KeyValueStatsJson {
812                        total_processed: kv_stats.total_processed,
813                        passed_count: kv_stats.passed_count,
814                        dropped_count: kv_stats.dropped_count,
815                        drop_rate,
816                        timings_us: kv_stats.bounce_samples.to_vec(),
817                        bounce_histogram: Self::create_histogram_json(&kv_stats.bounce_histogram),
818                        min_us: kv_stats.bounce_summary.min_us(),
819                        max_us: kv_stats.bounce_summary.max_us(),
820                        avg_us: kv_stats.bounce_summary.average_us(),
821                    }
822                };
823
824                // Populate the detailed stats structure for JSON
825                let detailed_stats_json = KeyStatsJson {
826                    // Add lifetime here
827                    press: create_kv_stats_json(&stats.press),
828                    release: create_kv_stats_json(&stats.release),
829                    // Repeat stats are included for structure, rate will be 0.0
830                    repeat: create_kv_stats_json(&stats.repeat),
831                };
832
833                per_key_stats_json_vec.push(PerKeyStatsJson {
834                    key_code,
835                    key_name,
836                    total_processed: total_processed_for_key,
837                    total_dropped: total_dropped_for_key,
838                    drop_percentage,
839                    stats: detailed_stats_json, // Use the new detailed struct // Add lifetime here
840                });
841            }
842        }
843
844        // --- Prepare Near-Miss Stats for JSON ---
845        let mut near_miss_json_vec = Vec::new();
846        for (idx, near_miss_stats) in self.per_key_near_miss_stats.iter().enumerate() {
847            if near_miss_stats.summary.count() > 0 {
848                let key_code = (idx / NUM_KEY_STATES) as u16;
849                let key_value = (idx % NUM_KEY_STATES) as i32;
850                let key_name = get_key_name(key_code);
851                let value_name = get_value_name(key_value);
852
853                near_miss_json_vec.push(NearMissStatsJson {
854                    key_code,
855                    key_value,
856                    key_name,
857                    value_name,
858                    count: near_miss_stats.summary.count() as usize,
859                    timings_us: near_miss_stats.samples.to_vec(),
860                    near_miss_histogram: Self::create_histogram_json(&near_miss_stats.histogram),
861                    min_us: near_miss_stats.summary.min_us(),
862                    max_us: near_miss_stats.summary.max_us(),
863                    avg_us: near_miss_stats.summary.average_us(),
864                });
865            }
866        }
867
868        #[derive(Serialize)]
869        struct ReportData<'a> {
870            report_type: &'a str,
871            #[serde(skip_serializing_if = "Option::is_none")]
872            runtime_us: Option<u64>,
873            #[serde(skip_serializing_if = "Option::is_none")]
874            runtime_human: Option<String>,
875            // Add raw config values as well for machine readability
876            debounce_time_us: u64,
877            near_miss_threshold_us: u64,
878            log_interval_us: u64,
879            debounce_time_human: String,
880            near_miss_threshold_human: String,
881            log_interval_human: String,
882            key_events_processed: u64,
883            key_events_passed: u64,
884            key_events_dropped: u64,
885            // Overall Histograms
886            overall_bounce_histogram: TimingHistogramJson,
887            overall_near_miss_histogram: TimingHistogramJson,
888            // Per-Key and Per-Near-Miss details
889            per_key_stats: Vec<PerKeyStatsJson>,
890            per_key_near_miss_stats: Vec<NearMissStatsJson>,
891        }
892
893        let runtime_human = runtime_us.map(|us| util::format_duration(Duration::from_micros(us)));
894        let debounce_human = util::format_duration(config.debounce_time());
895        let near_miss_human = util::format_duration(config.near_miss_threshold());
896        let log_interval_human = util::format_duration(config.log_interval());
897
898        let report = ReportData {
899            report_type,
900            runtime_us, // Will be None for periodic reports
901            runtime_human,
902            debounce_time_us: config.debounce_us(), // Add raw value
903            near_miss_threshold_us: config.near_miss_threshold_us(), // Add raw value
904            log_interval_us: config.log_interval_us(), // Add raw value
905            debounce_time_human: debounce_human,
906            near_miss_threshold_human: near_miss_human,
907            log_interval_human,
908            key_events_processed: self.key_events_processed,
909            key_events_passed: self.key_events_passed,
910            key_events_dropped: self.key_events_dropped,
911            overall_bounce_histogram: Self::create_histogram_json(&self.overall_bounce_histogram),
912            overall_near_miss_histogram: Self::create_histogram_json(
913                &self.overall_near_miss_histogram,
914            ),
915            per_key_stats: per_key_stats_json_vec, // Use the prepared Vec
916            per_key_near_miss_stats: near_miss_json_vec, // Use the prepared Vec
917        };
918
919        // We are printing individual reports (cumulative or periodic) as separate JSON objects
920        // to stderr. The logger thread handles the overall structure (e.g., a list of periodic
921        // reports).
922        let _ = serde_json::to_writer_pretty(&mut writer, &report);
923        let _ = writeln!(writer);
924    }
925}