Skip to content

Commit

Permalink
Use utils.StringWidth to optimize rendering performance
Browse files Browse the repository at this point in the history
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
  • Loading branch information
stefanhaller committed Jun 23, 2024
1 parent a67eda3 commit 26132cf
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 14 deletions.
7 changes: 3 additions & 4 deletions pkg/gui/controllers/helpers/window_arrangement_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mattn/go-runewidth"
"golang.org/x/exp/slices"
)

Expand Down Expand Up @@ -272,7 +271,7 @@ func infoSectionChildren(args WindowArrangementArgs) []*boxlayout.Box {
return []*boxlayout.Box{
{
Window: "searchPrefix",
Size: runewidth.StringWidth(args.SearchPrefix),
Size: utils.StringWidth(args.SearchPrefix),
},
{
Window: "search",
Expand Down Expand Up @@ -325,7 +324,7 @@ func infoSectionChildren(args WindowArrangementArgs) []*boxlayout.Box {
// app status appears very briefly in demos and dislodges the caption,
// so better not to show it at all
if args.AppStatus != "" {
result = append(result, &boxlayout.Box{Window: "appStatus", Size: runewidth.StringWidth(args.AppStatus)})
result = append(result, &boxlayout.Box{Window: "appStatus", Size: utils.StringWidth(args.AppStatus)})
}
}

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

Expand Down
7 changes: 3 additions & 4 deletions pkg/gui/information_panel.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/constants"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mattn/go-runewidth"
)

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

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

// if we're not in an active mode we show the donate button
if cx <= runewidth.StringWidth(gui.c.Tr.Donate) {
if cx <= utils.StringWidth(gui.c.Tr.Donate) {
url = constants.Links.Donate
title = gui.c.Tr.Donate
} else if cx <= runewidth.StringWidth(gui.c.Tr.Donate)+1+runewidth.StringWidth(gui.c.Tr.AskQuestion) {
} else if cx <= utils.StringWidth(gui.c.Tr.Donate)+1+utils.StringWidth(gui.c.Tr.AskQuestion) {
url = constants.Links.Discussions
title = gui.c.Tr.AskQuestion
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/gui/presentation/branches.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func getBranchDisplayStrings(
// Recency is always three characters, plus one for the space
availableWidth := viewWidth - 4
if len(branchStatus) > 0 {
availableWidth -= runewidth.StringWidth(utils.Decolorise(branchStatus)) + 1
availableWidth -= utils.StringWidth(utils.Decolorise(branchStatus)) + 1
}
if icons.IsIconEnabled() {
availableWidth -= 2 // one for the icon, one for the space
Expand All @@ -65,7 +65,7 @@ func getBranchDisplayStrings(
availableWidth -= utils.COMMIT_HASH_SHORT_SIZE + 1
}
if checkedOutByWorkTree {
availableWidth -= runewidth.StringWidth(worktreeIcon) + 1
availableWidth -= utils.StringWidth(worktreeIcon) + 1
}

displayName := b.Name
Expand All @@ -79,7 +79,7 @@ func getBranchDisplayStrings(
}

// Don't bother shortening branch names that are already 3 characters or less
if runewidth.StringWidth(displayName) > max(availableWidth, 3) {
if utils.StringWidth(displayName) > max(availableWidth, 3) {
// Never shorten the branch name to less then 3 characters
len := max(availableWidth, 4)
displayName = runewidth.Truncate(displayName, len, "…")
Expand Down
19 changes: 16 additions & 3 deletions pkg/utils/formatting.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package utils
import (
"fmt"
"strings"
"unicode"

"github.com/mattn/go-runewidth"
"github.com/samber/lo"
Expand All @@ -21,10 +22,22 @@ type ColumnConfig struct {
Alignment Alignment
}

func StringWidth(s string) int {
// We are intentionally not using a range loop here, because that would
// convert the characters to runes, which is unnecessary work in this case.
for i := 0; i < len(s); i++ {
if s[i] > unicode.MaxASCII {
return runewidth.StringWidth(s)
}
}

return len(s)
}

// WithPadding pads a string as much as you want
func WithPadding(str string, padding int, alignment Alignment) string {
uncoloredStr := Decolorise(str)
width := runewidth.StringWidth(uncoloredStr)
width := StringWidth(uncoloredStr)
if padding < width {
return str
}
Expand Down Expand Up @@ -144,7 +157,7 @@ func getPadWidths(stringArrays [][]string) []int {
return MaxFn(stringArrays, func(stringArray []string) int {
uncoloredStr := Decolorise(stringArray[i])

return runewidth.StringWidth(uncoloredStr)
return StringWidth(uncoloredStr)
})
})
}
Expand All @@ -161,7 +174,7 @@ func MaxFn[T any](items []T, fn func(T) int) int {

// TruncateWithEllipsis returns a string, truncated to a certain length, with an ellipsis
func TruncateWithEllipsis(str string, limit int) string {
if runewidth.StringWidth(str) > limit && limit <= 2 {
if StringWidth(str) > limit && limit <= 2 {
return strings.Repeat(".", limit)
}
return runewidth.Truncate(str, limit, "…")
Expand Down
25 changes: 25 additions & 0 deletions pkg/utils/formatting_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"strings"
"testing"

"github.com/mattn/go-runewidth"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -250,3 +251,27 @@ func TestRenderDisplayStrings(t *testing.T) {
assert.EqualValues(t, test.expectedColumnPositions, columnPositions)
}
}

func BenchmarkStringWidthAsciiOriginal(b *testing.B) {
for i := 0; i < b.N; i++ {
runewidth.StringWidth("some ASCII string")
}
}

func BenchmarkStringWidthAsciiOptimized(b *testing.B) {
for i := 0; i < b.N; i++ {
StringWidth("some ASCII string")
}
}

func BenchmarkStringWidthNonAsciiOriginal(b *testing.B) {
for i := 0; i < b.N; i++ {
runewidth.StringWidth("some non-ASCII string 🍉")
}
}

func BenchmarkStringWidthNonAsciiOptimized(b *testing.B) {
for i := 0; i < b.N; i++ {
StringWidth("some non-ASCII string 🍉")
}
}

0 comments on commit 26132cf

Please sign in to comment.