diff --git a/kittens/choose_files/scan.go b/kittens/choose_files/scan.go index 9993779d3..21a788ecf 100644 --- a/kittens/choose_files/scan.go +++ b/kittens/choose_files/scan.go @@ -9,6 +9,7 @@ import ( "path/filepath" "slices" "sort" + "strings" "sync" "sync/atomic" "time" @@ -55,7 +56,7 @@ func (r *ResultItem) sorted_positions() []int { } type FileSystemScanner struct { - listeners []chan int + listeners []chan bool in_progress, keep_going atomic.Bool root_dir string mutex sync.Mutex @@ -64,8 +65,8 @@ type FileSystemScanner struct { err error } -func NewFileSystemScanner(root_dir string, notify chan int) (fss *FileSystemScanner) { - ans := &FileSystemScanner{root_dir: root_dir, listeners: []chan int{notify}, results: make([]ResultItem, 0, 1024)} +func NewFileSystemScanner(root_dir string, notify chan bool) (fss *FileSystemScanner) { + ans := &FileSystemScanner{root_dir: root_dir, listeners: []chan bool{notify}, results: make([]ResultItem, 0, 1024)} ans.in_progress.Store(true) ans.keep_going.Store(true) ans.dir_reader = os.ReadDir @@ -75,7 +76,7 @@ func NewFileSystemScanner(root_dir string, notify chan int) (fss *FileSystemScan type Scanner interface { Start() Cancel() - AddListener(chan int) + AddListener(chan bool) Len() int Batch(offset int) []ResultItem Finished() bool @@ -99,7 +100,7 @@ func (fss *FileSystemScanner) Cancel() { fss.keep_going.Store(false) } -func (fss *FileSystemScanner) AddListener(x chan int) { +func (fss *FileSystemScanner) AddListener(x chan bool) { fss.lock() defer fss.unlock() if !fss.in_progress.Load() { @@ -200,7 +201,10 @@ func (fss *FileSystemScanner) worker() { seen_dirs := make(map[string]bool) symlink_dir_map := make(map[string]string) root_dir, _ := filepath.Abs(fss.root_dir) - dir := root_dir + string(os.PathSeparator) + dir := root_dir + if !strings.HasSuffix(dir, string(os.PathSeparator)) { + dir += string(os.PathSeparator) + } base := "" pos := 0 var arena []sortable_dir_entry @@ -252,7 +256,7 @@ func (fss *FileSystemScanner) worker() { ns := fss.results new_sz := len(ns) + len(entries) if cap(ns) < new_sz { - ns = make([]ResultItem, len(ns), max(16*1024, new_sz, cap(ns)*2)) + ns = make([]ResultItem, len(ns), max(1024, new_sz, cap(ns)*2)) copy(ns, fss.results) } new_items := ns[len(ns):new_sz] @@ -267,11 +271,10 @@ func (fss *FileSystemScanner) worker() { fss.lock() fss.results = ns listeners := fss.listeners - num := len(fss.results) fss.unlock() for _, l := range listeners { select { - case l <- num: + case l <- true: default: } } @@ -309,7 +312,7 @@ func (fss *FileSystemScorer) lock() { fss.mutex.Lock() } func (fss *FileSystemScorer) unlock() { fss.mutex.Unlock() } func (fss *FileSystemScorer) Start() { - on_results := make(chan int) + on_results := make(chan bool) fss.is_complete.Store(false) fss.keep_going.Store(true) if fss.scanner == nil { @@ -338,7 +341,7 @@ func (fss *FileSystemScorer) Change_query(query string) { fss.Start() } -func (fss *FileSystemScorer) worker(on_results chan int, worker_wait *sync.WaitGroup) { +func (fss *FileSystemScorer) worker(on_results chan bool, worker_wait *sync.WaitGroup) { defer func() { fss.is_complete.Store(true) defer worker_wait.Done() diff --git a/kittens/choose_files/scan_test.go b/kittens/choose_files/scan_test.go index 26c522121..be37d2013 100644 --- a/kittens/choose_files/scan_test.go +++ b/kittens/choose_files/scan_test.go @@ -2,7 +2,11 @@ package choose_files import ( "fmt" + "io/fs" + "math/rand" + "os" "strings" + "sync" "testing" "github.com/google/go-cmp/cmp" @@ -23,3 +27,114 @@ func TestAsLower(t *testing.T) { } } } + +type node struct { + name string + children map[string]*node +} + +func (n node) Name() string { + return n.name +} + +func (n node) IsDir() bool { + return n.children != nil +} + +func (n node) String() string { + return fmt.Sprintf("{name: %s num_children: %d}", n.name, len(n.children)) +} + +func (n node) Type() fs.FileMode { + if n.children == nil { + return 0 + } + return fs.ModeDir +} + +func (n node) Info() (fs.FileInfo, error) { + return nil, fmt.Errorf("Info() not implemented") +} + +func random_name() string { + length := 3 + rand.Intn(23) + bytes := make([]byte, length) + for i := range length { + // Printable ASCII range: 32 (space) to 126 (~) + bytes[i] = byte(rand.Intn(26) + 'a') + } + return string(bytes) +} + +func (n *node) generate_random_tree(depth, breadth int) { + n.children = make(map[string]*node) + for range breadth { + c := &node{name: random_name()} + n.children[c.name] = c + if depth > 0 && rand.Intn(10) < 3 { + c.generate_random_tree(depth-1, breadth) + } + } +} + +func (n node) dir_entries() []fs.DirEntry { + entries := make([]fs.DirEntry, 0, len(n.children)) + for _, v := range n.children { + entries = append(entries, v) + } + return entries +} + +func (n node) ReadDir(name string) ([]fs.DirEntry, error) { + if name == string(os.PathSeparator) { + return n.dir_entries(), nil + } + p := &n + for _, x := range strings.Split(strings.Trim(name, string(os.PathSeparator)), string(os.PathSeparator)) { + c, found := p.children[x] + if !found { + return nil, fs.ErrNotExist + } + if !c.IsDir() { + return nil, fs.ErrExist + } + p = c + } + return p.dir_entries(), nil +} + +func run_scoring(b *testing.B, depth, breadth int, query string) { + b.StopTimer() + root := node{name: string(os.PathSeparator)} + root.generate_random_tree(depth, breadth) + b.StartTimer() + for range b.N { + b.StopTimer() + wg := sync.WaitGroup{} + wg.Add(1) + s := NewFileSystemScorer(string(os.PathSeparator), query, false, func(err error, is_complete bool) { + if is_complete { + wg.Done() + } + }) + sc := NewFileSystemScanner(s.root_dir, make(chan bool)) + s.scanner = sc + sc.dir_reader = root.ReadDir + b.StartTimer() + s.scanner.Start() + s.Start() + wg.Wait() + } +} + +// To run this benchmark with profiling use: +// go test -bench=FileNameScoringWithoutQuery -benchmem -cpuprofile=/tmp/cpu.prof -memprofile=/tmp/mem.prof github.com/kovidgoyal/kitty/kittens/choose_files -o /tmp/cfexe +func BenchmarkFileNameScoringWithoutQuery(b *testing.B) { + run_scoring(b, 5, 20, "") +} + +// To run this benchmark with profiling use: +// go test -bench=FileNameScoringWithQuery -benchmem -cpuprofile=/tmp/cpu.prof -memprofile=/tmp/mem.prof github.com/kovidgoyal/kitty/kittens/choose_files -o /tmp/cfexe +func BenchmarkFileNameScoringWithQuery(b *testing.B) { + run_scoring(b, 5, 30, "abc") +}