From a3df10be92e2c046fef6fa740a9c93b7d7d85d65 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Sun, 12 Nov 2023 09:31:15 +0100 Subject: [PATCH] Improve BenchmarkHighlightedSearch performance By about 18%. By working on uints instead of strings. --- m/ansiTokenizer.go | 206 ++++++++++++++++++++++++---------------- m/ansiTokenizer_test.go | 54 ++--------- 2 files changed, 131 insertions(+), 129 deletions(-) diff --git a/m/ansiTokenizer.go b/m/ansiTokenizer.go index fa261b79..e4094187 100644 --- a/m/ansiTokenizer.go +++ b/m/ansiTokenizer.go @@ -423,6 +423,55 @@ type _StyledString struct { Style twin.Style } +func splitIntoNumbers(s string) ([]uint, error) { + // "5" gives us the best numbers from BenchmarkHighlightedSearch. Higher + // gives us larger memory allocations for no extra performance, lower gives + // us more memory allocations and lower performance. + // + // To repro the tuning: + // go test -benchmem -run='^$' -bench=BenchmarkHighlightedSearch . ./... + numbers := make([]uint, 0, 5) + + afterLastSeparator := 0 + for i, char := range s { + if char >= '0' && char <= '9' { + continue + } + + if char == ';' || char == ':' { + numberString := s[afterLastSeparator:i] + if numberString == "" { + numbers = append(numbers, 0) + continue + } + + number, err := strconv.ParseUint(numberString, 10, 64) + if err != nil { + return nil, err + } + numbers = append(numbers, uint(number)) + afterLastSeparator = i + 1 + continue + } + + return nil, fmt.Errorf("Unrecognized character in <%s>: %c", s, char) + } + + // Now we have to handle the last number + numberString := s[afterLastSeparator:] + if numberString == "" { + numbers = append(numbers, 0) + return numbers, nil + } + number, err := strconv.ParseUint(numberString, 10, 64) + if err != nil { + return nil, err + } + numbers = append(numbers, uint(number)) + + return numbers, nil +} + // rawUpdateStyle parses a string of the form "33m" into changes to style. This // is what comes after ESC[ in an ANSI SGR sequence. func rawUpdateStyle(style twin.Style, escapeSequenceWithoutHeader string) (twin.Style, error) { @@ -433,63 +482,64 @@ func rawUpdateStyle(style twin.Style, escapeSequenceWithoutHeader string) (twin. return style, fmt.Errorf("escape sequence does not end with 'm': %s", escapeSequenceWithoutHeader) } - numbers := strings.FieldsFunc(escapeSequenceWithoutHeader[:len(escapeSequenceWithoutHeader)-1], func(r rune) bool { - return r == ';' || r == ':' - }) + numbers, err := splitIntoNumbers(escapeSequenceWithoutHeader[:len(escapeSequenceWithoutHeader)-1]) + if err != nil { + return style, fmt.Errorf("splitIntoNumbers: %w", err) + } index := 0 for index < len(numbers) { number := numbers[index] index++ - switch strings.TrimLeft(number, "0") { - case "": + switch number { + case 0: style = twin.StyleDefault - case "1": + case 1: style = style.WithAttr(twin.AttrBold) - case "2": + case 2: style = style.WithAttr(twin.AttrDim) - case "3": + case 3: style = style.WithAttr(twin.AttrItalic) - case "4": + case 4: style = style.WithAttr(twin.AttrUnderline) - case "7": + case 7: style = style.WithAttr(twin.AttrReverse) - case "22": + case 22: style = style.WithoutAttr(twin.AttrBold).WithoutAttr(twin.AttrDim) - case "23": + case 23: style = style.WithoutAttr(twin.AttrItalic) - case "24": + case 24: style = style.WithoutAttr(twin.AttrUnderline) - case "27": + case 27: style = style.WithoutAttr(twin.AttrReverse) // Foreground colors, https://pkg.go.dev/github.com/gdamore/tcell#Color - case "30": + case 30: style = style.Foreground(twin.NewColor16(0)) - case "31": + case 31: style = style.Foreground(twin.NewColor16(1)) - case "32": + case 32: style = style.Foreground(twin.NewColor16(2)) - case "33": + case 33: style = style.Foreground(twin.NewColor16(3)) - case "34": + case 34: style = style.Foreground(twin.NewColor16(4)) - case "35": + case 35: style = style.Foreground(twin.NewColor16(5)) - case "36": + case 36: style = style.Foreground(twin.NewColor16(6)) - case "37": + case 37: style = style.Foreground(twin.NewColor16(7)) - case "38": + case 38: var err error var color *twin.Color index, color, err = consumeCompositeColor(numbers, index-1) @@ -497,27 +547,27 @@ func rawUpdateStyle(style twin.Style, escapeSequenceWithoutHeader string) (twin. return style, fmt.Errorf("Foreground: %w", err) } style = style.Foreground(*color) - case "39": + case 39: style = style.Foreground(twin.ColorDefault) // Background colors, see https://pkg.go.dev/github.com/gdamore/Color - case "40": + case 40: style = style.Background(twin.NewColor16(0)) - case "41": + case 41: style = style.Background(twin.NewColor16(1)) - case "42": + case 42: style = style.Background(twin.NewColor16(2)) - case "43": + case 43: style = style.Background(twin.NewColor16(3)) - case "44": + case 44: style = style.Background(twin.NewColor16(4)) - case "45": + case 45: style = style.Background(twin.NewColor16(5)) - case "46": + case 46: style = style.Background(twin.NewColor16(6)) - case "47": + case 47: style = style.Background(twin.NewColor16(7)) - case "48": + case 48: var err error var color *twin.Color index, color, err = consumeCompositeColor(numbers, index-1) @@ -525,7 +575,7 @@ func rawUpdateStyle(style twin.Style, escapeSequenceWithoutHeader string) (twin. return style, fmt.Errorf("Background: %w", err) } style = style.Background(*color) - case "49": + case 49: style = style.Background(twin.ColorDefault) // Bright foreground colors: see https://pkg.go.dev/github.com/gdamore/Color @@ -534,61 +584,67 @@ func rawUpdateStyle(style twin.Style, escapeSequenceWithoutHeader string) (twin. // 10.15.4 that's how they seem to handle this, tested with: // * TERM=xterm-256color // * TERM=screen-256color - case "90": + case 90: style = style.Foreground(twin.NewColor16(8)) - case "91": + case 91: style = style.Foreground(twin.NewColor16(9)) - case "92": + case 92: style = style.Foreground(twin.NewColor16(10)) - case "93": + case 93: style = style.Foreground(twin.NewColor16(11)) - case "94": + case 94: style = style.Foreground(twin.NewColor16(12)) - case "95": + case 95: style = style.Foreground(twin.NewColor16(13)) - case "96": + case 96: style = style.Foreground(twin.NewColor16(14)) - case "97": + case 97: style = style.Foreground(twin.NewColor16(15)) - case "100": + case 100: style = style.Background(twin.NewColor16(8)) - case "101": + case 101: style = style.Background(twin.NewColor16(9)) - case "102": + case 102: style = style.Background(twin.NewColor16(10)) - case "103": + case 103: style = style.Background(twin.NewColor16(11)) - case "104": + case 104: style = style.Background(twin.NewColor16(12)) - case "105": + case 105: style = style.Background(twin.NewColor16(13)) - case "106": + case 106: style = style.Background(twin.NewColor16(14)) - case "107": + case 107: style = style.Background(twin.NewColor16(15)) default: - return style, fmt.Errorf("Unrecognized ANSI SGR code <%s>", number) + return style, fmt.Errorf("Unrecognized ANSI SGR code <%d>", number) } } return style, nil } +func joinUints(ints []uint) string { + joinedWithBrackets := strings.ReplaceAll(fmt.Sprint(ints), " ", ";") + joined := joinedWithBrackets[1 : len(joinedWithBrackets)-1] + return joined +} + // numbers is a list of numbers from a ANSI SGR string // index points to either 38 or 48 in that string // // This method will return: // * The first index in the string that this function did not consume // * A color value that can be applied to a style -func consumeCompositeColor(numbers []string, index int) (int, *twin.Color, error) { +func consumeCompositeColor(numbers []uint, index int) (int, *twin.Color, error) { baseIndex := index - if numbers[index] != "38" && numbers[index] != "48" { + if numbers[index] != 38 && numbers[index] != 48 { err := fmt.Errorf( - "unknown start of color sequence <%s>, expected 38 (foreground) or 48 (background): ", + "unknown start of color sequence <%d>, expected 38 (foreground) or 48 (background): ", numbers[index], - strings.Join(numbers[baseIndex:], ";")) + joinUints(numbers[baseIndex:])) return -1, nil, err } @@ -596,30 +652,27 @@ func consumeCompositeColor(numbers []string, index int) (int, *twin.Color, error if index >= len(numbers) { err := fmt.Errorf( "incomplete color sequence: ", - strings.Join(numbers[baseIndex:], ";")) + joinUints(numbers[baseIndex:])) return -1, nil, err } - if numbers[index] == "5" { + if numbers[index] == 5 { // Handle 8 bit color index++ if index >= len(numbers) { err := fmt.Errorf( "incomplete 8 bit color sequence: ", - strings.Join(numbers[baseIndex:], ";")) + joinUints(numbers[baseIndex:])) return -1, nil, err } - colorNumber, err := strconv.Atoi(numbers[index]) - if err != nil { - return -1, nil, err - } + colorNumber := numbers[index] colorValue := twin.NewColor256(uint8(colorNumber)) return index + 1, &colorValue, nil } - if numbers[index] == "2" { + if numbers[index] == 2 { // Handle 24 bit color rIndex := index + 1 gIndex := index + 2 @@ -627,35 +680,24 @@ func consumeCompositeColor(numbers []string, index int) (int, *twin.Color, error if bIndex >= len(numbers) { err := fmt.Errorf( "incomplete 24 bit color sequence, expected N8;2;R;G;Bm: ", - strings.Join(numbers[baseIndex:], ";")) - return -1, nil, err - } + joinUints(numbers[baseIndex:])) - rValueX, err := strconv.ParseInt(numbers[rIndex], 10, 32) - if err != nil { return -1, nil, err } - rValue := uint8(rValueX) - gValueX, err := strconv.Atoi(numbers[gIndex]) - if err != nil { - return -1, nil, err - } - gValue := uint8(gValueX) - - bValueX, err := strconv.Atoi(numbers[bIndex]) - if err != nil { - return -1, nil, err - } - bValue := uint8(bValueX) + rValue := uint8(numbers[rIndex]) + gValue := uint8(numbers[gIndex]) + bValue := uint8(numbers[bIndex]) colorValue := twin.NewColor24Bit(rValue, gValue, bValue) + return bIndex + 1, &colorValue, nil } err := fmt.Errorf( - "unknown color type <%s>, expected 5 (8 bit color) or 2 (24 bit color): ", + "unknown color type <%d>, expected 5 (8 bit color) or 2 (24 bit color): ", numbers[index], - strings.Join(numbers[baseIndex:], ";")) + joinUints(numbers[baseIndex:])) + return -1, nil, err } diff --git a/m/ansiTokenizer_test.go b/m/ansiTokenizer_test.go index 59d11b93..f2cd1c5a 100644 --- a/m/ansiTokenizer_test.go +++ b/m/ansiTokenizer_test.go @@ -146,87 +146,47 @@ func TestManPages(t *testing.T) { func TestConsumeCompositeColorHappy(t *testing.T) { // 8 bit color // Example from: https://github.com/walles/moar/issues/14 - newIndex, color, err := consumeCompositeColor([]string{"38", "5", "74"}, 0) + newIndex, color, err := consumeCompositeColor([]uint{38, 5, 74}, 0) assert.NilError(t, err) assert.Equal(t, newIndex, 3) assert.Equal(t, *color, twin.NewColor256(74)) // 24 bit color - newIndex, color, err = consumeCompositeColor([]string{"38", "2", "10", "20", "30"}, 0) + newIndex, color, err = consumeCompositeColor([]uint{38, 2, 10, 20, 30}, 0) assert.NilError(t, err) assert.Equal(t, newIndex, 5) assert.Equal(t, *color, twin.NewColor24Bit(10, 20, 30)) } -func TestConsumeCompositeColorHappyMidSequence(t *testing.T) { - // 8 bit color - // Example from: https://github.com/walles/moar/issues/14 - newIndex, color, err := consumeCompositeColor([]string{"whatever", "38", "5", "74"}, 1) - assert.NilError(t, err) - assert.Equal(t, newIndex, 4) - assert.Equal(t, *color, twin.NewColor256(74)) - - // 24 bit color - newIndex, color, err = consumeCompositeColor([]string{"whatever", "38", "2", "10", "20", "30"}, 1) - assert.NilError(t, err) - assert.Equal(t, newIndex, 6) - assert.Equal(t, *color, twin.NewColor24Bit(10, 20, 30)) -} - func TestConsumeCompositeColorBadPrefix(t *testing.T) { // 8 bit color // Example from: https://github.com/walles/moar/issues/14 - _, color, err := consumeCompositeColor([]string{"29"}, 0) - assert.Equal(t, err.Error(), "unknown start of color sequence <29>, expected 38 (foreground) or 48 (background): ") - assert.Assert(t, color == nil) - - // Same test but mid-sequence, with initial index > 0 - _, color, err = consumeCompositeColor([]string{"whatever", "29"}, 1) + _, color, err := consumeCompositeColor([]uint{29}, 0) assert.Equal(t, err.Error(), "unknown start of color sequence <29>, expected 38 (foreground) or 48 (background): ") assert.Assert(t, color == nil) } func TestConsumeCompositeColorBadType(t *testing.T) { - _, color, err := consumeCompositeColor([]string{"38", "4"}, 0) + _, color, err := consumeCompositeColor([]uint{38, 4}, 0) // https://en.wikipedia.org/wiki/ANSI_escape_code#Colors assert.Equal(t, err.Error(), "unknown color type <4>, expected 5 (8 bit color) or 2 (24 bit color): ") assert.Assert(t, color == nil) - - // Same test but mid-sequence, with initial index > 0 - _, color, err = consumeCompositeColor([]string{"whatever", "38", "4"}, 1) - assert.Equal(t, err.Error(), "unknown color type <4>, expected 5 (8 bit color) or 2 (24 bit color): ") - assert.Assert(t, color == nil) } func TestConsumeCompositeColorIncomplete(t *testing.T) { - _, color, err := consumeCompositeColor([]string{"38"}, 0) - assert.Equal(t, err.Error(), "incomplete color sequence: ") - assert.Assert(t, color == nil) - - // Same test, mid-sequence - _, color, err = consumeCompositeColor([]string{"whatever", "38"}, 1) + _, color, err := consumeCompositeColor([]uint{38}, 0) assert.Equal(t, err.Error(), "incomplete color sequence: ") assert.Assert(t, color == nil) } func TestConsumeCompositeColorIncomplete8Bit(t *testing.T) { - _, color, err := consumeCompositeColor([]string{"38", "5"}, 0) - assert.Equal(t, err.Error(), "incomplete 8 bit color sequence: ") - assert.Assert(t, color == nil) - - // Same test, mid-sequence - _, color, err = consumeCompositeColor([]string{"whatever", "38", "5"}, 1) + _, color, err := consumeCompositeColor([]uint{38, 5}, 0) assert.Equal(t, err.Error(), "incomplete 8 bit color sequence: ") assert.Assert(t, color == nil) } func TestConsumeCompositeColorIncomplete24Bit(t *testing.T) { - _, color, err := consumeCompositeColor([]string{"38", "2", "10", "20"}, 0) - assert.Equal(t, err.Error(), "incomplete 24 bit color sequence, expected N8;2;R;G;Bm: ") - assert.Assert(t, color == nil) - - // Same test, mid-sequence - _, color, err = consumeCompositeColor([]string{"whatever", "38", "2", "10", "20"}, 1) + _, color, err := consumeCompositeColor([]uint{38, 2, 10, 20}, 0) assert.Equal(t, err.Error(), "incomplete 24 bit color sequence, expected N8;2;R;G;Bm: ") assert.Assert(t, color == nil) }