portlist: further reduce allocations on Linux

Make Linux parsePorts also an append-style API and attach it to
caller's provided append base memory.

And add a little string intern pool in front of the []byte to string
for inode names.

    name       old time/op    new time/op    delta
    GetList-8    11.1ms ± 4%     9.8ms ± 6%  -11.68%  (p=0.000 n=9+10)

    name       old alloc/op   new alloc/op   delta
    GetList-8    92.8kB ± 2%    79.7kB ± 0%  -14.11%  (p=0.000 n=10+9)

    name       old allocs/op  new allocs/op  delta
    GetList-8     2.94k ± 1%     2.76k ± 0%   -6.16%  (p=0.000 n=10+10)

More coming. (the bulk of the allocations are in addProcesses and
filesystem operations, most of which we should usually be able to
skip)

Updates #5958

Change-Id: I3f0c03646d314a16fef7f8346aefa7d5c96701e7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2022-10-22 09:29:37 -07:00 committed by Brad Fitzpatrick
parent def089f9c9
commit 7149155b80
3 changed files with 47 additions and 14 deletions

View File

@ -52,7 +52,7 @@ func (a *Port) lessThan(b *Port) bool {
}
func (a List) sameInodes(b List) bool {
if a == nil || b == nil || len(a) != len(b) {
if len(a) != len(b) {
return false
}
for i := range a {

View File

@ -6,6 +6,7 @@ package portlist
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
@ -13,12 +14,14 @@ import (
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"go4.org/mem"
"golang.org/x/sys/unix"
"tailscale.com/util/mak"
)
// Reading the sockfiles on Linux is very fast, so we can do it often.
@ -35,13 +38,42 @@ const (
v4Any = "00000000:0000"
)
var eofReader = bytes.NewReader(nil)
var bufioReaderPool = &sync.Pool{
New: func() any { return bufio.NewReader(eofReader) },
}
type internedStrings struct {
m map[string]string
}
func (v *internedStrings) get(b []byte) string {
if s, ok := v.m[string(b)]; ok {
return s
}
s := string(b)
mak.Set(&v.m, s, s)
return s
}
var internedStringsPool = &sync.Pool{
New: func() any { return new(internedStrings) },
}
func appendListeningPorts(base []Port) ([]Port, error) {
ret := base
if sawProcNetPermissionErr.Load() {
return ret, nil
}
var br *bufio.Reader
br := bufioReaderPool.Get().(*bufio.Reader)
defer bufioReaderPool.Put(br)
defer br.Reset(eofReader)
stringCache := internedStringsPool.Get().(*internedStrings)
defer internedStringsPool.Put(stringCache)
for _, fname := range sockfiles {
// Android 10+ doesn't allow access to this anymore.
// https://developer.android.com/about/versions/10/privacy/changes#proc-net-filesystem
@ -59,25 +91,24 @@ func appendListeningPorts(base []Port) ([]Port, error) {
if err != nil {
return nil, fmt.Errorf("%s: %s", fname, err)
}
if br == nil {
br = bufio.NewReader(f)
} else {
br.Reset(f)
}
ports, err := parsePorts(br, filepath.Base(fname))
br.Reset(f)
ret, err = appendParsePorts(ret, stringCache, br, filepath.Base(fname))
f.Close()
if err != nil {
return nil, fmt.Errorf("parsing %q: %w", fname, err)
}
ret = append(ret, ports...)
}
if len(stringCache.m) >= len(ret)*2 {
// Prevent unbounded growth of the internedStrings map.
stringCache.m = nil
}
return ret, nil
}
// fileBase is one of "tcp", "tcp6", "udp", "udp6".
func parsePorts(r *bufio.Reader, fileBase string) ([]Port, error) {
func appendParsePorts(base []Port, stringCache *internedStrings, r *bufio.Reader, fileBase string) ([]Port, error) {
proto := strings.TrimSuffix(fileBase, "6")
var ret []Port
ret := base
// skip header row
_, err := r.ReadSlice('\n')
@ -171,7 +202,7 @@ func parsePorts(r *bufio.Reader, fileBase string) ([]Port, error) {
ret = append(ret, Port{
Proto: proto,
Port: uint16(portv),
inode: string(inoBuf),
inode: stringCache.get(inoBuf),
})
}

View File

@ -76,6 +76,7 @@ func TestParsePorts(t *testing.T) {
},
}
stringCache := new(internedStrings)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buf := bytes.NewBufferString(tt.in)
@ -84,7 +85,7 @@ func TestParsePorts(t *testing.T) {
if tt.file != "" {
file = tt.file
}
got, err := parsePorts(r, file)
got, err := appendParsePorts(nil, stringCache, r, file)
if err != nil {
t.Fatal(err)
}
@ -116,11 +117,12 @@ func BenchmarkParsePorts(b *testing.B) {
r := bytes.NewReader(contents.Bytes())
br := bufio.NewReader(&contents)
stringCache := new(internedStrings)
b.ResetTimer()
for i := 0; i < b.N; i++ {
r.Seek(0, io.SeekStart)
br.Reset(r)
got, err := parsePorts(br, "tcp6")
got, err := appendParsePorts(nil, stringCache, br, "tcp6")
if err != nil {
b.Fatal(err)
}