Path: blob/main/components/ws-daemon/pkg/cpulimit/cfs_v2.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)1920type CgroupV2CFSController string2122func (basePath CgroupV2CFSController) Usage() (CPUTime, error) {23usage, err := basePath.getFlatKeyedValue("usage_usec")24if err != nil {25return 0, err26}2728return CPUTime(time.Duration(usage) * time.Microsecond), nil29}3031func (basePath CgroupV2CFSController) SetLimit(limit Bandwidth) (changed bool, err error) {32quota, period, err := basePath.readCpuMax()33if err != nil {34return false, xerrors.Errorf("failed to read cpu max from %s: %w", basePath, err)35}3637target := limit.Quota(period)38if quota == target {39return false, nil40}4142err = basePath.writeQuota(target)43if err != nil {44return false, xerrors.Errorf("cannot set CFS quota of %d (period is %d, parent quota is %d): %w",45target.Microseconds(), period.Microseconds(), basePath.readParentQuota().Microseconds(), err)46}4748return true, nil49}5051func (basePath CgroupV2CFSController) NrThrottled() (uint64, error) {52throttled, err := basePath.getFlatKeyedValue("nr_throttled")53if err != nil {54return 0, err55}5657return uint64(throttled), nil58}5960func (basePath CgroupV2CFSController) readCpuMax() (time.Duration, time.Duration, error) {61cpuMaxPath := filepath.Join(string(basePath), "cpu.max")62cpuMax, err := os.ReadFile(cpuMaxPath)63if err != nil {64return 0, 0, xerrors.Errorf("unable to read cpu.max: %w", err)65}6667parts := strings.Fields(string(cpuMax))68if len(parts) < 2 {69return 0, 0, xerrors.Errorf("cpu.max did not have expected number of fields: %s", parts)70}7172var quota int6473if parts[0] == "max" {74quota = math.MaxInt6475} else {76quota, err = strconv.ParseInt(parts[0], 10, 64)77if err != nil {78return 0, 0, xerrors.Errorf("could not parse quota of %s: %w", parts[0], err)79}80}8182period, err := strconv.ParseInt(parts[1], 10, 64)83if err != nil {84return 0, 0, xerrors.Errorf("could not parse period of %s: %w", parts[1], err)85}8687return time.Duration(quota) * time.Microsecond, time.Duration(period) * time.Microsecond, nil88}8990func (basePath CgroupV2CFSController) writeQuota(quota time.Duration) error {91cpuMaxPath := filepath.Join(string(basePath), "cpu.max")92return os.WriteFile(cpuMaxPath, []byte(strconv.FormatInt(quota.Microseconds(), 10)), 0644)93}9495func (basePath CgroupV2CFSController) readParentQuota() time.Duration {96parent := CgroupV2CFSController(filepath.Dir(string(basePath)))97quota, _, err := parent.readCpuMax()98if err != nil {99return time.Duration(0)100}101102return time.Duration(quota)103}104105func (basePath CgroupV2CFSController) getFlatKeyedValue(key string) (int64, error) {106stats, err := os.Open(filepath.Join(string(basePath), "cpu.stat"))107if err != nil {108if errors.Is(err, fs.ErrNotExist) {109return 0, nil110}111112return 0, xerrors.Errorf("cannot read cpu.stat: %w", err)113}114defer stats.Close()115116scanner := bufio.NewScanner(stats)117for scanner.Scan() {118entry := scanner.Text()119if !strings.HasPrefix(entry, key) {120continue121}122123r, err := strconv.ParseInt(strings.TrimSpace(strings.TrimPrefix(entry, key)), 10, 64)124if err != nil {125return 0, xerrors.Errorf("cannot parse cpu.stat: %s: %w", entry, err)126}127return int64(r), nil128}129return 0, xerrors.Errorf("cpu.stat did not contain nr_throttled")130}131132133