Skip to content

Commit cf40a5b

Browse files
authored
Improve render performance (#3686)
- **PR Description** Fix a performance regression that I introduced with v0.41: when entering or leaving staging mode for a file, or when switching from a file that has only unstaged changes to one that has both staged and unstaged changes, there was a noticeable lag of about 500ms on my machine. With the improvements in this PR we get this back down to about 20ms.
2 parents a62a508 + 26132cf commit cf40a5b

14 files changed

+98
-26
lines changed

pkg/gui/context/base_context.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ type BaseContext struct {
2323
focusable bool
2424
transient bool
2525
hasControlledBounds bool
26-
needsRerenderOnWidthChange bool
26+
needsRerenderOnWidthChange types.NeedsRerenderOnWidthChangeLevel
2727
needsRerenderOnHeightChange bool
2828
highlightOnFocus bool
2929

@@ -46,7 +46,7 @@ type NewBaseContextOpts struct {
4646
Transient bool
4747
HasUncontrolledBounds bool // negating for the sake of making false the default
4848
HighlightOnFocus bool
49-
NeedsRerenderOnWidthChange bool
49+
NeedsRerenderOnWidthChange types.NeedsRerenderOnWidthChangeLevel
5050
NeedsRerenderOnHeightChange bool
5151

5252
OnGetOptionsMap func() map[string]string
@@ -201,7 +201,7 @@ func (self *BaseContext) HasControlledBounds() bool {
201201
return self.hasControlledBounds
202202
}
203203

204-
func (self *BaseContext) NeedsRerenderOnWidthChange() bool {
204+
func (self *BaseContext) NeedsRerenderOnWidthChange() types.NeedsRerenderOnWidthChangeLevel {
205205
return self.needsRerenderOnWidthChange
206206
}
207207

pkg/gui/context/branches_context.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
4646
Key: LOCAL_BRANCHES_CONTEXT_KEY,
4747
Kind: types.SIDE_CONTEXT,
4848
Focusable: true,
49-
NeedsRerenderOnWidthChange: true,
49+
NeedsRerenderOnWidthChange: types.NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_WIDTH_CHANGES,
5050
})),
5151
ListRenderer: ListRenderer{
5252
list: viewModel,

pkg/gui/context/local_commits_context.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
7777
Key: LOCAL_COMMITS_CONTEXT_KEY,
7878
Kind: types.SIDE_CONTEXT,
7979
Focusable: true,
80-
NeedsRerenderOnWidthChange: true,
80+
NeedsRerenderOnWidthChange: types.NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_SCREEN_MODE_CHANGES,
8181
NeedsRerenderOnHeightChange: true,
8282
})),
8383
ListRenderer: ListRenderer{

pkg/gui/context/reflog_commits_context.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func NewReflogCommitsContext(c *ContextCommon) *ReflogCommitsContext {
4848
Key: REFLOG_COMMITS_CONTEXT_KEY,
4949
Kind: types.SIDE_CONTEXT,
5050
Focusable: true,
51-
NeedsRerenderOnWidthChange: true,
51+
NeedsRerenderOnWidthChange: types.NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_SCREEN_MODE_CHANGES,
5252
})),
5353
ListRenderer: ListRenderer{
5454
list: viewModel,

pkg/gui/context/remote_branches_context.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ func NewRemoteBranchesContext(
4343
Kind: types.SIDE_CONTEXT,
4444
Focusable: true,
4545
Transient: true,
46-
NeedsRerenderOnWidthChange: true,
4746
NeedsRerenderOnHeightChange: true,
4847
})),
4948
ListRenderer: ListRenderer{

pkg/gui/context/sub_commits_context.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ func NewSubCommitsContext(
121121
Kind: types.SIDE_CONTEXT,
122122
Focusable: true,
123123
Transient: true,
124-
NeedsRerenderOnWidthChange: true,
124+
NeedsRerenderOnWidthChange: types.NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_SCREEN_MODE_CHANGES,
125125
NeedsRerenderOnHeightChange: true,
126126
})),
127127
ListRenderer: ListRenderer{

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/controllers/screen_mode_actions.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package controllers
22

33
import (
4+
"github.com/jesseduffield/gocui"
45
"github.com/jesseduffield/lazygit/pkg/gui/types"
56
)
67

@@ -16,7 +17,7 @@ func (self *ScreenModeActions) Next() error {
1617
),
1718
)
1819

19-
return nil
20+
return self.rerenderViewsWithScreenModeDependentContent()
2021
}
2122

2223
func (self *ScreenModeActions) Prev() error {
@@ -27,9 +28,33 @@ func (self *ScreenModeActions) Prev() error {
2728
),
2829
)
2930

31+
return self.rerenderViewsWithScreenModeDependentContent()
32+
}
33+
34+
// these views need to be re-rendered when the screen mode changes. The commits view,
35+
// for example, will show authorship information in half and full screen mode.
36+
func (self *ScreenModeActions) rerenderViewsWithScreenModeDependentContent() error {
37+
for _, context := range self.c.Context().AllList() {
38+
if context.NeedsRerenderOnWidthChange() == types.NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_SCREEN_MODE_CHANGES {
39+
if err := self.rerenderView(context.GetView()); err != nil {
40+
return err
41+
}
42+
}
43+
}
44+
3045
return nil
3146
}
3247

48+
func (self *ScreenModeActions) rerenderView(view *gocui.View) error {
49+
context, ok := self.c.Helpers().View.ContextForView(view.Name())
50+
if !ok {
51+
self.c.Log.Errorf("no context found for view %s", view.Name())
52+
return nil
53+
}
54+
55+
return context.HandleRender()
56+
}
57+
3358
func nextIntInCycle(sl []types.WindowMaximisation, current types.WindowMaximisation) types.WindowMaximisation {
3459
for i, val := range sl {
3560
if val == current {

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/layout.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
7373
}
7474

7575
mustRerender := false
76-
if context.NeedsRerenderOnWidthChange() {
76+
if context.NeedsRerenderOnWidthChange() == types.NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_WIDTH_CHANGES {
7777
// view.Width() returns the width -1 for some reason
7878
oldWidth := view.Width() + 1
7979
newWidth := dimensionsObj.X1 - dimensionsObj.X0 + 2*frameOffset

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/gui/types/context.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ type ParentContexter interface {
3939
GetParentContext() (Context, bool)
4040
}
4141

42+
type NeedsRerenderOnWidthChangeLevel int
43+
44+
const (
45+
// view doesn't render differently when its width changes
46+
NEEDS_RERENDER_ON_WIDTH_CHANGE_NONE NeedsRerenderOnWidthChangeLevel = iota
47+
// view renders differently when its width changes. An example is a view
48+
// that truncates long lines to the view width, e.g. the branches view
49+
NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_WIDTH_CHANGES
50+
// view renders differently only when the screen mode changes
51+
NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_SCREEN_MODE_CHANGES
52+
)
53+
4254
type IBaseContext interface {
4355
HasKeybindings
4456
ParentContexter
@@ -60,8 +72,8 @@ type IBaseContext interface {
6072
// determined independently.
6173
HasControlledBounds() bool
6274

63-
// true if the view needs to be rerendered when its width changes
64-
NeedsRerenderOnWidthChange() bool
75+
// to what extent the view needs to be rerendered when its width changes
76+
NeedsRerenderOnWidthChange() NeedsRerenderOnWidthChangeLevel
6577

6678
// true if the view needs to be rerendered when its height changes
6779
NeedsRerenderOnHeightChange() bool

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)