logtail/filch: preallocate a scanner buffer

Scanning log lines is a frequent source of allocations.
Pre-allocate a re-usable buffer.

This still doesn't help when there are giant log lines.
Those will still be problematic from an iOS memory perspective.
For more on that, see https://github.com/tailscale/corp/issues/2423.

(For those who cannot follow that link, it is a discussion
of particular problematic types of log lines for
particular categories of customers. The "categories of customers"
part is the reason that it is a private issue.)

There is also a latent bug here. If we ever encounter
a log line longer than bufio.MaxScanTokenSize,
then bufio.Scan will return an error,
and we'll truncate the file and discard the rest of the log.
That's not good, but bufio.MaxScanTokenSize is really big,
so it probably doesn't matter much in practice now.
Unfortunately, it does prevent us from easily capping the potential
memory usage here, on pain of losing log entries.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
pull/2652/head
Josh Bleecher Snyder 3 years ago committed by Josh Bleecher Snyder
parent 8ab44b339e
commit 93284209bc

@ -30,6 +30,15 @@ type Filch struct {
alt *os.File alt *os.File
altscan *bufio.Scanner altscan *bufio.Scanner
recovered int64 recovered int64
// buf is an initial buffer for altscan.
// As of August 2021, 99.96% of all log lines
// are below 4096 bytes in length.
// Since this cutoff is arbitrary, instead of using 4096,
// we subtract off the size of the rest of the struct
// so that the whole struct takes 4096 bytes
// (less on 32 bit platforms).
// This reduces allocation waste.
buf [4096 - 48]byte
} }
// TryReadline implements the logtail.Buffer interface. // TryReadline implements the logtail.Buffer interface.
@ -53,6 +62,7 @@ func (f *Filch) TryReadLine() ([]byte, error) {
return nil, err return nil, err
} }
f.altscan = bufio.NewScanner(f.alt) f.altscan = bufio.NewScanner(f.alt)
f.altscan.Buffer(f.buf[:], bufio.MaxScanTokenSize)
f.altscan.Split(splitLines) f.altscan.Split(splitLines)
return f.scan() return f.scan()
} }
@ -188,6 +198,7 @@ func New(filePrefix string, opts Options) (f *Filch, err error) {
} }
if f.recovered > 0 { if f.recovered > 0 {
f.altscan = bufio.NewScanner(f.alt) f.altscan = bufio.NewScanner(f.alt)
f.altscan.Buffer(f.buf[:], bufio.MaxScanTokenSize)
f.altscan.Split(splitLines) f.altscan.Split(splitLines)
} }

@ -12,6 +12,7 @@ import (
"strings" "strings"
"testing" "testing"
"unicode" "unicode"
"unsafe"
) )
type filchTest struct { type filchTest struct {
@ -169,3 +170,10 @@ func TestFilchStderr(t *testing.T) {
t.Errorf("unexpected write to fake stderr: %s", b) t.Errorf("unexpected write to fake stderr: %s", b)
} }
} }
func TestSizeOf(t *testing.T) {
s := unsafe.Sizeof(Filch{})
if s > 4096 {
t.Fatalf("Filch{} has size %d on %v, decrease size of buf field", s, runtime.GOARCH)
}
}

Loading…
Cancel
Save