mirror of
https://github.com/tailscale/tailscale.git
synced 2025-10-09 16:11:23 +00:00
util/dirwalk, metrics, portlist: add new package for fast directory walking
This is similar to the golang.org/x/tools/internal/fastwalk I'd previously written but not recursive and using mem.RO. The metrics package already had some Linux-specific directory reading code in it. Move that out to a new general package that can be reused by portlist too, which helps its scanning of all /proc files: name old time/op new time/op delta FindProcessNames-8 2.79ms ± 6% 2.45ms ± 7% -12.11% (p=0.000 n=10+10) name old alloc/op new alloc/op delta FindProcessNames-8 62.9kB ± 0% 33.5kB ± 0% -46.76% (p=0.000 n=9+10) name old allocs/op new allocs/op delta FindProcessNames-8 2.25k ± 0% 0.38k ± 0% -82.98% (p=0.000 n=9+10) Change-Id: I75db393032c328f12d95c39f71c9742c375f207a Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:

committed by
Brad Fitzpatrick

parent
21ef7e5c35
commit
db2cc393af
@@ -5,105 +5,38 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
"io/fs"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"go4.org/mem"
|
||||
"tailscale.com/util/dirwalk"
|
||||
)
|
||||
|
||||
// counter is a reusable counter for counting file descriptors.
|
||||
type counter struct {
|
||||
n int
|
||||
|
||||
// cb is the (*counter).count method value. Creating it allocates,
|
||||
// so we have to save it away and use a sync.Pool to keep currentFDs
|
||||
// amortized alloc-free.
|
||||
cb func(name mem.RO, de fs.DirEntry) error
|
||||
}
|
||||
|
||||
var counterPool = &sync.Pool{New: func() any {
|
||||
c := new(counter)
|
||||
c.cb = c.count
|
||||
return c
|
||||
}}
|
||||
|
||||
func (c *counter) count(name mem.RO, de fs.DirEntry) error {
|
||||
c.n++
|
||||
return nil
|
||||
}
|
||||
|
||||
func currentFDs() int {
|
||||
fd, err := openProcSelfFD()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
|
||||
count := 0
|
||||
|
||||
const blockSize = 8 << 10
|
||||
buf := make([]byte, blockSize) // stack-allocated; doesn't escape
|
||||
bufp := 0 // starting read position in buf
|
||||
nbuf := 0 // end valid data in buf
|
||||
dirent := &syscall.Dirent{}
|
||||
for {
|
||||
if bufp >= nbuf {
|
||||
bufp = 0
|
||||
nbuf, err = readDirent(fd, buf)
|
||||
if err != nil {
|
||||
log.Printf("currentFDs: readDirent: %v", err)
|
||||
return 0
|
||||
}
|
||||
if nbuf <= 0 {
|
||||
return count
|
||||
}
|
||||
}
|
||||
consumed, name := parseDirEnt(dirent, buf[bufp:nbuf])
|
||||
bufp += consumed
|
||||
if len(name) == 0 || string(name) == "." || string(name) == ".." {
|
||||
continue
|
||||
}
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
func direntNamlen(dirent *syscall.Dirent) int {
|
||||
const fixedHdr = uint16(unsafe.Offsetof(syscall.Dirent{}.Name))
|
||||
limit := dirent.Reclen - fixedHdr
|
||||
const dirNameLen = 256 // sizeof syscall.Dirent.Name
|
||||
if limit > dirNameLen {
|
||||
limit = dirNameLen
|
||||
}
|
||||
for i := uint16(0); i < limit; i++ {
|
||||
if dirent.Name[i] == 0 {
|
||||
return int(i)
|
||||
}
|
||||
}
|
||||
panic("failed to find terminating 0 byte in dirent")
|
||||
}
|
||||
|
||||
func parseDirEnt(dirent *syscall.Dirent, buf []byte) (consumed int, name []byte) {
|
||||
// golang.org/issue/37269
|
||||
copy(unsafe.Slice((*byte)(unsafe.Pointer(dirent)), unsafe.Sizeof(syscall.Dirent{})), buf)
|
||||
if v := unsafe.Offsetof(dirent.Reclen) + unsafe.Sizeof(dirent.Reclen); uintptr(len(buf)) < v {
|
||||
panic(fmt.Sprintf("buf size of %d smaller than dirent header size %d", len(buf), v))
|
||||
}
|
||||
if len(buf) < int(dirent.Reclen) {
|
||||
panic(fmt.Sprintf("buf size %d < record length %d", len(buf), dirent.Reclen))
|
||||
}
|
||||
consumed = int(dirent.Reclen)
|
||||
if dirent.Ino == 0 { // File absent in directory.
|
||||
return
|
||||
}
|
||||
name = unsafe.Slice((*byte)(unsafe.Pointer(&dirent.Name[0])), direntNamlen(dirent))
|
||||
return
|
||||
}
|
||||
|
||||
var procSelfFDName = []byte("/proc/self/fd\x00")
|
||||
|
||||
func openProcSelfFD() (fd int, err error) {
|
||||
var dirfd int = unix.AT_FDCWD
|
||||
for {
|
||||
r0, _, e1 := syscall.Syscall(unix.SYS_OPENAT, uintptr(dirfd),
|
||||
uintptr(unsafe.Pointer(&procSelfFDName[0])), 0)
|
||||
if e1 == 0 {
|
||||
return int(r0), nil
|
||||
}
|
||||
if e1 == syscall.EINTR {
|
||||
// Since https://golang.org/doc/go1.14#runtime we
|
||||
// need to loop on EINTR on more places.
|
||||
continue
|
||||
}
|
||||
return 0, syscall.Errno(e1)
|
||||
}
|
||||
}
|
||||
|
||||
func readDirent(fd int, buf []byte) (n int, err error) {
|
||||
for {
|
||||
nbuf, err := syscall.ReadDirent(fd, buf)
|
||||
if err != syscall.EINTR {
|
||||
return nbuf, err
|
||||
}
|
||||
}
|
||||
c := counterPool.Get().(*counter)
|
||||
defer counterPool.Put(c)
|
||||
c.n = 0
|
||||
dirwalk.WalkShallow(mem.S("/proc/self/fd"), c.cb)
|
||||
return c.n
|
||||
}
|
||||
|
Reference in New Issue
Block a user