The data bars' frame cache of shades was keyed by the role a run was drawn in, which could be the cell's base role, so escape analysis moved cellsText's Shade (a whole lipgloss.Style) to the heap for every cell drawn: 700 KB a frame at 200 x 60 on an empty sheet, and plain frames 6 to 14% slower than before the rules and layout merge. barText now keeps the theme's roles apart from the base role, cellsText keeps one shade across its loop and passes it and the base role by pointer, a line's spans and column edges are laid out in the model's scratch, a cell's style is read once per layout, and printable ASCII is measured without grapheme segmentation.
- commit
- fbafdc716f2b09e6a2713b2806392621acdb6f01
- parent
- 48b85b0e7864f579527318a5a400d5168680ce15
- author
- Dami <dami.etoile@gmail.com>
- kind
- refactor risk score 1.6 of 3, Jev's estimate
| M | internal/ | 56 | ++++++++++++++++++++---------- |
| M | internal/ | 5 | ++- |
| M | internal/ | 4 | ++ |
| M | internal/ | 54 | +++++++++++++++++++++++++---- |
| M | internal/ | 10 | +++++ |
| M | internal/ | 17 | +++++---- |
@@ -81,25 +81,34 @@ func (m *Model) barText(sp rowtext.Span, w int, look *sheet.Look, base *lipgloss
8181 st := spanStyle(&m.th, *base, sp)8282 text = &st8383 }84+ // A run is drawn in the cell's role (penBase), its text's style85+ // (penText) or a role of the theme (penRole, role). The theme's roles86+ // are held apart from base, so base, often a shade on the caller's87+ // stack, never flows into the frame's cache of shades.8488 var b strings.Builder8589 var run strings.Builder86- var cur *lipgloss.Style90+ cur, curRole := penBase, (*lipgloss.Style)(nil)8791 flush := func() {8892 switch {8993 case run.Len() == 0:90- case cur == base && !colored:94+ case cur == penBase && !colored:9195 b.WriteString(run.String())92- case cur == text || cur == base: // either may be the cell's own, made for it93- b.WriteString(cur.Render(run.String()))96+ case cur == penBase:97+ b.WriteString(base.Render(run.String()))98+ case cur == penText:99+ b.WriteString(text.Render(run.String()))94100 default: // a theme's role, whose address stays put for the frame95- b.WriteString(m.shade(cur).Wrap(run.String()))101+ b.WriteString(m.shade(curRole).Wrap(run.String()))96102 }97103 run.Reset()98104 }99- put := func(role *lipgloss.Style, s string) {100- if role != cur {105+ put := func(p pen, role *lipgloss.Style, s string) {106+ if p == penText && text == base {107+ p = penBase108+ }109+ if p != cur || role != curRole {101110 flush()102- cur = role111+ cur, curRole = p, role103112 }104113 run.WriteString(s)105114 }@@ -107,15 +116,17 @@ func (m *Model) barText(sp rowtext.Span, w int, look *sheet.Look, base *lipgloss
107116 switch n := coverAt(cover, x); {108117 case ch == "":109118 case icon && x == iconColumn(w):110- put(m.ruleRole(look.IconColor, base, colored), look.Icon)119+ p, role := m.ruleRole(look.IconColor, colored)120+ put(p, role, look.Icon)111121 case n > 0 && ch == " ":112- put(m.ruleRole(look.BarColor, base, colored), theme.Block(n))122+ p, role := m.ruleRole(look.BarColor, colored)123+ put(p, role, theme.Block(n))113124 case n >= 4 && !colored:114- put(&m.th.BarOn[look.BarColor], ch)125+ put(penRole, &m.th.BarOn[look.BarColor], ch)115126 case ch == " ":116- put(base, ch)127+ put(penBase, nil, ch)117128 default:118- put(text, ch)129+ put(penText, nil, ch)119130 }120131 }121132 flush()@@ -142,13 +153,22 @@ func coverAt(cover []int, x int) int {
142153 return 0143154 }144155 145-// ruleRole is the role a rule color draws a glyph in: the color, or the146-// highlight's own role on a highlighted cell.147-func (m *Model) ruleRole(c sheet.Color, base *lipgloss.Style, colored bool) *lipgloss.Style {156+// pen is what barText draws a run of a cell's columns in.157+type pen uint8158+159+const (160+ penBase pen = iota // the cell's role161+ penText // its text's style162+ penRole // a role of the theme, kept for the frame163+)164+165+// ruleRole is the pen a rule color draws a glyph in: the color's role,166+// or the cell's own role on a highlighted cell.167+func (m *Model) ruleRole(c sheet.Color, colored bool) (pen, *lipgloss.Style) {148168 if colored || c == sheet.ColorNone || int(c) >= sheet.NumColors {149- return base169+ return penBase, nil150170 }151- return &m.th.RuleText[c]171+ return penRole, &m.th.RuleText[c]152172 }153173 154174 // chipText draws a dropdown's value as a chip in a column w wide: its@@ -97,9 +97,10 @@ func (lc *lineCtx) merged(a, focus sheet.Addr) bool {
9797 }9898 9999 // layoutLine lays out ncols columns of the line from lo, with the merges100-// crossing them drawn over.100+// crossing them drawn over. The spans are the model's scratch, good101+// until the next line is laid out.101102 func (m *Model) layoutLine(lc *lineCtx, lo, ncols, minCol, maxCol int) []rowtext.Span {102- spans := rowtext.LayoutLine(m.sheet, lc.row, lo, ncols, minCol, maxCol, lc.ln)103+ spans := m.rowScratch.LayoutLine(m.sheet, lc.row, lo, ncols, minCol, maxCol, lc.ln)103104 hi := lo + ncols - 1104105 for _, mg := range lc.merges {105106 from, to := max(lo, mg.From.Col), min(hi, mg.To.Col)@@ -16,6 +16,7 @@ import (
1616 "github.com/FelineStateMachine/012/internal/ui/findbar"1717 "github.com/FelineStateMachine/012/internal/ui/lineedit"1818 "github.com/FelineStateMachine/012/internal/ui/overlay"19+ "github.com/FelineStateMachine/012/internal/ui/rowtext"1920 "github.com/FelineStateMachine/012/internal/ui/tabstrip"2021 "github.com/FelineStateMachine/012/internal/ui/theme"2122 "github.com/FelineStateMachine/012/internal/ui/transfer"@@ -102,6 +103,9 @@ type Model struct {
102103 // mergeLines are the lines merges show their values on, worked out103104 // once a frame (see mergeText).104105 mergeLines map[sheet.Rect][2]int106+ // rowScratch holds the spans of the line of cells being drawn, which107+ // each line lays out in again (see layoutLine).108+ rowScratch rowtext.Scratch105109 106110 // Components. Each owns its state and the handling of the input it107111 // takes; Model routes messages to them and composes what they draw.@@ -56,13 +56,30 @@ type Line struct{ K, N int }
5656 // stays in its column. Text doesn't run on across a border or into5757 // merged cells, whose contents the caller draws (see Merged).5858 func LayoutLine(s *sheet.Sheet, row, lo, ncols, minCol, maxCol int, ln Line) []Span {59- l := rowLayout{s: s, loc: s.Locale(), row: row, lo: lo, hi: lo + ncols - 1, out: make([]Span, ncols), ln: ln, shaped: s.Shaped()}59+ var sc Scratch60+ return sc.LayoutLine(s, row, lo, ncols, minCol, maxCol, ln)61+}62+63+// Scratch keeps the slices a layout works in, so the lines of a frame64+// reuse them rather than allocate their own. The spans a layout through65+// it returns hold until its next layout.66+type Scratch struct {67+ spans []Span68+ x []int69+}70+71+// LayoutLine is the package's LayoutLine, laid out in sc's slices.72+func (sc *Scratch) LayoutLine(s *sheet.Sheet, row, lo, ncols, minCol, maxCol int, ln Line) []Span {73+ sc.spans = resize(sc.spans, ncols)74+ l := rowLayout{s: s, loc: s.Locale(), row: row, lo: lo, hi: lo + ncols - 1, out: sc.spans, ln: ln, shaped: s.Shaped()}6075 for i := range l.out {6176 l.out[i] = Span{Trail: s.ColWidth(lo + i)}6277 }6378 l.first, l.last = l.reach(minCol, maxCol)6479 // x[k] is where column first+k starts, relative to column first.65- l.x = make([]int, l.last-l.first+2)80+ sc.x = resize(sc.x, l.last-l.first+2)81+ l.x = sc.x82+ l.x[0] = 06683 for c := l.first; c <= l.last; c++ {6784 l.x[c-l.first+1] = l.x[c-l.first] + s.ColWidth(c)6885 }@@ -75,6 +92,15 @@ func LayoutLine(s *sheet.Sheet, row, lo, ncols, minCol, maxCol int, ln Line) []S
7592 return l.out7693 }7794 95+// resize returns s n long, reusing its array when it has room; what it96+// holds is the caller's to set.97+func resize[T any](s []T, n int) []T {98+ if cap(s) < n {99+ return make([]T, n)100+ }101+ return s[:n]102+}103+78104 // rowLayout is Layout's work in progress on one row.79105 type rowLayout struct {80106 s *sheet.Sheet@@ -146,8 +172,8 @@ func (l *rowLayout) free(k, side int) bool {
146172 // reaches.147173 func (l *rowLayout) place(c int, v sheet.Value) {148174 x0, x1 := l.col(c)149- text, align, pad := l.display(c, v, x1-x0)150175 style := l.s.CellStyle(sheet.Addr{Col: c, Row: l.row})176+ text, align, pad := l.display(c, v, style.Align, x1-x0)151177 own := style.Wrap != sheet.WrapOverflow // the text stays in its column152178 if l.shaped {153179 var show bool@@ -155,7 +181,7 @@ func (l *rowLayout) place(c int, v sheet.Value) {
155181 return156182 }157183 }158- tw := ansi.StringWidth(text)184+ tw := textWidth(text)159185 start := x0 + pad160186 switch align {161187 case sheet.AlignFill:@@ -184,9 +210,10 @@ func (l *rowLayout) place(c int, v sheet.Value) {
184210 }185211 }186212 187-// display formats the cell in column c for a column w wide, returning188-// the text, its alignment and the padding on its aligned side.189-func (l *rowLayout) display(c int, v sheet.Value, w int) (string, sheet.Align, int) {213+// display formats the cell in column c, which its style aligns as a214+// says, for a column w wide, returning the text, its alignment and the215+// padding on its aligned side.216+func (l *rowLayout) display(c int, v sheet.Value, a sheet.Align, w int) (string, sheet.Align, int) {190217 f := l.s.DisplayFormat(sheet.Addr{Col: c, Row: l.row})191218 text, align := sheet.DisplayIn(v, f, w, l.loc)192219 pad := 1@@ -198,7 +225,7 @@ func (l *rowLayout) display(c int, v sheet.Value, w int) (string, sheet.Align, i
198225 text, pad = wider, 0199226 }200227 }201- if a := l.s.CellStyle(sheet.Addr{Col: c, Row: l.row}).Align; a != sheet.AlignAuto && align != sheet.AlignFill {228+ if a != sheet.AlignAuto && align != sheet.AlignFill {202229 align = a203230 }204231 return text, align, pad@@ -299,3 +326,14 @@ func nextCluster(s string) (n, w int) {
299326 g, w := ansi.FirstGraphemeCluster(s, ansi.GraphemeWidth)300327 return len(g), w301328 }329+330+// textWidth is ansi.StringWidth of s, counted at once when s is331+// printable ASCII, as numbers and most text are: one column a byte.332+func textWidth(s string) int {333+ for i := 0; i < len(s); i++ {334+ if b := s[i]; b < 0x20 || b >= 0x7f {335+ return ansi.StringWidth(s)336+ }337+ }338+ return len(s)339+}@@ -31,3 +31,13 @@ func TestTextCutterMatchesCut(t *testing.T) {
3131 }3232 }3333 }34+35+// textWidth must measure what ansi.StringWidth does, its ASCII shortcut36+// included.37+func TestTextWidthMatchesStringWidth(t *testing.T) {38+ for _, s := range []string{"", "1234.5", " ~plain ascii! ", "tab\there", "\x1b[1mbold\x1b[0m", "café", "東京", "🙂 a", "á"} {39+ if got, want := textWidth(s), ansi.StringWidth(s); got != want {40+ t.Errorf("%q: got %d, want %d", s, got, want)41+ }42+ }43+}@@ -170,11 +170,13 @@ func (m *Model) cellsText(lc *lineCtx, first int, spans []rowtext.Span) string {
170170 if rules {171171 rgb = m.slotColor172172 }173+ // A shade is a whole role, so it is kept across the loop and read174+ // through a pointer rather than zeroed and copied for every cell.175+ var shade theme.Shade173176 for i, sp := range spans {174177 a := sheet.Addr{Col: first + i, Row: row}175178 base, colored := m.cellRole(lc, a, &sp, spills)176179 var look sheet.Look177- var shade theme.Shade178180 shaded := false179181 if piece, typing := m.entryPiece(lc, a); typing { // gridlines.go180182 sp = rowtext.Span{Text: piece}@@ -195,7 +197,7 @@ func (m *Model) cellsText(lc *lineCtx, first int, spans []rowtext.Span) string {
195197 marked := theme.Drawable(base.Inherit(m.th.Copied))196198 base, colored, shaded = &marked, true, false197199 }198- text := m.cellText(lc, a, sp, &look, base, colored, shade, shaded)200+ text := m.cellText(lc, a, sp, &look, base, colored, &shade, shaded)199201 b.WriteString(m.cellMarks(lc, a, text, &look, base, colored))200202 }201203 return b.String()@@ -204,7 +206,7 @@ func (m *Model) cellsText(lc *lineCtx, first int, spans []rowtext.Span) string {
204206 // cellText draws a cell's span on its role: as a rule's shade, with its205207 // data bar or icon, as a dropdown's chip (taking the look's ▾, which the206208 // chip holds), or plainly.207-func (m *Model) cellText(lc *lineCtx, a sheet.Addr, sp rowtext.Span, look *sheet.Look, base *lipgloss.Style, colored bool, shade theme.Shade, shaded bool) string {209+func (m *Model) cellText(lc *lineCtx, a sheet.Addr, sp rowtext.Span, look *sheet.Look, base *lipgloss.Style, colored bool, shade *theme.Shade, shaded bool) string {208210 switch {209211 case look.Bar || look.Icon != "" || look.ValueHidden:210212 return m.barText(sp, m.sheet.ColWidth(a.Col), look, base, colored, m.valueLine(lc, a))@@ -222,7 +224,7 @@ func (m *Model) cellText(lc *lineCtx, a sheet.Addr, sp rowtext.Span, look *sheet
222224 case shaded && plainSpan(sp):223225 return shade.Wrap(strings.Repeat(" ", sp.Lead) + sp.Text + strings.Repeat(" ", sp.Trail))224226 }225- return renderSpan(&m.th, sp, *base, colored)227+ return renderSpan(&m.th, sp, base, colored)226228 }227229 228230 // cellRole is the role a cell is drawn in: the pointer, the selection,@@ -293,7 +295,8 @@ func plainSpan(sp rowtext.Span) bool {
293295 294296 // renderSpan draws a span on base, one of the cell roles. Plain cells295297 // with no text style are written without escape codes.296-func renderSpan(th *theme.Theme, sp rowtext.Span, base lipgloss.Style, colored bool) string {298+// Roles are large, so base is read through a pointer.299+func renderSpan(th *theme.Theme, sp rowtext.Span, base *lipgloss.Style, colored bool) string {297300 lead, trail := strings.Repeat(" ", sp.Lead), strings.Repeat(" ", sp.Trail)298301 plain := plainSpan(sp)299302 switch {@@ -302,9 +305,9 @@ func renderSpan(th *theme.Theme, sp rowtext.Span, base lipgloss.Style, colored b
302305 case plain:303306 return base.Render(lead + sp.Text + trail)304307 case !colored:305- return lead + spanStyle(th, base, sp).Render(sp.Text) + trail308+ return lead + spanStyle(th, *base, sp).Render(sp.Text) + trail306309 }307- return base.Render(lead) + spanStyle(th, base, sp).Render(sp.Text) + base.Render(trail)310+ return base.Render(lead) + spanStyle(th, *base, sp).Render(sp.Text) + base.Render(trail)308311 }309312 310313 // inCellText shows the entry being typed inside the cell, keeping the end