From 26132cf5bdae7ec81e9a0708c722ad2a9cf0c2cf Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 22 Jun 2024 13:34:15 +0200 Subject: [PATCH] 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 --- .../helpers/window_arrangement_helper.go | 7 +++--- pkg/gui/information_panel.go | 7 +++--- pkg/gui/presentation/branches.go | 6 ++--- pkg/utils/formatting.go | 19 +++++++++++--- pkg/utils/formatting_test.go | 25 +++++++++++++++++++ 5 files changed, 50 insertions(+), 14 deletions(-) diff --git a/pkg/gui/controllers/helpers/window_arrangement_helper.go b/pkg/gui/controllers/helpers/window_arrangement_helper.go index 0eb7cdb4aa3..322cd1bd646 100644 --- a/pkg/gui/controllers/helpers/window_arrangement_helper.go +++ b/pkg/gui/controllers/helpers/window_arrangement_helper.go @@ -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" ) @@ -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", @@ -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)}) } } @@ -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)), }) } diff --git a/pkg/gui/information_panel.go b/pkg/gui/information_panel.go index 00867fb9254..3eac1e77cf4 100644 --- a/pkg/gui/information_panel.go +++ b/pkg/gui/information_panel.go @@ -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 { @@ -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() @@ -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 } diff --git a/pkg/gui/presentation/branches.go b/pkg/gui/presentation/branches.go index b4c4a75c75e..b75dfc95b72 100644 --- a/pkg/gui/presentation/branches.go +++ b/pkg/gui/presentation/branches.go @@ -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 @@ -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 @@ -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, "…") diff --git a/pkg/utils/formatting.go b/pkg/utils/formatting.go index a6bbc56709b..b7817346ab7 100644 --- a/pkg/utils/formatting.go +++ b/pkg/utils/formatting.go @@ -3,6 +3,7 @@ package utils import ( "fmt" "strings" + "unicode" "github.com/mattn/go-runewidth" "github.com/samber/lo" @@ -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 } @@ -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) }) }) } @@ -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, "…") diff --git a/pkg/utils/formatting_test.go b/pkg/utils/formatting_test.go index 5b56a9b333c..ac2adee5f6c 100644 --- a/pkg/utils/formatting_test.go +++ b/pkg/utils/formatting_test.go @@ -4,6 +4,7 @@ import ( "strings" "testing" + "github.com/mattn/go-runewidth" "github.com/stretchr/testify/assert" ) @@ -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 🍉") + } +}