diff --git a/internal/integrations/agentsmd.go b/internal/integrations/agentsmd.go index 5af031b..6103076 100644 --- a/internal/integrations/agentsmd.go +++ b/internal/integrations/agentsmd.go @@ -11,6 +11,11 @@ import ( "github.com/marcus/nightshift/internal/config" ) +var ( + agentsMDHeaderRE = regexp.MustCompile(`^#+\s*(.+)`) + agentsMDBulletRE = regexp.MustCompile(`^[-*]\s+(.+)`) +) + // AgentsMDReader reads agents.md files for agent behavior configuration. type AgentsMDReader struct { enabled bool @@ -106,8 +111,8 @@ func parseAgentsMD(content string) agentsMDParsed { "constraint": "safety", } - headerRE := regexp.MustCompile(`^#+\s*(.+)`) - bulletRE := regexp.MustCompile(`^[-*]\s+(.+)`) + headerRE := agentsMDHeaderRE + bulletRE := agentsMDBulletRE for scanner.Scan() { line := scanner.Text() diff --git a/internal/integrations/bench_test.go b/internal/integrations/bench_test.go new file mode 100644 index 0000000..bc0dd57 --- /dev/null +++ b/internal/integrations/bench_test.go @@ -0,0 +1,48 @@ +package integrations + +import "testing" + +var sampleClaudeMD = `# Project + +## Conventions +- Use Go standard library where possible +- Run tests before committing +- Keep functions under 50 lines + +## Tasks +- Refactor database layer +- Add integration tests + +## Constraints +- No external HTTP calls in unit tests +- Must support Go 1.21+ +` + +var sampleAgentsMD = `# Agent Configuration + +## Allowed Actions +- Read any file in the repository +- Run go test and go build +- Create git branches + +## Forbidden Actions +- Push to main directly +- Delete production databases +- Modify CI/CD pipelines without review + +## Tool Restrictions +- No shell access outside project directory +- File writes limited to src/ and test/ +` + +func BenchmarkParseClaudeMD(b *testing.B) { + for b.Loop() { + _ = parseClaudeMD(sampleClaudeMD) + } +} + +func BenchmarkParseAgentsMD(b *testing.B) { + for b.Loop() { + _ = parseAgentsMD(sampleAgentsMD) + } +} diff --git a/internal/integrations/claudemd.go b/internal/integrations/claudemd.go index 44b5ec6..1622c36 100644 --- a/internal/integrations/claudemd.go +++ b/internal/integrations/claudemd.go @@ -11,6 +11,11 @@ import ( "github.com/marcus/nightshift/internal/config" ) +var ( + claudeMDHeaderRE = regexp.MustCompile(`^#+\s*(.+)`) + claudeMDBulletRE = regexp.MustCompile(`^[-*]\s+(.+)`) +) + // ClaudeMDReader reads claude.md files for project context. type ClaudeMDReader struct { enabled bool @@ -95,9 +100,9 @@ func parseClaudeMD(content string) claudeMDParsed { "safety": HintConstraint, } - // Patterns for extracting hints - headerRE := regexp.MustCompile(`^#+\s*(.+)`) - bulletRE := regexp.MustCompile(`^[-*]\s+(.+)`) + // Patterns for extracting hints (compiled at package level) + headerRE := claudeMDHeaderRE + bulletRE := claudeMDBulletRE for scanner.Scan() { line := scanner.Text() diff --git a/internal/reporting/run_report.go b/internal/reporting/run_report.go index 88f76a8..764c540 100644 --- a/internal/reporting/run_report.go +++ b/internal/reporting/run_report.go @@ -21,7 +21,10 @@ func RenderRunReport(results *RunResults, logPath string) (string, error) { return "", fmt.Errorf("results cannot be nil") } - var completed, failed, skipped []TaskResult + n := len(results.Tasks) + completed := make([]TaskResult, 0, n) + failed := make([]TaskResult, 0, n) + skipped := make([]TaskResult, 0, n) for _, task := range results.Tasks { switch task.Status { case "completed": diff --git a/internal/stats/stats.go b/internal/stats/stats.go index 0c93fe9..d6ec877 100644 --- a/internal/stats/stats.go +++ b/internal/stats/stats.go @@ -188,7 +188,7 @@ func (s *Stats) loadReports() []*reporting.RunResults { return nil } - var results []*reporting.RunResults + results := make([]*reporting.RunResults, 0, len(entries)) for _, entry := range entries { if entry.IsDir() { continue diff --git a/internal/tmux/scraper.go b/internal/tmux/scraper.go index 55f4d31..73f4894 100644 --- a/internal/tmux/scraper.go +++ b/internal/tmux/scraper.go @@ -194,16 +194,27 @@ func ScrapeCodexUsage(ctx context.Context) (UsageResult, error) { var claudeWeekRegex = regexp.MustCompile(`(?i)current\s+week`) var codexWeekRegex = regexp.MustCompile(`(?i)weekly\s+limit`) +// Pre-compiled regexes for parse functions (avoid recompiling on every call). +var ( + claudeWeeklyAllModelsPctRe = regexp.MustCompile(`(?is)current\s+week\s*\(all\s+models\).*?(\d{1,3}(?:\.\d+)?)%`) + claudeWeeklyFallbackPctRe = regexp.MustCompile(`(?is)current\s+week.*?(\d{1,3}(?:\.\d+)?)%`) + codexWeeklyPctRe = regexp.MustCompile(`(?i)weekly\s+limit[^\n]*?(\d{1,3}(?:\.\d+)?)%\s*(left|used)?`) + claudeSessionResetRe = regexp.MustCompile(`(?is)current\s+session.*?resets\s+(.+?)(?:\n|$)`) + claudeWeeklyResetRe = regexp.MustCompile(`(?is)current\s+week\s*\(all\s+models\).*?resets\s+(.+?)(?:\n|$)`) + codexSessionResetRe = regexp.MustCompile(`(?i)5h\s+limit[^\n]*\(resets\s+(\d{1,2}:\d{2}(?:\s+on\s+\d{1,2}\s+\w+)?)\)`) + codexWeeklyResetRe = regexp.MustCompile(`(?i)weekly\s+limit[^\n]*\(resets\s+(\d{1,2}:\d{2}\s+on\s+\d{1,2}\s+\w+)\)`) + codexResetFallbackRe = regexp.MustCompile(`\(resets\s+(\d{1,2}:\d{2}\s+on\s+\d{1,2}\s+\w+)\)`) +) + func parseClaudeWeeklyPct(output string) (float64, error) { output = StripANSI(output) // Match "Current week" followed by a percentage, possibly on the next line. // The (?s) flag makes . match newlines so the pattern crosses lines. - re := regexp.MustCompile(`(?is)current\s+week\s*\(all\s+models\).*?(\d{1,3}(?:\.\d+)?)%`) - if match := re.FindStringSubmatch(output); len(match) == 2 { + if match := claudeWeeklyAllModelsPctRe.FindStringSubmatch(output); len(match) == 2 { return parsePct(match[1]) } // Fallback: any "Current week" header followed by a percentage - re2 := regexp.MustCompile(`(?is)current\s+week.*?(\d{1,3}(?:\.\d+)?)%`) + re2 := claudeWeeklyFallbackPctRe if match := re2.FindStringSubmatch(output); len(match) == 2 { return parsePct(match[1]) } @@ -213,8 +224,7 @@ func parseClaudeWeeklyPct(output string) (float64, error) { func parseCodexWeeklyPct(output string) (float64, error) { output = StripANSI(output) // Codex /status shows "77% left" -- extract the number and qualifier. - re := regexp.MustCompile(`(?i)weekly\s+limit[^\n]*?(\d{1,3}(?:\.\d+)?)%\s*(left|used)?`) - if match := re.FindStringSubmatch(output); len(match) >= 2 { + if match := codexWeeklyPctRe.FindStringSubmatch(output); len(match) >= 2 { pct, err := parsePct(match[1]) if err != nil { return 0, err @@ -297,15 +307,13 @@ func parseClaudeResetTimes(output string) (sessionReset, weeklyReset string) { // Session reset: appears after "Current session" and before "Current week". // Format: "Resets 9pm (America/Los_Angeles)" or "Resets 8:59pm (America/Los_Angeles)" - sessionRe := regexp.MustCompile(`(?is)current\s+session.*?resets\s+(.+?)(?:\n|$)`) - if m := sessionRe.FindStringSubmatch(output); len(m) == 2 { + if m := claudeSessionResetRe.FindStringSubmatch(output); len(m) == 2 { sessionReset = strings.TrimSpace(m[1]) } // Weekly reset: appears after "Current week (all models)". // Format: "Resets Feb 8 at 10am (America/Los_Angeles)" or "Resets Feb 8 at 9:59am (America/Los_Angeles)" - weeklyRe := regexp.MustCompile(`(?is)current\s+week\s*\(all\s+models\).*?resets\s+(.+?)(?:\n|$)`) - if m := weeklyRe.FindStringSubmatch(output); len(m) == 2 { + if m := claudeWeeklyResetRe.FindStringSubmatch(output); len(m) == 2 { weeklyReset = strings.TrimSpace(m[1]) } @@ -323,14 +331,12 @@ func parseCodexResetTimes(output string) (sessionReset, weeklyReset string) { output = StripANSI(output) // Session (5h) reset: "(resets HH:MM)" or "(resets HH:MM on D Mon)" - sessionRe := regexp.MustCompile(`(?i)5h\s+limit[^\n]*\(resets\s+(\d{1,2}:\d{2}(?:\s+on\s+\d{1,2}\s+\w+)?)\)`) - if m := sessionRe.FindStringSubmatch(output); len(m) == 2 { + if m := codexSessionResetRe.FindStringSubmatch(output); len(m) == 2 { sessionReset = m[1] } // Weekly reset: "(resets HH:MM on D Mon)" - weeklyRe := regexp.MustCompile(`(?i)weekly\s+limit[^\n]*\(resets\s+(\d{1,2}:\d{2}\s+on\s+\d{1,2}\s+\w+)\)`) - if m := weeklyRe.FindStringSubmatch(output); len(m) == 2 { + if m := codexWeeklyResetRe.FindStringSubmatch(output); len(m) == 2 { weeklyReset = m[1] } @@ -339,8 +345,7 @@ func parseCodexResetTimes(output string) (sessionReset, weeklyReset string) { // Only use the fallback when we find a match distinct from the session reset // (avoids misidentifying the 5h line as weekly when it's the only line). if weeklyReset == "" { - fallbackRe := regexp.MustCompile(`\(resets\s+(\d{1,2}:\d{2}\s+on\s+\d{1,2}\s+\w+)\)`) - matches := fallbackRe.FindAllStringSubmatch(output, -1) + matches := codexResetFallbackRe.FindAllStringSubmatch(output, -1) if len(matches) > 0 { candidate := matches[len(matches)-1][1] if candidate != sessionReset { diff --git a/internal/tmux/scraper_bench_test.go b/internal/tmux/scraper_bench_test.go new file mode 100644 index 0000000..8a8e8a8 --- /dev/null +++ b/internal/tmux/scraper_bench_test.go @@ -0,0 +1,44 @@ +package tmux + +import "testing" + +// Representative Claude /usage output for benchmarks. +var claudeUsageOutput = ` +Current session + ████████████████████░░░░░░░░ 0% used + Resets 9pm (America/Los_Angeles) + +Current week (all models) + ████████████████████░░░░░░░░ 59% used + Resets Feb 8 at 10am (America/Los_Angeles) +` + +// Representative Codex /status output for benchmarks. +var codexStatusOutput = ` +5h limit: [████████████████████░░░░] 100% left (resets 02:50 on 8 Feb) +Weekly limit: [██████░░░░░░░░░░░░░░░░░░] 77% left (resets 20:08 on 9 Feb) +` + +func BenchmarkParseClaudeWeeklyPct(b *testing.B) { + for b.Loop() { + _, _ = parseClaudeWeeklyPct(claudeUsageOutput) + } +} + +func BenchmarkParseCodexWeeklyPct(b *testing.B) { + for b.Loop() { + _, _ = parseCodexWeeklyPct(codexStatusOutput) + } +} + +func BenchmarkParseClaudeResetTimes(b *testing.B) { + for b.Loop() { + _, _ = parseClaudeResetTimes(claudeUsageOutput) + } +} + +func BenchmarkParseCodexResetTimes(b *testing.B) { + for b.Loop() { + _, _ = parseCodexResetTimes(codexStatusOutput) + } +} diff --git a/internal/trends/analyzer.go b/internal/trends/analyzer.go index ddfbaa5..0dc1c52 100644 --- a/internal/trends/analyzer.go +++ b/internal/trends/analyzer.go @@ -102,7 +102,7 @@ func (a *Analyzer) getHourlyAverages(provider string, lookbackDays int) ([]hourl } defer func() { _ = rows.Close() }() - values := make([]hourlyAverage, 0) + values := make([]hourlyAverage, 0, 24) for rows.Next() { var avg hourlyAverage if err := rows.Scan(&avg.hour, &avg.avg); err != nil {