diff --git a/cell.go b/cell.go index 43faedb3..4431b407 100644 --- a/cell.go +++ b/cell.go @@ -16,7 +16,6 @@ package tcell import ( "os" - "reflect" runewidth "github.com/mattn/go-runewidth" ) @@ -32,6 +31,19 @@ type cell struct { lock bool } +func (c *cell) setDirty(dirty bool) { + if dirty { + c.lastMain = rune(0) + } else { + if c.currMain == rune(0) { + c.currMain = ' ' + } + c.lastMain = c.currMain + c.lastComb = append(c.lastComb[:0], c.currComb...) + c.lastStyle = c.currStyle + } +} + // CellBuffer represents a two-dimensional array of character cells. // This is primarily intended for use by Screen implementors; it // contains much of the common code they need. To create one, just @@ -44,6 +56,21 @@ type CellBuffer struct { cells []cell } +// we purposefully don't use slices.Equal in order to stay compatible +// with earlier go versions. +func runeSliceEqual(a, b []rune) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + + return true +} + // SetContent sets the contents (primary rune, combining runes, // and style) for a cell at a given location. If the background or // foreground of the style is set to ColorNone, then the respective @@ -58,13 +85,17 @@ func (cb *CellBuffer) SetContent(x int, y int, // dirty as well as the base cell, to make sure we consider // both cells as dirty together. We only need to do this // if we're changing content - if (c.width > 0) && (mainc != c.currMain || len(combc) != len(c.currComb) || (len(combc) > 0 && !reflect.DeepEqual(combc, c.currComb))) { - for i := 0; i < c.width; i++ { + if c.width > 0 && (mainc != c.currMain || !runeSliceEqual(combc, c.currComb)) { + // Prevent unnecessary boundchecks for first cell, since we already + // received that one. + c.setDirty(true) + for i := 1; i < c.width; i++ { cb.SetDirty(x+i, y, true) } } - c.currComb = append([]rune{}, combc...) + // Reuse slice to prevent allocations + c.currComb = append(c.currComb[:0], combc...) if c.currMain != mainc { c.width = runewidth.RuneWidth(mainc) @@ -148,16 +179,7 @@ func (cb *CellBuffer) Dirty(x, y int) bool { func (cb *CellBuffer) SetDirty(x, y int, dirty bool) { if x >= 0 && y >= 0 && x < cb.w && y < cb.h { c := &cb.cells[(y*cb.w)+x] - if dirty { - c.lastMain = rune(0) - } else { - if c.currMain == rune(0) { - c.currMain = ' ' - } - c.lastMain = c.currMain - c.lastComb = c.currComb - c.lastStyle = c.currStyle - } + c.setDirty(dirty) } } diff --git a/cell_test.go b/cell_test.go new file mode 100644 index 00000000..80b3d464 --- /dev/null +++ b/cell_test.go @@ -0,0 +1,114 @@ +package tcell + +import ( + "reflect" + "testing" + + runewidth "github.com/mattn/go-runewidth" +) + +// SetContent sets the contents (primary rune, combining runes, +// and style) for a cell at a given location. If the background or +// foreground of the style is set to ColorNone, then the respective +// color is left un changed. +func (cb *CellBuffer) SetContentOld(x int, y int, + mainc rune, combc []rune, style Style, +) { + if x >= 0 && y >= 0 && x < cb.w && y < cb.h { + c := &cb.cells[(y*cb.w)+x] + + // Wide characters: we want to mark the "wide" cells + // dirty as well as the base cell, to make sure we consider + // both cells as dirty together. We only need to do this + // if we're changing content + if (c.width > 0) && (mainc != c.currMain || len(combc) != len(c.currComb) || (len(combc) > 0 && !reflect.DeepEqual(combc, c.currComb))) { + for i := 0; i < c.width; i++ { + cb.SetDirty(x+i, y, true) + } + } + + c.currComb = append([]rune{}, combc...) + + if c.currMain != mainc { + c.width = runewidth.RuneWidth(mainc) + } + c.currMain = mainc + if style.fg == ColorNone { + style.fg = c.currStyle.fg + } + if style.bg == ColorNone { + style.bg = c.currStyle.bg + } + c.currStyle = style + } +} + +func Benchmark_SetContentOld_ascii(b *testing.B) { + buffer := &CellBuffer{} + buffer.Resize(100, 100) + for i := 0; i < b.N; i++ { + for w := 0; w < 100; w++ { + for h := 0; h < 100; h++ { + buffer.SetContentOld(w, h, 'a', nil, StyleDefault) + } + } + for w := 0; w < 100; w++ { + for h := 0; h < 100; h++ { + buffer.SetContentOld(w, h, 'b', nil, StyleDefault) + } + } + } +} + +func Benchmark_SetContent_ascii(b *testing.B) { + buffer := &CellBuffer{} + buffer.Resize(100, 100) + for i := 0; i < b.N; i++ { + for w := 0; w < 100; w++ { + for h := 0; h < 100; h++ { + buffer.SetContent(w, h, 'a', nil, StyleDefault) + } + } + for w := 0; w < 100; w++ { + for h := 0; h < 100; h++ { + buffer.SetContent(w, h, 'b', nil, StyleDefault) + } + } + } +} + +func Benchmark_SetContentOld(b *testing.B) { + buffer := &CellBuffer{} + buffer.Resize(100, 100) + flag := []rune("🇦🇺") + for i := 0; i < b.N; i++ { + for w := 0; w < 100; w++ { + for h := 0; h < 100; h++ { + buffer.SetContentOld(w, h, 'a', nil, StyleDefault) + } + } + for w := 0; w < 100; w++ { + for h := 0; h < 100; h++ { + buffer.SetContentOld(w, h, flag[0], flag[1:], StyleDefault) + } + } + } +} + +func Benchmark_SetContent(b *testing.B) { + buffer := &CellBuffer{} + buffer.Resize(100, 100) + flag := []rune("🇦🇺") + for i := 0; i < b.N; i++ { + for w := 0; w < 100; w++ { + for h := 0; h < 100; h++ { + buffer.SetContent(w, h, 'a', nil, StyleDefault) + } + } + for w := 0; w < 100; w++ { + for h := 0; h < 100; h++ { + buffer.SetContent(w, h, flag[0], flag[1:], StyleDefault) + } + } + } +}