1
1
mirror of https://github.com/walles/moar.git synced 2024-08-16 15:30:34 +03:00

Compare commits

...

22 Commits

Author SHA1 Message Date
Johan Walles
c8b4ecf38a Fix a hang 2024-05-22 07:01:14 +02:00
Johan Walles
e9bc0dffda Have only one searcher
Before this change we were passing things by value and our updates to
local state never had any effect.
2024-05-22 07:01:14 +02:00
Johan Walles
5eb548a126 Fix the warnings 2024-05-22 07:01:10 +02:00
Johan Walles
6325a1cd95 WIP: Go to the line with the search hit
Some warnings, we should at least be using scrollPosition.isVisible().
2024-05-19 09:13:07 +02:00
Johan Walles
c1641c8456 WIP Implement searching
Now we just need to jump to the result.
2024-05-19 09:13:07 +02:00
Johan Walles
6bb2919459 WIP Trigger background search
Except we don't have anything doing anything in the background yet.
2024-05-19 09:13:07 +02:00
Johan Walles
eae1915b5e Fix formatting 2024-05-18 15:34:17 +02:00
Johan Walles
11095496b2 Tune logging 2024-05-18 15:23:37 +02:00
Johan Walles
d1fac66a23 Improve search logging 2024-05-18 15:15:45 +02:00
Johan Walles
f434895eef Fix backwards search performance issue
This problem was here all along, but it was excarberated by the newly
introduced parallel search.
2024-05-18 08:37:06 +02:00
Johan Walles
7b032b2fa4 Merge branch 'johan/parallel-search' 2024-05-18 08:17:13 +02:00
Johan Walles
686882ffcd Improve docs 2024-05-18 08:13:08 +02:00
Johan Walles
60d3577b59 Don't crash searching in empty buffer 2024-05-18 07:59:44 +02:00
Johan Walles
64ae443e1f Log searches 2024-05-18 07:55:46 +02:00
Johan Walles
856574c289 Don't divide by zero 2024-05-18 07:52:20 +02:00
Johan Walles
ee35c6b166 Fix the chunk before positions 2024-05-18 07:47:40 +02:00
Johan Walles
625193933d Fix the line counts 2024-05-18 07:44:01 +02:00
Johan Walles
387fb348c7 Fix loop variables warning 2024-05-18 07:33:53 +02:00
Johan Walles
84cf4a9cd9 Return the search results
And fix backwards searches.
2024-05-18 07:31:28 +02:00
Johan Walles
aa342d4325 Implement more of the parallel search 2024-05-18 07:28:17 +02:00
Johan Walles
27d03630a5 Half way implement the parallel search 2024-05-18 07:23:40 +02:00
Johan Walles
0ba0415a50 Set the stage for parallel search 2024-05-18 07:15:31 +02:00
6 changed files with 225 additions and 42 deletions

View File

@ -28,4 +28,6 @@ fi
go build -trimpath -ldflags="-s -w -X main.versionString=${VERSION}" -o "${BINARY}"
# Alternative build line, if you want to attach to the running process in the Go debugger:
#
# -gcflags='-N -l' disables optimizations and inlining, which makes debugging easier.
# go build -ldflags="-X main.versionString=${VERSION}" -gcflags='-N -l' -o "${BINARY}"

View File

@ -445,6 +445,12 @@ func (p *Pager) StartPaging(screen twin.Screen, chromaStyle *chroma.Style, chrom
case twin.EventTerminalBackgroundDetected:
// Do nothing, we don't care about background color updates
case eventGoToLine:
line := NewScrollPositionFromLineNumber(event.lineNumber, "goToLine")
if !line.isVisible(p) {
p.scrollPosition = line
}
default:
log.Warnf("Unhandled event type: %v", event)
}

View File

