Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add allocated CPU time to instance state #1807

Merged
merged 7 commits into from
Mar 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/api-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2748,3 +2748,7 @@ Adds `acme.http.port` to control an alternative HTTP port for `HTTP-01` validati
## `network_ovn_ipv4_dhcp_expiry`

Introduces `ipv4.dhcp.expiry` for OVN networks.

## `instance_state_cpu_time`

This adds an `allocated_time` field below `CPU` in the instance state API.
6 changes: 6 additions & 0 deletions doc/rest-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2306,6 +2306,12 @@ definitions:
x-go-package: github.com/lxc/incus/v6/shared/api
InstanceStateCPU:
properties:
allocated_time:
description: CPU time available per second, in nanoseconds
example: 4000000000
format: int64
type: integer
x-go-name: AllocatedTime
usage:
description: CPU usage in nanoseconds
example: 3637691016
Expand Down
59 changes: 59 additions & 0 deletions internal/server/cgroup/abstraction.go
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,65 @@ func (cg *CGroup) SetCPUCfsLimit(limitPeriod int64, limitQuota int64) error {
return ErrUnknownVersion
}

// GetCPUCfsLimit gets the quota and duration in ms for each scheduling period.
func (cg *CGroup) GetCPUCfsLimit() (int64, int64, error) {
version := cgControllers["cpu"]
switch version {
case Unavailable:
return -1, -1, ErrControllerMissing
case V1:
limitQuotaStr, err := cg.rw.Get(version, "cpu", "cpu.cfs_quota_us")
if err != nil {
return -1, -1, err
}

limitQuota, err := strconv.ParseInt(limitQuotaStr, 10, 64)
if err != nil {
return -1, -1, err
}

limitPeriodStr, err := cg.rw.Get(version, "cpu", "cpu.cfs_period_us")
if err != nil {
return -1, -1, err
}

limitPeriod, err := strconv.ParseInt(limitPeriodStr, 10, 64)
if err != nil {
return -1, -1, err
}

return limitPeriod, limitQuota, nil
case V2:
cpuMax, err := cg.rw.Get(version, "cpu", "cpu.max")
if err != nil {
return -1, -1, err
}

cpuMaxFields := strings.Split(cpuMax, " ")
if len(cpuMaxFields) != 2 {
return -1, -1, errors.New("Couldn't parse CFS limits")
}

if cpuMaxFields[0] == "max" {
return -1, -1, nil
}

limitQuota, err := strconv.ParseInt(cpuMaxFields[0], 10, 64)
if err != nil {
return -1, -1, err
}

limitPeriod, err := strconv.ParseInt(cpuMaxFields[1], 10, 64)
if err != nil {
return -1, -1, err
}

return limitPeriod, limitQuota, nil
}

return -1, -1, ErrUnknownVersion
}

// SetHugepagesLimit applies a limit to the number of processes.
func (cg *CGroup) SetHugepagesLimit(pageType string, limit int64) error {
version := cgControllers["hugetlb"]
Expand Down
31 changes: 26 additions & 5 deletions internal/server/instance/drivers/driver_lxc.go
Original file line number Diff line number Diff line change
Expand Up @@ -7521,6 +7521,19 @@ func (d *lxc) Exec(req api.InstanceExecPost, stdin *os.File, stdout *os.File, st
return instCmd, nil
}

func (d *lxc) cpuStateUsage(cg *cgroup.CGroup) (int64, bool) {
if !d.state.OS.CGInfo.Supports(cgroup.CPUAcct, cg) {
return -1, false
}

value, err := cg.GetCPUAcctUsage()
if err != nil {
return -1, true
}

return value, true
}

func (d *lxc) cpuState() api.InstanceStateCPU {
cpu := api.InstanceStateCPU{}

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

// CPU usage in seconds
cg, err := d.cgroup(cc, true)
if err != nil {
return cpu
}

if !d.state.OS.CGInfo.Supports(cgroup.CPUAcct, cg) {
cpuUsage, ok := d.cpuStateUsage(cg)
if ok {
cpu.Usage = cpuUsage
}

cpuCount, err := cg.GetEffectiveCPUs()
if err != nil {
return cpu
}

value, err := cg.GetCPUAcctUsage()
limitPeriod, limitQuota, err := cg.GetCPUCfsLimit()
if err != nil {
cpu.Usage = -1
return cpu
}

cpu.Usage = value
if limitQuota == -1 {
cpu.AllocatedTime = int64(cpuCount) * 1_000_000_000
} else {
cpu.AllocatedTime = 1_000_000_000 * limitQuota / limitPeriod
}

return cpu
}
Expand Down
11 changes: 11 additions & 0 deletions internal/server/instance/drivers/driver_qemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -8138,6 +8138,17 @@ func (d *qemu) renderState(statusCode api.StatusCode) (*api.InstanceState, error
}
}

// Populate the CPU time allocation
limitsCPU, ok := d.expandedConfig["limits.cpu"]
if ok {
cpuCount, err := strconv.ParseInt(limitsCPU, 10, 64)
if err != nil {
status.CPU.AllocatedTime = cpuCount * 1_000_000_000
}
} else {
status.CPU.AllocatedTime = qemudefault.CPUCores * 1_000_000_000
}

// Populate host_name for network devices.
for k, m := range d.ExpandedDevices() {
// We only care about nics.
Expand Down
1 change: 1 addition & 0 deletions internal/version/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ var APIExtensions = []string{
"network_dns_nameservers",
"acme_http01_port",
"network_ovn_ipv4_dhcp_expiry",
"instance_state_cpu_time",
}

// APIExtensionsCount returns the number of available API extensions.
Expand Down
6 changes: 6 additions & 0 deletions shared/api/instance_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ type InstanceStateCPU struct {
// CPU usage in nanoseconds
// Example: 3637691016
Usage int64 `json:"usage" yaml:"usage"`

// CPU time available per second, in nanoseconds
// Example: 4000000000
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing API extension comment here

//
// API extension: instance_state_cpu_time
AllocatedTime int64 `json:"allocated_time" yaml:"allocated_time"`
}

// InstanceStateMemory represents the memory information section of an instance's state.
Expand Down
12 changes: 12 additions & 0 deletions test/suites/basic.sh
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,18 @@ test_basic_usage() {
[ "$(incus config get test-limits limits.cpu)" = "1" ]
[ "$(incus config get test-limits limits.cpu.allowance)" = "50%" ]
[ "$(incus config get test-limits limits.memory)" = "204MiB" ]

# Test CPU allocation information
[ "$(incus query /1.0/instances/test-limits/state | jq -r '.cpu.allocated_time')" = "1000000000" ]
incus config set test-limits limits.cpu.allowance 100ms/200ms
[ "$(incus query /1.0/instances/test-limits/state | jq -r '.cpu.allocated_time')" = "500000000" ]
incus config set test-limits limits.cpu 2
[ "$(incus query /1.0/instances/test-limits/state | jq -r '.cpu.allocated_time')" = "500000000" ]
incus config unset test-limits limits.cpu.allowance
[ "$(incus query /1.0/instances/test-limits/state | jq -r '.cpu.allocated_time')" = "2000000000" ]
incus config unset test-limits limits.cpu
[ "$(incus query /1.0/instances/test-limits/state | jq -r '.cpu.allocated_time')" = "$(nproc)000000000" ]

incus delete -f test-limits

# Test last_used_at field is working properly
Expand Down