Skip to content

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

Description

@guanchzhou

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:46if format != .table || noStream { try await runStatic() }
  • :162-196collectStats gathers stats1, try await Task.sleep(for: .seconds(2)), then stats2
  • :206-217calculateCPUPercent(cpuUsage1:cpuUsage2:timeInterval:) turns the pair into the percentage the table shows
  • :105Output.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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions