Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

process walker perfs: optimize readLimits and readStats #2491

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 0 additions & 41 deletions probe/process/cache.go

This file was deleted.

195 changes: 140 additions & 55 deletions probe/process/walker_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package process

import (
"bytes"
"encoding/binary"
"fmt"
"os"
"path"
"strconv"
"strings"

linuxproc "github.com/c9s/goprocinfo/linux"
"github.com/coocood/freecache"

"github.com/weaveworks/common/fs"
"github.com/weaveworks/scope/probe/host"
Expand All @@ -18,12 +20,64 @@ type walker struct {
procRoot string
}

var (
// limitsCache caches /proc/<pid>/limits
// key: filename in /proc. Example: "42"
// value: max open files (soft limit) stored in a [8]byte (uint64, little endian)
limitsCache = freecache.NewCache(1024 * 16)

// cmdlineCache caches /proc/<pid>/cmdline and /proc/<pid>/name
// key: filename in /proc. Example: "42"
// value: two strings separated by a '\0'
cmdlineCache = freecache.NewCache(1024 * 16)
)

const (
limitsCacheTimeout = 60
cmdlineCacheTimeout = 60
)

// NewWalker creates a new process Walker.
func NewWalker(procRoot string) Walker {
return &walker{procRoot: procRoot}
}

// skipNSpaces skips nSpaces in buf and updates the cursor 'pos'
func skipNSpaces(buf *[]byte, pos *int, nSpaces int) {
for spaceCount := 0; *pos < len(*buf) && spaceCount < nSpaces; *pos++ {
if (*buf)[*pos] == ' ' {
spaceCount++
}
}
}

// parseUint64WithSpaces is similar to strconv.ParseUint64 but stops parsing
// when reading a space instead of returning an error
func parseUint64WithSpaces(buf *[]byte, pos *int) (ret uint64) {
for ; *pos < len(*buf) && (*buf)[*pos] != ' '; *pos++ {
ret = ret*10 + uint64((*buf)[*pos]-'0')
}
return
}

// parseIntWithSpaces is similar to strconv.ParseInt but stops parsing when
// reading a space instead of returning an error
func parseIntWithSpaces(buf *[]byte, pos *int) (ret int) {
return int(parseUint64WithSpaces(buf, pos))
}

// readStats reads and parses '/proc/<pid>/stat' files
func readStats(path string) (ppid, threads int, jiffies, rss, rssLimit uint64, err error) {
const (
// /proc/<pid>/stat field positions, counting from zero
// see "man 5 proc"
procStatFieldPpid int = 3
procStatFieldUserJiffies int = 13
procStatFieldSysJiffies int = 14
procStatFieldThreads int = 19
procStatFieldRssPages int = 23
procStatFieldRssLimit int = 24
)
var (
buf []byte
userJiffies, sysJiffies, rssPages uint64
Expand All @@ -32,53 +86,84 @@ func readStats(path string) (ppid, threads int, jiffies, rss, rssLimit uint64, e
if err != nil {
return
}
splits := strings.Fields(string(buf))
if len(splits) < 25 {
err = fmt.Errorf("Invalid /proc/PID/stat")
return
}
ppid, err = strconv.Atoi(splits[3])
if err != nil {
return
}
threads, err = strconv.Atoi(splits[19])
if err != nil {
return
}
userJiffies, err = strconv.ParseUint(splits[13], 10, 64)
if err != nil {
return
}
sysJiffies, err = strconv.ParseUint(splits[14], 10, 64)
if err != nil {
return
}

// Parse the file without using expensive extra string allocations

pos := 0
skipNSpaces(&buf, &pos, procStatFieldPpid)
ppid = parseIntWithSpaces(&buf, &pos)

skipNSpaces(&buf, &pos, procStatFieldUserJiffies-procStatFieldPpid)
userJiffies = parseUint64WithSpaces(&buf, &pos)

pos++ // 1 space between userJiffies and sysJiffies
sysJiffies = parseUint64WithSpaces(&buf, &pos)

skipNSpaces(&buf, &pos, procStatFieldThreads-procStatFieldSysJiffies)
threads = parseIntWithSpaces(&buf, &pos)

skipNSpaces(&buf, &pos, procStatFieldRssPages-procStatFieldThreads)
rssPages = parseUint64WithSpaces(&buf, &pos)

pos++ // 1 space between rssPages and rssLimit
rssLimit = parseUint64WithSpaces(&buf, &pos)

jiffies = userJiffies + sysJiffies
rssPages, err = strconv.ParseUint(splits[23], 10, 64)
if err != nil {
return
}
rss = rssPages * uint64(os.Getpagesize())
rssLimit, err = strconv.ParseUint(splits[24], 10, 64)
return
}

func readLimits(path string) (openFilesLimit uint64, err error) {
buf, err := cachedReadFile(path)
buf, err := fs.ReadFile(path)
if err != nil {
return 0, err
}
for _, line := range strings.Split(string(buf), "\n") {
if strings.HasPrefix(line, "Max open files") {
splits := strings.Fields(line)
if len(splits) < 6 {
return 0, fmt.Errorf("Invalid /proc/PID/limits")
}
openFilesLimit, err := strconv.Atoi(splits[3])
return uint64(openFilesLimit), err
content := string(buf)

// File format: one line header + one line per limit
//
// Limit Soft Limit Hard Limit Units
// ...
// Max open files 1024 4096 files
// ...
delim := "\nMax open files"
pos := strings.Index(content, delim)

if pos < 0 {
// Tests such as TestWalker can synthetise empty files
return 0, nil
}
pos += len(delim)

for pos < len(content) && content[pos] == ' ' {
pos++
}

var softLimit uint64
softLimit = parseUint64WithSpaces(&buf, &pos)

return softLimit, nil
}

func (w *walker) readCmdline(filename string) (cmdline, name string) {
if cmdlineBuf, err := fs.ReadFile(path.Join(w.procRoot, filename, "cmdline")); err == nil {
// like proc, treat name as the first element of command line
i := bytes.IndexByte(cmdlineBuf, '\000')
if i == -1 {
i = len(cmdlineBuf)
}
name = string(cmdlineBuf[:i])
cmdlineBuf = bytes.Replace(cmdlineBuf, []byte{'\000'}, []byte{' '}, -1)
cmdline = string(cmdlineBuf)
}
if name == "" {
if commBuf, err := fs.ReadFile(path.Join(w.procRoot, filename, "comm")); err == nil {
name = "[" + strings.TrimSpace(string(commBuf)) + "]"
} else {
name = "(unknown)"
}
}
return 0, nil
return
}

// Walk walks the supplied directory (expecting it to look like /proc)
Expand Down Expand Up @@ -107,29 +192,29 @@ func (w *walker) Walk(f func(Process, Process)) error {
continue
}

openFilesLimit, err := readLimits(path.Join(w.procRoot, filename, "limits"))
if err != nil {
continue
var openFilesLimit uint64
if v, err := limitsCache.Get([]byte(filename)); err == nil {
openFilesLimit = binary.LittleEndian.Uint64(v)
} else {
openFilesLimit, err = readLimits(path.Join(w.procRoot, filename, "limits"))
if err != nil {
continue
}
buf := make([]byte, 8)
binary.LittleEndian.PutUint64(buf, openFilesLimit)
limitsCache.Set([]byte(filename), buf, limitsCacheTimeout)
}

cmdline, name := "", ""
if cmdlineBuf, err := cachedReadFile(path.Join(w.procRoot, filename, "cmdline")); err == nil {
// like proc, treat name as the first element of command line
i := bytes.IndexByte(cmdlineBuf, '\000')
if i == -1 {
i = len(cmdlineBuf)
}
name = string(cmdlineBuf[:i])
cmdlineBuf = bytes.Replace(cmdlineBuf, []byte{'\000'}, []byte{' '}, -1)
cmdline = string(cmdlineBuf)
}
if name == "" {
if commBuf, err := cachedReadFile(path.Join(w.procRoot, filename, "comm")); err == nil {
name = "[" + strings.TrimSpace(string(commBuf)) + "]"
} else {
name = "(unknown)"
}
if v, err := cmdlineCache.Get([]byte(filename)); err == nil {
separatorPos := strings.Index(string(v), "\x00")
cmdline = string(v[:separatorPos])
name = string(v[separatorPos+1:])
} else {
cmdline, name = w.readCmdline(filename)
cmdlineCache.Set([]byte(filename), []byte(fmt.Sprintf("%s\x00%s", cmdline, name)), cmdlineCacheTimeout)
}

f(Process{
PID: pid,
PPID: ppid,
Expand Down
2 changes: 1 addition & 1 deletion probe/process/walker_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ var mockFS = fs.Dir("",
},
fs.File{
FName: "limits",
FContents: `Max open files 32768 65536 files`,
FContents: "Limit Soft-Limit Hard-Limit Units\nMax open files 32768 65536 files",
},
fs.Dir("fd", fs.File{FName: "0"}, fs.File{FName: "1"}, fs.File{FName: "2"}),
),
Expand Down