@ -2,18 +2,37 @@ package m
import (
"regexp"
"sync"
"unicode"
"unicode/utf8"
log "github.com/sirupsen/logrus"
"github.com/walles/moar/m/linenumbers"
"github.com/walles/moar/twin"
)
type PagerModeSearch struct {
pager *Pager
type searchCommand int
const (
searchCommandSearch searchCommand = iota
searchCommandDone
)
type eventGoToLine struct {
lineNumber linenumbers.LineNumber
}
func (m PagerModeSearch) drawFooter(_ string, _ string) {
type PagerModeSearch struct {
pager *Pager
pattern *regexp.Regexp
startLine linenumbers.LineNumber
lock sync.Mutex
searcher chan searchCommand
}
func (m *PagerModeSearch) drawFooter(_ string, _ string) {
width, height := m.pager.screen.Size()
pos := 0
@ -33,12 +52,77 @@ func (m PagerModeSearch) drawFooter(_ string, _ string) {
}
}
func (m *PagerModeSearch) searcherSearch() *linenumbers.LineNumber {
// Search to the end
for position := m.startLine; ; position = position.NonWrappingAdd(1) {
line := m.pager.reader.GetLine(position)
if line == nil {
// Reached end of input without any match, give up
break
}
if m.pattern.MatchString(line.Plain(&position)) {
return &position
}
}
// Search from the beginning
for position := m.startLine; position != m.startLine; position = position.NonWrappingAdd(1) {
line := m.pager.reader.GetLine(position)
if m.pattern.MatchString(line.Plain(&position)) {
return &position
}
}
return nil
}
func (m *PagerModeSearch) initSearcher() {
m.searcher = make(chan searchCommand, 1)
go func() {
for command := range m.searcher {
switch command {
case searchCommandSearch:
found := m.searcherSearch()
if found != nil {
m.pager.screen.Events() <- eventGoToLine{*found}
}
case searchCommandDone:
return
}
}
}()
}
func (m *PagerModeSearch) updateSearchPattern() {
// For highlighting
m.pager.searchPattern = toPattern(m.pager.searchString)
if m.pager.searchPattern == nil {
// Nothing to search for, never mind
return
}
startLine := m.pager.scrollPosition.lineNumber(m.pager)
if startLine == nil {
// Nothing to search in, never mind
return
}
m.pager.scrollToSearchHits()
if m.searcher == nil {
m.initSearcher()
}
// FIXME: If the user is typing, indicate to user if we didn't find anything
// Give the searcher the new pattern
m.lock.Lock()
m.pattern = m.pager.searchPattern
m.startLine = *startLine
m.lock.Unlock()
// Tell the searcher there's a new pattern to look for
select {
case m.searcher <- searchCommandSearch:
default:
}
}
// toPattern compiles a search string into a pattern.
@ -92,9 +176,12 @@ func removeLastChar(s string) string {
return s[:len(s)-size]
}
func (m PagerModeSearch) onKey(key twin.KeyCode) {
func (m *PagerModeSearch) onKey(key twin.KeyCode) {
switch key {
case twin.KeyEscape, twin.KeyEnter:
if m.searcher != nil {
m.searcher <- searchCommandDone
}
//nolint:gosimple // The linter's advice is just wrong here
m.pager.mode = PagerModeViewing{pager: m.pager}
@ -107,6 +194,9 @@ func (m PagerModeSearch) onKey(key twin.KeyCode) {
m.updateSearchPattern()
case twin.KeyUp, twin.KeyDown, twin.KeyPgUp, twin.KeyPgDown:
if m.searcher != nil {
m.searcher <- searchCommandDone
}
//nolint:gosimple // The linter's advice is just wrong here
m.pager.mode = PagerModeViewing{pager: m.pager}
m.pager.mode.onKey(key)
@ -116,7 +206,7 @@ func (m PagerModeSearch) onKey(key twin.KeyCode) {
}
}
func (m PagerModeSearch) onRune(char rune) {
func (m *PagerModeSearch) onRune(char rune) {
m.pager.searchString = m.pager.searchString + string(char)
m.updateSearchPattern()
}

View File

@ -144,7 +144,7 @@ func (m PagerModeViewing) onRune(char rune) {
p.handleScrolledDown()
case '/':
p.mode = PagerModeSearch{pager: p}
p.mode = &PagerModeSearch{pager: p}
p.TargetLineNumber = nil
p.searchString = ""
p.searchPattern = nil

View File

@ -2,40 +2,125 @@ package m
import (
"fmt"
"runtime"
"time"
log "github.com/sirupsen/logrus"
"github.com/walles/moar/m/linenumbers"
)
func (p *Pager) scrollToSearchHits() {
if p.searchPattern == nil {
// This is not a search
return
}
firstHitPosition := p.findFirstHit(*p.scrollPosition.lineNumber(p), nil, false)
if firstHitPosition == nil {
// Try again from the top
firstHitPosition = p.findFirstHit(linenumbers.LineNumber{}, p.scrollPosition.lineNumber(p), false)
}
if firstHitPosition == nil {
// No match, give up
return
}
if firstHitPosition.isVisible(p) {
// Already on-screen, never mind
return
}
p.scrollPosition = *firstHitPosition
}
// NOTE: When we search, we do that by looping over the *input lines*, not
// the screen lines. That's why we're using a line number rather than a
// scrollPosition for searching.
// NOTE: When we search, we do that by looping over the *input lines*, not the
// screen lines. That's why startPosition is a LineNumber rather than a
// scrollPosition.
//
// The `beforePosition` parameter is exclusive, meaning that line will not be
// searched.
//
// For the actual searching, this method will call _findFirstHit() in parallel
// on multiple cores, to help large file search performance.
//
// FIXME: We should take startPosition.deltaScreenLines into account as well!
func (p *Pager) findFirstHit(startPosition linenumbers.LineNumber, beforePosition *linenumbers.LineNumber, backwards bool) *scrollPosition {
// If the number of lines to search matches the number of cores (or more),
// divide the search into chunks. Otherwise use one chunk.
chunkCount := runtime.NumCPU()
var linesCount int
if backwards {
// If the startPosition is zero, that should make the count one
linesCount = startPosition.AsZeroBased() + 1
if beforePosition != nil {
// Searching from 1 with before set to 0 should make the count 1
linesCount = startPosition.AsZeroBased() - beforePosition.AsZeroBased()
}
} else {
linesCount = p.reader.GetLineCount() - startPosition.AsZeroBased()
if beforePosition != nil {
// Searching from 1 with before set to 2 should make the count 1
linesCount = beforePosition.AsZeroBased() - startPosition.AsZeroBased()
}
}
if linesCount < chunkCount {
chunkCount = 1
}
chunkSize := linesCount / chunkCount
log.Debugf("Searching %d lines across %d cores with %d lines per core...", linesCount, chunkCount, chunkSize)
t0 := time.Now()
defer func() {
linesPerSecond := float64(linesCount) / time.Since(t0).Seconds()
linesPerSecondS := fmt.Sprintf("%.0f", linesPerSecond)
if linesPerSecond > 7_000_000.0 {
linesPerSecondS = fmt.Sprintf("%.0fM", linesPerSecond/1000_000.0)
} else if linesPerSecond > 7_000.0 {
linesPerSecondS = fmt.Sprintf("%.0fk", linesPerSecond/1000.0)
}
if linesCount > 0 {
log.Debugf("Searched %d lines in %s at %slines/s or %s/line",
linesCount,
time.Since(t0),
linesPerSecondS,
time.Since(t0)/time.Duration(linesCount))
} else {
log.Debugf("Searched %d lines in %s at %slines/s", linesCount, time.Since(t0), linesPerSecondS)
}
}()
// Each parallel search will start at one of these positions
searchStarts := make([]linenumbers.LineNumber, chunkCount)
direction := 1
if backwards {
direction = -1
}
for i := 0; i < chunkCount; i++ {
searchStarts[i] = startPosition.NonWrappingAdd(i * direction * chunkSize)
}
// Make a results array, with one result per chunk
findings := make([]chan *scrollPosition, chunkCount)
// Search all chunks in parallel
for i, searchStart := range searchStarts {
findings[i] = make(chan *scrollPosition)
searchEndIndex := i + 1
var chunkBefore *linenumbers.LineNumber
if searchEndIndex < len(searchStarts) {
chunkBefore = &searchStarts[searchEndIndex]
} else if beforePosition != nil {
chunkBefore = beforePosition
}
go func(i int, searchStart linenumbers.LineNumber, chunkBefore *linenumbers.LineNumber) {
findings[i] <- p._findFirstHit(searchStart, chunkBefore, backwards)
}(i, searchStart, chunkBefore)
}
// Return the first non-nil result
for _, finding := range findings {
result := <-finding
if result != nil {
return result
}
}
return nil
}
// NOTE: When we search, we do that by looping over the *input lines*, not the
// screen lines. That's why startPosition is a LineNumber rather than a
// scrollPosition.
//
// The `beforePosition` parameter is exclusive, meaning that line will not be
// searched.
//
// This method will run over multiple chunks of the input file in parallel to
// help large file search performance.
//
// FIXME: We should take startPosition.deltaScreenLines into account as well!
func (p *Pager) _findFirstHit(startPosition linenumbers.LineNumber, beforePosition *linenumbers.LineNumber, backwards bool) *scrollPosition {
searchPosition := startPosition
for {
line := p.reader.GetLine(searchPosition)
@ -51,18 +136,18 @@ func (p *Pager) findFirstHit(startPosition linenumbers.LineNumber, beforePositio
if backwards {
if (searchPosition == linenumbers.LineNumber{}) {
// No match, give up
// Reached the top without any match, give up
return nil
}
searchPosition = searchPosition.NonWrappingAdd(-1)
} else {
searchPosition = searchPosition.NonWrappingAdd(1)
}
if beforePosition != nil && searchPosition == *beforePosition {
// No match, give up
return nil
}
if beforePosition != nil && searchPosition == *beforePosition {
// No match, give up
return nil
}
}
}

View File

@ -13,7 +13,7 @@ func modeName(pager *Pager) string {
return "Viewing"
case PagerModeNotFound:
return "NotFound"
case PagerModeSearch:
case *PagerModeSearch:
return "Search"
case *PagerModeGotoLine:
return "GotoLine"
@ -116,7 +116,7 @@ func Test152(t *testing.T) {
// Search for the first not-visible hit
pager.searchString = "abcde"
searchMode := PagerModeSearch{pager: pager}
searchMode := &PagerModeSearch{pager: pager}
pager.mode = searchMode
// Scroll to the next search hit