Skip to main content

tale_ndjson/
json_profiler.rs

1//! JSON deserialization profiling for Phase 2.3
2//!
3//! This module provides tools to profile JSON parsing performance and
4//! measure which Printable variants are hit most frequently.
5
6use std::sync::LazyLock;
7use std::sync::atomic::{AtomicUsize, Ordering};
8
9use crate::logpatterns::Printable;
10
11/// Global counters for tracking Printable variant usage
12#[derive(Debug, Default)]
13pub struct VariantCounters {
14    pub canonical: AtomicUsize,
15    pub java: AtomicUsize,
16    pub message: AtomicUsize,
17    pub time_only: AtomicUsize,
18    pub json: AtomicUsize,
19    pub logfmt: AtomicUsize,
20    pub text: AtomicUsize,
21    pub parse_errors: AtomicUsize,
22}
23
24static COUNTERS: LazyLock<VariantCounters> = LazyLock::new(Default::default);
25
26impl VariantCounters {
27    /// Record that we successfully parsed a specific variant
28    pub fn record_variant(&self, variant: &Printable<'_>) {
29        match variant {
30            Printable::Canonical(_) => self.canonical.fetch_add(1, Ordering::Relaxed),
31            Printable::Java(_) => self.java.fetch_add(1, Ordering::Relaxed),
32            Printable::Message(_) => self.message.fetch_add(1, Ordering::Relaxed),
33            Printable::TimeOnly(_) => self.time_only.fetch_add(1, Ordering::Relaxed),
34            Printable::Json(_) => self.json.fetch_add(1, Ordering::Relaxed),
35            Printable::Logfmt(_) => self.logfmt.fetch_add(1, Ordering::Relaxed),
36            Printable::Text(_) => self.text.fetch_add(1, Ordering::Relaxed),
37        };
38    }
39
40    /// Record that JSON parsing failed entirely
41    pub fn record_parse_error(&self) {
42        self.parse_errors.fetch_add(1, Ordering::Relaxed);
43    }
44
45    /// Get current counts for all variants
46    pub fn get_counts(&self) -> VariantCounts {
47        VariantCounts {
48            canonical: self.canonical.load(Ordering::Relaxed),
49            java: self.java.load(Ordering::Relaxed),
50            message: self.message.load(Ordering::Relaxed),
51            time_only: self.time_only.load(Ordering::Relaxed),
52            json: self.json.load(Ordering::Relaxed),
53            logfmt: self.logfmt.load(Ordering::Relaxed),
54            text: self.text.load(Ordering::Relaxed),
55            parse_errors: self.parse_errors.load(Ordering::Relaxed),
56        }
57    }
58
59    /// Reset all counters to zero
60    pub fn reset(&self) {
61        self.canonical.store(0, Ordering::Relaxed);
62        self.java.store(0, Ordering::Relaxed);
63        self.message.store(0, Ordering::Relaxed);
64        self.time_only.store(0, Ordering::Relaxed);
65        self.json.store(0, Ordering::Relaxed);
66        self.logfmt.store(0, Ordering::Relaxed);
67        self.text.store(0, Ordering::Relaxed);
68        self.parse_errors.store(0, Ordering::Relaxed);
69    }
70}
71
72/// Snapshot of variant counts at a point in time
73#[derive(Debug, Clone)]
74pub struct VariantCounts {
75    pub canonical: usize,
76    pub java: usize,
77    pub message: usize,
78    pub time_only: usize,
79    pub json: usize,
80    pub logfmt: usize,
81    pub text: usize,
82    pub parse_errors: usize,
83}
84
85impl VariantCounts {
86    pub fn total(&self) -> usize {
87        self.canonical + self.java + self.message + self.time_only + self.json + self.logfmt + self.text
88    }
89
90    pub fn successful_parses(&self) -> usize {
91        self.total() - self.text // Text is plain text, not JSON
92    }
93
94    pub fn json_parses(&self) -> usize {
95        self.canonical + self.java + self.message + self.time_only + self.json
96    }
97
98    /// Calculate percentage of each variant
99    pub fn percentages(&self) -> VariantPercentages {
100        let total = self.total() as f64;
101        if total == 0.0 {
102            return VariantPercentages::default();
103        }
104
105        VariantPercentages {
106            canonical: (self.canonical as f64 / total) * 100.0,
107            java: (self.java as f64 / total) * 100.0,
108            message: (self.message as f64 / total) * 100.0,
109            time_only: (self.time_only as f64 / total) * 100.0,
110            json: (self.json as f64 / total) * 100.0,
111            logfmt: (self.logfmt as f64 / total) * 100.0,
112            text: (self.text as f64 / total) * 100.0,
113            parse_errors: (self.parse_errors as f64 / total) * 100.0,
114        }
115    }
116
117    /// Print a formatted report of variant usage
118    pub fn print_report(&self) {
119        let percentages = self.percentages();
120        let total = self.total();
121
122        println!("JSON Parsing Profile Report:");
123        println!("============================");
124        println!("Total lines processed: {}", total);
125        println!(
126            "JSON lines: {} ({:.1}%)",
127            self.json_parses(),
128            (self.json_parses() as f64 / total as f64) * 100.0
129        );
130        println!("Plain text: {} ({:.1}%)", self.text, percentages.text);
131        println!("Parse errors: {} ({:.1}%)", self.parse_errors, percentages.parse_errors);
132        println!();
133        println!("JSON Variant Breakdown:");
134        println!(
135            "  Canonical: {} ({:.1}%) - FASTEST",
136            self.canonical, percentages.canonical
137        );
138        println!("  Java:      {} ({:.1}%)", self.java, percentages.java);
139        println!(
140            "  Message:   {} ({:.1}%) - SUPERSET (includes GCP, Logstash, etc.)",
141            self.message, percentages.message
142        );
143        println!("  TimeOnly:  {} ({:.1}%)", self.time_only, percentages.time_only);
144        println!("  Generic:   {} ({:.1}%) - FALLBACK", self.json, percentages.json);
145        println!("  Logfmt:    {} ({:.1}%)", self.logfmt, percentages.logfmt);
146        println!();
147
148        if percentages.canonical > 50.0 {
149            println!(
150                "✅ Good: {}% of logs use the fast Canonical path",
151                percentages.canonical
152            );
153        } else if percentages.canonical > 25.0 {
154            println!(
155                "⚠️  Moderate: {}% of logs use the fast Canonical path",
156                percentages.canonical
157            );
158        } else {
159            println!(
160                "❌ Poor: Only {}% of logs use the fast Canonical path",
161                percentages.canonical
162            );
163            println!("   Consider adding more fields to Canonical or optimizing Message parsing");
164        }
165    }
166}
167
168#[derive(Debug, Clone, Default)]
169pub struct VariantPercentages {
170    pub canonical: f64,
171    pub java: f64,
172    pub message: f64,
173    pub time_only: f64,
174    pub json: f64,
175    pub logfmt: f64,
176    pub text: f64,
177    pub parse_errors: f64,
178}
179
180/// Global profiling interface
181pub fn record_variant(variant: &Printable<'_>) {
182    COUNTERS.record_variant(variant);
183}
184
185pub fn record_parse_error() {
186    COUNTERS.record_parse_error();
187}
188
189pub fn get_counts() -> VariantCounts {
190    COUNTERS.get_counts()
191}
192
193pub fn reset_counters() {
194    COUNTERS.reset();
195}
196
197pub fn print_report() {
198    get_counts().print_report();
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn can_count_variants() {
207        reset_counters();
208
209        // This would require creating actual Printable instances
210        // For now, just test the percentage calculation
211        let counts = VariantCounts {
212            canonical: 50,
213            message: 30,
214            json: 15,
215            text: 5,
216            java: 0,
217            logfmt: 0,
218            time_only: 0,
219            parse_errors: 0,
220        };
221
222        let percentages = counts.percentages();
223        assert_eq!(counts.total(), 100);
224        assert!((percentages.canonical - 50.0).abs() < 0.1);
225        assert!((percentages.message - 30.0).abs() < 0.1);
226        assert!((percentages.json - 15.0).abs() < 0.1);
227        assert!((percentages.text - 5.0).abs() < 0.1);
228    }
229}