From b7296ead19bea682a1980927967f6ca7f59ef88a Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Mon, 16 Jun 2025 15:54:41 +0200 Subject: [PATCH 1/3] perf: make spinner less cpu demanding --- internal/tui/components/anim/anim.go | 65 +++++++++++++---------- internal/tui/components/anim/anim_test.go | 63 ++++++++++++++++++++++ todos.md | 2 +- 3 files changed, 100 insertions(+), 30 deletions(-) create mode 100644 internal/tui/components/anim/anim_test.go diff --git a/internal/tui/components/anim/anim.go b/internal/tui/components/anim/anim.go index 0bd7a7753f114c7bfb6c8f9898772c49ae8f7d80..34f7a67e28f8b50d79d25f8cf766a7797e7d125d 100644 --- a/internal/tui/components/anim/anim.go +++ b/internal/tui/components/anim/anim.go @@ -17,12 +17,23 @@ import ( ) const ( - charCyclingFPS = time.Second / 22 - colorCycleFPS = time.Second / 5 - maxCyclingChars = 120 + charCyclingFPS = time.Second / 8 // Reduced from 22 to 8 FPS for better CPU efficiency + colorCycleFPS = time.Second / 3 // Reduced from 5 to 3 FPS + maxCyclingChars = 60 // Reduced from 120 to 60 characters ) -var charRunes = []rune("0123456789abcdefABCDEF~!@#$£€%^&*()+=_") +var ( + charRunes = []rune("0123456789abcdefABCDEF~!@#$£€%^&*()+=_") + charRunePool = make([]rune, 1000) // Pre-generated pool of random characters + poolIndex = 0 +) + +func init() { + // Pre-populate the character pool to avoid runtime random generation + for i := range charRunePool { + charRunePool[i] = charRunes[rand.IntN(len(charRunes))] + } +} type charState int @@ -41,7 +52,9 @@ type cyclingChar struct { } func (c cyclingChar) randomRune() rune { - return (charRunes)[rand.IntN(len(charRunes))] //nolint:gosec + // Use pre-generated pool instead of runtime random generation + poolIndex = (poolIndex + 1) % len(charRunePool) + return charRunePool[poolIndex] } func (c cyclingChar) state(start time.Time) charState { @@ -126,14 +139,18 @@ func New(cyclingCharsSize uint, label string, opts ...animOption) Animation { // color the cycling characters with a gradient ramp. const minRampSize = 3 if n >= minRampSize { - // Note: double capacity for color cycling as we'll need to reverse and - // append the ramp for seamless transitions. - c.ramp = make([]lipgloss.Style, n, n*2) //nolint:mnd + // Optimized: single capacity allocation for color cycling + c.ramp = make([]lipgloss.Style, 0, n*2) ramp := makeGradientRamp(n) - for i, color := range ramp { - c.ramp[i] = lipgloss.NewStyle().Foreground(color) + for _, color := range ramp { + c.ramp = append(c.ramp, lipgloss.NewStyle().Foreground(color)) } - c.ramp = append(c.ramp, reverse(c.ramp)...) // reverse and append for color cycling + // Create reversed copy for seamless color cycling + reversed := make([]lipgloss.Style, len(c.ramp)) + for i, style := range c.ramp { + reversed[len(c.ramp)-1-i] = style + } + c.ramp = append(c.ramp, reversed...) } makeDelay := func(a int32, b time.Duration) time.Duration { @@ -246,29 +263,28 @@ func (a anim) View() tea.View { b strings.Builder ) - // Pre-allocate builder capacity to avoid reallocations. - // Estimate: cycling chars + label chars + ellipsis + style overhead. + // Optimized capacity calculation to reduce allocations const ( - bytesPerChar = 20 // ANSI styling - bufferSize = 50 // ellipsis and safety margin + bytesPerChar = 15 // Reduced estimate for ANSI styling + bufferSize = 30 // Reduced safety margin ) estimatedCap := len(a.cyclingChars)*bytesPerChar + len(a.labelChars)*bytesPerChar + bufferSize b.Grow(estimatedCap) + // Render cycling characters with gradient (if available) for i, c := range a.cyclingChars { if len(a.ramp) > i { b.WriteString(a.ramp[i].Render(string(c.currentValue))) - continue + } else { + b.WriteRune(c.currentValue) } - b.WriteRune(c.currentValue) } + // Render label characters and ellipsis if len(a.labelChars) > 1 { textStyle := t.S().Text for _, c := range a.labelChars { - b.WriteString( - textStyle.Render(string(c.currentValue)), - ) + b.WriteString(textStyle.Render(string(c.currentValue))) } b.WriteString(textStyle.Render(a.ellipsis.View())) } @@ -296,12 +312,3 @@ func makeGradientRamp(length int) []color.Color { } return c } - -func reverse[T any](in []T) []T { - out := make([]T, len(in)) - copy(out, in[:]) - for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 { - out[i], out[j] = out[j], out[i] - } - return out -} diff --git a/internal/tui/components/anim/anim_test.go b/internal/tui/components/anim/anim_test.go new file mode 100644 index 0000000000000000000000000000000000000000..264f5fd9f905cc90c52eedff5d470677fbf83c45 --- /dev/null +++ b/internal/tui/components/anim/anim_test.go @@ -0,0 +1,63 @@ +package anim + +import ( + "testing" + "time" +) + +func BenchmarkAnimationUpdate(b *testing.B) { + anim := New(30, "Loading test data") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + // Simulate character cycling update + _, _ = anim.Update(StepCharsMsg{id: anim.ID()}) + } +} + +func BenchmarkAnimationView(b *testing.B) { + anim := New(30, "Loading test data") + + // Initialize with some cycling + for i := 0; i < 10; i++ { + anim.Update(StepCharsMsg{id: anim.ID()}) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = anim.View() + } +} + +func BenchmarkRandomRune(b *testing.B) { + c := cyclingChar{} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = c.randomRune() + } +} + +func TestAnimationPerformance(t *testing.T) { + anim := New(30, "Performance test") + + start := time.Now() + iterations := 1000 + + // Simulate rapid updates + for i := 0; i < iterations; i++ { + anim.Update(StepCharsMsg{id: anim.ID()}) + _ = anim.View() + } + + duration := time.Since(start) + avgPerUpdate := duration / time.Duration(iterations) + + // Should complete 1000 updates in reasonable time + if avgPerUpdate > time.Millisecond { + t.Errorf("Animation update too slow: %v per update (should be < 1ms)", avgPerUpdate) + } + + t.Logf("Animation performance: %v per update (%d updates in %v)", + avgPerUpdate, iterations, duration) +} \ No newline at end of file diff --git a/todos.md b/todos.md index 85ce7a39c019fe86508888c1254049091ff87e2c..1e723f749bd56edf41c54c66b8de0c9732bf6bd6 100644 --- a/todos.md +++ b/todos.md @@ -25,7 +25,7 @@ - [ ] Implement responsive mode - [ ] Revisit the core list component - [ ] This component has become super complex we might need to fix this. -- [ ] Investigate ways to make the spinner less CPU intensive +- [x] Investigate ways to make the spinner less CPU intensive - [ ] General cleanup and documentation - [ ] Update the readme From 69b649df37f2af634ec41d1bc49bef324fa30103 Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Mon, 16 Jun 2025 15:55:08 +0200 Subject: [PATCH 2/3] fix: anim fmt files --- internal/tui/components/anim/anim.go | 6 +++--- internal/tui/components/anim/anim_test.go | 22 +++++++++++----------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/internal/tui/components/anim/anim.go b/internal/tui/components/anim/anim.go index 34f7a67e28f8b50d79d25f8cf766a7797e7d125d..5cb9652e95d0bc3ddf40ab3d97db636a5f2ec1ff 100644 --- a/internal/tui/components/anim/anim.go +++ b/internal/tui/components/anim/anim.go @@ -17,9 +17,9 @@ import ( ) const ( - charCyclingFPS = time.Second / 8 // Reduced from 22 to 8 FPS for better CPU efficiency - colorCycleFPS = time.Second / 3 // Reduced from 5 to 3 FPS - maxCyclingChars = 60 // Reduced from 120 to 60 characters + charCyclingFPS = time.Second / 8 // Reduced from 22 to 8 FPS for better CPU efficiency + colorCycleFPS = time.Second / 3 // Reduced from 5 to 3 FPS + maxCyclingChars = 60 // Reduced from 120 to 60 characters ) var ( diff --git a/internal/tui/components/anim/anim_test.go b/internal/tui/components/anim/anim_test.go index 264f5fd9f905cc90c52eedff5d470677fbf83c45..86d18ab4f2235775b69d22420966dad2e1193b86 100644 --- a/internal/tui/components/anim/anim_test.go +++ b/internal/tui/components/anim/anim_test.go @@ -7,7 +7,7 @@ import ( func BenchmarkAnimationUpdate(b *testing.B) { anim := New(30, "Loading test data") - + b.ResetTimer() for i := 0; i < b.N; i++ { // Simulate character cycling update @@ -17,12 +17,12 @@ func BenchmarkAnimationUpdate(b *testing.B) { func BenchmarkAnimationView(b *testing.B) { anim := New(30, "Loading test data") - + // Initialize with some cycling for i := 0; i < 10; i++ { anim.Update(StepCharsMsg{id: anim.ID()}) } - + b.ResetTimer() for i := 0; i < b.N; i++ { _ = anim.View() @@ -31,7 +31,7 @@ func BenchmarkAnimationView(b *testing.B) { func BenchmarkRandomRune(b *testing.B) { c := cyclingChar{} - + b.ResetTimer() for i := 0; i < b.N; i++ { _ = c.randomRune() @@ -40,24 +40,24 @@ func BenchmarkRandomRune(b *testing.B) { func TestAnimationPerformance(t *testing.T) { anim := New(30, "Performance test") - + start := time.Now() iterations := 1000 - + // Simulate rapid updates for i := 0; i < iterations; i++ { anim.Update(StepCharsMsg{id: anim.ID()}) _ = anim.View() } - + duration := time.Since(start) avgPerUpdate := duration / time.Duration(iterations) - + // Should complete 1000 updates in reasonable time if avgPerUpdate > time.Millisecond { t.Errorf("Animation update too slow: %v per update (should be < 1ms)", avgPerUpdate) } - - t.Logf("Animation performance: %v per update (%d updates in %v)", + + t.Logf("Animation performance: %v per update (%d updates in %v)", avgPerUpdate, iterations, duration) -} \ No newline at end of file +} From ea9491078f7fb69546479bce174ec9501069d465 Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Mon, 16 Jun 2025 23:10:49 +0200 Subject: [PATCH 3/3] feat: remove test file --- internal/tui/components/anim/anim.go | 6 +-- internal/tui/components/anim/anim_test.go | 63 ----------------------- 2 files changed, 3 insertions(+), 66 deletions(-) delete mode 100644 internal/tui/components/anim/anim_test.go diff --git a/internal/tui/components/anim/anim.go b/internal/tui/components/anim/anim.go index 5cb9652e95d0bc3ddf40ab3d97db636a5f2ec1ff..63cb5d5a05a95a8768c22d3b2ac3cd887b63e694 100644 --- a/internal/tui/components/anim/anim.go +++ b/internal/tui/components/anim/anim.go @@ -17,9 +17,9 @@ import ( ) const ( - charCyclingFPS = time.Second / 8 // Reduced from 22 to 8 FPS for better CPU efficiency - colorCycleFPS = time.Second / 3 // Reduced from 5 to 3 FPS - maxCyclingChars = 60 // Reduced from 120 to 60 characters + charCyclingFPS = time.Second / 8 // 8 FPS for better CPU efficiency + colorCycleFPS = time.Second / 3 // 3 FPS + maxCyclingChars = 60 // 60 characters ) var ( diff --git a/internal/tui/components/anim/anim_test.go b/internal/tui/components/anim/anim_test.go deleted file mode 100644 index 86d18ab4f2235775b69d22420966dad2e1193b86..0000000000000000000000000000000000000000 --- a/internal/tui/components/anim/anim_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package anim - -import ( - "testing" - "time" -) - -func BenchmarkAnimationUpdate(b *testing.B) { - anim := New(30, "Loading test data") - - b.ResetTimer() - for i := 0; i < b.N; i++ { - // Simulate character cycling update - _, _ = anim.Update(StepCharsMsg{id: anim.ID()}) - } -} - -func BenchmarkAnimationView(b *testing.B) { - anim := New(30, "Loading test data") - - // Initialize with some cycling - for i := 0; i < 10; i++ { - anim.Update(StepCharsMsg{id: anim.ID()}) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = anim.View() - } -} - -func BenchmarkRandomRune(b *testing.B) { - c := cyclingChar{} - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = c.randomRune() - } -} - -func TestAnimationPerformance(t *testing.T) { - anim := New(30, "Performance test") - - start := time.Now() - iterations := 1000 - - // Simulate rapid updates - for i := 0; i < iterations; i++ { - anim.Update(StepCharsMsg{id: anim.ID()}) - _ = anim.View() - } - - duration := time.Since(start) - avgPerUpdate := duration / time.Duration(iterations) - - // Should complete 1000 updates in reasonable time - if avgPerUpdate > time.Millisecond { - t.Errorf("Animation update too slow: %v per update (should be < 1ms)", avgPerUpdate) - } - - t.Logf("Animation performance: %v per update (%d updates in %v)", - avgPerUpdate, iterations, duration) -}