Skip to content
Snippets Groups Projects
Unverified Commit 9e85cba0 authored by Junegunn Choi's avatar Junegunn Choi
Browse files

Reduce memory footprint of Item struct

parent 4b59ced0
No related branches found
No related tags found
No related merge requests found
...@@ -283,8 +283,9 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C ...@@ -283,8 +283,9 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C
// Phase 1. Check if there's a match and calculate bonus for each point // Phase 1. Check if there's a match and calculate bonus for each point
pidx, lastIdx, prevClass := 0, 0, charNonWord pidx, lastIdx, prevClass := 0, 0, charNonWord
input.CopyRunes(T)
for idx := 0; idx < N; idx++ { for idx := 0; idx < N; idx++ {
char := input.Get(idx) char := T[idx]
var class charClass var class charClass
if char <= unicode.MaxASCII { if char <= unicode.MaxASCII {
class = charClassOfAscii(char) class = charClassOfAscii(char)
...@@ -389,7 +390,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C ...@@ -389,7 +390,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C
if i == 0 { if i == 0 {
fmt.Print(" ") fmt.Print(" ")
for j := int(F[i]); j <= lastIdx; j++ { for j := int(F[i]); j <= lastIdx; j++ {
fmt.Printf(" " + string(input.Get(j)) + " ") fmt.Printf(" " + string(T[j]) + " ")
} }
fmt.Println() fmt.Println()
} }
......
...@@ -33,8 +33,8 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Result) { ...@@ -33,8 +33,8 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Result) {
(*qc)[key] = list (*qc)[key] = list
} }
// Find is called to lookup ChunkCache // Lookup is called to lookup ChunkCache
func (cc *ChunkCache) Find(chunk *Chunk, key string) []*Result { func (cc *ChunkCache) Lookup(chunk *Chunk, key string) []*Result {
if len(key) == 0 || !chunk.IsFull() { if len(key) == 0 || !chunk.IsFull() {
return nil return nil
} }
......
...@@ -14,27 +14,27 @@ func TestChunkCache(t *testing.T) { ...@@ -14,27 +14,27 @@ func TestChunkCache(t *testing.T) {
cache.Add(chunk2p, "bar", items2) cache.Add(chunk2p, "bar", items2)
{ // chunk1 is not full { // chunk1 is not full
cached, found := cache.Find(chunk1p, "foo") cached := cache.Lookup(chunk1p, "foo")
if found { if cached != nil {
t.Error("Cached disabled for non-empty chunks", found, cached) t.Error("Cached disabled for non-empty chunks", cached)
} }
} }
{ {
cached, found := cache.Find(chunk2p, "foo") cached := cache.Lookup(chunk2p, "foo")
if !found || len(cached) != 1 { if cached == nil || len(cached) != 1 {
t.Error("Expected 1 item cached", found, cached) t.Error("Expected 1 item cached", cached)
} }
} }
{ {
cached, found := cache.Find(chunk2p, "bar") cached := cache.Lookup(chunk2p, "bar")
if !found || len(cached) != 2 { if cached == nil || len(cached) != 2 {
t.Error("Expected 2 items cached", found, cached) t.Error("Expected 2 items cached", cached)
} }
} }
{ {
cached, found := cache.Find(chunk1p, "foobar") cached := cache.Lookup(chunk1p, "foobar")
if found { if cached != nil {
t.Error("Expected 0 item cached", found, cached) t.Error("Expected 0 item cached", cached)
} }
} }
} }
...@@ -12,7 +12,9 @@ func TestChunkList(t *testing.T) { ...@@ -12,7 +12,9 @@ func TestChunkList(t *testing.T) {
sortCriteria = []criterion{byScore, byLength} sortCriteria = []criterion{byScore, byLength}
cl := NewChunkList(func(s []byte, i int) Item { cl := NewChunkList(func(s []byte, i int) Item {
return Item{text: util.ToChars(s), index: int32(i * 2)} chars := util.ToChars(s)
chars.Index = int32(i * 2)
return Item{text: chars}
}) })
// Snapshot // Snapshot
...@@ -41,8 +43,8 @@ func TestChunkList(t *testing.T) { ...@@ -41,8 +43,8 @@ func TestChunkList(t *testing.T) {
if len(*chunk1) != 2 { if len(*chunk1) != 2 {
t.Error("Snapshot should contain only two items") t.Error("Snapshot should contain only two items")
} }
if (*chunk1)[0].text.ToString() != "hello" || (*chunk1)[0].index != 0 || if (*chunk1)[0].text.ToString() != "hello" || (*chunk1)[0].Index() != 0 ||
(*chunk1)[1].text.ToString() != "world" || (*chunk1)[1].index != 2 { (*chunk1)[1].text.ToString() != "world" || (*chunk1)[1].Index() != 2 {
t.Error("Invalid data") t.Error("Invalid data")
} }
if chunk1.IsFull() { if chunk1.IsFull() {
......
...@@ -98,11 +98,8 @@ func Run(opts *Options, revision string) { ...@@ -98,11 +98,8 @@ func Run(opts *Options, revision string) {
return nilItem return nilItem
} }
chars, colors := ansiProcessor(data) chars, colors := ansiProcessor(data)
return Item{ chars.Index = int32(index)
index: int32(index), return Item{text: chars, colors: colors}
trimLength: -1,
text: chars,
colors: colors}
}) })
} else { } else {
chunkList = NewChunkList(func(data []byte, index int) Item { chunkList = NewChunkList(func(data []byte, index int) Item {
...@@ -114,16 +111,9 @@ func Run(opts *Options, revision string) { ...@@ -114,16 +111,9 @@ func Run(opts *Options, revision string) {
return nilItem return nilItem
} }
textRunes := joinTokens(trans) textRunes := joinTokens(trans)
item := Item{
index: int32(index),
trimLength: -1,
origText: &data,
colors: nil}
trimmed, colors := ansiProcessorRunes(textRunes) trimmed, colors := ansiProcessorRunes(textRunes)
item.text = trimmed trimmed.Index = int32(index)
item.colors = colors return Item{text: trimmed, colors: colors, origText: &data}
return item
}) })
} }
......
...@@ -4,33 +4,27 @@ import ( ...@@ -4,33 +4,27 @@ import (
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
// Item represents each input line // Item represents each input line. 56 bytes.
type Item struct { type Item struct {
index int32 text util.Chars // 32 = 24 + 1 + 1 + 2 + 4
trimLength int32 transformed *[]Token // 8
text util.Chars origText *[]byte // 8
origText *[]byte colors *[]ansiOffset // 8
colors *[]ansiOffset
transformed []Token
} }
// Index returns ordinal index of the Item // Index returns ordinal index of the Item
func (item *Item) Index() int32 { func (item *Item) Index() int32 {
return item.index return item.text.Index
} }
var nilItem = Item{index: -1} var nilItem = Item{text: util.Chars{Index: -1}}
func (item *Item) Nil() bool { func (item *Item) Nil() bool {
return item.index < 0 return item.Index() < 0
} }
func (item *Item) TrimLength() int32 { func (item *Item) TrimLength() uint16 {
if item.trimLength >= 0 { return item.text.TrimLength()
return item.trimLength
}
item.trimLength = int32(item.text.TrimLength())
return item.trimLength
} }
// Colors returns ansiOffsets of the Item // Colors returns ansiOffsets of the Item
......
...@@ -247,7 +247,7 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []*Result { ...@@ -247,7 +247,7 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []*Result {
// ChunkCache: Exact match // ChunkCache: Exact match
cacheKey := p.CacheKey() cacheKey := p.CacheKey()
if p.cacheable { if p.cacheable {
if cached := _cache.Find(chunk, cacheKey); cached != nil { if cached := _cache.Lookup(chunk, cacheKey); cached != nil {
return cached return cached
} }
} }
...@@ -352,18 +352,17 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of ...@@ -352,18 +352,17 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of
} }
func (p *Pattern) prepareInput(item *Item) []Token { func (p *Pattern) prepareInput(item *Item) []Token {
if item.transformed != nil { if len(p.nth) == 0 {
return item.transformed return []Token{Token{text: &item.text, prefixLength: 0}}
} }
var ret []Token if item.transformed != nil {
if len(p.nth) == 0 { return *item.transformed
ret = []Token{Token{text: &item.text, prefixLength: 0}}
} else {
tokens := Tokenize(item.text, p.delimiter)
ret = Transform(tokens, p.nth)
} }
item.transformed = ret
tokens := Tokenize(item.text, p.delimiter)
ret := Transform(tokens, p.nth)
item.transformed = &ret
return ret return ret
} }
......
...@@ -142,13 +142,13 @@ func TestOrigTextAndTransformed(t *testing.T) { ...@@ -142,13 +142,13 @@ func TestOrigTextAndTransformed(t *testing.T) {
Item{ Item{
text: util.RunesToChars([]rune("junegunn")), text: util.RunesToChars([]rune("junegunn")),
origText: &origBytes, origText: &origBytes,
transformed: trans}, transformed: &trans},
} }
pattern.extended = extended pattern.extended = extended
matches := pattern.matchChunk(&chunk, nil, slab) // No cache matches := pattern.matchChunk(&chunk, nil, slab) // No cache
if !(matches[0].item.text.ToString() == "junegunn" && if !(matches[0].item.text.ToString() == "junegunn" &&
string(*matches[0].item.origText) == "junegunn.choi" && string(*matches[0].item.origText) == "junegunn.choi" &&
reflect.DeepEqual(matches[0].item.transformed, trans)) { reflect.DeepEqual(*matches[0].item.transformed, trans)) {
t.Error("Invalid match result", matches) t.Error("Invalid match result", matches)
} }
...@@ -156,7 +156,7 @@ func TestOrigTextAndTransformed(t *testing.T) { ...@@ -156,7 +156,7 @@ func TestOrigTextAndTransformed(t *testing.T) {
if !(match.item.text.ToString() == "junegunn" && if !(match.item.text.ToString() == "junegunn" &&
string(*match.item.origText) == "junegunn.choi" && string(*match.item.origText) == "junegunn.choi" &&
offsets[0][0] == 0 && offsets[0][1] == 5 && offsets[0][0] == 0 && offsets[0][1] == 5 &&
reflect.DeepEqual(match.item.transformed, trans)) { reflect.DeepEqual(*match.item.transformed, trans)) {
t.Error("Invalid match result", match, offsets, extended) t.Error("Invalid match result", match, offsets, extended)
} }
if !((*pos)[0] == 4 && (*pos)[1] == 0) { if !((*pos)[0] == 4 && (*pos)[1] == 0) {
......
...@@ -34,7 +34,7 @@ func buildResult(item *Item, offsets []Offset, score int) *Result { ...@@ -34,7 +34,7 @@ func buildResult(item *Item, offsets []Offset, score int) *Result {
sort.Sort(ByOrder(offsets)) sort.Sort(ByOrder(offsets))
} }
result := Result{item: item, rank: rank{index: item.index}} result := Result{item: item, rank: rank{index: item.Index()}}
numChars := item.text.Length() numChars := item.text.Length()
minBegin := math.MaxUint16 minBegin := math.MaxUint16
minEnd := math.MaxUint16 minEnd := math.MaxUint16
...@@ -57,7 +57,7 @@ func buildResult(item *Item, offsets []Offset, score int) *Result { ...@@ -57,7 +57,7 @@ func buildResult(item *Item, offsets []Offset, score int) *Result {
// Higher is better // Higher is better
val = math.MaxUint16 - util.AsUint16(score) val = math.MaxUint16 - util.AsUint16(score)
case byLength: case byLength:
val = util.AsUint16(int(item.TrimLength())) val = item.TrimLength()
case byBegin, byEnd: case byBegin, byEnd:
if validOffsetFound { if validOffsetFound {
whitePrefixLen := 0 whitePrefixLen := 0
...@@ -86,7 +86,7 @@ var sortCriteria []criterion ...@@ -86,7 +86,7 @@ var sortCriteria []criterion
// Index returns ordinal index of the Item // Index returns ordinal index of the Item
func (result *Result) Index() int32 { func (result *Result) Index() int32 {
return result.item.index return result.item.Index()
} }
func minRank() rank { func minRank() rank {
......
...@@ -11,6 +11,11 @@ import ( ...@@ -11,6 +11,11 @@ import (
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
func withIndex(i *Item, index int) *Item {
(*i).text.Index = int32(index)
return i
}
func TestOffsetSort(t *testing.T) { func TestOffsetSort(t *testing.T) {
offsets := []Offset{ offsets := []Offset{
Offset{3, 5}, Offset{2, 7}, Offset{3, 5}, Offset{2, 7},
...@@ -52,12 +57,13 @@ func TestResultRank(t *testing.T) { ...@@ -52,12 +57,13 @@ func TestResultRank(t *testing.T) {
sortCriteria = []criterion{byScore, byLength} sortCriteria = []criterion{byScore, byLength}
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")} strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
item1 := buildResult(&Item{text: util.RunesToChars(strs[0]), index: 1, trimLength: -1}, []Offset{}, 2) item1 := buildResult(
withIndex(&Item{text: util.RunesToChars(strs[0])}, 1), []Offset{}, 2)
if item1.rank.points[0] != math.MaxUint16-2 || // Bonus if item1.rank.points[0] != math.MaxUint16-2 || // Bonus
item1.rank.points[1] != 3 || // Length item1.rank.points[1] != 3 || // Length
item1.rank.points[2] != 0 || // Unused item1.rank.points[2] != 0 || // Unused
item1.rank.points[3] != 0 || // Unused item1.rank.points[3] != 0 || // Unused
item1.item.index != 1 { item1.item.Index() != 1 {
t.Error(item1.rank) t.Error(item1.rank)
} }
// Only differ in index // Only differ in index
...@@ -73,14 +79,18 @@ func TestResultRank(t *testing.T) { ...@@ -73,14 +79,18 @@ func TestResultRank(t *testing.T) {
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
if items[0] != item2 || items[1] != item2 || if items[0] != item2 || items[1] != item2 ||
items[2] != item1 || items[3] != item1 { items[2] != item1 || items[3] != item1 {
t.Error(items, item1, item1.item.index, item2, item2.item.index) t.Error(items, item1, item1.item.Index(), item2, item2.item.Index())
} }
// Sort by relevance // Sort by relevance
item3 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 3) item3 := buildResult(
item4 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 4) withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 3)
item5 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 5) item4 := buildResult(
item6 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 6) withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 4)
item5 := buildResult(
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 5)
item6 := buildResult(
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 6)
items = []*Result{item1, item2, item3, item4, item5, item6} items = []*Result{item1, item2, item3, item4, item5, item6}
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
if !(items[0] == item6 && items[1] == item5 && if !(items[0] == item6 && items[1] == item5 &&
......
...@@ -3,63 +3,81 @@ package util ...@@ -3,63 +3,81 @@ package util
import ( import (
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
"unsafe"
) )
type Chars struct { type Chars struct {
runes []rune slice []byte // or []rune
bytes []byte inBytes bool
trimLengthKnown bool
trimLength uint16
// XXX Piggybacking item index here is a horrible idea. But I'm trying to
// minimize the memory footprint by not wasting padded spaces.
Index int32
} }
// ToChars converts byte array into rune array // ToChars converts byte array into rune array
func ToChars(bytea []byte) Chars { func ToChars(bytes []byte) Chars {
var runes []rune var runes []rune
ascii := true inBytes := true
numBytes := len(bytea) numBytes := len(bytes)
for i := 0; i < numBytes; { for i := 0; i < numBytes; {
if bytea[i] < utf8.RuneSelf { if bytes[i] < utf8.RuneSelf {
if !ascii { if !inBytes {
runes = append(runes, rune(bytea[i])) runes = append(runes, rune(bytes[i]))
} }
i++ i++
} else { } else {
if ascii { if inBytes {
ascii = false inBytes = false
runes = make([]rune, i, numBytes) runes = make([]rune, i, numBytes)
for j := 0; j < i; j++ { for j := 0; j < i; j++ {
runes[j] = rune(bytea[j]) runes[j] = rune(bytes[j])
} }
} }
r, sz := utf8.DecodeRune(bytea[i:]) r, sz := utf8.DecodeRune(bytes[i:])
i += sz i += sz
runes = append(runes, r) runes = append(runes, r)
} }
} }
if ascii { if inBytes {
return Chars{bytes: bytea} return Chars{slice: bytes, inBytes: inBytes}
} }
return Chars{runes: runes} return RunesToChars(runes)
} }
func RunesToChars(runes []rune) Chars { func RunesToChars(runes []rune) Chars {
return Chars{runes: runes} return Chars{slice: *(*[]byte)(unsafe.Pointer(&runes)), inBytes: false}
}
func (chars *Chars) optionalRunes() []rune {
if chars.inBytes {
return nil
}
return *(*[]rune)(unsafe.Pointer(&chars.slice))
} }
func (chars *Chars) Get(i int) rune { func (chars *Chars) Get(i int) rune {
if chars.runes != nil { if runes := chars.optionalRunes(); runes != nil {
return chars.runes[i] return runes[i]
} }
return rune(chars.bytes[i]) return rune(chars.slice[i])
} }
func (chars *Chars) Length() int { func (chars *Chars) Length() int {
if chars.runes != nil { if runes := chars.optionalRunes(); runes != nil {
return len(chars.runes) return len(runes)
} }
return len(chars.bytes) return len(chars.slice)
} }
// TrimLength returns the length after trimming leading and trailing whitespaces // TrimLength returns the length after trimming leading and trailing whitespaces
func (chars *Chars) TrimLength() int { func (chars *Chars) TrimLength() uint16 {
if chars.trimLengthKnown {
return chars.trimLength
}
chars.trimLengthKnown = true
var i int var i int
len := chars.Length() len := chars.Length()
for i = len - 1; i >= 0; i-- { for i = len - 1; i >= 0; i-- {
...@@ -80,7 +98,8 @@ func (chars *Chars) TrimLength() int { ...@@ -80,7 +98,8 @@ func (chars *Chars) TrimLength() int {
break break
} }
} }
return i - j + 1 chars.trimLength = AsUint16(i - j + 1)
return chars.trimLength
} }
func (chars *Chars) TrailingWhitespaces() int { func (chars *Chars) TrailingWhitespaces() int {
...@@ -96,28 +115,40 @@ func (chars *Chars) TrailingWhitespaces() int { ...@@ -96,28 +115,40 @@ func (chars *Chars) TrailingWhitespaces() int {
} }
func (chars *Chars) ToString() string { func (chars *Chars) ToString() string {
if chars.runes != nil { if runes := chars.optionalRunes(); runes != nil {
return string(chars.runes) return string(runes)
} }
return string(chars.bytes) return string(chars.slice)
} }
func (chars *Chars) ToRunes() []rune { func (chars *Chars) ToRunes() []rune {
if chars.runes != nil { if runes := chars.optionalRunes(); runes != nil {
return chars.runes return runes
} }
runes := make([]rune, len(chars.bytes)) bytes := chars.slice
for idx, b := range chars.bytes { runes := make([]rune, len(bytes))
for idx, b := range bytes {
runes[idx] = rune(b) runes[idx] = rune(b)
} }
return runes return runes
} }
func (chars *Chars) CopyRunes(dest []rune) {
if runes := chars.optionalRunes(); runes != nil {
copy(dest, runes)
return
}
for idx, b := range chars.slice {
dest[idx] = rune(b)
}
return
}
func (chars *Chars) Slice(b int, e int) Chars { func (chars *Chars) Slice(b int, e int) Chars {
if chars.runes != nil { if runes := chars.optionalRunes(); runes != nil {
return Chars{runes: chars.runes[b:e]} return RunesToChars(runes[b:e])
} }
return Chars{bytes: chars.bytes[b:e]} return Chars{slice: chars.slice[b:e], inBytes: true}
} }
func (chars *Chars) Split(delimiter string) []Chars { func (chars *Chars) Split(delimiter string) []Chars {
......
...@@ -2,27 +2,16 @@ package util ...@@ -2,27 +2,16 @@ package util
import "testing" import "testing"
func TestToCharsNil(t *testing.T) {
bs := Chars{bytes: []byte{}}
if bs.bytes == nil || bs.runes != nil {
t.Error()
}
rs := RunesToChars([]rune{})
if rs.bytes != nil || rs.runes == nil {
t.Error()
}
}
func TestToCharsAscii(t *testing.T) { func TestToCharsAscii(t *testing.T) {
chars := ToChars([]byte("foobar")) chars := ToChars([]byte("foobar"))
if chars.ToString() != "foobar" || chars.runes != nil { if !chars.inBytes || chars.ToString() != "foobar" || !chars.inBytes {
t.Error() t.Error()
} }
} }
func TestCharsLength(t *testing.T) { func TestCharsLength(t *testing.T) {
chars := ToChars([]byte("\tabc한글 ")) chars := ToChars([]byte("\tabc한글 "))
if chars.Length() != 8 || chars.TrimLength() != 5 { if chars.inBytes || chars.Length() != 8 || chars.TrimLength() != 5 {
t.Error() t.Error()
} }
} }
...@@ -36,7 +25,7 @@ func TestCharsToString(t *testing.T) { ...@@ -36,7 +25,7 @@ func TestCharsToString(t *testing.T) {
} }
func TestTrimLength(t *testing.T) { func TestTrimLength(t *testing.T) {
check := func(str string, exp int) { check := func(str string, exp uint16) {
chars := ToChars([]byte(str)) chars := ToChars([]byte(str))
trimmed := chars.TrimLength() trimmed := chars.TrimLength()
if trimmed != exp { if trimmed != exp {
...@@ -61,7 +50,8 @@ func TestSplit(t *testing.T) { ...@@ -61,7 +50,8 @@ func TestSplit(t *testing.T) {
input := ToChars([]byte(str)) input := ToChars([]byte(str))
result := input.Split(delim) result := input.Split(delim)
if len(result) != len(tokens) { if len(result) != len(tokens) {
t.Errorf("Invalid Split result for '%s': %d tokens found (expected %d): %s", t.Errorf(
"Invalid Split result for '%s': %d tokens found (expected %d): %s",
str, len(result), len(tokens), result) str, len(result), len(tokens), result)
} }
for idx, token := range tokens { for idx, token := range tokens {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment