Skip to main content

php_rs_parser/
instrument.rs

1//! Lightweight instrumentation for profiling array parsing, expression parsing, and statement parsing.
2//!
3//! This module provides zero-cost abstractions for profiling when the `instrument` feature
4//! is enabled. When disabled, all instrumentation macros and calls compile to nothing.
5
6use std::sync::Mutex;
7
8/// Global instrumentation statistics
9pub struct InstrumentStats {
10    // Expression & Array Parsing
11    /// Total calls to parse_expr
12    pub parse_expr_calls: u64,
13    /// Total calls to parse_expr_bp (with min_bp != 0)
14    pub parse_expr_bp_recursive_calls: u64,
15    /// Total calls to parse_array_element
16    pub parse_array_element_calls: u64,
17    /// Total calls to parse_array_element that had =>
18    pub parse_array_element_with_arrow: u64,
19    /// Total parse_expr calls in array elements (first expression)
20    pub parse_expr_array_first: u64,
21    /// Total parse_expr calls in array elements (second expression after =>)
22    pub parse_expr_array_second: u64,
23    /// Total arrays parsed
24    pub parse_array_count: u64,
25    /// Total array elements parsed
26    pub parse_array_element_count: u64,
27    /// Number of times parse_atom was called
28    pub parse_atom_calls: u64,
29    /// Number of simple/atomic values in arrays (not followed by operators)
30    pub parse_array_simple_values: u64,
31
32    // Statement Parsing
33    /// Total statements parsed
34    pub parse_stmt_calls: u64,
35    /// Total function/method declarations
36    pub parse_function_calls: u64,
37    /// Total class/trait/interface declarations
38    pub parse_class_calls: u64,
39    /// Total foreach statements
40    pub parse_foreach_calls: u64,
41    /// Total for/while/do-while statements
42    pub parse_loop_calls: u64,
43    /// Total if/elseif/else statements
44    pub parse_if_calls: u64,
45    /// Total switch statements
46    pub parse_switch_calls: u64,
47    /// Total try/catch/finally statements
48    pub parse_try_calls: u64,
49    /// Total attribute groups parsed (PHP 8.0+)
50    pub parse_attribute_calls: u64,
51
52    // Memory Allocation Patterns
53    /// Total ArenaVec allocations
54    pub arena_vec_allocations: u64,
55    /// Total bytes allocated in ArenaVec
56    pub arena_vec_bytes: u64,
57    /// Total times arena.alloc was called
58    pub arena_alloc_calls: u64,
59    /// Total ArenaVec reallocations (growth)
60    pub arena_vec_reallocations: u64,
61    /// Total bytes wasted in pre-allocated but unused capacity
62    pub arena_vec_wasted_capacity: u64,
63    /// Number of empty ArenaVec allocations (capacity but no elements)
64    pub arena_vec_empty: u64,
65}
66
67lazy_static::lazy_static! {
68    static ref STATS: Mutex<InstrumentStats> = Mutex::new(InstrumentStats {
69        parse_expr_calls: 0,
70        parse_expr_bp_recursive_calls: 0,
71        parse_array_element_calls: 0,
72        parse_array_element_with_arrow: 0,
73        parse_expr_array_first: 0,
74        parse_expr_array_second: 0,
75        parse_array_count: 0,
76        parse_array_element_count: 0,
77        parse_atom_calls: 0,
78        parse_array_simple_values: 0,
79        parse_stmt_calls: 0,
80        parse_function_calls: 0,
81        parse_class_calls: 0,
82        parse_foreach_calls: 0,
83        parse_loop_calls: 0,
84        parse_if_calls: 0,
85        parse_switch_calls: 0,
86        parse_try_calls: 0,
87        parse_attribute_calls: 0,
88        arena_vec_allocations: 0,
89        arena_vec_bytes: 0,
90        arena_alloc_calls: 0,
91        arena_vec_reallocations: 0,
92        arena_vec_wasted_capacity: 0,
93        arena_vec_empty: 0,
94    });
95}
96
97/// Record a parse_expr call
98#[inline]
99pub fn record_parse_expr() {
100    #[cfg(feature = "instrument")]
101    {
102        if let Ok(mut stats) = STATS.lock() {
103            stats.parse_expr_calls += 1;
104        }
105    }
106}
107
108/// Record a recursive parse_expr_bp call (min_bp != 0)
109#[inline]
110pub fn record_parse_expr_bp_recursive() {
111    #[cfg(feature = "instrument")]
112    {
113        if let Ok(mut stats) = STATS.lock() {
114            stats.parse_expr_bp_recursive_calls += 1;
115        }
116    }
117}
118
119/// Record array parsing started
120#[inline]
121pub fn record_parse_array() {
122    #[cfg(feature = "instrument")]
123    {
124        if let Ok(mut stats) = STATS.lock() {
125            stats.parse_array_count += 1;
126        }
127    }
128}
129
130/// Record array element parsing started
131#[inline]
132pub fn record_parse_array_element() {
133    #[cfg(feature = "instrument")]
134    {
135        if let Ok(mut stats) = STATS.lock() {
136            stats.parse_array_element_calls += 1;
137        }
138    }
139}
140
141/// Record first parse_expr call in array element (key or value)
142#[inline]
143pub fn record_parse_expr_array_first() {
144    #[cfg(feature = "instrument")]
145    {
146        if let Ok(mut stats) = STATS.lock() {
147            stats.parse_expr_array_first += 1;
148        }
149    }
150}
151
152/// Record second parse_expr call in array element (after =>)
153#[inline]
154pub fn record_parse_expr_array_second() {
155    #[cfg(feature = "instrument")]
156    {
157        if let Ok(mut stats) = STATS.lock() {
158            stats.parse_expr_array_second += 1;
159        }
160    }
161}
162
163/// Record array element with => operator
164#[inline]
165pub fn record_parse_array_element_with_arrow() {
166    #[cfg(feature = "instrument")]
167    {
168        if let Ok(mut stats) = STATS.lock() {
169            stats.parse_array_element_with_arrow += 1;
170        }
171    }
172}
173
174/// Record total array elements parsed
175#[inline]
176pub fn record_parse_array_element_count(_count: usize) {
177    #[cfg(feature = "instrument")]
178    {
179        if let Ok(mut stats) = STATS.lock() {
180            stats.parse_array_element_count += _count as u64;
181        }
182    }
183}
184
185/// Record parse_atom call
186#[inline]
187pub fn record_parse_atom() {
188    #[cfg(feature = "instrument")]
189    {
190        if let Ok(mut stats) = STATS.lock() {
191            stats.parse_atom_calls += 1;
192        }
193    }
194}
195
196/// Record a simple/atomic value in an array (not followed by binary operators)
197#[inline]
198pub fn record_parse_array_simple_value() {
199    #[cfg(feature = "instrument")]
200    {
201        if let Ok(mut stats) = STATS.lock() {
202            stats.parse_array_simple_values += 1;
203        }
204    }
205}
206
207// ==================== STATEMENT PARSING METRICS ====================
208
209/// Record a statement parse call
210#[inline]
211pub fn record_parse_stmt() {
212    #[cfg(feature = "instrument")]
213    {
214        if let Ok(mut stats) = STATS.lock() {
215            stats.parse_stmt_calls += 1;
216        }
217    }
218}
219
220/// Record a function/method declaration
221#[inline]
222pub fn record_parse_function() {
223    #[cfg(feature = "instrument")]
224    {
225        if let Ok(mut stats) = STATS.lock() {
226            stats.parse_function_calls += 1;
227        }
228    }
229}
230
231/// Record a class/trait/interface declaration
232#[inline]
233pub fn record_parse_class() {
234    #[cfg(feature = "instrument")]
235    {
236        if let Ok(mut stats) = STATS.lock() {
237            stats.parse_class_calls += 1;
238        }
239    }
240}
241
242/// Record a foreach statement
243#[inline]
244pub fn record_parse_foreach() {
245    #[cfg(feature = "instrument")]
246    {
247        if let Ok(mut stats) = STATS.lock() {
248            stats.parse_foreach_calls += 1;
249        }
250    }
251}
252
253/// Record for/while/do-while statements
254#[inline]
255pub fn record_parse_loop() {
256    #[cfg(feature = "instrument")]
257    {
258        if let Ok(mut stats) = STATS.lock() {
259            stats.parse_loop_calls += 1;
260        }
261    }
262}
263
264/// Record if/elseif/else statements
265#[inline]
266pub fn record_parse_if() {
267    #[cfg(feature = "instrument")]
268    {
269        if let Ok(mut stats) = STATS.lock() {
270            stats.parse_if_calls += 1;
271        }
272    }
273}
274
275/// Record switch statements
276#[inline]
277pub fn record_parse_switch() {
278    #[cfg(feature = "instrument")]
279    {
280        if let Ok(mut stats) = STATS.lock() {
281            stats.parse_switch_calls += 1;
282        }
283    }
284}
285
286/// Record try/catch/finally statements
287#[inline]
288pub fn record_parse_try() {
289    #[cfg(feature = "instrument")]
290    {
291        if let Ok(mut stats) = STATS.lock() {
292            stats.parse_try_calls += 1;
293        }
294    }
295}
296
297/// Record attribute group parsing
298#[inline]
299pub fn record_parse_attribute() {
300    #[cfg(feature = "instrument")]
301    {
302        if let Ok(mut stats) = STATS.lock() {
303            stats.parse_attribute_calls += 1;
304        }
305    }
306}
307
308// ==================== MEMORY ALLOCATION METRICS ====================
309
310/// Record an ArenaVec allocation with size info
311#[inline]
312pub fn record_arena_vec_allocation(_capacity: usize) {
313    #[cfg(feature = "instrument")]
314    {
315        if let Ok(mut stats) = STATS.lock() {
316            stats.arena_vec_allocations += 1;
317            stats.arena_vec_bytes += _capacity as u64;
318        }
319    }
320}
321
322/// Record an arena.alloc call
323#[inline]
324pub fn record_arena_alloc() {
325    #[cfg(feature = "instrument")]
326    {
327        if let Ok(mut stats) = STATS.lock() {
328            stats.arena_alloc_calls += 1;
329        }
330    }
331}
332
333/// Record wasted capacity in ArenaVec (allocated but not used)
334#[inline]
335pub fn record_arena_vec_waste(_wasted_bytes: usize) {
336    #[cfg(feature = "instrument")]
337    {
338        if let Ok(mut stats) = STATS.lock() {
339            stats.arena_vec_wasted_capacity += _wasted_bytes as u64;
340        }
341    }
342}
343
344/// Record an empty ArenaVec that was allocated
345#[inline]
346pub fn record_arena_vec_empty() {
347    #[cfg(feature = "instrument")]
348    {
349        if let Ok(mut stats) = STATS.lock() {
350            stats.arena_vec_empty += 1;
351        }
352    }
353}
354
355/// Get current statistics (snapshot)
356pub fn get_stats() -> InstrumentStats {
357    #[cfg(feature = "instrument")]
358    {
359        STATS
360            .lock()
361            .ok()
362            .map(|stats| InstrumentStats {
363                parse_expr_calls: stats.parse_expr_calls,
364                parse_expr_bp_recursive_calls: stats.parse_expr_bp_recursive_calls,
365                parse_array_element_calls: stats.parse_array_element_calls,
366                parse_array_element_with_arrow: stats.parse_array_element_with_arrow,
367                parse_expr_array_first: stats.parse_expr_array_first,
368                parse_expr_array_second: stats.parse_expr_array_second,
369                parse_array_count: stats.parse_array_count,
370                parse_array_element_count: stats.parse_array_element_count,
371                parse_atom_calls: stats.parse_atom_calls,
372                parse_array_simple_values: stats.parse_array_simple_values,
373                parse_stmt_calls: stats.parse_stmt_calls,
374                parse_function_calls: stats.parse_function_calls,
375                parse_class_calls: stats.parse_class_calls,
376                parse_foreach_calls: stats.parse_foreach_calls,
377                parse_loop_calls: stats.parse_loop_calls,
378                parse_if_calls: stats.parse_if_calls,
379                parse_switch_calls: stats.parse_switch_calls,
380                parse_try_calls: stats.parse_try_calls,
381                parse_attribute_calls: stats.parse_attribute_calls,
382                arena_vec_allocations: stats.arena_vec_allocations,
383                arena_vec_bytes: stats.arena_vec_bytes,
384                arena_alloc_calls: stats.arena_alloc_calls,
385                arena_vec_reallocations: stats.arena_vec_reallocations,
386                arena_vec_wasted_capacity: stats.arena_vec_wasted_capacity,
387                arena_vec_empty: stats.arena_vec_empty,
388            })
389            .unwrap_or(InstrumentStats {
390                parse_expr_calls: 0,
391                parse_expr_bp_recursive_calls: 0,
392                parse_array_element_calls: 0,
393                parse_array_element_with_arrow: 0,
394                parse_expr_array_first: 0,
395                parse_expr_array_second: 0,
396                parse_array_count: 0,
397                parse_array_element_count: 0,
398                parse_atom_calls: 0,
399                parse_array_simple_values: 0,
400                parse_stmt_calls: 0,
401                parse_function_calls: 0,
402                parse_class_calls: 0,
403                parse_foreach_calls: 0,
404                parse_loop_calls: 0,
405                parse_if_calls: 0,
406                parse_switch_calls: 0,
407                parse_try_calls: 0,
408                parse_attribute_calls: 0,
409                arena_vec_allocations: 0,
410                arena_vec_bytes: 0,
411                arena_alloc_calls: 0,
412                arena_vec_reallocations: 0,
413                arena_vec_wasted_capacity: 0,
414                arena_vec_empty: 0,
415            })
416    }
417    #[cfg(not(feature = "instrument"))]
418    {
419        InstrumentStats {
420            parse_expr_calls: 0,
421            parse_expr_bp_recursive_calls: 0,
422            parse_array_element_calls: 0,
423            parse_array_element_with_arrow: 0,
424            parse_expr_array_first: 0,
425            parse_expr_array_second: 0,
426            parse_array_count: 0,
427            parse_array_element_count: 0,
428            parse_atom_calls: 0,
429            parse_array_simple_values: 0,
430            parse_stmt_calls: 0,
431            parse_function_calls: 0,
432            parse_class_calls: 0,
433            parse_foreach_calls: 0,
434            parse_loop_calls: 0,
435            parse_if_calls: 0,
436            parse_switch_calls: 0,
437            parse_try_calls: 0,
438            parse_attribute_calls: 0,
439            arena_vec_allocations: 0,
440            arena_vec_bytes: 0,
441            arena_alloc_calls: 0,
442            arena_vec_reallocations: 0,
443            arena_vec_wasted_capacity: 0,
444            arena_vec_empty: 0,
445        }
446    }
447}
448
449/// Print a formatted report of statistics
450pub fn report_stats() {
451    #[cfg(feature = "instrument")]
452    {
453        let stats = get_stats();
454        println!("\n╔════════════════════════════════════════════════════════════╗");
455        println!("║      Parser Instrumentation Report (Full Analysis)        ║");
456        println!("╠════════════════════════════════════════════════════════════╣");
457
458        println!("║ STATEMENT PARSING:                                        ║");
459        println!(
460            "║ Total statements:                       {:18} ║",
461            stats.parse_stmt_calls
462        );
463        println!(
464            "║   - Functions:                          {:18} ║",
465            stats.parse_function_calls
466        );
467        println!(
468            "║   - Classes/Traits:                     {:18} ║",
469            stats.parse_class_calls
470        );
471        println!(
472            "║   - If statements:                      {:18} ║",
473            stats.parse_if_calls
474        );
475        println!(
476            "║   - Loops (for/while/do):               {:18} ║",
477            stats.parse_loop_calls
478        );
479        println!(
480            "║   - Foreach:                            {:18} ║",
481            stats.parse_foreach_calls
482        );
483        println!(
484            "║   - Switch:                             {:18} ║",
485            stats.parse_switch_calls
486        );
487        println!(
488            "║   - Try/Catch:                          {:18} ║",
489            stats.parse_try_calls
490        );
491        println!(
492            "║   - Attributes:                         {:18} ║",
493            stats.parse_attribute_calls
494        );
495
496        println!("╠════════════════════════════════════════════════════════════╣");
497        println!("║ ARRAY & EXPRESSION PARSING:                               ║");
498        println!(
499            "║ Arrays Parsed:                          {:18} ║",
500            stats.parse_array_count
501        );
502        println!(
503            "║ Array Elements:                         {:18} ║",
504            stats.parse_array_element_count
505        );
506        println!(
507            "║ Array Elements with =>:                 {:18} ║",
508            stats.parse_array_element_with_arrow
509        );
510
511        let arrow_rate = if stats.parse_array_element_calls > 0 {
512            (stats.parse_array_element_with_arrow as f64 / stats.parse_array_element_calls as f64)
513                * 100.0
514        } else {
515            0.0
516        };
517        println!(
518            "║ => Rate:                                    {:15.1}% ║",
519            arrow_rate
520        );
521
522        println!("╠════════════════════════════════════════════════════════════╣");
523        println!(
524            "║ Total parse_expr calls:                 {:18} ║",
525            stats.parse_expr_calls
526        );
527        println!(
528            "║ parse_expr calls (array, first):        {:18} ║",
529            stats.parse_expr_array_first
530        );
531        println!(
532            "║ parse_expr calls (array, second =>):    {:18} ║",
533            stats.parse_expr_array_second
534        );
535
536        let second_expr_overhead = stats.parse_expr_array_second;
537        let second_expr_pct = if stats.parse_expr_calls > 0 {
538            (second_expr_overhead as f64 / stats.parse_expr_calls as f64) * 100.0
539        } else {
540            0.0
541        };
542        println!(
543            "║ Double-parse overhead (%):                  {:15.1}% ║",
544            second_expr_pct
545        );
546
547        println!("╠════════════════════════════════════════════════════════════╣");
548        println!(
549            "║ parse_atom calls:                       {:18} ║",
550            stats.parse_atom_calls
551        );
552        println!(
553            "║ Simple array values (no operators):     {:18} ║",
554            stats.parse_array_simple_values
555        );
556
557        let simple_pct = if stats.parse_array_element_count > 0 {
558            (stats.parse_array_simple_values as f64 / stats.parse_array_element_count as f64)
559                * 100.0
560        } else {
561            0.0
562        };
563        println!(
564            "║ Simple value rate:                          {:15.1}% ║",
565            simple_pct
566        );
567
568        println!("╚════════════════════════════════════════════════════════════╝\n");
569    }
570    #[cfg(not(feature = "instrument"))]
571    {
572        eprintln!("⚠️  Instrumentation not enabled. Compile with `--features instrument`");
573    }
574}
575
576/// Reset all statistics
577pub fn reset_stats() {
578    #[cfg(feature = "instrument")]
579    {
580        if let Ok(mut stats) = STATS.lock() {
581            stats.parse_expr_calls = 0;
582            stats.parse_expr_bp_recursive_calls = 0;
583            stats.parse_array_element_calls = 0;
584            stats.parse_array_element_with_arrow = 0;
585            stats.parse_expr_array_first = 0;
586            stats.parse_expr_array_second = 0;
587            stats.parse_array_count = 0;
588            stats.parse_array_element_count = 0;
589            stats.parse_atom_calls = 0;
590            stats.parse_array_simple_values = 0;
591            stats.parse_stmt_calls = 0;
592            stats.parse_function_calls = 0;
593            stats.parse_class_calls = 0;
594            stats.parse_foreach_calls = 0;
595            stats.parse_loop_calls = 0;
596            stats.parse_if_calls = 0;
597            stats.parse_switch_calls = 0;
598            stats.parse_try_calls = 0;
599            stats.parse_attribute_calls = 0;
600            stats.arena_vec_allocations = 0;
601            stats.arena_vec_bytes = 0;
602            stats.arena_alloc_calls = 0;
603            stats.arena_vec_reallocations = 0;
604            stats.arena_vec_wasted_capacity = 0;
605            stats.arena_vec_empty = 0;
606        }
607    }
608}