Skip to content

Commit c1e305c

Browse files
committed
Use utils.StringWidth to optimize rendering performance
runewidth.StringWidth is an expensive call, even if the input string is pure ASCII. Improve this by providing a wrapper that short-circuits the call to len if the input is ASCII. Benchmark results show that for non-ASCII strings it makes no noticable difference, but for ASCII strings it provides a more than 200x speedup. BenchmarkStringWidthAsciiOriginal-10 718135 1637 ns/op BenchmarkStringWidthAsciiOptimized-10 159197538 7.545 ns/op BenchmarkStringWidthNonAsciiOriginal-10 486290 2391 ns/op BenchmarkStringWidthNonAsciiOptimized-10 502286 2383 ns/op
1 parent 02b18ce commit c1e305c

File tree

5 files changed

+50
-14
lines changed

5 files changed

+50
-14
lines changed

pkg/gui/controllers/helpers/window_arrangement_helper.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"github.com/jesseduffield/lazygit/pkg/config"
99
"github.com/jesseduffield/lazygit/pkg/gui/types"
1010
"github.com/jesseduffield/lazygit/pkg/utils"
11-
"github.com/mattn/go-runewidth"
1211
"golang.org/x/exp/slices"
1312
)
1413

@@ -272,7 +271,7 @@ func infoSectionChildren(args WindowArrangementArgs) []*boxlayout.Box {
272271
return []*boxlayout.Box{
273272
{
274273
Window: "searchPrefix",
275-
Size: runewidth.StringWidth(args.SearchPrefix),
274+
Size: utils.StringWidth(args.SearchPrefix),
276275
},
277276
{
278277
Window: "search",
@@ -325,7 +324,7 @@ func infoSectionChildren(args WindowArrangementArgs) []*boxlayout.Box {
325324
// app status appears very briefly in demos and dislodges the caption,
326325
// so better not to show it at all
327326
if args.AppStatus != "" {
328-
result = append(result, &boxlayout.Box{Window: "appStatus", Size: runewidth.StringWidth(args.AppStatus)})
327+
result = append(result, &boxlayout.Box{Window: "appStatus", Size: utils.StringWidth(args.AppStatus)})
329328
}
330329
}
331330

@@ -338,7 +337,7 @@ func infoSectionChildren(args WindowArrangementArgs) []*boxlayout.Box {
338337
&boxlayout.Box{
339338
Window: "information",
340339
// unlike appStatus, informationStr has various colors so we need to decolorise before taking the length
341-
Size: runewidth.StringWidth(utils.Decolorise(args.InformationStr)),
340+
Size: utils.StringWidth(utils.Decolorise(args.InformationStr)),
342341
})
343342
}
344343

pkg/gui/information_panel.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"github.com/jesseduffield/lazygit/pkg/constants"
77
"github.com/jesseduffield/lazygit/pkg/gui/style"
88
"github.com/jesseduffield/lazygit/pkg/utils"
9-
"github.com/mattn/go-runewidth"
109
)
1110

1211
func (gui *Gui) informationStr() string {
@@ -34,7 +33,7 @@ func (gui *Gui) handleInfoClick() error {
3433
width, _ := view.Size()
3534

3635
if activeMode, ok := gui.helpers.Mode.GetActiveMode(); ok {
37-
if width-cx > runewidth.StringWidth(gui.c.Tr.ResetInParentheses) {
36+
if width-cx > utils.StringWidth(gui.c.Tr.ResetInParentheses) {
3837
return nil
3938
}
4039
return activeMode.Reset()
@@ -43,10 +42,10 @@ func (gui *Gui) handleInfoClick() error {
4342
var title, url string
4443

4544
// if we're not in an active mode we show the donate button
46-
if cx <= runewidth.StringWidth(gui.c.Tr.Donate) {
45+
if cx <= utils.StringWidth(gui.c.Tr.Donate) {
4746
url = constants.Links.Donate
4847
title = gui.c.Tr.Donate
49-
} else if cx <= runewidth.StringWidth(gui.c.Tr.Donate)+1+runewidth.StringWidth(gui.c.Tr.AskQuestion) {
48+
} else if cx <= utils.StringWidth(gui.c.Tr.Donate)+1+utils.StringWidth(gui.c.Tr.AskQuestion) {
5049
url = constants.Links.Discussions
5150
title = gui.c.Tr.AskQuestion
5251
}

pkg/gui/presentation/branches.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func getBranchDisplayStrings(
5656
// Recency is always three characters, plus one for the space
5757
availableWidth := viewWidth - 4
5858
if len(branchStatus) > 0 {
59-
availableWidth -= runewidth.StringWidth(utils.Decolorise(branchStatus)) + 1
59+
availableWidth -= utils.StringWidth(utils.Decolorise(branchStatus)) + 1
6060
}
6161
if icons.IsIconEnabled() {
6262
availableWidth -= 2 // one for the icon, one for the space
@@ -65,7 +65,7 @@ func getBranchDisplayStrings(
6565
availableWidth -= utils.COMMIT_HASH_SHORT_SIZE + 1
6666
}
6767
if checkedOutByWorkTree {
68-
availableWidth -= runewidth.StringWidth(worktreeIcon) + 1
68+
availableWidth -= utils.StringWidth(worktreeIcon) + 1
6969
}
7070

7171
displayName := b.Name
@@ -79,7 +79,7 @@ func getBranchDisplayStrings(
7979
}
8080

8181
// Don't bother shortening branch names that are already 3 characters or less
82-
if runewidth.StringWidth(displayName) > max(availableWidth, 3) {
82+
if utils.StringWidth(displayName) > max(availableWidth, 3) {
8383
// Never shorten the branch name to less then 3 characters
8484
len := max(availableWidth, 4)
8585
displayName = runewidth.Truncate(displayName, len, "…")

pkg/utils/formatting.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package utils
33
import (
44
"fmt"
55
"strings"
6+
"unicode"
67

78
"github.com/mattn/go-runewidth"
89
"github.com/samber/lo"
@@ -21,10 +22,22 @@ type ColumnConfig struct {
2122
Alignment Alignment
2223
}
2324

25+
func StringWidth(s string) int {
26+
// We are intentionally not using a range loop here, because that would
27+
// convert the characters to runes, which is unnecessary work in this case.
28+
for i := 0; i < len(s); i++ {
29+
if s[i] > unicode.MaxASCII {
30+
return runewidth.StringWidth(s)
31+
}
32+
}
33+
34+
return len(s)
35+
}
36+
2437
// WithPadding pads a string as much as you want
2538
func WithPadding(str string, padding int, alignment Alignment) string {
2639
uncoloredStr := Decolorise(str)
27-
width := runewidth.StringWidth(uncoloredStr)
40+
width := StringWidth(uncoloredStr)
2841
if padding < width {
2942
return str
3043
}
@@ -144,7 +157,7 @@ func getPadWidths(stringArrays [][]string) []int {
144157
return MaxFn(stringArrays, func(stringArray []string) int {
145158
uncoloredStr := Decolorise(stringArray[i])
146159

147-
return runewidth.StringWidth(uncoloredStr)
160+
return StringWidth(uncoloredStr)
148161
})
149162
})
150163
}
@@ -161,7 +174,7 @@ func MaxFn[T any](items []T, fn func(T) int) int {
161174

162175
// TruncateWithEllipsis returns a string, truncated to a certain length, with an ellipsis
163176
func TruncateWithEllipsis(str string, limit int) string {
164-
if runewidth.StringWidth(str) > limit && limit <= 2 {
177+
if StringWidth(str) > limit && limit <= 2 {
165178
return strings.Repeat(".", limit)
166179
}
167180
return runewidth.Truncate(str, limit, "…")

pkg/utils/formatting_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"strings"
55
"testing"
66

7+
"github.com/mattn/go-runewidth"
78
"github.com/stretchr/testify/assert"
89
)
910

@@ -250,3 +251,27 @@ func TestRenderDisplayStrings(t *testing.T) {
250251
assert.EqualValues(t, test.expectedColumnPositions, columnPositions)
251252
}
252253
}
254+
255+
func BenchmarkStringWidthAsciiOriginal(b *testing.B) {
256+
for i := 0; i < b.N; i++ {
257+
runewidth.StringWidth("some ASCII string")
258+
}
259+
}
260+
261+
func BenchmarkStringWidthAsciiOptimized(b *testing.B) {
262+
for i := 0; i < b.N; i++ {
263+
StringWidth("some ASCII string")
264+
}
265+
}
266+
267+
func BenchmarkStringWidthNonAsciiOriginal(b *testing.B) {
268+
for i := 0; i < b.N; i++ {
269+
runewidth.StringWidth("some non-ASCII string 🍉")
270+
}
271+
}
272+
273+
func BenchmarkStringWidthNonAsciiOptimized(b *testing.B) {
274+
for i := 0; i < b.N; i++ {
275+
StringWidth("some non-ASCII string 🍉")
276+
}
277+
}

0 commit comments

Comments
 (0)