diff --git a/filter/bench_test.go b/filter/bench_test.go index 24f2949..61c9992 100644 --- a/filter/bench_test.go +++ b/filter/bench_test.go @@ -2,15 +2,17 @@ package filter import ( "context" + "strings" "testing" "time" + "github.com/peco/peco/internal/util" "github.com/peco/peco/line" "github.com/peco/peco/pipeline" ) // BenchmarkFuzzyFilter benchmarks the fuzzy filter to measure allocations -// in the hot path. +// in the hot path (CaseInsensitiveIndexFunc closure, match slices, etc). func BenchmarkFuzzyFilter(b *testing.B) { lines := make([]line.Line, 200) for i := range lines { @@ -18,7 +20,7 @@ func BenchmarkFuzzyFilter(b *testing.B) { } f := NewFuzzy(false) - query := "trfp" + query := "trfp" // matches scattered chars across the line b.ResetTimer() b.ReportAllocs() @@ -29,3 +31,28 @@ func BenchmarkFuzzyFilter(b *testing.B) { cancel() } } + +// BenchmarkCaseInsensitiveIndexClosure measures the old closure-based approach. +func BenchmarkCaseInsensitiveIndexClosure(b *testing.B) { + txt := "this is a reasonably long line for benchmarking" + r := 'r' + + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { + fn := util.CaseInsensitiveIndexFunc(r) + strings.IndexFunc(txt, fn) + } +} + +// BenchmarkCaseInsensitiveIndexDirect measures the new direct approach. +func BenchmarkCaseInsensitiveIndexDirect(b *testing.B) { + txt := "this is a reasonably long line for benchmarking" + r := 'r' + + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { + util.CaseInsensitiveIndex(txt, r) + } +} diff --git a/filter/fuzzy.go b/filter/fuzzy.go index 31d1090..27b096e 100644 --- a/filter/fuzzy.go +++ b/filter/fuzzy.go @@ -108,7 +108,7 @@ LINE: if hasUpper { idx = strings.IndexRune(remaining, firstRune) } else { - idx = strings.IndexFunc(remaining, util.CaseInsensitiveIndexFunc(firstRune)) + idx = util.CaseInsensitiveIndex(remaining, firstRune) } if idx == -1 { break @@ -157,7 +157,7 @@ LINE: if hasUpper { idx = strings.IndexRune(candidateTxt, r) } else { - idx = strings.IndexFunc(candidateTxt, util.CaseInsensitiveIndexFunc(r)) + idx = util.CaseInsensitiveIndex(candidateTxt, r) } if idx == -1 { continue OUTER diff --git a/internal/util/util.go b/internal/util/util.go index 7dbd805..61e34da 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -18,6 +18,19 @@ func CaseInsensitiveIndexFunc(r rune) func(rune) bool { } } +// CaseInsensitiveIndex returns the byte index of the first rune in s that +// is case-insensitively equal to r. Returns -1 if not found. This avoids +// the closure allocation of CaseInsensitiveIndexFunc + strings.IndexFunc. +func CaseInsensitiveIndex(s string, r rune) int { + upper := unicode.ToUpper(r) + for i, c := range s { + if unicode.ToUpper(c) == upper { + return i + } + } + return -1 +} + // ContainsUpper reports whether the string contains any uppercase letter. func ContainsUpper(query string) bool { for _, c := range query {