Files
g3/crates/g3-cli/src/metrics.rs
Dhanji R. Prasanna cf3727f50d refactor(g3-cli): Extract focused modules from lib.rs for improved readability
Extract three cohesive modules from the monolithic lib.rs (3188 -> 2785 lines):

- metrics.rs (147 lines): Turn metrics tracking and histogram generation
  - TurnMetrics struct
  - format_elapsed_time() for human-readable durations
  - generate_turn_histogram() for performance visualization
  - Added unit tests for core functions

- project_files.rs (181 lines): Project file reading utilities
  - read_agents_config() for AGENTS.md loading
  - read_project_readme() for README detection
  - read_project_memory() for .g3/memory.md
  - extract_readme_heading() for display
  - Added unit tests

- coach_feedback.rs (129 lines): Coach feedback extraction from session logs
  - extract_from_logs() main entry point
  - Helper functions for log parsing and text extraction

All modules have clear single responsibilities, improved documentation,
and maintain identical behavior to the original inline functions.

Agent: carmack
2026-01-11 16:41:41 +05:30

148 lines
5.3 KiB
Rust

//! Turn metrics and histogram generation for performance visualization.
use std::time::Duration;
/// Metrics captured for a single turn of interaction.
#[derive(Debug, Clone)]
pub struct TurnMetrics {
pub turn_number: usize,
pub tokens_used: u32,
pub wall_clock_time: Duration,
}
/// Format a Duration as human-readable elapsed time (e.g., "1h 23m 45s").
pub fn format_elapsed_time(duration: Duration) -> String {
let total_secs = duration.as_secs();
let hours = total_secs / 3600;
let minutes = (total_secs % 3600) / 60;
let seconds = total_secs % 60;
match (hours, minutes, seconds) {
(h, m, s) if h > 0 => format!("{}h {}m {}s", h, m, s),
(_, m, s) if m > 0 => format!("{}m {}s", m, s),
(_, _, s) if s > 0 => format!("{}s", s),
_ => format!("{}ms", duration.as_millis()),
}
}
/// Generate a histogram showing tokens used and wall clock time per turn.
pub fn generate_turn_histogram(turn_metrics: &[TurnMetrics]) -> String {
if turn_metrics.is_empty() {
return " No turn data available".to_string();
}
const MAX_BAR_WIDTH: usize = 40;
const TOKEN_CHAR: char = '█';
const TIME_CHAR: char = '▓';
let max_tokens = turn_metrics.iter().map(|t| t.tokens_used).max().unwrap_or(1);
let max_time_ms = turn_metrics
.iter()
.map(|t| t.wall_clock_time.as_millis().min(u32::MAX as u128) as u32)
.max()
.unwrap_or(1);
let mut histogram = String::new();
histogram.push_str("\n📊 Per-Turn Performance Histogram:\n");
histogram.push_str(&format!(" {} = Tokens Used (max: {})\n", TOKEN_CHAR, max_tokens));
histogram.push_str(&format!(
" {} = Wall Clock Time (max: {:.1}s)\n\n",
TIME_CHAR,
max_time_ms as f64 / 1000.0
));
for metrics in turn_metrics {
let turn_time_ms = metrics.wall_clock_time.as_millis().min(u32::MAX as u128) as u32;
let token_bar_len = scale_bar(metrics.tokens_used, max_tokens, MAX_BAR_WIDTH);
let time_bar_len = scale_bar(turn_time_ms, max_time_ms, MAX_BAR_WIDTH);
let time_str = format_duration_ms(turn_time_ms);
let token_bar = TOKEN_CHAR.to_string().repeat(token_bar_len);
let time_bar = TIME_CHAR.to_string().repeat(time_bar_len);
histogram.push_str(&format!(
" Turn {:2}: {:>6} tokens │{:<40}\n",
metrics.turn_number, metrics.tokens_used, token_bar
));
histogram.push_str(&format!(" {:>6}{:<40}\n", time_str, time_bar));
// Separator between turns (except for last)
if metrics.turn_number != turn_metrics.last().unwrap().turn_number {
histogram.push_str(
" ────────────┼────────────────────────────────────────┤\n",
);
}
}
append_summary_statistics(&mut histogram, turn_metrics);
histogram
}
/// Scale a value to a bar length proportional to max.
fn scale_bar(value: u32, max: u32, max_width: usize) -> usize {
if max == 0 {
0
} else {
((value as f64 / max as f64) * max_width as f64) as usize
}
}
/// Format milliseconds as a human-readable duration string.
fn format_duration_ms(ms: u32) -> String {
match ms {
ms if ms < 1000 => format!("{}ms", ms),
ms if ms < 60_000 => format!("{:.1}s", ms as f64 / 1000.0),
ms => {
let minutes = ms / 60_000;
let seconds = (ms % 60_000) as f64 / 1000.0;
format!("{}m{:.1}s", minutes, seconds)
}
}
}
/// Append summary statistics to the histogram output.
fn append_summary_statistics(histogram: &mut String, turn_metrics: &[TurnMetrics]) {
let total_tokens: u32 = turn_metrics.iter().map(|t| t.tokens_used).sum();
let total_time: Duration = turn_metrics.iter().map(|t| t.wall_clock_time).sum();
let avg_tokens = total_tokens as f64 / turn_metrics.len() as f64;
let avg_time_ms = total_time.as_millis() as f64 / turn_metrics.len() as f64;
histogram.push_str("\n📈 Summary Statistics:\n");
histogram.push_str(&format!(
" • Total Tokens: {} across {} turns\n",
total_tokens,
turn_metrics.len()
));
histogram.push_str(&format!(" • Average Tokens/Turn: {:.1}\n", avg_tokens));
histogram.push_str(&format!(" • Total Time: {:.1}s\n", total_time.as_secs_f64()));
histogram.push_str(&format!(" • Average Time/Turn: {:.1}s\n", avg_time_ms / 1000.0));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_elapsed_time() {
assert_eq!(format_elapsed_time(Duration::from_millis(500)), "500ms");
assert_eq!(format_elapsed_time(Duration::from_secs(45)), "45s");
assert_eq!(format_elapsed_time(Duration::from_secs(90)), "1m 30s");
assert_eq!(format_elapsed_time(Duration::from_secs(3661)), "1h 1m 1s");
}
#[test]
fn test_empty_histogram() {
let result = generate_turn_histogram(&[]);
assert!(result.contains("No turn data available"));
}
#[test]
fn test_scale_bar() {
assert_eq!(scale_bar(50, 100, 40), 20);
assert_eq!(scale_bar(100, 100, 40), 40);
assert_eq!(scale_bar(0, 100, 40), 0);
assert_eq!(scale_bar(50, 0, 40), 0);
}
}