Skip to content

Commit a9ab184

Browse files
authored
progress: minor performance optimizations (#379)
1 parent 015f58d commit a9ab184

File tree

5 files changed

+67
-31
lines changed

5 files changed

+67
-31
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
/profile/
44
coverage.*
55
.coverprofile
6+
*.pprof
67
*.swp

bench_test.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,22 @@ func BenchmarkProgress_Render(b *testing.B) {
4343
trackSomething := func(pw progress.Writer, tracker *progress.Tracker) {
4444
tracker.Reset()
4545
pw.AppendTracker(tracker)
46-
time.Sleep(time.Millisecond * 100)
47-
tracker.Increment(tracker.Total / 2)
48-
time.Sleep(time.Millisecond * 100)
49-
tracker.Increment(tracker.Total / 2)
46+
parts := 4
47+
for i := 0; i < parts; i++ {
48+
tracker.Increment(tracker.Total / int64(parts))
49+
}
5050
}
5151

5252
for i := 0; i < b.N; i++ {
5353
pw := progress.NewWriter()
5454
pw.SetAutoStop(true)
5555
pw.SetOutputWriter(io.Discard)
56+
// Set very short update frequency for faster benchmark execution
57+
pw.SetUpdateFrequency(time.Millisecond)
5658
go trackSomething(pw, &tracker1)
5759
go trackSomething(pw, &tracker2)
5860
go trackSomething(pw, &tracker3)
59-
time.Sleep(time.Millisecond * 50)
61+
// Render once to test rendering performance
6062
pw.Render()
6163
}
6264
}

cmd/profile-progress/profile.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ func main() {
4949
numRenders := 5
5050
if len(os.Args) > 1 {
5151
var err error
52-
numRenders, err = strconv.Atoi(os.Args[2])
52+
numRenders, err = strconv.Atoi(os.Args[1])
5353
if err != nil {
54-
fmt.Printf("Invalid Argument: '%s'\n", os.Args[2])
54+
fmt.Printf("Invalid Argument: '%s'\n", os.Args[1])
5555
os.Exit(1)
5656
}
5757
}

progress/progress.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,4 +387,5 @@ type renderHint struct {
387387
hideTime bool // hide the time
388388
hideValue bool // hide the value
389389
isOverallTracker bool // is the Overall Progress tracker
390+
terminalWidth int // cached terminal width for this render cycle
390391
}

progress/render.go

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,22 @@ func (p *Progress) beginRender() bool {
4545
}
4646

4747
func (p *Progress) consumeQueuedTrackers() {
48-
if p.LengthInQueue() > 0 {
49-
p.trackersActiveMutex.Lock()
50-
p.trackersInQueueMutex.Lock()
51-
p.trackersActive = append(p.trackersActive, p.trackersInQueue...)
52-
p.trackersInQueue = make([]*Tracker, 0)
48+
p.trackersInQueueMutex.Lock()
49+
queueLen := len(p.trackersInQueue)
50+
if queueLen == 0 {
5351
p.trackersInQueueMutex.Unlock()
54-
p.trackersActiveMutex.Unlock()
52+
return
5553
}
54+
// copy the slice to avoid race condition - another goroutine may append
55+
// to p.trackersInQueue while we're appending to p.trackersActive
56+
queued := make([]*Tracker, len(p.trackersInQueue))
57+
copy(queued, p.trackersInQueue)
58+
p.trackersInQueue = p.trackersInQueue[:0] // reuse slice capacity
59+
p.trackersInQueueMutex.Unlock()
60+
61+
p.trackersActiveMutex.Lock()
62+
p.trackersActive = append(p.trackersActive, queued...)
63+
p.trackersActiveMutex.Unlock()
5664
}
5765

5866
func (p *Progress) endRender() {
@@ -69,8 +77,18 @@ func (p *Progress) extractDoneAndActiveTrackers() ([]*Tracker, []*Tracker) {
6977
// separate the active and done trackers
7078
var trackersActive, trackersDone []*Tracker
7179
var activeTrackersProgress int64
72-
p.trackersActiveMutex.RLock()
7380
var maxETA time.Duration
81+
var lengthDone int
82+
83+
// Get lengthDone while we have access to trackersDone
84+
p.trackersDoneMutex.RLock()
85+
lengthDone = len(p.trackersDone)
86+
p.trackersDoneMutex.RUnlock()
87+
88+
p.trackersActiveMutex.RLock()
89+
// Pre-allocate slices with estimated capacity to reduce allocations
90+
trackersActive = make([]*Tracker, 0, len(p.trackersActive))
91+
trackersDone = make([]*Tracker, 0, len(p.trackersActive)/4) // estimate ~25% done
7492
for _, tracker := range p.trackersActive {
7593
if !tracker.IsDone() {
7694
trackersActive = append(trackersActive, tracker)
@@ -87,7 +105,7 @@ func (p *Progress) extractDoneAndActiveTrackers() ([]*Tracker, []*Tracker) {
87105
p.sortBy.Sort(trackersActive)
88106

89107
// calculate the overall tracker's progress value
90-
p.overallTracker.value = int64(p.LengthDone()+len(trackersDone)) * 100
108+
p.overallTracker.value = int64(lengthDone+len(trackersDone)) * 100
91109
p.overallTracker.value += activeTrackersProgress
92110
p.overallTracker.minETA = maxETA
93111
if len(trackersActive) == 0 {
@@ -133,7 +151,13 @@ func (p *Progress) generateTrackerStrDeterminate(value int64, total int64, maxLe
133151
} else if pFinishedDotsFraction == 0 {
134152
pInProgress = ""
135153
}
136-
pFinishedStrLen := text.StringWidthWithoutEscSequences(pFinished + pInProgress)
154+
155+
// Use strings.Builder to avoid temporary string allocation
156+
var combined strings.Builder
157+
combined.Grow(len(pFinished) + len(pInProgress))
158+
combined.WriteString(pFinished)
159+
combined.WriteString(pInProgress)
160+
pFinishedStrLen := text.StringWidthWithoutEscSequences(combined.String())
137161
if pFinishedStrLen < maxLen {
138162
pUnfinished = strings.Repeat(p.style.Chars.Unfinished, maxLen-pFinishedStrLen)
139163
}
@@ -181,16 +205,16 @@ func (p *Progress) moveCursorToTheTop(out *strings.Builder) {
181205
}
182206
}
183207

184-
func (p *Progress) renderPinnedMessages(out *strings.Builder) {
208+
func (p *Progress) renderPinnedMessages(out *strings.Builder, hint renderHint) {
185209
p.pinnedMessageMutex.RLock()
186210
defer p.pinnedMessageMutex.RUnlock()
187211

188212
numLines := len(p.pinnedMessages)
189213
for _, msg := range p.pinnedMessages {
190214
msg = strings.TrimSpace(msg)
191215
msg = p.style.Colors.Pinned.Sprint(msg)
192-
if width := p.getTerminalWidth(); width > 0 {
193-
msg = text.Trim(msg, width)
216+
if hint.terminalWidth > 0 {
217+
msg = text.Trim(msg, hint.terminalWidth)
194218
}
195219
out.WriteString(msg)
196220
out.WriteRune('\n')
@@ -202,8 +226,11 @@ func (p *Progress) renderPinnedMessages(out *strings.Builder) {
202226

203227
func (p *Progress) renderTracker(out *strings.Builder, t *Tracker, hint renderHint) {
204228
message := t.message()
205-
message = strings.ReplaceAll(message, "\t", " ")
206-
message = strings.ReplaceAll(message, "\r", "") // replace with text.ProcessCRLF?
229+
// Optimize: only process if message contains tabs or carriage returns
230+
if strings.ContainsAny(message, "\t\r") {
231+
message = strings.ReplaceAll(message, "\t", " ")
232+
message = strings.ReplaceAll(message, "\r", "")
233+
}
207234
if p.lengthMessage > 0 {
208235
messageLen := text.StringWidthWithoutEscSequences(message)
209236
if messageLen < p.lengthMessage {
@@ -217,21 +244,21 @@ func (p *Progress) renderTracker(out *strings.Builder, t *Tracker, hint renderHi
217244
tOut.Grow(p.lengthProgressOverall)
218245
if hint.isOverallTracker {
219246
if !t.IsDone() {
220-
hint := renderHint{hideValue: true, isOverallTracker: true}
247+
hint := renderHint{hideValue: true, isOverallTracker: true, terminalWidth: hint.terminalWidth}
221248
p.renderTrackerProgress(tOut, t, message, p.generateTrackerStr(t, p.lengthProgressOverall, hint), hint)
222249
}
223250
} else {
224251
if t.IsDone() {
225252
p.renderTrackerDone(tOut, t, message)
226253
} else {
227-
hint := renderHint{hideTime: !p.style.Visibility.Time, hideValue: !p.style.Visibility.Value}
254+
hint := renderHint{hideTime: !p.style.Visibility.Time, hideValue: !p.style.Visibility.Value, terminalWidth: hint.terminalWidth}
228255
p.renderTrackerProgress(tOut, t, message, p.generateTrackerStr(t, p.lengthProgress, hint), hint)
229256
}
230257
}
231258

232259
outStr := tOut.String()
233-
if width := p.getTerminalWidth(); width > 0 {
234-
outStr = text.Trim(outStr, width)
260+
if hint.terminalWidth > 0 {
261+
outStr = text.Trim(outStr, hint.terminalWidth)
235262
}
236263
out.WriteString(outStr)
237264
out.WriteRune('\n')
@@ -298,6 +325,10 @@ func (p *Progress) renderTrackers(lastRenderLength int) int {
298325
return 0
299326
}
300327

328+
// Cache terminal width once per render cycle to avoid repeated mutex locks
329+
terminalWidth := p.getTerminalWidth()
330+
hint := renderHint{terminalWidth: terminalWidth}
331+
301332
// buffer all output into a strings.Builder object
302333
var out strings.Builder
303334
out.Grow(lastRenderLength)
@@ -308,11 +339,12 @@ func (p *Progress) renderTrackers(lastRenderLength int) int {
308339
}
309340

310341
// render the trackers that are done, and then the ones that are active
311-
p.renderTrackersDoneAndActive(&out)
342+
p.renderTrackersDoneAndActive(&out, hint)
312343

313344
// render the overall tracker
314345
if p.style.Visibility.TrackerOverall {
315-
p.renderTracker(&out, p.overallTracker, renderHint{isOverallTracker: true})
346+
overallHint := renderHint{isOverallTracker: true, terminalWidth: terminalWidth}
347+
p.renderTracker(&out, p.overallTracker, overallHint)
316348
}
317349

318350
// write the text to the output writer
@@ -326,13 +358,13 @@ func (p *Progress) renderTrackers(lastRenderLength int) int {
326358
return out.Len()
327359
}
328360

329-
func (p *Progress) renderTrackersDoneAndActive(out *strings.Builder) {
361+
func (p *Progress) renderTrackersDoneAndActive(out *strings.Builder, hint renderHint) {
330362
// find the currently "active" and "done" trackers
331363
trackersActive, trackersDone := p.extractDoneAndActiveTrackers()
332364

333365
// sort and render the done trackers
334366
for _, tracker := range trackersDone {
335-
p.renderTracker(out, tracker, renderHint{})
367+
p.renderTracker(out, tracker, hint)
336368
}
337369
p.trackersDoneMutex.Lock()
338370
p.trackersDone = append(p.trackersDone, trackersDone...)
@@ -350,12 +382,12 @@ func (p *Progress) renderTrackersDoneAndActive(out *strings.Builder) {
350382

351383
// render pinned messages
352384
if len(trackersActive) > 0 && p.style.Visibility.Pinned {
353-
p.renderPinnedMessages(out)
385+
p.renderPinnedMessages(out, hint)
354386
}
355387

356388
// sort and render the active trackers
357389
for _, tracker := range trackersActive {
358-
p.renderTracker(out, tracker, renderHint{})
390+
p.renderTracker(out, tracker, hint)
359391
}
360392
p.trackersActiveMutex.Lock()
361393
p.trackersActive = trackersActive

0 commit comments

Comments
 (0)