mirror of
https://github.com/siderolabs/talos.git
synced 2025-09-11 08:51:11 +02:00
Fixes #4688 Instead of using generic library, build some handcrafted code to reuse buffers, do partial parsing of the data we need for the processes API. Benchmark (it runs with significant number of processes on the host): ``` name time/op PrometheusProcfs-16 3.42ms ± 8% Processes-16 2.36ms ± 5% name alloc/op PrometheusProcfs-16 366kB ± 0% Processes-16 255kB ± 0% name allocs/op PrometheusProcfs-16 6.76k ± 0% Processes-16 3.83k ± 0% ``` Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com>
197 lines
4.0 KiB
Go
197 lines
4.0 KiB
Go
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
package miniprocfs
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
|
|
"github.com/talos-systems/talos/pkg/machinery/api/machine"
|
|
)
|
|
|
|
const (
|
|
procsPageSize = 256
|
|
procsBufSize = 16 * 1024
|
|
userHz = 100
|
|
)
|
|
|
|
// Processes wraps iterative walker over processes under /proc.
|
|
type Processes struct {
|
|
fd *os.File
|
|
dirnames []string
|
|
idx int
|
|
|
|
buf []byte
|
|
pagesize int
|
|
|
|
RootPath string
|
|
}
|
|
|
|
// NewProcesses initializes process info iterator with path /proc.
|
|
func NewProcesses() (*Processes, error) {
|
|
return NewProcessesWithPath("/proc")
|
|
}
|
|
|
|
// NewProcessesWithPath initializes process info iterator with non-default path.
|
|
func NewProcessesWithPath(rootPath string) (*Processes, error) {
|
|
procs := &Processes{
|
|
RootPath: rootPath,
|
|
buf: make([]byte, procsBufSize),
|
|
}
|
|
|
|
var err error
|
|
|
|
procs.fd, err = os.Open(rootPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
procs.pagesize = os.Getpagesize()
|
|
|
|
return procs, nil
|
|
}
|
|
|
|
// Close the iterator.
|
|
func (procs *Processes) Close() error {
|
|
return procs.fd.Close()
|
|
}
|
|
|
|
// Next returns process info until the list of processes is exhausted.
|
|
//
|
|
// Next returns nil, nil when all processes were processed.
|
|
// Next skips processes which can't be analyzed.
|
|
func (procs *Processes) Next() (*machine.ProcessInfo, error) {
|
|
for {
|
|
if procs.idx >= len(procs.dirnames) {
|
|
var err error
|
|
|
|
procs.dirnames, err = procs.fd.Readdirnames(procsPageSize)
|
|
if err == io.EOF {
|
|
return nil, nil
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
procs.idx = 0
|
|
}
|
|
|
|
info, err := procs.readProc(procs.dirnames[procs.idx])
|
|
procs.idx++
|
|
|
|
// if err != nil, this process was killed before we were able to read /proc data
|
|
if err == nil {
|
|
return info, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
//nolint:gocyclo
|
|
func (procs *Processes) readProc(pidString string) (*machine.ProcessInfo, error) {
|
|
pid, err := strconv.ParseInt(pidString, 10, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
path := procs.RootPath + "/" + pidString + "/"
|
|
|
|
executable, err := os.Readlink(path + "exe")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err = procs.readFileIntoBuf(path + "comm"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
command := string(bytes.TrimSpace(procs.buf))
|
|
|
|
if err = procs.readFileIntoBuf(path + "cmdline"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
args := string(bytes.ReplaceAll(bytes.TrimRight(procs.buf, "\x00"), []byte{0}, []byte{' '}))
|
|
|
|
if err = procs.readFileIntoBuf(path + "stat"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rbracket := bytes.LastIndexByte(procs.buf, ')')
|
|
if rbracket == -1 {
|
|
return nil, fmt.Errorf("unexpected format")
|
|
}
|
|
|
|
fields := bytes.Fields(procs.buf[rbracket+2:])
|
|
|
|
state := string(fields[0])
|
|
|
|
ppid, err := strconv.ParseInt(string(fields[1]), 10, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
numThreads, err := strconv.ParseInt(string(fields[17]), 10, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
uTime, err := strconv.ParseUint(string(fields[11]), 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sTime, err := strconv.ParseUint(string(fields[12]), 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
vSize, err := strconv.ParseUint(string(fields[20]), 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rss, err := strconv.ParseUint(string(fields[21]), 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &machine.ProcessInfo{
|
|
Pid: int32(pid),
|
|
Ppid: int32(ppid),
|
|
State: state,
|
|
Threads: int32(numThreads),
|
|
CpuTime: float64(uTime+sTime) / userHz,
|
|
VirtualMemory: vSize,
|
|
ResidentMemory: rss * uint64(procs.pagesize),
|
|
Command: command,
|
|
Executable: executable,
|
|
Args: args,
|
|
}, nil
|
|
}
|
|
|
|
func (procs *Processes) readFileIntoBuf(path string) error {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer f.Close() //nolint:errcheck
|
|
|
|
procs.buf = procs.buf[:cap(procs.buf)]
|
|
|
|
n, err := f.Read(procs.buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
procs.buf = procs.buf[:n]
|
|
|
|
return f.Close()
|
|
}
|