Performance fix - unnecessary rune convertion on --ansi

> time cat /tmp/list | fzf-0.10.1-darwin_amd64 --ansi -fqwerty > /dev/null

    real    0m4.364s
    user    0m8.231s
    sys     0m0.820s

    > time cat /tmp/list | fzf --ansi -fqwerty > /dev/null

    real    0m4.624s
    user    0m5.755s
    sys     0m0.732s
This commit is contained in:
Junegunn Choi 2015-08-02 14:25:57 +09:00
parent 0ea66329b8
commit e13bafc1ab
6 changed files with 52 additions and 42 deletions

View File

@ -7,7 +7,7 @@ type Chunk []*Item // >>> []Item
// ItemBuilder is a closure type that builds Item object from a pointer to a // ItemBuilder is a closure type that builds Item object from a pointer to a
// string and an integer // string and an integer
type ItemBuilder func([]rune, int) *Item type ItemBuilder func([]byte, int) *Item
// ChunkList is a list of Chunks // ChunkList is a list of Chunks
type ChunkList struct { type ChunkList struct {
@ -26,7 +26,7 @@ func NewChunkList(trans ItemBuilder) *ChunkList {
trans: trans} trans: trans}
} }
func (c *Chunk) push(trans ItemBuilder, data []rune, index int) bool { func (c *Chunk) push(trans ItemBuilder, data []byte, index int) bool {
item := trans(data, index) item := trans(data, index)
if item != nil { if item != nil {
*c = append(*c, item) *c = append(*c, item)
@ -53,7 +53,7 @@ func CountItems(cs []*Chunk) int {
} }
// Push adds the item to the list // Push adds the item to the list
func (cl *ChunkList) Push(data []rune) bool { func (cl *ChunkList) Push(data []byte) bool {
cl.mutex.Lock() cl.mutex.Lock()
defer cl.mutex.Unlock() defer cl.mutex.Unlock()

View File

@ -6,8 +6,8 @@ import (
) )
func TestChunkList(t *testing.T) { func TestChunkList(t *testing.T) {
cl := NewChunkList(func(s []rune, i int) *Item { cl := NewChunkList(func(s []byte, i int) *Item {
return &Item{text: s, rank: Rank{0, 0, uint32(i * 2)}} return &Item{text: []rune(string(s)), rank: Rank{0, 0, uint32(i * 2)}}
}) })
// Snapshot // Snapshot
@ -17,8 +17,8 @@ func TestChunkList(t *testing.T) {
} }
// Add some data // Add some data
cl.Push([]rune("hello")) cl.Push([]byte("hello"))
cl.Push([]rune("world")) cl.Push([]byte("world"))
// Previously created snapshot should remain the same // Previously created snapshot should remain the same
if len(snapshot) > 0 { if len(snapshot) > 0 {
@ -46,7 +46,7 @@ func TestChunkList(t *testing.T) {
// Add more data // Add more data
for i := 0; i < chunkSize*2; i++ { for i := 0; i < chunkSize*2; i++ {
cl.Push([]rune(fmt.Sprintf("item %d", i))) cl.Push([]byte(fmt.Sprintf("item %d", i)))
} }
// Previous snapshot should remain the same // Previous snapshot should remain the same
@ -64,8 +64,8 @@ func TestChunkList(t *testing.T) {
t.Error("Unexpected number of items") t.Error("Unexpected number of items")
} }
cl.Push([]rune("hello")) cl.Push([]byte("hello"))
cl.Push([]rune("world")) cl.Push([]byte("world"))
lastChunkCount := len(*snapshot[len(snapshot)-1]) lastChunkCount := len(*snapshot[len(snapshot)-1])
if lastChunkCount != 2 { if lastChunkCount != 2 {

View File

@ -63,48 +63,54 @@ func Run(opts *Options) {
eventBox := util.NewEventBox() eventBox := util.NewEventBox()
// ANSI code processor // ANSI code processor
ansiProcessor := func(runes []rune) ([]rune, []ansiOffset) { ansiProcessor := func(data []byte) ([]rune, []ansiOffset) {
// By default, we do nothing return util.BytesToRunes(data), nil
return runes, nil }
ansiProcessorRunes := func(data []rune) ([]rune, []ansiOffset) {
return data, nil
} }
if opts.Ansi { if opts.Ansi {
if opts.Theme != nil { if opts.Theme != nil {
var state *ansiState var state *ansiState
ansiProcessor = func(runes []rune) ([]rune, []ansiOffset) { ansiProcessor = func(data []byte) ([]rune, []ansiOffset) {
trimmed, offsets, newState := extractColor(string(runes), state) trimmed, offsets, newState := extractColor(string(data), state)
state = newState state = newState
return []rune(trimmed), offsets return []rune(trimmed), offsets
} }
} else { } else {
// When color is disabled but ansi option is given, // When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input // we simply strip out ANSI codes from the input
ansiProcessor = func(runes []rune) ([]rune, []ansiOffset) { ansiProcessor = func(data []byte) ([]rune, []ansiOffset) {
trimmed, _, _ := extractColor(string(runes), nil) trimmed, _, _ := extractColor(string(data), nil)
return []rune(trimmed), nil return []rune(trimmed), nil
} }
} }
ansiProcessorRunes = func(data []rune) ([]rune, []ansiOffset) {
return ansiProcessor([]byte(string(data)))
}
} }
// Chunk list // Chunk list
var chunkList *ChunkList var chunkList *ChunkList
header := make([]string, 0, opts.HeaderLines) header := make([]string, 0, opts.HeaderLines)
if len(opts.WithNth) == 0 { if len(opts.WithNth) == 0 {
chunkList = NewChunkList(func(data []rune, index int) *Item { chunkList = NewChunkList(func(data []byte, index int) *Item {
if len(header) < opts.HeaderLines { if len(header) < opts.HeaderLines {
header = append(header, string(data)) header = append(header, string(data))
eventBox.Set(EvtHeader, header) eventBox.Set(EvtHeader, header)
return nil return nil
} }
data, colors := ansiProcessor(data) runes, colors := ansiProcessor(data)
return &Item{ return &Item{
text: data, text: runes,
index: uint32(index), index: uint32(index),
colors: colors, colors: colors,
rank: Rank{0, 0, uint32(index)}} rank: Rank{0, 0, uint32(index)}}
}) })
} else { } else {
chunkList = NewChunkList(func(data []rune, index int) *Item { chunkList = NewChunkList(func(data []byte, index int) *Item {
tokens := Tokenize(data, opts.Delimiter) runes := util.BytesToRunes(data)
tokens := Tokenize(runes, opts.Delimiter)
trans := Transform(tokens, opts.WithNth) trans := Transform(tokens, opts.WithNth)
if len(header) < opts.HeaderLines { if len(header) < opts.HeaderLines {
header = append(header, string(joinTokens(trans))) header = append(header, string(joinTokens(trans)))
@ -113,12 +119,12 @@ func Run(opts *Options) {
} }
item := Item{ item := Item{
text: joinTokens(trans), text: joinTokens(trans),
origText: &data, origText: &runes,
index: uint32(index), index: uint32(index),
colors: nil, colors: nil,
rank: Rank{0, 0, uint32(index)}} rank: Rank{0, 0, uint32(index)}}
trimmed, colors := ansiProcessor(item.text) trimmed, colors := ansiProcessorRunes(item.text)
item.text = trimmed item.text = trimmed
item.colors = colors item.colors = colors
return &item return &item
@ -128,7 +134,7 @@ func Run(opts *Options) {
// Reader // Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
if !streamingFilter { if !streamingFilter {
reader := Reader{func(data []rune) bool { reader := Reader{func(data []byte) bool {
return chunkList.Push(data) return chunkList.Push(data)
}, eventBox, opts.ReadZero} }, eventBox, opts.ReadZero}
go reader.ReadSource() go reader.ReadSource()
@ -151,7 +157,7 @@ func Run(opts *Options) {
if streamingFilter { if streamingFilter {
reader := Reader{ reader := Reader{
func(runes []rune) bool { func(runes []byte) bool {
item := chunkList.trans(runes, 0) item := chunkList.trans(runes, 0)
if item != nil && pattern.MatchItem(item) { if item != nil && pattern.MatchItem(item) {
fmt.Println(string(item.text)) fmt.Println(string(item.text))

View File

@ -5,14 +5,13 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"unicode/utf8"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
// Reader reads from command or standard input // Reader reads from command or standard input
type Reader struct { type Reader struct {
pusher func([]rune) bool pusher func([]byte) bool
eventBox *util.EventBox eventBox *util.EventBox
delimNil bool delimNil bool
} }
@ -42,21 +41,10 @@ func (r *Reader) feed(src io.Reader) {
// end in delim. // end in delim.
bytea, err := reader.ReadBytes(delim) bytea, err := reader.ReadBytes(delim)
if len(bytea) > 0 { if len(bytea) > 0 {
runes := make([]rune, 0, len(bytea))
for i := 0; i < len(bytea); {
if bytea[i] < utf8.RuneSelf {
runes = append(runes, rune(bytea[i]))
i++
} else {
r, sz := utf8.DecodeRune(bytea[i:])
i += sz
runes = append(runes, r)
}
}
if err == nil { if err == nil {
runes = runes[:len(runes)-1] bytea = bytea[:len(bytea)-1]
} }
if r.pusher(runes) { if r.pusher(bytea) {
r.eventBox.Set(EvtReadNew, nil) r.eventBox.Set(EvtReadNew, nil)
} }
} }

View File

@ -10,7 +10,7 @@ func TestReadFromCommand(t *testing.T) {
strs := []string{} strs := []string{}
eb := util.NewEventBox() eb := util.NewEventBox()
reader := Reader{ reader := Reader{
pusher: func(s []rune) bool { strs = append(strs, string(s)); return true }, pusher: func(s []byte) bool { strs = append(strs, string(s)); return true },
eventBox: eb} eventBox: eb}
// Check EventBox // Check EventBox

View File

@ -6,6 +6,7 @@ import "C"
import ( import (
"os" "os"
"time" "time"
"unicode/utf8"
) )
// Max returns the largest integer // Max returns the largest integer
@ -88,3 +89,18 @@ func TrimRight(runes []rune) []rune {
} }
return runes[0 : i+1] return runes[0 : i+1]
} }
func BytesToRunes(bytea []byte) []rune {
runes := make([]rune, 0, len(bytea))
for i := 0; i < len(bytea); {
if bytea[i] < utf8.RuneSelf {
runes = append(runes, rune(bytea[i]))
i++
} else {
r, sz := utf8.DecodeRune(bytea[i:])
i += sz
runes = append(runes, r)
}
}
return runes
}