Skip to content

Commit 5f00b5f

Browse files
authored
Merge pull request #1807 from bensmrs/instance-state-cpu-time
Add allocated CPU time to instance state
2 parents 917ebcc + 1fb0730 commit 5f00b5f

File tree

8 files changed

+125
-5
lines changed

8 files changed

+125
-5
lines changed

doc/api-extensions.md

+4
Original file line numberDiff line numberDiff line change
@@ -2748,3 +2748,7 @@ Adds `acme.http.port` to control an alternative HTTP port for `HTTP-01` validati
27482748
## `network_ovn_ipv4_dhcp_expiry`
27492749

27502750
Introduces `ipv4.dhcp.expiry` for OVN networks.
2751+
2752+
## `instance_state_cpu_time`
2753+
2754+
This adds an `allocated_time` field below `CPU` in the instance state API.

doc/rest-api.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -2306,6 +2306,12 @@ definitions:
23062306
x-go-package: github.com/lxc/incus/v6/shared/api
23072307
InstanceStateCPU:
23082308
properties:
2309+
allocated_time:
2310+
description: CPU time available per second, in nanoseconds
2311+
example: 4000000000
2312+
format: int64
2313+
type: integer
2314+
x-go-name: AllocatedTime
23092315
usage:
23102316
description: CPU usage in nanoseconds
23112317
example: 3637691016

internal/server/cgroup/abstraction.go

+59
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,65 @@ func (cg *CGroup) SetCPUCfsLimit(limitPeriod int64, limitQuota int64) error {
749749
return ErrUnknownVersion
750750
}
751751

