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:
Two details worth your opinion:
- 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.
- 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.
container stats --format jsonwaits 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
cpuUsageUsecis 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
collectStatstakes two samples unconditionally, sleeping two seconds between them, and the static path is what--format jsonuses:Sources/ContainerCommands/Container/ContainerStats.swift:46—if format != .table || noStream { try await runStatic() }:162-196—collectStatsgathersstats1,try await Task.sleep(for: .seconds(2)), thenstats2: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 sampleSo 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 statstwice and diffcpuUsageUsecyourself — 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:
Two details worth your opinion:
calculateCPUPercentdocuments "100% = one fully utilized core", matchingtopanddocker 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 checkingcontainer statsas ground truth.ContainerResource.ContainerStatsis 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 listreports the CPU allocation (CPUS) whilestatsreportsmemoryLimitBytesbut 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,
container1.2.2 from the Homebrew formula.