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