package fzf import ( "fmt" "runtime" "sort" "time" ) type MatchRequest struct { chunks []*Chunk pattern *Pattern } type Matcher struct { patternBuilder func([]rune) *Pattern sort bool eventBox *EventBox reqBox *EventBox partitions int mergerCache map[string]*Merger } const ( REQ_RETRY EventType = iota REQ_RESET ) const ( STAT_CANCELLED int = iota STAT_QCH STAT_CHUNKS ) const ( PROGRESS_MIN_DURATION = 200 * time.Millisecond ) func NewMatcher(patternBuilder func([]rune) *Pattern, sort bool, eventBox *EventBox) *Matcher { return &Matcher{ patternBuilder: patternBuilder, sort: sort, eventBox: eventBox, reqBox: NewEventBox(), partitions: runtime.NumCPU(), mergerCache: make(map[string]*Merger)} } func (m *Matcher) Loop() { prevCount := 0 for { var request MatchRequest m.reqBox.Wait(func(events *Events) { for _, val := range *events { switch val := val.(type) { case MatchRequest: request = val default: panic(fmt.Sprintf("Unexpected type: %T", val)) } } events.Clear() }) // Restart search patternString := request.pattern.AsString() var merger *Merger cancelled := false count := CountItems(request.chunks) foundCache := false if count == prevCount { // Look up mergerCache if cached, found := m.mergerCache[patternString]; found { foundCache = true merger = cached } } else { // Invalidate mergerCache prevCount = count m.mergerCache = make(map[string]*Merger) } if !foundCache { merger, cancelled = m.scan(request, 0) } if !cancelled { m.mergerCache[patternString] = merger m.eventBox.Set(EVT_SEARCH_FIN, merger) } } } func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk { perSlice := len(chunks) / m.partitions // No need to parallelize if perSlice == 0 { return [][]*Chunk{chunks} } slices := make([][]*Chunk, m.partitions) for i := 0; i < m.partitions; i++ { start := i * perSlice end := start + perSlice if i == m.partitions-1 { end = len(chunks) } slices[i] = chunks[start:end] } return slices } type partialResult struct { index int matches []*Item } func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { startedAt := time.Now() numChunks := len(request.chunks) if numChunks == 0 { return EmptyMerger, false } pattern := request.pattern empty := pattern.IsEmpty() cancelled := NewAtomicBool(false) slices := m.sliceChunks(request.chunks) numSlices := len(slices) resultChan := make(chan partialResult, numSlices) countChan := make(chan int, numSlices) for idx, chunks := range slices { go func(idx int, chunks []*Chunk) { sliceMatches := []*Item{} for _, chunk := range chunks { var matches []*Item if empty { matches = *chunk } else { matches = request.pattern.Match(chunk) } sliceMatches = append(sliceMatches, matches...) if cancelled.Get() { return } countChan <- len(matches) } if !empty && m.sort { sort.Sort(ByRelevance(sliceMatches)) } resultChan <- partialResult{idx, sliceMatches} }(idx, chunks) } count := 0 matchCount := 0 for matchesInChunk := range countChan { count += 1 matchCount += matchesInChunk if limit > 0 && matchCount > limit { return nil, true // For --select-1 and --exit-0 } if count == numChunks { break } if !empty && m.reqBox.Peak(REQ_RESET) { cancelled.Set(true) return nil, true } if time.Now().Sub(startedAt) > PROGRESS_MIN_DURATION { m.eventBox.Set(EVT_SEARCH_PROGRESS, float32(count)/float32(numChunks)) } } partialResults := make([][]*Item, numSlices) for range slices { partialResult := <-resultChan partialResults[partialResult.index] = partialResult.matches } return NewMerger(partialResults, !empty && m.sort), false } func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool) { pattern := m.patternBuilder(patternRunes) var event EventType if cancel { event = REQ_RESET } else { event = REQ_RETRY } m.reqBox.Set(event, MatchRequest{chunks, pattern}) }