752+
// GetCPUCfsLimit gets the quota and duration in ms for each scheduling period.
753+
func (cg *CGroup) GetCPUCfsLimit() (int64, int64, error) {
754+
version := cgControllers["cpu"]
755+
switch version {
756+
case Unavailable:
757+
return -1, -1, ErrControllerMissing
758+
case V1:
759+
limitQuotaStr, err := cg.rw.Get(version, "cpu", "cpu.cfs_quota_us")
760+
if err != nil {
761+
return -1, -1, err
762+
}
763+
764+
limitQuota, err := strconv.ParseInt(limitQuotaStr, 10, 64)
765+
if err != nil {
766+
return -1, -1, err
767+
}
768+
769+
limitPeriodStr, err := cg.rw.Get(version, "cpu", "cpu.cfs_period_us")
770+
if err != nil {
771+
return -1, -1, err
772+
}
773+
774+
limitPeriod, err := strconv.ParseInt(limitPeriodStr, 10, 64)
775+
if err != nil {
776+
return -1, -1, err
777+
}
778+
779+
return limitPeriod, limitQuota, nil
780+
case V2:
781+
cpuMax, err := cg.rw.Get(version, "cpu", "cpu.max")
782+
if err != nil {
783+
return -1, -1, err
784+
}
785+
786+
cpuMaxFields := strings.Split(cpuMax, " ")
787+
if len(cpuMaxFields) != 2 {
788+
return -1, -1, errors.New("Couldn't parse CFS limits")
789+
}
790+
791+
if cpuMaxFields[0] == "max" {
792+
return -1, -1, nil
793+
}
794+
795+
limitQuota, err := strconv.ParseInt(cpuMaxFields[0], 10, 64)
796+
if err != nil {
797+
return -1, -1, err
798+
}
799+
800+
limitPeriod, err := strconv.ParseInt(cpuMaxFields[1], 10, 64)
801+
if err != nil {
802+
return -1, -1, err
803+
}
804+
805+
return limitPeriod, limitQuota, nil
806+
}
807+
808+
return -1, -1, ErrUnknownVersion
809+
}
810+
752811
// SetHugepagesLimit applies a limit to the number of processes.
753812
func (cg *CGroup) SetHugepagesLimit(pageType string, limit int64) error {
754813
version := cgControllers["hugetlb"]

internal/server/instance/drivers/driver_lxc.go

+26-5
Original file line numberDiff line numberDiff line change
@@ -7521,6 +7521,19 @@ func (d *lxc) Exec(req api.InstanceExecPost, stdin *os.File, stdout *os.File, st
75217521
return instCmd, nil
75227522
}
75237523

7524+
func (d *lxc) cpuStateUsage(cg *cgroup.CGroup) (int64, bool) {
7525+
if !d.state.OS.CGInfo.Supports(cgroup.CPUAcct, cg) {
7526+
return -1, false
7527+
}
7528+
7529+
value, err := cg.GetCPUAcctUsage()
7530+
if err != nil {
7531+
return -1, true
7532+
}
7533+
7534+
return value, true
7535+
}
7536+
75247537
func (d *lxc) cpuState() api.InstanceStateCPU {
75257538
cpu := api.InstanceStateCPU{}
75267539

@@ -7529,23 +7542,31 @@ func (d *lxc) cpuState() api.InstanceStateCPU {
75297542
return cpu
75307543
}
75317544

7532-
// CPU usage in seconds
75337545
cg, err := d.cgroup(cc, true)
75347546
if err != nil {
75357547
return cpu
75367548
}
75377549

7538-
if !d.state.OS.CGInfo.Supports(cgroup.CPUAcct, cg) {
7550+
cpuUsage, ok := d.cpuStateUsage(cg)
7551+
if ok {
7552+
cpu.Usage = cpuUsage
7553+
}
7554+
7555+
cpuCount, err := cg.GetEffectiveCPUs()
7556+
if err != nil {
75397557
return cpu
75407558
}
75417559

7542-
value, err := cg.GetCPUAcctUsage()
7560+
limitPeriod, limitQuota, err := cg.GetCPUCfsLimit()
75437561
if err != nil {
7544-
cpu.Usage = -1
75457562
return cpu
75467563
}
75477564

7548-
cpu.Usage = value
7565+
if limitQuota == -1 {
7566+
cpu.AllocatedTime = int64(cpuCount) * 1_000_000_000
7567+
} else {
7568+
cpu.AllocatedTime = 1_000_000_000 * limitQuota / limitPeriod
7569+
}
75497570

75507571
return cpu
75517572
}

internal/server/instance/drivers/driver_qemu.go

+11
Original file line numberDiff line numberDiff line change
@@ -8138,6 +8138,17 @@ func (d *qemu) renderState(statusCode api.StatusCode) (*api.InstanceState, error
81388138
}
81398139
}
81408140

8141+
// Populate the CPU time allocation
8142+
limitsCPU, ok := d.expandedConfig["limits.cpu"]
8143+
if ok {
8144+
cpuCount, err := strconv.ParseInt(limitsCPU, 10, 64)
8145+
if err != nil {
8146+
status.CPU.AllocatedTime = cpuCount * 1_000_000_000
8147+
}
8148+
} else {
8149+
status.CPU.AllocatedTime = qemudefault.CPUCores * 1_000_000_000
8150+
}
8151+
81418152
// Populate host_name for network devices.
81428153
for k, m := range d.ExpandedDevices() {
81438154
// We only care about nics.

internal/version/api.go

+1
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,7 @@ var APIExtensions = []string{
471471
"network_dns_nameservers",
472472
"acme_http01_port",
473473
"network_ovn_ipv4_dhcp_expiry",
474+
"instance_state_cpu_time",
474475
}
475476

476477
// APIExtensionsCount returns the number of available API extensions.

shared/api/instance_state.go

+6
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ type InstanceStateCPU struct {
9898
// CPU usage in nanoseconds
9999
// Example: 3637691016
100100
Usage int64 `json:"usage" yaml:"usage"`
101+
102+
// CPU time available per second, in nanoseconds
103+
// Example: 4000000000
104+
//
105+
// API extension: instance_state_cpu_time
106+
AllocatedTime int64 `json:"allocated_time" yaml:"allocated_time"`
101107
}
102108

103109
// InstanceStateMemory represents the memory information section of an instance's state.

test/suites/basic.sh

+12
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,18 @@ test_basic_usage() {
399399
[ "$(incus config get test-limits limits.cpu)" = "1" ]
400400
[ "$(incus config get test-limits limits.cpu.allowance)" = "50%" ]
401401
[ "$(incus config get test-limits limits.memory)" = "204MiB" ]
402+
403+
# Test CPU allocation information
404+
[ "$(incus query /1.0/instances/test-limits/state | jq -r '.cpu.allocated_time')" = "1000000000" ]
405+
incus config set test-limits limits.cpu.allowance 100ms/200ms
406+
[ "$(incus query /1.0/instances/test-limits/state | jq -r '.cpu.allocated_time')" = "500000000" ]
407+
incus config set test-limits limits.cpu 2
408+
[ "$(incus query /1.0/instances/test-limits/state | jq -r '.cpu.allocated_time')" = "500000000" ]
409+
incus config unset test-limits limits.cpu.allowance
410+
[ "$(incus query /1.0/instances/test-limits/state | jq -r '.cpu.allocated_time')" = "2000000000" ]
411+
incus config unset test-limits limits.cpu
412+
[ "$(incus query /1.0/instances/test-limits/state | jq -r '.cpu.allocated_time')" = "$(nproc)000000000" ]
413+
402414
incus delete -f test-limits
403415

404416
# Test last_used_at field is working properly

0 commit comments

Comments
 (0)