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
This commit is contained in:
147
crates/g3-cli/src/metrics.rs
Normal file
147
crates/g3-cli/src/metrics.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user