on main help sign in
perf: make spinner less cpu demanding
commitb7296ead19bea682a1980927967f6ca7f59ef88a authorRaphael Amorim <rapha850@gmail.com> dateMon 16 Jun 2025, 15:54 (1 year ago) parent6261a57
3 files changed+100−30
internal/tui/components/anim/anim.go+36−29 internal/tui/components/anim/anim_test.go+63 todos.md+1−1
internal/tui/components/anim/anim.go+36−29
@@ -17,12 +17,23 @@ import (
1717)
1818
1919const (
20− charCyclingFPS = time.Second / 22
21− colorCycleFPS = time.Second / 5
22− maxCyclingChars = 120
20+ charCyclingFPS = time.Second / 8 // Reduced from 22 to 8 FPS for better CPU efficiency
21+ colorCycleFPS = time.Second / 3 // Reduced from 5 to 3 FPS
22+ maxCyclingChars = 60 // Reduced from 120 to 60 characters
2323)
2424
25−var charRunes = []rune("0123456789abcdefABCDEF~!@#$£€%^&*()+=_")
25+var (
26+ charRunes = []rune("0123456789abcdefABCDEF~!@#$£€%^&*()+=_")
27+ charRunePool = make([]rune, 1000) // Pre-generated pool of random characters
28+ poolIndex = 0
29+)
30+
31+func init() {
32+ // Pre-populate the character pool to avoid runtime random generation
33+ for i := range charRunePool {
34+ charRunePool[i] = charRunes[rand.IntN(len(charRunes))]
35+ }
36+}
2637
2738type charState int
2839
@@ -41,7 +52,9 @@ type cyclingChar struct {
4152}
4253
4354func (c cyclingChar) randomRune() rune {
44− return (charRunes)[rand.IntN(len(charRunes))] //nolint:gosec
55+ // Use pre-generated pool instead of runtime random generation
56+ poolIndex = (poolIndex + 1) % len(charRunePool)
57+ return charRunePool[poolIndex]
4558}
4659
4760func (c cyclingChar) state(start time.Time) charState {
@@ -126,14 +139,18 @@ func New(cyclingCharsSize uint, label string, opts ...animOption) Animation {
126139 // color the cycling characters with a gradient ramp.
127140 const minRampSize = 3
128141 if n >= minRampSize {
129− // Note: double capacity for color cycling as we'll need to reverse and
130− // append the ramp for seamless transitions.
131− c.ramp = make([]lipgloss.Style, n, n*2) //nolint:mnd
142+ // Optimized: single capacity allocation for color cycling
143+ c.ramp = make([]lipgloss.Style, 0, n*2)
132144 ramp := makeGradientRamp(n)
133− for i, color := range ramp {
134− c.ramp[i] = lipgloss.NewStyle().Foreground(color)
145+ for _, color := range ramp {
146+ c.ramp = append(c.ramp, lipgloss.NewStyle().Foreground(color))
135147 }
136− c.ramp = append(c.ramp, reverse(c.ramp)...) // reverse and append for color cycling
148+ // Create reversed copy for seamless color cycling
149+ reversed := make([]lipgloss.Style, len(c.ramp))
150+ for i, style := range c.ramp {
151+ reversed[len(c.ramp)-1-i] = style
152+ }
153+ c.ramp = append(c.ramp, reversed...)
137154 }
138155
139156 makeDelay := func(a int32, b time.Duration) time.Duration {
@@ -246,29 +263,28 @@ func (a anim) View() tea.View {
246263 b strings.Builder
247264 )
248265
249− // Pre-allocate builder capacity to avoid reallocations.
250− // Estimate: cycling chars + label chars + ellipsis + style overhead.
266+ // Optimized capacity calculation to reduce allocations
251267 const (
252− bytesPerChar = 20 // ANSI styling
253− bufferSize = 50 // ellipsis and safety margin
268+ bytesPerChar = 15 // Reduced estimate for ANSI styling
269+ bufferSize = 30 // Reduced safety margin
254270 )
255271 estimatedCap := len(a.cyclingChars)*bytesPerChar + len(a.labelChars)*bytesPerChar + bufferSize
256272 b.Grow(estimatedCap)
257273
274+ // Render cycling characters with gradient (if available)
258275 for i, c := range a.cyclingChars {
259276 if len(a.ramp) > i {
260277 b.WriteString(a.ramp[i].Render(string(c.currentValue)))
261− continue
278+ } else {
279+ b.WriteRune(c.currentValue)
262280 }
263− b.WriteRune(c.currentValue)
264281 }
265282
283+ // Render label characters and ellipsis
266284 if len(a.labelChars) > 1 {
267285 textStyle := t.S().Text
268286 for _, c := range a.labelChars {
269− b.WriteString(
270− textStyle.Render(string(c.currentValue)),
271− )
287+ b.WriteString(textStyle.Render(string(c.currentValue)))
272288 }
273289 b.WriteString(textStyle.Render(a.ellipsis.View()))
274290 }
@@ -296,12 +312,3 @@ func makeGradientRamp(length int) []color.Color {
296312 }
297313 return c
298314}
299−
300−func reverse[T any](in []T) []T {
301− out := make([]T, len(in))
302− copy(out, in[:])
303− for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
304− out[i], out[j] = out[j], out[i]
305− }
306− return out
307−}
internal/tui/components/anim/anim_test.gonew file+63
@@ -0,0 +1,63 @@
1+package anim
2+
3+import (
4+ "testing"
5+ "time"
6+)
7+
8+func BenchmarkAnimationUpdate(b *testing.B) {
9+ anim := New(30, "Loading test data")
10+
11+ b.ResetTimer()
12+ for i := 0; i < b.N; i++ {
13+ // Simulate character cycling update
14+ _, _ = anim.Update(StepCharsMsg{id: anim.ID()})
15+ }
16+}
17+
18+func BenchmarkAnimationView(b *testing.B) {
19+ anim := New(30, "Loading test data")
20+
21+ // Initialize with some cycling
22+ for i := 0; i < 10; i++ {
23+ anim.Update(StepCharsMsg{id: anim.ID()})
24+ }
25+
26+ b.ResetTimer()
27+ for i := 0; i < b.N; i++ {
28+ _ = anim.View()
29+ }
30+}
31+
32+func BenchmarkRandomRune(b *testing.B) {
33+ c := cyclingChar{}
34+
35+ b.ResetTimer()
36+ for i := 0; i < b.N; i++ {
37+ _ = c.randomRune()
38+ }
39+}
40+
41+func TestAnimationPerformance(t *testing.T) {
42+ anim := New(30, "Performance test")
43+
44+ start := time.Now()
45+ iterations := 1000
46+
47+ // Simulate rapid updates
48+ for i := 0; i < iterations; i++ {
49+ anim.Update(StepCharsMsg{id: anim.ID()})
50+ _ = anim.View()
51+ }
52+
53+ duration := time.Since(start)
54+ avgPerUpdate := duration / time.Duration(iterations)
55+
56+ // Should complete 1000 updates in reasonable time
57+ if avgPerUpdate > time.Millisecond {
58+ t.Errorf("Animation update too slow: %v per update (should be < 1ms)", avgPerUpdate)
59+ }
60+
61+ t.Logf("Animation performance: %v per update (%d updates in %v)",
62+ avgPerUpdate, iterations, duration)
63+}
todos.md+1−1
@@ -25,7 +25,7 @@
2525- [ ] Implement responsive mode
2626- [ ] Revisit the core list component
2727 - [ ] This component has become super complex we might need to fix this.
28−- [ ] Investigate ways to make the spinner less CPU intensive
28+- [x] Investigate ways to make the spinner less CPU intensive
2929- [ ] General cleanup and documentation
3030- [ ] Update the readme
3131