Add --bench flag for repeatable filter-mode timing

fzf --filter PATTERN --bench 3s < input

Repeats matcher.scan() for the given duration, clears cache between
iterations, and prints stats (iterations, avg, min, max) to stderr.
This commit is contained in:
Junegunn Choi
2026-02-25 10:15:13 +09:00
parent 55193ee4dc
commit 12e24d368c
2 changed files with 45 additions and 1 deletions

View File

@@ -2,6 +2,7 @@
package fzf
import (
"fmt"
"maps"
"os"
"sync"
@@ -181,7 +182,7 @@ func Run(opts *Options) (int, error) {
}
// Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync && opts.Bench == 0
var reader *Reader
if !streamingFilter {
reader = NewReader(func(data []byte) bool {
@@ -274,6 +275,37 @@ func Run(opts *Options) (int, error) {
// NOTE: Streaming filter is inherently not compatible with --tail
snapshot, _, _ := chunkList.Snapshot(opts.Tail)
if opts.Bench > 0 {
// Benchmark mode: repeat scan for the given duration
var times []time.Duration
deadline := time.Now().Add(opts.Bench)
for time.Now().Before(deadline) {
cache.Clear()
start := time.Now()
matcher.scan(MatchRequest{
chunks: snapshot,
pattern: pattern})
times = append(times, time.Since(start))
}
// Print stats
var total time.Duration
minD, maxD := times[0], times[0]
for _, d := range times {
total += d
if d < minD {
minD = d
}
if d > maxD {
maxD = d
}
}
avg := total / time.Duration(len(times))
fmt.Printf(" %d iterations avg: %v min: %v max: %v total: %v\n",
len(times), avg, minD, maxD, total)
return ExitOk, nil
}
result := matcher.scan(MatchRequest{
chunks: snapshot,
pattern: pattern})

View File

@@ -8,6 +8,7 @@ import (
"regexp"
"strconv"
"strings"
"time"
"unicode"
"github.com/junegunn/fzf/src/algo"
@@ -677,6 +678,7 @@ type Options struct {
WalkerSkip []string
Version bool
Help bool
Bench time.Duration
CPUProfile string
MEMProfile string
BlockProfile string
@@ -3373,6 +3375,16 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
return err
}
opts.WalkerSkip = filterNonEmpty(strings.Split(str, ","))
case "--bench":
str, err := nextString("duration required (e.g. 3s, 500ms)")
if err != nil {
return err
}
dur, err := time.ParseDuration(str)
if err != nil {
return errors.New("invalid duration for --bench: " + str)
}
opts.Bench = dur
case "--profile-cpu":
if opts.CPUProfile, err = nextString("file path required: cpu"); err != nil {
return err