ai/
debug_output.rs

1use std::collections::HashMap;
2use std::time::{Duration, Instant};
3
4use colored::Colorize;
5use serde_json::Value;
6
7use crate::function_calling::{CommitFunctionArgs, FileChange};
8use crate::multi_step_analysis::{FileAnalysisResult, FileWithScore};
9
10/// Represents an individual file analysis result for the debug output
11#[derive(Clone)]
12pub struct FileAnalysisDebug {
13  pub file_path:    String,
14  pub operation:    String,
15  pub analysis:     FileAnalysisResult,
16  pub api_duration: Duration,
17  pub api_payload:  String
18}
19
20/// Represents a multi-step debug session with detailed information
21#[derive(Clone)]
22pub struct MultiStepDebug {
23  pub file_analyses:          Vec<FileAnalysisDebug>,
24  pub score_result:           Option<Vec<FileWithScore>>,
25  pub score_duration:         Option<Duration>,
26  pub score_payload:          Option<String>,
27  pub generate_result:        Option<Value>,
28  pub generate_duration:      Option<Duration>,
29  pub generate_payload:       Option<String>,
30  pub final_message_duration: Option<Duration>,
31  pub candidates:             Vec<String>,
32  pub reasoning:              Option<String>
33}
34
35/// Tracks timing information for various operations
36pub struct DebugSession {
37  start_time:          Instant,
38  timings:             HashMap<String, Duration>,
39  args:                String,
40  build_type:          String,
41  multi_step_error:    Option<String>,
42  single_step_success: bool,
43  commit_message:      Option<String>,
44  commit_reasoning:    Option<String>,
45  files_analyzed:      Option<CommitFunctionArgs>,
46  total_files_parsed:  usize,
47  api_duration:        Option<Duration>,
48  final_commit_hash:   Option<String>,
49  final_commit_branch: Option<String>,
50  files_changed_count: Option<(usize, usize, usize)>, // (files, insertions, deletions)
51  multi_step_debug:    Option<MultiStepDebug>         // Detailed multi-step debug info
52}
53
54impl DebugSession {
55  pub fn new(args: &str) -> Self {
56    Self {
57      start_time:          Instant::now(),
58      timings:             HashMap::new(),
59      args:                args.to_string(),
60      build_type:          if cfg!(debug_assertions) {
61        "Debug build with performance profiling enabled".to_string()
62      } else {
63        "Release build".to_string()
64      },
65      multi_step_error:    None,
66      single_step_success: false,
67      commit_message:      None,
68      commit_reasoning:    None,
69      files_analyzed:      None,
70      total_files_parsed:  0,
71      api_duration:        None,
72      final_commit_hash:   None,
73      final_commit_branch: None,
74      files_changed_count: None,
75      multi_step_debug:    None
76    }
77  }
78
79  pub fn record_timing(&mut self, operation: &str, duration: Duration) {
80    self.timings.insert(operation.to_string(), duration);
81  }
82
83  pub fn set_multi_step_error(&mut self, error: String) {
84    self.multi_step_error = Some(error);
85  }
86
87  pub fn set_single_step_success(&mut self, success: bool) {
88    self.single_step_success = success;
89  }
90
91  pub fn set_commit_result(&mut self, message: String, reasoning: String) {
92    self.commit_message = Some(message);
93    self.commit_reasoning = Some(reasoning);
94  }
95
96  pub fn set_files_analyzed(&mut self, args: CommitFunctionArgs) {
97    self.files_analyzed = Some(args);
98  }
99
100  pub fn set_total_files_parsed(&mut self, count: usize) {
101    self.total_files_parsed = count;
102  }
103
104  pub fn set_api_duration(&mut self, duration: Duration) {
105    self.api_duration = Some(duration);
106  }
107
108  pub fn init_multi_step_debug(&mut self) {
109    self.multi_step_debug = Some(MultiStepDebug {
110      file_analyses:          Vec::new(),
111      score_result:           None,
112      score_duration:         None,
113      score_payload:          None,
114      generate_result:        None,
115      generate_duration:      None,
116      generate_payload:       None,
117      final_message_duration: None,
118      candidates:             Vec::new(),
119      reasoning:              None
120    });
121  }
122
123  pub fn add_file_analysis_debug(
124    &mut self, file_path: String, operation: String, analysis: FileAnalysisResult, duration: Duration, payload: String
125  ) {
126    if let Some(ref mut multi_step) = self.multi_step_debug {
127      multi_step.file_analyses.push(FileAnalysisDebug {
128        file_path,
129        operation,
130        analysis,
131        api_duration: duration,
132        api_payload: payload
133      });
134    }
135  }
136
137  pub fn set_score_debug(&mut self, result: Vec<FileWithScore>, duration: Duration, payload: String) {
138    if let Some(ref mut multi_step) = self.multi_step_debug {
139      multi_step.score_result = Some(result);
140      multi_step.score_duration = Some(duration);
141      multi_step.score_payload = Some(payload);
142    }
143  }
144
145  pub fn set_generate_debug(&mut self, result: Value, duration: Duration, payload: String) {
146    if let Some(ref mut multi_step) = self.multi_step_debug {
147      // Extract candidates before moving result
148      let mut candidates_vec = Vec::new();
149      if let Some(candidates) = result.get("candidates") {
150        if let Some(candidates_array) = candidates.as_array() {
151          candidates_vec = candidates_array
152            .iter()
153            .filter_map(|v| v.as_str().map(|s| s.to_string()))
154            .collect();
155        }
156      }
157
158      // Extract reasoning before moving result
159      let reasoning_str = result
160        .get("reasoning")
161        .and_then(|r| r.as_str())
162        .map(|s| s.to_string());
163
164      // Now store everything
165      multi_step.generate_result = Some(result);
166      multi_step.generate_duration = Some(duration);
167      multi_step.generate_payload = Some(payload);
168      multi_step.candidates = candidates_vec;
169      multi_step.reasoning = reasoning_str;
170    }
171  }
172
173  pub fn set_final_message_debug(&mut self, duration: Duration) {
174    if let Some(ref mut multi_step) = self.multi_step_debug {
175      multi_step.final_message_duration = Some(duration);
176    }
177  }
178
179  pub fn set_final_commit_info(&mut self, branch: String, hash: String, files: usize, insertions: usize, deletions: usize) {
180    self.final_commit_branch = Some(branch);
181    self.final_commit_hash = Some(hash);
182    self.files_changed_count = Some((files, insertions, deletions));
183  }
184
185  pub fn print_debug_output(&self) {
186    eprintln!("\n{}", "=== GIT AI HOOK DEBUG SESSION ===".bright_cyan().bold());
187
188    // Initialization
189    eprintln!("\n{} {}", "📋".bright_yellow(), "INITIALIZATION".bright_white().bold());
190    eprintln!("  {}        {}", "Args:".bright_white(), self.args);
191    eprintln!("  {}       {}", "Build:".bright_white(), self.build_type);
192
193    // Setup & Preparation
194    eprintln!("\n{} {}", "⚙️ ".bright_yellow(), "SETUP & PREPARATION".bright_white().bold());
195    self.print_timing_line("Generate instruction template", "Generate instruction template", false);
196    self.print_timing_line("Count tokens", "Count tokens", false);
197    self.print_timing_line("Calculate instruction tokens", "Calculate instruction tokens", false);
198    self.print_timing_line("Get context size", "Get context size", true);
199
200    // Git Diff Processing
201    eprintln!("\n{} {}", "📝".bright_yellow(), "GIT DIFF PROCESSING".bright_white().bold());
202    self.print_timing_line("Git diff generation", "Git diff generation", false);
203    self.print_timing_line("Processing diff changes", "Processing diff changes", false);
204    self.print_timing_line("Repository patch generation", "Repository patch generation", false);
205
206    let files_status = if self.total_files_parsed == 0 {
207      format!("{} files   {}", self.total_files_parsed, "⚠️".yellow())
208    } else {
209      format!("{} files   ✓", self.total_files_parsed)
210        .green()
211        .to_string()
212    };
213    eprintln!("  └ Files parsed from diff           {files_status}");
214
215    // Discovered Files
216    if self.total_files_parsed > 0 {
217      eprintln!("\n{} {}", "🔍".bright_yellow(), "DISCOVERED FILES".bright_white().bold());
218
219      if let Some(ref multi_step) = self.multi_step_debug {
220        for (files_shown, file) in multi_step.file_analyses.iter().enumerate() {
221          let change_type = match file.operation.as_str() {
222            "added" => "[added]".green(),
223            "deleted" => "[deleted]".red(),
224            "modified" => "[modified]".yellow(),
225            "renamed" => "[renamed]".blue(),
226            _ => format!("[{}]", file.operation).normal()
227          };
228
229          let lines_info = format!("{} lines", file.analysis.lines_added + file.analysis.lines_removed);
230          let prefix = if files_shown == multi_step.file_analyses.len() - 1 {
231            "└"
232          } else {
233            "│"
234          };
235          eprintln!("  {} {:<30} {:<12} {}", prefix, file.file_path.bright_cyan(), change_type, lines_info);
236        }
237      } else if let Some(ref files) = self.files_analyzed {
238        let mut file_list: Vec<(&String, &FileChange)> = files.files.iter().collect();
239        file_list.sort_by(|a, b| {
240          b.1
241            .impact_score
242            .partial_cmp(&a.1.impact_score)
243            .unwrap_or(std::cmp::Ordering::Equal)
244        });
245
246        let total_files = file_list.len();
247        for (files_shown, (path, change)) in file_list.iter().enumerate() {
248          let change_type = match change.change_type.as_str() {
249            "added" => "[added]".green(),
250            "deleted" => "[deleted]".red(),
251            "modified" => "[modified]".yellow(),
252            "renamed" => "[renamed]".blue(),
253            _ => format!("[{}]", change.change_type).normal()
254          };
255
256          let prefix = if files_shown == total_files - 1 {
257            "└"
258          } else {
259            "│"
260          };
261          eprintln!(
262            "  {} {:<30} {:<12} {} lines",
263            prefix,
264            path.bright_cyan(),
265            change_type,
266            change.lines_changed
267          );
268        }
269      }
270    }
271
272    // AI Processing
273    eprintln!("\n{} {}", "🤖".bright_yellow(), "AI PROCESSING".bright_white().bold());
274
275    if let Some(ref multi_step) = self.multi_step_debug {
276      eprintln!(
277        "\n  {} {}",
278        "📋".bright_yellow(),
279        "STEP 1: INDIVIDUAL FILE ANALYSIS".bright_white().bold()
280      );
281
282      for (i, file) in multi_step.file_analyses.iter().enumerate() {
283        let file_num = i + 1;
284        let total_files = multi_step.file_analyses.len();
285
286        eprintln!("    ");
287        eprintln!("    🔸 File {}/{}: {}", file_num, total_files, file.file_path.bright_cyan());
288        eprintln!("      │ OpenAI Request [analyze]:");
289        eprintln!(
290          "      │   └ Payload: {{\"file_path\": \"{}\", \"operation_type\": \"{}\", \"diff_content\": \"...\"}}",
291          file.file_path, file.operation
292        );
293        eprintln!(
294          "      │ API Response Time:              {:<7}    ✓",
295          format!("{:.2}s", file.api_duration.as_secs_f32())
296        );
297        eprintln!("      │ Results:");
298        eprintln!("      │   ├ Lines Added:                {}", file.analysis.lines_added);
299        eprintln!("      │   ├ Lines Removed:              {}", file.analysis.lines_removed);
300        eprintln!("      │   ├ File Category:              {}", file.analysis.file_category);
301        eprintln!("      │   └ Summary:                    {}", file.analysis.summary);
302      }
303
304      eprintln!(
305        "\n  {} {}",
306        "📊".bright_yellow(),
307        "STEP 2: IMPACT SCORE CALCULATION".bright_white().bold()
308      );
309
310      if let Some(ref score_result) = multi_step.score_result {
311        if let Some(score_duration) = multi_step.score_duration {
312          eprintln!("    │ OpenAI Request [score]:");
313          eprintln!(
314            "    │   └ Payload: {{\"files_data\": [{{\"{}\", ...}}, ...]}}",
315            if !multi_step.file_analyses.is_empty() {
316              &multi_step.file_analyses[0].file_path
317            } else {
318              "no files"
319            }
320          );
321          eprintln!(
322            "    │ API Response Time:              {:<7}    ✓",
323            format!("{:.2}s", score_duration.as_secs_f32())
324          );
325          eprintln!("    │ Results:");
326
327          let mut sorted_files = score_result.clone();
328          sorted_files.sort_by(|a, b| {
329            b.impact_score
330              .partial_cmp(&a.impact_score)
331              .unwrap_or(std::cmp::Ordering::Equal)
332          });
333
334          for (i, file) in sorted_files.iter().enumerate() {
335            let prefix = if i == sorted_files.len() - 1 {
336              "└"
337            } else {
338              "├"
339            };
340            eprintln!(
341              "    │   {} {:<30} Impact Score {:.2} {}",
342              prefix,
343              file.file_path,
344              file.impact_score,
345              if i == 0 {
346                "(highest)".bright_green()
347              } else {
348                "".normal()
349              }
350            );
351          }
352        }
353      }
354
355      eprintln!(
356        "\n  {} {}",
357        "💭".bright_yellow(),
358        "STEP 3: COMMIT MESSAGE GENERATION".bright_white().bold()
359      );
360
361      if let Some(generate_duration) = multi_step.generate_duration {
362        eprintln!("    │ OpenAI Request [generate]:");
363        eprintln!("    │   └ Payload: {{\"files_with_scores\": [...], \"max_length\": 72}}");
364        eprintln!(
365          "    │ API Response Time:              {:<7}    ✓",
366          format!("{:.2}s", generate_duration.as_secs_f32())
367        );
368
369        if !multi_step.candidates.is_empty() {
370          eprintln!("    │ Candidates Generated:");
371
372          for (i, candidate) in multi_step.candidates.iter().enumerate() {
373            let prefix = if i == multi_step.candidates.len() - 1 {
374              "└"
375            } else {
376              "├"
377            };
378            eprintln!("    │   {} \"{}\"", prefix, candidate.bright_cyan());
379          }
380
381          if let Some(ref reasoning) = multi_step.reasoning {
382            eprintln!("    │ Reasoning: {reasoning}");
383          }
384        }
385      }
386    } else {
387      // Multi-Step Attempt
388      let multi_step_status = if self.multi_step_error.is_some() {
389        "FAILED".red().to_string()
390      } else if self.single_step_success {
391        "SKIPPED".yellow().to_string()
392      } else {
393        "SUCCESS".green().to_string()
394      };
395      eprintln!("  Multi-Step Attempt:                           {multi_step_status}");
396
397      if let Some(ref error) = self.multi_step_error {
398        eprintln!("    │ Creating score function tool              ✓");
399        eprintln!("    │ OpenAI connection                         ✓");
400        eprintln!(
401          "    └ Error: {}             {} {}",
402          error.trim_end_matches('.'),
403          "✗".red(),
404          error.split(':').next_back().unwrap_or("").trim()
405        );
406      }
407
408      // Single-Step Fallback
409      if self.single_step_success {
410        eprintln!("\n  Single-Step Fallback:                        {}", "SUCCESS".green());
411        eprintln!("    │ Creating commit function tool             ✓ max_length=72");
412        if let Some(duration) = self.api_duration {
413          eprintln!(
414            "    │ OpenAI API call                   {:<7} ✓",
415            format!("{:.2}s", duration.as_secs_f32())
416          );
417        }
418        eprintln!("    └ Response parsing                          ✓");
419      }
420    }
421
422    // Analysis Results
423    if let Some(ref message) = self.commit_message {
424      eprintln!("\n{} {}", "📊".bright_yellow(), "ANALYSIS RESULTS".bright_white().bold());
425      eprintln!("  Selected Message: '{}'", message.bright_cyan());
426      eprintln!("  Message Length:   {} characters (within 72 limit)", message.len());
427
428      if let Some(ref reasoning) = self.commit_reasoning {
429        eprintln!("\n  Final Reasoning:");
430        // Word wrap the reasoning at ~70 characters
431        let words: Vec<&str> = reasoning.split_whitespace().collect();
432        let mut line = String::new();
433        for word in words {
434          if line.len() + word.len() + 1 > 70 {
435            eprintln!("    {line}");
436            line = word.to_string();
437          } else {
438            if !line.is_empty() {
439              line.push(' ');
440            }
441            line.push_str(word);
442          }
443        }
444        if !line.is_empty() {
445          eprintln!("    {line}");
446        }
447      }
448    }
449
450    // Detailed File Analysis
451    if let Some(ref files) = self.files_analyzed {
452      eprintln!("\n{} {}", "📁".bright_yellow(), "DETAILED FILE ANALYSIS".bright_white().bold());
453      eprintln!("  Total Files: {}", files.files.len());
454
455      // Sort files by impact score
456      let mut sorted_files: Vec<(&String, &FileChange)> = files.files.iter().collect();
457      sorted_files.sort_by(|a, b| {
458        b.1
459          .impact_score
460          .partial_cmp(&a.1.impact_score)
461          .unwrap_or(std::cmp::Ordering::Equal)
462      });
463
464      for (path, change) in sorted_files.iter() {
465        eprintln!();
466        eprintln!("  🔸 {}", path.bright_cyan());
467        eprintln!("    │ Summary:      {}", change.summary);
468        eprintln!(
469          "    │ Impact Score: {:.2} {}",
470          change.impact_score,
471          if change.impact_score >= 0.9 {
472            "(highest - drives commit message)".bright_green()
473          } else if change.impact_score >= 0.8 {
474            "(high - mentioned in commit)".bright_yellow()
475          } else if change.impact_score >= 0.5 {
476            "(medium - supporting change)".normal()
477          } else {
478            "(low)".normal()
479          }
480        );
481
482        // Not using this variable directly, but keeping the match logic for clarity
483        let _change_type_str = match change.change_type.as_str() {
484          "added" => "added",
485          "modified" => "modified",
486          "deleted" => "deleted",
487          "renamed" => "renamed",
488          _ => &change.change_type
489        };
490
491        eprintln!(
492          "    │ Lines:        +{}, -{} ({} total)",
493          change.lines_changed / 2, // Approximation for display
494          change.lines_changed / 2,
495          change.lines_changed
496        );
497        eprintln!("    │ Category:     {}", change.file_category);
498        eprintln!(
499          "    │ Significance: {}",
500          if change.impact_score >= 0.9 {
501            "Core functionality"
502          } else if change.impact_score >= 0.8 {
503            "Supporting infrastructure"
504          } else if change.impact_score >= 0.5 {
505            "Minor improvement"
506          } else {
507            "Peripheral change"
508          }
509        );
510
511        let weight_str = if change.impact_score >= 0.9 {
512          "Primary focus for commit message"
513        } else if change.impact_score >= 0.8 {
514          "Secondary mention in commit"
515        } else if change.impact_score >= 0.6 {
516          "Implicit support (not explicitly mentioned)"
517        } else {
518          "Not reflected in commit message"
519        };
520
521        eprintln!("    └ Weight:       {weight_str}");
522      }
523    }
524
525    // Statistics Summary
526    if let Some(ref files) = self.files_analyzed {
527      eprintln!("\n{} {}", "📈".bright_yellow(), "STATISTICS SUMMARY".bright_white().bold());
528
529      let total_lines: u32 = files.files.values().map(|f| f.lines_changed).sum();
530      let avg_impact: f32 = if files.files.is_empty() {
531        0.0
532      } else {
533        files.files.values().map(|f| f.impact_score).sum::<f32>() / files.files.len() as f32
534      };
535
536      eprintln!("  │ Total Lines Changed:     {total_lines}");
537      eprintln!("  │ Average Impact Score:    {avg_impact:.2}");
538      eprintln!("  │");
539
540      // Count by category
541      let mut category_counts: HashMap<&str, usize> = HashMap::new();
542      for change in files.files.values() {
543        *category_counts.entry(&change.file_category).or_insert(0) += 1;
544      }
545
546      eprintln!("  │ By Category:");
547      for (category, count) in category_counts {
548        eprintln!("  │   └ {category}: {count}");
549      }
550
551      eprintln!("  │");
552
553      // Count by change type
554      let mut type_counts: HashMap<&str, usize> = HashMap::new();
555      for change in files.files.values() {
556        *type_counts.entry(&change.change_type).or_insert(0) += 1;
557      }
558
559      eprintln!("  │ By Change Type:");
560      for (change_type, count) in type_counts {
561        eprintln!("  │   └ {change_type}: {count}");
562      }
563    }
564
565    // Performance Summary
566    eprintln!("\n{} {}", "⏱️ ".bright_yellow(), "PERFORMANCE SUMMARY".bright_white().bold());
567
568    if let Some(ref multi_step) = self.multi_step_debug {
569      let mut total_file_analysis = Duration::default();
570      for file in &multi_step.file_analyses {
571        total_file_analysis += file.api_duration;
572      }
573
574      eprintln!(
575        "  │ Individual file analysis:         {:.2}s ({} files)",
576        total_file_analysis.as_secs_f32(),
577        multi_step.file_analyses.len()
578      );
579
580      if let Some(score_duration) = multi_step.score_duration {
581        eprintln!("  │ Impact score calculation:         {:.2}s", score_duration.as_secs_f32());
582      }
583
584      if let Some(generate_duration) = multi_step.generate_duration {
585        eprintln!("  │ Commit message generation:        {:.2}s", generate_duration.as_secs_f32());
586      }
587
588      eprintln!("  │ ─────────────────────────────────────────");
589
590      let total_ai_processing = total_file_analysis
591        + multi_step.score_duration.unwrap_or_default()
592        + multi_step.generate_duration.unwrap_or_default()
593        + multi_step.final_message_duration.unwrap_or_default();
594
595      eprintln!("  │ Total AI processing:              {:.2}s", total_ai_processing.as_secs_f32());
596    } else if let Some(duration) = self.api_duration {
597      eprintln!("  │ OpenAI request/response:          {:.2}s", duration.as_secs_f32());
598    }
599
600    let total_duration = self.start_time.elapsed();
601    eprintln!("  │ Total execution time:             {:.2}s", total_duration.as_secs_f32());
602    eprintln!("  └ Status:                           {} ✓", "SUCCESS".green());
603
604    // Final Result
605    if let (Some(ref branch), Some(ref hash), Some(ref message)) =
606      (&self.final_commit_branch, &self.final_commit_hash, &self.commit_message)
607    {
608      eprintln!("\n{} {}", "🎯".bright_yellow(), "FINAL RESULT".bright_white().bold());
609
610      let short_hash = if hash.len() > 7 {
611        &hash[..7]
612      } else {
613        hash
614      };
615      eprintln!("  [{} {}] {}", branch.bright_green(), short_hash.bright_yellow(), message.bright_cyan());
616
617      if let Some((files, insertions, deletions)) = self.files_changed_count {
618        let files_text = if files == 1 {
619          "file"
620        } else {
621          "files"
622        };
623        let insertions_text = if insertions == 1 {
624          "insertion"
625        } else {
626          "insertions"
627        };
628        let deletions_text = if deletions == 1 {
629          "deletion"
630        } else {
631          "deletions"
632        };
633
634        eprintln!(
635          "   {} {} changed, {} {}(+), {} {}(-)",
636          files,
637          files_text,
638          insertions.to_string().green(),
639          insertions_text,
640          deletions.to_string().red(),
641          deletions_text
642        );
643      }
644    }
645  }
646
647  fn print_timing_line(&self, key: &str, label: &str, last: bool) {
648    let prefix = if last {
649      "└"
650    } else {
651      "│"
652    };
653
654    if let Some(duration) = self.timings.get(key) {
655      let duration_str = format_duration(*duration);
656      eprintln!("  {prefix} {label:<35} {duration_str:<10} ✓");
657    } else {
658      eprintln!("  {} {:<35} {:<10} ✓", prefix, label, "0.00ms");
659    }
660  }
661}
662
663fn format_duration(duration: Duration) -> String {
664  let micros = duration.as_micros();
665  if micros < 1000 {
666    format!("{micros:.0}µs")
667  } else if micros < 1_000_000 {
668    format!("{:.2}ms", duration.as_secs_f32() * 1000.0)
669  } else {
670    format!("{:.2}s", duration.as_secs_f32())
671  }
672}
673
674/// Global debug session instance
675pub static mut DEBUG_SESSION: Option<DebugSession> = None;
676
677/// Initialize the debug session
678pub fn init_debug_session(args: &str) {
679  unsafe {
680    DEBUG_SESSION = Some(DebugSession::new(args));
681  }
682}
683
684/// Get a mutable reference to the debug session
685#[allow(static_mut_refs)]
686pub fn debug_session() -> Option<&'static mut DebugSession> {
687  unsafe { DEBUG_SESSION.as_mut() }
688}
689
690/// Print the final debug output
691pub fn print_final_output() {
692  if let Some(session) = debug_session() {
693    session.print_debug_output();
694  }
695}
696
697/// Record a timing for an operation
698pub fn record_timing(operation: &str, duration: Duration) {
699  if let Some(session) = debug_session() {
700    session.record_timing(operation, duration);
701  }
702}