Skip to content

Commit c91d96c

Browse files
committed
[feat] improve memory usage by bit packing rune widths
1 parent 1c40173 commit c91d96c

File tree

2 files changed

+35
-15
lines changed

2 files changed

+35
-15
lines changed

internal/viewport/linebuffer/linebuffer.go

+34-14
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
type LineBuffer struct {
1515
line string // underlying string with ansi codes. utf-8 encoded bytes
1616
lineNoAnsi string // line without ansi codes. utf-8 encoded bytes
17-
lineNoAnsiRuneWidths []uint8 // terminal cell widths
17+
lineNoAnsiRuneWidths []uint8 // packed terminal cell widths, 4 widths per byte (2 bits each)
1818
ansiCodeIndexes [][]uint32 // slice of startByte, endByte indexes of ansi codes
1919
numNoAnsiRunes int // number of runes in lineNoAnsi
2020
totalWidth int // total width in terminal cells
@@ -73,15 +73,24 @@ func New(line string) LineBuffer {
7373
lb.sparseRuneIdxToNoAnsiByteOffset = make([]uint32, sparseLen)
7474
lb.sparseLineNoAnsiCumRuneWidths = make([]uint32, sparseLen)
7575

76-
lb.lineNoAnsiRuneWidths = make([]uint8, numRunes)
76+
// calculate size needed for packed rune widths (4 widths per byte)
77+
packedLen := (numRunes + 3) / 4
78+
lb.lineNoAnsiRuneWidths = make([]uint8, packedLen)
7779

7880
var currentOffset uint32
7981
var cumWidth uint32
8082
runeIdx := 0
8183
for byteOffset := 0; byteOffset < len(lb.lineNoAnsi); {
8284
r, runeNumBytes := utf8.DecodeRuneInString(lb.lineNoAnsi[byteOffset:])
8385
width := uint8(runewidth.RuneWidth(r))
84-
lb.lineNoAnsiRuneWidths[runeIdx] = width
86+
87+
// pack 4 widths per byte (2 bits each)
88+
packedIdx := runeIdx / 4
89+
bitPos := (runeIdx % 4) * 2
90+
// clear the 2 bits at the position and set the new width
91+
lb.lineNoAnsiRuneWidths[packedIdx] &= ^(uint8(3) << bitPos)
92+
lb.lineNoAnsiRuneWidths[packedIdx] |= width << bitPos
93+
8594
cumWidth += uint32(width)
8695
if runeIdx%lb.sparsity == 0 {
8796
lb.sparseRuneIdxToNoAnsiByteOffset[runeIdx/lb.sparsity] = currentOffset
@@ -123,7 +132,7 @@ func (l LineBuffer) Take(
123132
widthToLeft = min(widthToLeft, l.Width())
124133
startRuneIdx := l.findRuneIndexWithWidthToLeft(widthToLeft)
125134

126-
if startRuneIdx >= len(l.lineNoAnsiRuneWidths) || takeWidth == 0 {
135+
if startRuneIdx >= l.numNoAnsiRunes || takeWidth == 0 {
127136
return "", 0
128137
}
129138

@@ -133,9 +142,9 @@ func (l LineBuffer) Take(
133142
startByteOffset := l.getByteOffsetAtRuneIdx(startRuneIdx)
134143

135144
runesWritten := 0
136-
for ; remainingWidth > 0 && leftRuneIdx < len(l.lineNoAnsiRuneWidths); leftRuneIdx++ {
145+
for ; remainingWidth > 0 && leftRuneIdx < l.numNoAnsiRunes; leftRuneIdx++ {
137146
r := l.runeAt(leftRuneIdx)
138-
runeWidth := l.lineNoAnsiRuneWidths[leftRuneIdx]
147+
runeWidth := l.getRuneWidth(leftRuneIdx)
139148
if int(runeWidth) > remainingWidth {
140149
break
141150
}
@@ -157,7 +166,7 @@ func (l LineBuffer) Take(
157166

158167
// write the subsequent zero-width runes, e.g. the accent on an 'e'
159168
if result.Len() > 0 {
160-
for ; leftRuneIdx < len(l.lineNoAnsiRuneWidths); leftRuneIdx++ {
169+
for ; leftRuneIdx < l.numNoAnsiRunes; leftRuneIdx++ {
161170
r := l.runeAt(leftRuneIdx)
162171
if runewidth.RuneWidth(r) == 0 {
163172
result.WriteRune(r)
@@ -175,7 +184,7 @@ func (l LineBuffer) Take(
175184
}
176185

177186
// apply left/right line continuation indicators
178-
if len(continuation) > 0 && (startRuneIdx > 0 || leftRuneIdx < len(l.lineNoAnsiRuneWidths)) {
187+
if len(continuation) > 0 && (startRuneIdx > 0 || leftRuneIdx < l.numNoAnsiRunes) {
179188
continuationRunes := []rune(continuation)
180189

181190
// if more runes to the left of the result, replace start runes with continuation indicator
@@ -184,7 +193,7 @@ func (l LineBuffer) Take(
184193
}
185194

186195
// if more runes to the right, replace final runes in result with continuation indicator
187-
if leftRuneIdx < len(l.lineNoAnsiRuneWidths) {
196+
if leftRuneIdx < l.numNoAnsiRunes {
188197
res = replaceEndWithContinuation(res, continuationRunes)
189198
}
190199
}
@@ -223,7 +232,7 @@ func (l LineBuffer) WrappedLines(
223232
return []string{l.line}
224233
}
225234

226-
lastRuneIdx := len(l.lineNoAnsiRuneWidths) - 1
235+
lastRuneIdx := l.numNoAnsiRunes - 1
227236
totalWidth := l.getCumulativeWidthAtRuneIdx(lastRuneIdx)
228237
totalLines := (int(totalWidth) + width - 1) / width
229238
return getWrappedLines(
@@ -289,6 +298,17 @@ func (l LineBuffer) getByteOffsetAtRuneIdx(runeIdx int) uint32 {
289298
return byteOffset
290299
}
291300

301+
// getRuneWidth extracts the width of a rune from the packed array
302+
func (l LineBuffer) getRuneWidth(runeIdx int) uint8 {
303+
if runeIdx < 0 || runeIdx >= l.numNoAnsiRunes {
304+
return 0
305+
}
306+
307+
packedIdx := runeIdx / 4
308+
bitPos := (runeIdx % 4) * 2
309+
return (l.lineNoAnsiRuneWidths[packedIdx] >> bitPos) & 3
310+
}
311+
292312
func (l LineBuffer) getCumulativeWidthAtRuneIdx(runeIdx int) uint32 {
293313
if runeIdx < 0 {
294314
return 0
@@ -308,7 +328,7 @@ func (l LineBuffer) getCumulativeWidthAtRuneIdx(runeIdx int) uint32 {
308328
// sum the widths from the last stored point to our target index
309329
var additionalWidth uint32
310330
for i := baseRuneIdx + 1; i <= runeIdx; i++ {
311-
additionalWidth += uint32(l.lineNoAnsiRuneWidths[i])
331+
additionalWidth += uint32(l.getRuneWidth(i))
312332
}
313333

314334
return l.sparseLineNoAnsiCumRuneWidths[sparseIdx] + additionalWidth
@@ -319,16 +339,16 @@ func (l LineBuffer) findRuneIndexWithWidthToLeft(widthToLeft int) int {
319339
if widthToLeft < 0 {
320340
panic("widthToLeft less than 0")
321341
}
322-
if widthToLeft == 0 || len(l.lineNoAnsiRuneWidths) == 0 {
342+
if widthToLeft == 0 || l.numNoAnsiRunes == 0 {
323343
return 0
324344
}
325345
if widthToLeft > l.Width() {
326346
panic("widthToLeft greater than total width")
327347
}
328348

329-
left, right := 0, len(l.lineNoAnsiRuneWidths)-1
349+
left, right := 0, l.numNoAnsiRunes-1
330350
if l.getCumulativeWidthAtRuneIdx(right) < uint32(widthToLeft) {
331-
return len(l.lineNoAnsiRuneWidths)
351+
return l.numNoAnsiRunes
332352
}
333353

334354
for left < right {

internal/viewport/linebuffer/util.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ func getBytesRightOfWidth(nBytes int, buffers []LineBuffer, endBufferIdx int, wi
580580
currentBufferWidth := currentBuffer.Width()
581581
widthToLeft := currentBufferWidth - widthToRight
582582
startRuneIdx := currentBuffer.findRuneIndexWithWidthToLeft(widthToLeft)
583-
if startRuneIdx < len(currentBuffer.lineNoAnsiRuneWidths) {
583+
if startRuneIdx < currentBuffer.numNoAnsiRunes {
584584
startByteOffset := currentBuffer.getByteOffsetAtRuneIdx(startRuneIdx)
585585
noAnsiContent := currentBuffer.lineNoAnsi[startByteOffset:]
586586
if len(noAnsiContent) >= nBytes {

0 commit comments

Comments
 (0)