Skip to content

Commit

Permalink
portlist: further reduce allocations on Linux
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
bradfitz committed Oct 22, 2022
1 parent def089f commit 7705dfd
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 14 deletions.
2 changes: 1 addition & 1 deletion portlist/portlist.go
Expand Up @@ -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 {
Expand Down
53 changes: 42 additions & 11 deletions portlist/portlist_linux.go
Expand Up @@ -6,19 +6,22 @@ package portlist

import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"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.
Expand All @@ -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
Expand All @@ -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')
Expand Down Expand Up @@ -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),
})
}

Expand Down
6 changes: 4 additions & 2 deletions portlist/portlist_linux_test.go
Expand Up @@ -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)
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down

0 comments on commit 7705dfd

Please sign in to comment.