Skip to content

feat: what the process holds, beside the peak it once reached (ADR 0061) - #96

Merged
kfeofantov merged 2 commits into
mainfrom
feat/what-the-process-holds
Aug 31, 2026
Merged

kfeofantov merged 2 commits into
mainfrom
feat/what-the-process-holds

Conversation

@kfeofantov

@kfeofantov kfeofantov commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor

Three of the seven new fields are rows of a file the scanner already opens and
parses; the other four cost two small reads beside it.

What was missing

/proc/<pid>/status has about forty rows and ADR 0052 took two of them, because
the allow-list was drawn for one finding. Three questions the agent could not
answer are answered by rows in that same file or in two small neighbours:

  • How much of a container's memory is the program's own. A memory limit
    counts anonymous and file-backed pages alike, so 300 MB of heap plus 500 MB of
    page cache looks identical to 800 MB of heap. Every recommendation about a
    memory limit rests on that difference, and the agent was shipping the total.
  • What one more replica costs. Resident memory counts a shared page once per
    process mapping it; replicas of one image share their text. Pss is the figure
    that may be summed across replicas, and resident memory is not.
  • How close a process is to descriptor exhaustion, which is invisible in
    every metric until connections start failing.

What ships

process_peaks gains seven fields, merged across the processes of one build on
every node still running it:

"rss_anon_bytes_max": 421527552,
"rss_file_bytes_max": 63963136,
"pss_bytes_max": 461373440,
"private_dirty_bytes_max": 417333248,
"threads_max": 42,
"open_files_max": 61000,
"open_files_limit_min": 65536

Maxima, except the ceiling, which is a minimum — headroom is what the
tightest replica has: the most descriptors any replica held against the lowest
ceiling any was given.

The names carry a distinction the record now has to hold: peak_rss_bytes is a
mark the kernel maintains between readings; every *_max beside it is the
highest value the agent happened to sample. Naming them alike would have claimed
a continuity that isn't there.

Filtering

Descriptors are counted, not followed — what one points at is a path, a
socket or a pipe. The rollup's mapping paths are not read. Keys are matched
exactly, because Pss_Dirty sits directly under Pss and Private_Clean
directly above Private_Dirty, and a prefix match would be wrong by a plausible
amount. Name still has nowhere in the status struct to go, and the test that
keeps that true as the struct grows now covers five fields instead of two.

No new capability: the node already runs with hostPID and CAP_SYS_PTRACE to
open /proc/<pid>/exe, and every file here sits behind the same kernel check.

Stated, not implied

smaps_rollup costs a page-table walk proportional to the process's address
space, per kept process per pass. That cost is not measured, so ADR 0061
marks it rather than claiming otherwise (ADR 0039), and names how to close it:
the inventory e2e on a cluster with large processes, timed with the read and
without.

Evidence

go test ./... -race and golangci-lint run ./... on host and GOOS=linux:
0 issues. The inventory e2e now asserts the footprint against the kernel's own
arithmetic rather than magic numbers — anon plus file cannot exceed the
high-water mark the same kernel recorded. The e2e suites run against a kind
cluster and are not part of CI, so this change has not been exercised on one.

🤖 Generated with Claude Code

https://claude.ai/code/session_011YRKuL97KRndaopfpixrhp

`/proc/<pid>/status` has about forty rows and the scanner took two, because the
allow-list was drawn for one finding. Three more rows and two small neighbouring
files answer questions the agent could not:

- how much of a container's memory is the program's own, rather than page cache
  the limit counts alike — the distinction every memory recommendation rests on;
- what one more replica actually costs, which resident memory overstates because
  it counts a shared page once per process mapping it, and `Pss` does not;
- how close a process is to descriptor exhaustion, which no metric shows until
  connections start failing.

`process_peaks` gains seven fields. They merge as maxima across the processes of
one build, except the descriptor ceiling, which merges as a minimum: headroom is
what the tightest replica has. The names carry the distinction the record now
holds — `peak_rss_bytes` is a mark the kernel maintains, every `*_max` beside it
is the highest sample the agent took.

Descriptors are counted without being followed: what one points at is a path, a
socket or a pipe. The rollup's mapping paths are not read. `Name` still has
nowhere in the status struct to go, and the test that keeps that true as the
struct grows now covers five fields instead of two.

No new capability — the node already passes the same ptrace check to open
/proc/<pid>/exe. The unmeasured cost of the smaps_rollup page-table walk is
stated in the ADR rather than implied away (ADR 0039).

go test ./... -race, golangci-lint run ./... (host and GOOS=linux): 0 issues.
…et (ADR 0044)

The obligation is new, not restated: how the footprint samples differ from the
kernel's mark, which figure may be summed across replicas, and what an absent
descriptor ceiling means. Both the file and the "4. Ingest" section move to
exactly the size that obligation costs.
@kfeofantov
kfeofantov force-pushed the feat/what-the-process-holds branch from 09a87c3 to 0f6c8e7 Compare August 31, 2026 16:59
@kfeofantov
kfeofantov merged commit 992c36c into main Aug 31, 2026
3 checks passed
@kfeofantov
kfeofantov deleted the feat/what-the-process-holds branch August 31, 2026 19:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant