apple / apple/container

[Request]: container stats --format json omits the CPU percentage it already computes

Open
#2,144 2 comments 0 reactions 0 assignees View on GitHub
good first issue
Dominant language
Swift
Stars
49.9k
Forks
1.8k
Avg merge
1d 20h
Merged PRs (30d)
22

Description

`container stats --format json` waits two seconds to collect a CPU delta, then discards it. The table output computes and prints a percentage from the same data; the JSON consumer cannot.

## What happens today

```
$ container stats --no-stream
Container ID Cpu % Memory Usage Net Rx/Tx Block I/O Pids
cs-spin 103.23% 2.69 MiB / 256.00 MiB 27.84 KiB / 0.59 KiB 2.51 MiB / 0.00 KiB 1
arango 0.63% 5.37 GiB / 6.00 GiB 74.48 MiB / 9.88 GiB 96.52 MiB / 4.00 KiB 61
```

```
$ container stats --no-stream --format json
{
"id": "cs-spin",
"cpuUsageUsec": 5734022776,
"memoryUsageBytes": 2822144,
"memoryLimitBytes": 268435456,
"networkRxBytes": 28508,
"networkTxBytes": 602,
"blockReadBytes": 2633728,
"blockWriteBytes": 0,
"numProcesses": 1
}
```

`cpuUsageUsec` is a cumulative counter. A single sample of it cannot yield a percentage, so the most useful column of the table is absent from the machine-readable format.

## The cost is already paid

`collectStats` takes two samples unconditionally, sleeping two seconds between them, and the static path is what `--format json` uses:

- `Sources/ContainerCommands/Container/ContainerStats.swift:46` — `if format != .table || noStream { try await runStatic() }`
- `:162-196` — `collectStats` gathers `stats1`, `try await Task.sleep(for: .seconds(2))`, then `stats2`
- `:206-217` — `calculateCPUPercent(cpuUsage1:cpuUsage2:timeInterval:)` turns the pair into the percentage the table shows
- `:105` — `Output.render(payload: statsData.map { $0.stats2 }, format: format)` emits **only the second sample**

So a JSON consumer waits the same two seconds, and then receives neither the computed percentage nor the first sample it would need to compute one itself. To get a CPU percentage from the CLI today you must invoke `container stats` twice and diff `cpuUsageUsec` yourself — four seconds of sleeping for a number the tool already had after two.

## Suggestion

Emit the percentage in the non-table formats. The value already exists at the render site, so the smallest version is a field on the rendered payload:

```
"cpuPercent": 103.23
```

Two details worth your opinion:

1. **Naming and units.** `calculateCPUPercent` documents "100% = one fully utilized core", matching `top` and `docker stats`. Worth stating that in the field's doc comment so consumers do not divide by the container's CPU allocation — I made exactly that mistake against this API before checking `container stats` as ground truth.
2. **Where it belongs.** `ContainerResource.ContainerStats` is documented as "Statistics for a container suitable for CLI display" and models a single sample, so a derived rate may not belong on it. A small wrapper for the rendered payload, or emitting both samples, may be cleaner than widening the model. Happy to follow whichever you prefer.

Related but separate: `container list` reports the CPU allocation (`CPUS`) while `stats` reports `memoryLimitBytes` but no CPU equivalent. Not needed for the percentage above, but it means a consumer wanting "percent of what this container was granted" still needs a second command.

I am happy to send a PR. Environment: macOS 27.0 (Tahoe), Apple silicon, `container` 1.2.2 from the Homebrew formula.

Contributor guide

Open the contributing guide

Research direction

Start in Sources/ContainerCommands/Container/ContainerStats.swift, especially the static path at line 46, collectStats at lines 162-196, CPU calculation at lines 206-217, and rendering at line 105. Trace how the two samples reach each output format, then ensure non-table output includes the already computed CPU percentage while preserving the table output.

Written by the indexing model from the issue text.

Assessment

Tech stack
swift
Domain
cli
Issue type
Feature
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.