From b7296ead19bea682a1980927967f6ca7f59ef88a Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Mon, 16 Jun 2025 15:54:41 +0200 Subject: [PATCH] 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