on main help sign in
perf: make spinner less cpu demanding
b7296eaRaphael Amorim
internal/tui/components/anim/anim.go+36−29
@@ -17,12 +17,23 @@ import (
17)
17)
18
18
19const (
19const (
20− charCyclingFPS = time.Second / 22
20+ charCyclingFPS = time.Second / 8 // Reduced from 22 to 8 FPS for better CPU efficiency
21− colorCycleFPS = time.Second / 5
21+ colorCycleFPS = time.Second / 3 // Reduced from 5 to 3 FPS
22− maxCyclingChars = 120
22+ maxCyclingChars = 60 // Reduced from 120 to 60 characters
23)
23)
24
24
25+var (
25−var charRunes = []rune("0123456789abcdefABCDEF~!@#$£€%^&*()+=_")
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+}
26
37
27type charState int
38type charState int
28
39
@@ -41,7 +52,9 @@ type cyclingChar struct {
41}
52}
42
53
43func (c cyclingChar) randomRune() rune {
54func (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]
45}
58}
46
59
47func (c cyclingChar) state(start time.Time) charState {
60func (c cyclingChar) state(start time.Time) charState {
@@ -126,14 +139,18 @@ func New(cyclingCharsSize uint, label string, opts ...animOption) Animation {
126 // color the cycling characters with a gradient ramp.
139 // color the cycling characters with a gradient ramp.
127 const minRampSize = 3
140 const minRampSize = 3
128 if n >= minRampSize {
141 if n >= minRampSize {
129− // Note: double capacity for color cycling as we'll need to reverse and
142+ // Optimized: single capacity allocation for color cycling
130− // append the ramp for seamless transitions.
131− c.ramp = make([]lipgloss.Style, n, n*2) //nolint:mnd
143+ c.ramp = make([]lipgloss.Style, 0, n*2)
132 ramp := makeGradientRamp(n)
144 ramp := makeGradientRamp(n)
133− for i, color := range ramp {
145+ for _, color := range ramp {
134− c.ramp[i] = lipgloss.NewStyle().Foreground(color)
146+ c.ramp = append(c.ramp, lipgloss.NewStyle().Foreground(color))
135 }
147 }
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...)
137 }
154 }
138
155
139 makeDelay := func(a int32, b time.Duration) time.Duration {
156 makeDelay := func(a int32, b time.Duration) time.Duration {
@@ -246,29 +263,28 @@ func (a anim) View() tea.View {
246 b strings.Builder
263 b strings.Builder
247 )
264 )
248
265
249− // Pre-allocate builder capacity to avoid reallocations.
266+ // Optimized capacity calculation to reduce allocations
250− // Estimate: cycling chars + label chars + ellipsis + style overhead.
251 const (
267 const (
252− bytesPerChar = 20 // ANSI styling
268+ bytesPerChar = 15 // Reduced estimate for ANSI styling
253− bufferSize = 50 // ellipsis and safety margin
269+ bufferSize = 30 // Reduced safety margin
254 )
270 )
255 estimatedCap := len(a.cyclingChars)*bytesPerChar + len(a.labelChars)*bytesPerChar + bufferSize
271 estimatedCap := len(a.cyclingChars)*bytesPerChar + len(a.labelChars)*bytesPerChar + bufferSize
256 b.Grow(estimatedCap)
272 b.Grow(estimatedCap)
257
273
274+ // Render cycling characters with gradient (if available)
258 for i, c := range a.cyclingChars {
275 for i, c := range a.cyclingChars {
259 if len(a.ramp) > i {
276 if len(a.ramp) > i {
260 b.WriteString(a.ramp[i].Render(string(c.currentValue)))
277 b.WriteString(a.ramp[i].Render(string(c.currentValue)))
261− continue
278+ } else {
279+ b.WriteRune(c.currentValue)
262 }
280 }
263− b.WriteRune(c.currentValue)
264 }
281 }
265
282
283+ // Render label characters and ellipsis
266 if len(a.labelChars) > 1 {
284 if len(a.labelChars) > 1 {
267 textStyle := t.S().Text
285 textStyle := t.S().Text
268 for _, c := range a.labelChars {
286 for _, c := range a.labelChars {
269− b.WriteString(
270− textStyle.Render(string(c.currentValue)),
287+ b.WriteString(textStyle.Render(string(c.currentValue)))
271− )
272 }
288 }
273 b.WriteString(textStyle.Render(a.ellipsis.View()))
289 b.WriteString(textStyle.Render(a.ellipsis.View()))
274 }
290 }
@@ -296,12 +312,3 @@ func makeGradientRamp(length int) []color.Color {
296 }
312 }
297 return c
313 return c
298}
314}
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 @@
25- [ ] Implement responsive mode
25- [ ] Implement responsive mode
26- [ ] Revisit the core list component
26- [ ] Revisit the core list component
27 - [ ] This component has become super complex we might need to fix this.
27 - [ ] 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
29- [ ] General cleanup and documentation
29- [ ] General cleanup and documentation
30- [ ] Update the readme
30- [ ] Update the readme
31
31