Add systemd resource budgets with TOML profiles
This commit is contained in:
commit
be8749a384
1 changed files with 2591 additions and 0 deletions
169
internal/budget/proc_linux.go
Normal file
169
internal/budget/proc_linux.go
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
package budget
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type task struct {
|
||||
Parent uint32
|
||||
Group string
|
||||
Profile string
|
||||
Slice string
|
||||
}
|
||||
|
||||
type procSource interface {
|
||||
Snapshot(config) (map[uint32]task, error)
|
||||
Group(uint32) (string, error)
|
||||
}
|
||||
|
||||
type procFS struct {
|
||||
root string
|
||||
uid uint32
|
||||
}
|
||||
|
||||
var pythonName = regexp.MustCompile(`^python[23]?(?:\.\d+)?$`)
|
||||
|
||||
func gone(err error) bool {
|
||||
return errors.Is(err, os.ErrNotExist) || errors.Is(err, syscall.ESRCH)
|
||||
}
|
||||
|
||||
func (c config) sourceGroup(group string) (bool, bool) {
|
||||
unit := filepath.Base(group)
|
||||
terminal := false
|
||||
for _, prefix := range c.Terminals {
|
||||
if strings.HasPrefix(unit, prefix) {
|
||||
terminal = true
|
||||
break
|
||||
}
|
||||
}
|
||||
job := (strings.HasPrefix(unit, "budget-c") || strings.HasPrefix(unit, "budget-b")) && strings.HasSuffix(unit, ".scope")
|
||||
return terminal, job
|
||||
}
|
||||
|
||||
func (c config) classify(exe, group string) (string, string) {
|
||||
terminal, job := c.sourceGroup(group)
|
||||
if !terminal && !job {
|
||||
return "", ""
|
||||
}
|
||||
name := c.Commands[exe]
|
||||
slice := ""
|
||||
if shared, ok := c.Shared[exe]; ok {
|
||||
name, slice = shared, c.sharedSlice()
|
||||
}
|
||||
if pythonName.MatchString(exe) {
|
||||
name = c.Python
|
||||
}
|
||||
if job && name != "search" && slice == "" {
|
||||
name = ""
|
||||
}
|
||||
return name, slice
|
||||
}
|
||||
|
||||
func readGroup(path string) (string, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
return strings.TrimPrefix(strings.TrimSpace(string(data)), "0::"), err
|
||||
}
|
||||
|
||||
func (p procFS) Group(pid uint32) (string, error) {
|
||||
return readGroup(filepath.Join(p.root, strconv.FormatUint(uint64(pid), 10), "cgroup"))
|
||||
}
|
||||
|
||||
func parseParent(stat string) (uint32, error) {
|
||||
i := strings.LastIndex(stat, ") ")
|
||||
if i < 0 {
|
||||
return 0, fmt.Errorf("invalid proc stat")
|
||||
}
|
||||
fields := strings.Fields(stat[i+2:])
|
||||
if len(fields) < 2 {
|
||||
return 0, fmt.Errorf("missing parent in proc stat")
|
||||
}
|
||||
value, err := strconv.ParseUint(fields[1], 10, 32)
|
||||
return uint32(value), err
|
||||
}
|
||||
|
||||
func (p procFS) readTask(path string, c config) (task, bool, error) {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return task{}, false, err
|
||||
}
|
||||
if info.Sys().(*syscall.Stat_t).Uid != p.uid {
|
||||
return task{}, false, nil
|
||||
}
|
||||
group, err := readGroup(filepath.Join(path, "cgroup"))
|
||||
if err != nil {
|
||||
return task{}, false, err
|
||||
}
|
||||
terminal, job := c.sourceGroup(group)
|
||||
if !terminal && !job {
|
||||
return task{}, false, nil
|
||||
}
|
||||
exe, err := os.Readlink(filepath.Join(path, "exe"))
|
||||
if err != nil {
|
||||
return task{}, false, err
|
||||
}
|
||||
stat, err := os.ReadFile(filepath.Join(path, "stat"))
|
||||
if err != nil {
|
||||
return task{}, false, err
|
||||
}
|
||||
parent, err := parseParent(string(stat))
|
||||
if err != nil {
|
||||
return task{}, false, err
|
||||
}
|
||||
name, slice := c.classify(filepath.Base(exe), group)
|
||||
return task{Parent: parent, Group: group, Profile: name, Slice: slice}, true, nil
|
||||
}
|
||||
|
||||
func (p procFS) Snapshot(c config) (map[uint32]task, error) {
|
||||
entries, err := os.ReadDir(p.root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tasks := map[uint32]task{}
|
||||
for _, entry := range entries {
|
||||
pid, err := strconv.ParseUint(entry.Name(), 10, 32)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
value, include, err := p.readTask(filepath.Join(p.root, entry.Name()), c)
|
||||
if gone(err) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if include {
|
||||
tasks[uint32(pid)] = value
|
||||
}
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
func family(tasks map[uint32]task, pid uint32) []uint32 {
|
||||
group := tasks[pid].Group
|
||||
seen := map[uint32]bool{pid: true}
|
||||
pending := []uint32{pid}
|
||||
for len(pending) > 0 {
|
||||
parent := pending[0]
|
||||
pending = pending[1:]
|
||||
for child, value := range tasks {
|
||||
if value.Parent == parent && value.Group == group && !seen[child] {
|
||||
seen[child] = true
|
||||
pending = append(pending, child)
|
||||
}
|
||||
}
|
||||
}
|
||||
result := make([]uint32, 0, len(seen))
|
||||
for child := range seen {
|
||||
result = append(result, child)
|
||||
}
|
||||
slices.Sort(result)
|
||||
return result
|
||||
}
|
||||
Loading…
Reference in a new issue