Reduce memory footprint of Item struct

This commit is contained in:
Junegunn Choi 2017-07-16 23:31:19 +09:00
parent 4b59ced08f
commit 9e85cba0d0
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
12 changed files with 139 additions and 122 deletions

View File

@ -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
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()
} }

View File

@ -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
} }

View File

@ -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)
} }
} }
} }

View File

@ -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) {
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() {

View File

@ -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) {
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
}) })
} }

View File

@ -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

View File

@ -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
} }
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
} }

View File

@ -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) {
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) {

View File

@ -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 {
// 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
// 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 {

View File

@ -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) {
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) {
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 &&

View File

@ -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 {
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 {
} }
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) Slice(b int, e int) Chars { func (chars *Chars) CopyRunes(dest []rune) {
if chars.runes != nil { if runes := chars.optionalRunes(); runes != nil {
return Chars{runes: chars.runes[b:e]} copy(dest, runes)
return
} }
return Chars{bytes: chars.bytes[b:e]} for idx, b := range chars.slice {
dest[idx] = rune(b)
}
return
}
func (chars *Chars) Slice(b int, e int) Chars {
if runes := chars.optionalRunes(); runes != nil {
return RunesToChars(runes[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 {

View File

@ -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) {
} }
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) {
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 {