Skip to content

Commit f63ae9f

Browse files
authored
Merge pull request #361 from walles/johan/pre-allocated-getlines
Improve Search Cache Performance
2 parents d8799b9 + 5024502 commit f63ae9f

File tree

4 files changed

+75
-35
lines changed

4 files changed

+75
-35
lines changed

internal/filteringReader.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,16 @@ func (f *FilteringReader) GetLines(firstLine linemetadata.Index, wantedLineCount
174174
}
175175
}
176176

177+
func (f *FilteringReader) GetLinesPreallocated(firstLine linemetadata.Index, resultLines *[]reader.NumberedLine) string {
178+
if f.shouldPassThrough() {
179+
return f.BackingReader.GetLinesPreallocated(firstLine, resultLines)
180+
}
181+
182+
lines := f.GetLines(firstLine, cap(*resultLines))
183+
*resultLines = lines.Lines
184+
return lines.StatusText
185+
}
186+
177187
// In the general case, this will return a text like this:
178188
// "Filtered: 1234/5678 lines 22%"
179189
func (f *FilteringReader) createStatus(lastLine *linemetadata.Index) string {

internal/reader/reader.go

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ type Reader interface {
6262
// that the returned first line may be different from the requested one.
6363
GetLines(firstLine linemetadata.Index, wantedLineCount int) InputLines
6464

65+
// GetLines gets the indicated lines from the input. The lines will be stored
66+
// in the provided preallocated slice to avoid allocations. The line count is
67+
// determined by the capacity of the provided slice.
68+
//
69+
// The return value is the status text for the returned lines.
70+
GetLinesPreallocated(firstLine linemetadata.Index, resultLines *[]NumberedLine) string
71+
6572
// False when paused. Showing the paused line count is confusing, because
6673
// the user might think that the number is the total line count, even though
6774
// we are not done yet.
@@ -944,7 +951,6 @@ func clipRangeToLength(start linemetadata.Index, wantedCount int, maxIndex int)
944951
// GetLines gets the indicated lines from the input
945952
func (reader *ReaderImpl) GetLines(firstLine linemetadata.Index, wantedLineCount int) InputLines {
946953
reader.RLock()
947-
948954
if len(reader.lines) == 0 || wantedLineCount == 0 {
949955
statusText := reader.createStatusUnlocked(firstLine)
950956
reader.RUnlock()
@@ -953,29 +959,54 @@ func (reader *ReaderImpl) GetLines(firstLine linemetadata.Index, wantedLineCount
953959
StatusText: statusText,
954960
}
955961
}
962+
reader.RUnlock()
956963

957-
// Prevent reading past the end of the available lines
958964
firstLineIndex, lastLineIndex := clipRangeToLength(firstLine, wantedLineCount, len(reader.lines)-1)
965+
wantedLineCount = lastLineIndex - firstLineIndex + 1
966+
967+
resultLines := make([]NumberedLine, 0, wantedLineCount)
968+
statusText := reader.GetLinesPreallocated(linemetadata.IndexFromZeroBased(firstLineIndex), &resultLines)
969+
970+
return InputLines{
971+
Lines: resultLines,
972+
StatusText: statusText,
973+
}
974+
}
975+
976+
// GetLines gets the indicated lines from the input. The lines will be stored
977+
// in the provided preallocated slice to avoid allocations. The line count is
978+
// determined by the capacity of the provided slice.
979+
//
980+
// The return value is the status text for the returned lines.
981+
func (reader *ReaderImpl) GetLinesPreallocated(firstLine linemetadata.Index, resultLines *[]NumberedLine) string {
982+
// Clear the result slice
983+
*resultLines = (*resultLines)[:0]
984+
985+
reader.RLock()
986+
987+
if len(reader.lines) == 0 || cap(*resultLines) == 0 {
988+
statusText := reader.createStatusUnlocked(firstLine)
989+
reader.RUnlock()
990+
991+
return statusText
992+
}
993+
994+
// Prevent reading past the end of the available lines
995+
firstLineIndex, lastLineIndex := clipRangeToLength(firstLine, cap(*resultLines), len(reader.lines)-1)
959996

960997
statusText := reader.createStatusUnlocked(linemetadata.IndexFromZeroBased(lastLineIndex))
961998

962-
returnLines := make([]NumberedLine, 0, lastLineIndex-firstLineIndex+1)
963999
for loopIndex, returnLine := range reader.lines[firstLineIndex : lastLineIndex+1] {
964-
lineIndex := linemetadata.IndexFromZeroBased(firstLineIndex + loopIndex)
965-
returnLines = append(returnLines, NumberedLine{
966-
Index: lineIndex,
1000+
*resultLines = append(*resultLines, NumberedLine{
1001+
Index: linemetadata.IndexFromZeroBased(firstLineIndex + loopIndex),
9671002
Number: linemetadata.NumberFromZeroBased(firstLineIndex + loopIndex),
9681003
Line: returnLine,
9691004
})
9701005
}
9711006

972-
// Scary parts done, no lock needed anymore
9731007
reader.RUnlock()
9741008

975-
return InputLines{
976-
Lines: returnLines,
977-
StatusText: statusText,
978-
}
1009+
return statusText
9791010
}
9801011

9811012
func (reader *ReaderImpl) PumpToStdout() {

internal/search-line-cache.go

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,17 @@ import (
1010
// performance. But a larger cache without a performance increase has no value.
1111
// To evaluate:
1212
//
13-
// go test -run='^$' -bench 'Search' ./internal
13+
// go test -run='^$' -bench 'Warm' ./internal
1414
//
1515
// Results from Johan's laptop. The numbers are the test iteration counts for
16-
// BenchmarkHighlightedSearch and BenchmarkPlainTextSearch. The optimization has
17-
// been done to improve the sum of these two benchmarks.
16+
// BenchmarkHighlightedWarmSearch and BenchmarkPlainTextWarmSearch. The
17+
// optimization has been done to improve the sum of these two benchmarks.
1818
//
19-
// After introducing the RWMutex in reader.go these numbers became quite
20-
// volatile, they change a bunch from run to run so it's hard to draw obvious
21-
// conclusions. I ended up just picking one number.
22-
//
23-
// 200: 387+368=755
24-
// 1000: 400+392=792
25-
// 2000: 382+394=776
26-
// 3000: 373+402=775
27-
// 4000: 393+412=805
28-
// 5000: 374+366=740
19+
// 100: 741+726=1467
20+
// 500: 832+882=1714 <-- Best
21+
// 1000: 754+810=1564
22+
// 5000: 637+658=1295
23+
2924
const searchLineCacheSize = 500
3025

3126
type searchLineCache struct {
@@ -48,20 +43,19 @@ func (c *searchLineCache) GetLine(reader reader.Reader, index linemetadata.Index
4843
firstIndexToRequest = index.NonWrappingAdd(-searchLineCacheSize + 1)
4944
}
5045

51-
lines := reader.GetLines(firstIndexToRequest, searchLineCacheSize)
52-
if len(lines.Lines) == 0 {
53-
// No lines at all
54-
return nil
55-
}
56-
57-
c.lines = lines.Lines
46+
reader.GetLinesPreallocated(firstIndexToRequest, &c.lines)
5847

5948
// Get the line from the cache
6049
return c.getLineFromCache(index)
6150
}
6251

6352
// Or nil if that line isn't in the cache
6453
func (c *searchLineCache) getLineFromCache(index linemetadata.Index) *reader.NumberedLine {
54+
if cap(c.lines) != searchLineCacheSize {
55+
// Initialize cache
56+
c.lines = make([]reader.NumberedLine, 0, searchLineCacheSize)
57+
}
58+
6559
if len(c.lines) == 0 {
6660
return nil
6761
}

internal/search-linescanner_test.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,10 @@ func benchmarkSearch(b *testing.B, highlighted bool, warm bool) {
7676
assert.NilError(b, err)
7777
fileContents := string(sourceBytes)
7878

79-
// Read one copy of the example input
79+
// Repeat input enough times to get to some target size, before highlighting
80+
// to get the same amount of text in either case
81+
replications := 5_000_000 / len(fileContents)
82+
8083
if highlighted {
8184
highlightedSourceCode, err := reader.Highlight(fileContents, *styles.Get("native"), formatters.TTY16m, lexers.Get("go"))
8285
assert.NilError(b, err)
@@ -86,9 +89,11 @@ func benchmarkSearch(b *testing.B, highlighted bool, warm bool) {
8689
fileContents = *highlightedSourceCode
8790
}
8891

89-
// Repeat input enough times to get to some target size, before highlighting
90-
// to get the same amount of text in either case
91-
replications := 5_000_000 / len(fileContents)
92+
if !warm {
93+
// This makes the ns/op benchmark numbers more comparable between plain
94+
// and highlighted in the cold case.
95+
replications = 5_000_000 / len(fileContents)
96+
}
9297

9398
// Create some input to search. Use a Builder to avoid quadratic string concatenation time.
9499
var builder strings.Builder

0 commit comments

Comments
 (0)