merge master, resolve bench_test conflicts

This commit is contained in:
Daisuke Maki 2026-02-22 00:31:46 +09:00
commit bf8595be01
7 changed files with 143 additions and 15 deletions

View file

@ -111,3 +111,33 @@ func BenchmarkRegexpFilterMultiCycle(b *testing.B) {
}
}
}
// BenchmarkRegexpFilterOverlapping benchmarks the regexp filter with a query
// that produces overlapping match ranges, exercising the mergeMatches path.
// Each line contains repeating "aabb" patterns; the query terms "aab" and "abb"
// produce match ranges that overlap (e.g. [0,3] and [1,4]), forcing mergeMatches
// to be called on every line. With 10k lines the aggregate allocation difference
// from in-place vs make([]int,2) becomes measurable.
func BenchmarkRegexpFilterOverlapping(b *testing.B) {
// Build a line with many "aabb" repeats so that "aab" and "abb" each match
// many times with overlapping ranges between the two terms.
base := strings.Repeat("aabb", 20) // 80 chars
lines := make([]line.Line, 10_000)
for i := range lines {
lines[i] = line.NewRaw(uint64(i), base, false, false)
}
f := NewIgnoreCase()
// "aab" and "abb" share the middle "ab" in each "aabb" group, so their
// match ranges overlap after sorting by start position.
query := "aab abb"
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
ctx, cancel := context.WithTimeout(f.NewContext(context.Background(), query), 10*time.Second)
ch := make(chan line.Line, len(lines))
_ = f.Apply(ctx, lines, pipeline.ChanOutput(ch))
cancel()
}
}

View file

@ -93,15 +93,9 @@ func matchOverlaps(a []int, b []int) bool {
}
// mergeMatches combines two overlapping match ranges into a single range
// spanning both.
// spanning both. It mutates and returns a to avoid a heap allocation.
func mergeMatches(a []int, b []int) []int {
ret := make([]int, 2)
// Note: In practice this should never happen
// because we're sorting by N[0] before calling
// this routine, but for completeness' sake...
ret[0] = min(a[0], b[0])
ret[1] = max(a[1], b[1])
return ret
a[0] = min(a[0], b[0])
a[1] = max(a[1], b[1])
return a
}

View file

@ -634,6 +634,31 @@ func testFuzzyMatch(octx context.Context, t *testing.T, filter Filter) {
}
}
func TestMergeMatches(t *testing.T) {
t.Parallel()
tests := []struct {
name string
a []int
b []int
want []int
}{
{"a before b overlapping", []int{1, 5}, []int{3, 7}, []int{1, 7}},
{"b before a overlapping", []int{3, 7}, []int{1, 5}, []int{1, 7}},
{"identical ranges", []int{2, 4}, []int{2, 4}, []int{2, 4}},
{"a contains b", []int{0, 10}, []int{3, 7}, []int{0, 10}},
{"adjacent ranges", []int{0, 3}, []int{3, 6}, []int{0, 6}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := mergeMatches(tt.a, tt.b)
require.Equal(t, tt.want, got)
// Verify in-place mutation: got should be the same slice as a
require.Same(t, &tt.a[0], &got[0], "mergeMatches should mutate a in place")
})
}
}
// TestMatchAcrossANSIColorBoundary verifies that filter queries match
// against the ANSI-stripped text, so a pattern spanning characters
// rendered in different colors still produces a match.

View file

@ -0,0 +1,40 @@
package util
import (
"fmt"
"testing"
)
func BenchmarkStripANSISequenceBulk(b *testing.B) {
b.Run("10k_plain_lines", func(b *testing.B) {
lines := make([]string, 10_000)
for i := range lines {
lines[i] = fmt.Sprintf("line %d: this is a typical line without any ansi codes at all padding", i)
}
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
for _, l := range lines {
StripANSISequence(l)
}
}
})
b.Run("10k_mixed_95pct_plain", func(b *testing.B) {
lines := make([]string, 10_000)
for i := range lines {
if i%20 == 0 {
lines[i] = fmt.Sprintf("\x1b[31mline %d\x1b[0m: this has ansi codes in it for color", i)
} else {
lines[i] = fmt.Sprintf("line %d: this is a typical line without any ansi codes at all padding", i)
}
}
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
for _, l := range lines {
StripANSISequence(l)
}
}
})
}

View file

@ -3,6 +3,7 @@ package util
import (
"errors"
"regexp"
"strings"
"unicode"
)
@ -44,8 +45,13 @@ func ContainsUpper(query string) bool {
// Global var used to strips ansi sequences
var reANSIEscapeChars = regexp.MustCompile("\x1B\\[(?:[0-9]{1,2}(?:;[0-9]{1,2})?)*[a-zA-Z]")
// StripANSISequence strips ANSI escape sequences from the given string
// StripANSISequence strips ANSI escape sequences from the given string.
// Fast-path: if the string contains no ESC byte, return it unchanged
// to avoid a regexp allocation.
func StripANSISequence(s string) string {
if !strings.Contains(s, "\x1b") {
return s
}
return reANSIEscapeChars.ReplaceAllString(s, "")
}

View file

@ -45,3 +45,32 @@ func BenchmarkMatchedLifecycle(b *testing.B) {
}
})
}
// BenchmarkNewRawAndDisplay benchmarks creating Raw lines and calling
// DisplayString(), simulating peco's startup + render path for 10k lines.
func BenchmarkNewRawAndDisplay(b *testing.B) {
texts := make([]string, 10_000)
for i := range texts {
texts[i] = fmt.Sprintf("line %d: this is a typical plain text line without any ansi codes", i)
}
b.Run("enableANSI_true_plain_text", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
for i, t := range texts {
r := NewRaw(uint64(i), t, false, true)
_ = r.DisplayString()
}
}
})
b.Run("enableANSI_false_plain_text", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
for i, t := range texts {
r := NewRaw(uint64(i), t, false, false)
_ = r.DisplayString()
}
}
})
}

View file

@ -77,8 +77,12 @@ func NewRaw(id uint64, v string, enableSep bool, enableANSI bool) *Raw {
src = rl.buf[:rl.sepLoc]
}
r := ansi.Parse(src)
rl.displayString = r.Stripped
rl.ansiAttrs = r.Attrs
// Only store displayString when it actually differs from buf
// (i.e. when there's a separator or ANSI attributes)
if rl.sepLoc > -1 || r.Attrs != nil {
rl.displayString = r.Stripped
}
}
return rl
@ -121,10 +125,10 @@ func (rl *Raw) DisplayString() string {
if i := rl.sepLoc; i > -1 {
rl.displayString = util.StripANSISequence(rl.buf[:i])
} else {
rl.displayString = util.StripANSISequence(rl.buf)
return rl.displayString
}
return rl.displayString
// No separator: strip ANSI (fast-path returns buf unchanged when no ESC present)
return util.StripANSISequence(rl.buf)
}
// ANSIAttrs returns the run-length encoded ANSI attributes for this line.