[Request]: container stats --format json omits the CPU percentage it already computes
- 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
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