Path: blob/main/components/ws-daemon/pkg/cpulimit/cfs.go
2500 views
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package cpulimit56import (7"bufio"8"errors"9"io/fs"10"math"11"os"12"path/filepath"13"strconv"14"strings"15"time"1617"golang.org/x/xerrors"18)1920// CgroupV1CFSController controls a cgroup's CFS settings21type CgroupV1CFSController string2223// Usage returns the cpuacct.usage value of the cgroup24func (basePath CgroupV1CFSController) Usage() (usage CPUTime, err error) {25cputime, err := basePath.readCpuUsage()26if err != nil {27return 0, xerrors.Errorf("cannot read cpuacct.usage: %w", err)28}2930return CPUTime(cputime), nil31}3233// SetQuota sets a new CFS quota on the cgroup34func (basePath CgroupV1CFSController) SetLimit(limit Bandwidth) (changed bool, err error) {35period, err := basePath.readCfsPeriod()36if err != nil {37err = xerrors.Errorf("failed to read CFS period: %w", err)38return39}4041quota, err := basePath.readCfsQuota()42if err != nil {43err = xerrors.Errorf("cannot parse CFS quota: %w", err)44return45}46target := limit.Quota(period)47if quota == target {48return false, nil49}5051err = os.WriteFile(filepath.Join(string(basePath), "cpu.cfs_quota_us"), []byte(strconv.FormatInt(target.Microseconds(), 10)), 0644)52if err != nil {53return false, xerrors.Errorf("cannot set CFS quota of %d (period is %d, parent quota is %d): %w",54target.Microseconds(), period.Microseconds(), basePath.readParentQuota().Microseconds(), err)55}56return true, nil57}5859func (basePath CgroupV1CFSController) readParentQuota() time.Duration {60parent := CgroupV1CFSController(filepath.Dir(string(basePath)))61pq, err := parent.readCfsQuota()62if err != nil {63return time.Duration(0)64}6566return time.Duration(pq) * time.Microsecond67}6869func (basePath CgroupV1CFSController) readString(path string) (string, error) {70fn := filepath.Join(string(basePath), path)71fc, err := os.ReadFile(fn)72if err != nil {73return "", err74}7576s := strings.TrimSpace(string(fc))77return s, nil78}7980func (basePath CgroupV1CFSController) readCfsPeriod() (time.Duration, error) {81s, err := basePath.readString("cpu.cfs_period_us")82if err != nil {83return 0, err84}8586p, err := strconv.ParseInt(s, 10, 64)87if err != nil {88return 0, err89}90return time.Duration(uint64(p)) * time.Microsecond, nil91}9293func (basePath CgroupV1CFSController) readCfsQuota() (time.Duration, error) {94s, err := basePath.readString("cpu.cfs_quota_us")95if err != nil {96return 0, err97}9899p, err := strconv.ParseInt(s, 10, 64)100if err != nil {101return 0, err102}103104if p < 0 {105return time.Duration(math.MaxInt64), nil106}107return time.Duration(p) * time.Microsecond, nil108}109110func (basePath CgroupV1CFSController) readCpuUsage() (time.Duration, error) {111s, err := basePath.readString("cpuacct.usage")112if err != nil {113return 0, err114}115116p, err := strconv.ParseInt(s, 10, 64)117if err != nil {118return 0, err119}120return time.Duration(uint64(p)) * time.Nanosecond, nil121}122123// NrThrottled returns the number of CFS periods the cgroup was throttled in124func (basePath CgroupV1CFSController) NrThrottled() (uint64, error) {125f, err := os.Open(filepath.Join(string(basePath), "cpu.stat"))126if err != nil {127if errors.Is(err, fs.ErrNotExist) {128return 0, nil129}130131return 0, xerrors.Errorf("cannot read cpu.stat: %w", err)132}133defer f.Close()134135const prefixNrThrottled = "nr_throttled "136137scanner := bufio.NewScanner(f)138for scanner.Scan() {139l := scanner.Text()140if !strings.HasPrefix(l, prefixNrThrottled) {141continue142}143144r, err := strconv.ParseInt(strings.TrimSpace(strings.TrimPrefix(l, prefixNrThrottled)), 10, 64)145if err != nil {146return 0, xerrors.Errorf("cannot parse cpu.stat: %s: %w", l, err)147}148return uint64(r), nil149}150return 0, xerrors.Errorf("cpu.stat did not contain nr_throttled")151}152153154