From b47210b24faaa1bc0de7051c54a935ef20460d32 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 16 Sep 2026 16:12:48 +0500 Subject: [PATCH 1/3] fix(perf): the memory number carries the name of what it measures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `peak_rss_bytes` was false on Windows. A witness settles it rather than a manual page: a child that commits 256 MiB and never touches a page is reported as 262.5 MiB by the job object's PeakProcessMemoryUsed, while on Linux nothing becomes resident. The job object counts COMMITTED memory — job and process memory limits are defined on committed virtual memory — and `ru_maxrss` counts RESIDENT pages. One field name claimed both. The same witness refuted the other half of the suspicion. `wait4` on the immediate child does NOT stop at the shell: shell -> heavy child and shell -> shell -> heavy child both return the descendant's 260 MiB against 13 MiB for a light control, so the POSIX path already measures the tree it should. So the quantity now travels with the number. Each sample and each cell carries `memory_metric`, drawn from a closed set — `max_process_peak_resident` on the wait4 and `/usr/bin/time -v` paths, `max_process_peak_commit` on the Windows job object — and a value produced under any other kind raises rather than being recorded. No downstream reader infers semantics from `sys.platform`, and no alias lets Windows commit go on being read as RSS. Renamed with it, because the old nouns lied in the same way: `peak_rss` -> `peak_memory`, `rss_mechanism` -> `memory_mechanism`, `rss_unavailable_reason` -> `memory_unavailable_reason`, `raw_peak_rss_bytes` -> `raw_peak_memory_bytes`, `RssProbe` -> `MemoryProbe`. Deliberately NOT done: Windows resident-set sampling. `PeakWorkingSetSize` is the resident analogue, but obtaining it for a tree of processes that come and go needs handle tracking or polling, and this instrument does not sample. Two operating systems answering different, honest questions beats a profiler added so they can pronounce the same noun. This moves `measurement_harness_digest` 562a7f7232da -> 104c384d01bf. The bindings that name the old digest are re-bound in the next commit; nothing here tries to preserve it. perf instrument controls 16/16, round 7 apparatus controls 10/10. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh --- scripts/perf_baseline.py | 95 ++++++++++++++++++++++++---------- tests/test_perf_instrument.py | 8 +-- tests/test_round7_apparatus.py | 4 +- 3 files changed, 73 insertions(+), 34 deletions(-) diff --git a/scripts/perf_baseline.py b/scripts/perf_baseline.py index e53a0425..0d112d81 100644 --- a/scripts/perf_baseline.py +++ b/scripts/perf_baseline.py @@ -1112,18 +1112,43 @@ def environment_fingerprint() -> dict[str, object]: # --- peak RSS, by a NAMED tool (§8) ---------------------------------------- -class RssProbe: - """Peak resident set of a child process, captured by a NAMED mechanism. - - POSIX: ``os.wait4`` — the kernel's own per-child accounting, ``ru_maxrss`` - for exactly the process we spawned. Chosen over ``/usr/bin/time -v`` as the +# The two kernel-native memory quantities this instrument can name. They are +# NOT the same physical quantity, and a witness proves it rather than a manual +# page: a child that commits 256 MiB and never touches a page reports 262.5 MiB +# through the Windows job object and nothing resident on Linux. Both are +# comparable Rust-vs-Python WITHIN one platform stratum; neither may be pooled +# with, or compared against, the other across platforms. +MEMORY_METRIC_RESIDENT = "max_process_peak_resident" +MEMORY_METRIC_COMMIT = "max_process_peak_commit" +MEMORY_METRICS: frozenset[str] = frozenset({MEMORY_METRIC_RESIDENT, MEMORY_METRIC_COMMIT}) + + +class MemoryProbe: + """Peak memory of a process tree, captured by a NAMED mechanism that also + NAMES WHICH QUANTITY it captured. + + POSIX: ``os.wait4`` — the kernel's own accounting, ``ru_maxrss``, which is + the peak RESIDENT set of the child and of the descendants it waited for: a + witness run of shell -> child and shell -> shell -> child returns the heavy + descendant's peak, not the shell's. Chosen over ``/usr/bin/time -v`` as the primary because GNU time is a package that may simply be absent (it is absent on the container this was first calibrated on), and "the tool was missing" is not a memory measurement. ``/usr/bin/time -v`` remains as the - documented fallback where wait4 is unavailable. + documented fallback where wait4 is unavailable, and reports the same + quantity. Windows: a Job Object, read through ``QueryInformationJobObject`` for - ``PeakProcessMemoryUsed``. + ``PeakProcessMemoryUsed`` — the peak COMMITTED memory of any process ever + associated with the job. Committed, not resident: job and process memory + limits are defined on committed virtual memory, and the witness above shows + an untouched commit counted in full. The resident analogue would be + ``PeakWorkingSetSize`` per process, which for a tree of processes that come + and go needs handle tracking or polling; this instrument does not sample, + and will not perturb the elapsed-time interval to make two operating systems + pronounce the same noun. + + So the quantity travels WITH the number, in ``memory_metric``. Downstream + code never infers the semantics from ``sys.platform``. Anywhere else: None WITH A REASON, so a silent absence can never be read as a measured zero. @@ -1131,15 +1156,19 @@ class RssProbe: def __init__(self) -> None: self.reason = "" + self.metric = "" if hasattr(os, "wait4"): self.mechanism = "posix os.wait4 (ru_maxrss)" + self.metric = MEMORY_METRIC_RESIDENT elif os.name == "nt": self.mechanism = "win32 job object (PeakProcessMemoryUsed)" + self.metric = MEMORY_METRIC_COMMIT elif Path("/usr/bin/time").is_file(): self.mechanism = "/usr/bin/time -v" + self.metric = MEMORY_METRIC_RESIDENT else: self.mechanism = "none" - self.reason = f"no named RSS mechanism on {sys.platform}" + self.reason = f"no named memory mechanism on {sys.platform}" # ru_maxrss is kilobytes on Linux and bytes on macOS/BSD. Recorded, because # a memory number whose unit was guessed is worse than none. @@ -1565,7 +1594,7 @@ def rusage_seconds_to_ns(seconds: float) -> int: class Harness: gate: IdentityGate session: SessionIdentity - rss: RssProbe + memory: MemoryProbe tmp: Path candidate: Path warmup_discards: int @@ -1609,9 +1638,9 @@ def _run_once(self, argv: list[str], env: dict[str, str], cwd: Path) -> dict[str left inside: moving them would tighten it, and a tightened interval silently un-compares every future number against every recorded one. """ - wrapped, sidecar = self.rss.wrap(argv, self.tmp) + wrapped, sidecar = self.memory.wrap(argv, self.tmp) job = None - if os.name == "nt" and self.rss.mechanism.startswith("win32"): + if os.name == "nt" and self.memory.mechanism.startswith("win32"): job = ctypes.windll.kernel32.CreateJobObjectW(None, None) # type: ignore[attr-defined] peak: int | None = None why = "" @@ -1624,14 +1653,14 @@ def _run_once(self, argv: list[str], env: dict[str, str], cwd: Path) -> dict[str if job: ctypes.windll.kernel32.AssignProcessToJobObject( # type: ignore[attr-defined] job, int(proc._handle)) # type: ignore[attr-defined] - if self.rss.mechanism.startswith("posix"): + if self.memory.mechanism.startswith("posix"): # Reap through wait4 so the kernel hands back THIS child's rusage. _, status, ru = os.wait4(proc.pid, 0) proc.returncode = os.waitstatus_to_exitcode(status) rc = proc.returncode elapsed = time.perf_counter_ns() - t0 # --- the clock is stopped; everything below is bookkeeping ------- - peak = int(ru.ru_maxrss) * self.rss.maxrss_unit_bytes + peak = int(ru.ru_maxrss) * self.memory.maxrss_unit_bytes accounting = { "cpu_user_ns": rusage_seconds_to_ns(ru.ru_utime), "cpu_system_ns": rusage_seconds_to_ns(ru.ru_stime), @@ -1643,19 +1672,26 @@ def _run_once(self, argv: list[str], env: dict[str, str], cwd: Path) -> dict[str else: rc = proc.wait() elapsed = time.perf_counter_ns() - t0 - peak = self.rss.read(sidecar) + peak = self.memory.read(sidecar) if peak is None and job: - peak, why = self.rss.read_windows_peak(job) + peak, why = self.memory.read_windows_peak(job) accounting_why = ( "per-child CPU, fault and context-switch accounting comes from os.wait4's " f"rusage, which is not available here (platform {sys.platform!r}, RSS " - f"mechanism {self.rss.mechanism!r}); these fields were not measured") + f"mechanism {self.memory.mechanism!r}); these fields were not measured") if job: ctypes.windll.kernel32.CloseHandle(job) # type: ignore[attr-defined] if sidecar is not None and sidecar.is_file(): sidecar.unlink() - return {"elapsed_ns": elapsed, "rc": rc, "peak_rss_bytes": peak, - "rss_unavailable_reason": why or (self.rss.reason if peak is None else ""), + if peak is not None and self.memory.metric not in MEMORY_METRICS: + # Fail closed. A number whose quantity is unnamed is worse than no + # number: downstream it would be read as whatever the reader assumed. + raise InstrumentError( + f"the memory probe produced a value under an unknown metric kind " + f"{self.memory.metric!r}; the declared set is {sorted(MEMORY_METRICS)}") + return {"elapsed_ns": elapsed, "rc": rc, "peak_memory_bytes": peak, + "memory_metric": self.memory.metric if peak is not None else "", + "memory_unavailable_reason": why or (self.memory.reason if peak is None else ""), **accounting, "accounting_unavailable_reason": accounting_why} @@ -1757,7 +1793,7 @@ def measure_cell(self, rung: Rung, engine: str, w: Workload, target: Path | None "argv": ([*argv[:1], "<...>"] if rung.surface == "core" else [*argv[:2], "<...>"]), "outcome": outcome, - "timing": None, "peak_rss": None, "accounting": None, + "timing": None, "peak_memory": None, "accounting": None, "raw_elapsed_ns": [], "raw_accounting": [], "not_timed_because": "the rung did not do its work; measuring it would time " "the wrong path", @@ -1791,13 +1827,16 @@ def measure_cell(self, rung: Rung, engine: str, w: Workload, target: Path | None "exit_codes": rcs, "warmup_discarded": len(discarded), "timing": summarize([_as_int(s["elapsed_ns"]) for s in samples]), - "peak_rss": summarize( - [_as_int(s["peak_rss_bytes"]) for s in samples if s["peak_rss_bytes"] is not None], + "peak_memory": summarize( + [_as_int(s["peak_memory_bytes"]) for s in samples if s["peak_memory_bytes"] is not None], unit="bytes"), - "rss_mechanism": self.rss.mechanism, - "rss_unavailable_reason": next( - (str(s["rss_unavailable_reason"]) for s in samples - if s["peak_rss_bytes"] is None and s["rss_unavailable_reason"]), ""), + "memory_mechanism": self.memory.mechanism, + # WHICH quantity, beside HOW it was obtained. A cell that carries a + # number carries the name of what the number is. + "memory_metric": self.memory.metric, + "memory_unavailable_reason": next( + (str(s["memory_unavailable_reason"]) for s in samples + if s["peak_memory_bytes"] is None and s["memory_unavailable_reason"]), ""), # Summarized over the samples that HAVE the field. Where the # platform offers no rusage every one of these is {"n": 0} and the # reason below says why — an absence that reads as an absence, @@ -1809,7 +1848,7 @@ def measure_cell(self, rung: Rung, engine: str, w: Workload, target: Path | None (str(s["accounting_unavailable_reason"]) for s in samples if s["accounting_unavailable_reason"]), ""), "raw_elapsed_ns": [_as_int(s["elapsed_ns"]) for s in samples], # §9: raw retained - "raw_peak_rss_bytes": [s["peak_rss_bytes"] for s in samples], + "raw_peak_memory_bytes": [s["peak_memory_bytes"] for s in samples], "raw_accounting": [{name: s[name] for name in ACCOUNTING_FIELDS} for s in samples], "tag": CALIBRATION_ONLY, } @@ -2273,7 +2312,7 @@ def selftest() -> int: # The firewall, exercised rather than asserted. with tempfile.TemporaryDirectory(prefix="perf-selftest-") as td: - h = Harness(gate=gate, session=SessionIdentity("", "", 0, ""), rss=RssProbe(), + h = Harness(gate=gate, session=SessionIdentity("", "", 0, ""), memory=MemoryProbe(), tmp=Path(td), candidate=Path("/nonexistent"), warmup_discards=1, repetitions=1, seed=0) for w in workloads: @@ -2358,7 +2397,7 @@ def main(argv: list[str] | None = None) -> int: session = SessionIdentity.freeze(candidate) before = noise_probe() with tempfile.TemporaryDirectory(prefix="perf-cal-") as td: - h = Harness(gate=gate, session=session, rss=RssProbe(), tmp=Path(td), + h = Harness(gate=gate, session=session, memory=MemoryProbe(), tmp=Path(td), candidate=candidate, warmup_discards=a.warmup, repetitions=a.repeat, seed=a.seed) cal = [w for w in workloads if not w.decisive] diff --git a/tests/test_perf_instrument.py b/tests/test_perf_instrument.py index 9ca22d54..7679fd54 100644 --- a/tests/test_perf_instrument.py +++ b/tests/test_perf_instrument.py @@ -59,7 +59,7 @@ def _harness(tmp: Path, gate: pb.IdentityGate, candidate: Path | None = None) -> cand = candidate or (tmp / "fake-candidate") if not cand.exists(): cand.write_bytes(b"not a real binary, but it has an identity\n") - return pb.Harness(gate=gate, session=pb.SessionIdentity.freeze(cand), rss=pb.RssProbe(), + return pb.Harness(gate=gate, session=pb.SessionIdentity.freeze(cand), memory=pb.MemoryProbe(), tmp=tmp, candidate=cand, warmup_discards=0, repetitions=1, seed=1) @@ -1041,7 +1041,7 @@ def control_child_accounting() -> None: # (0) Two structural hazards, both of which would surface only mid-run. # The accounting is spread into the sample row, so a field named like an # existing key would silently overwrite the thing it collided with. - reserved = {"elapsed_ns", "rc", "peak_rss_bytes", "rss_unavailable_reason", + reserved = {"elapsed_ns", "rc", "peak_memory_bytes", "memory_unavailable_reason", "accounting_unavailable_reason"} collide = sorted(reserved & set(pb.ACCOUNTING_FIELDS)) if collide: @@ -1146,10 +1146,10 @@ def slow_wait4(pid: int, options: int) -> tuple[int, int, object]: # Windows test and does not claim to be one: it drives the real # non-POSIX path of the real function, which is where a Windows run # would land, and checks that absence is stated rather than zeroed. - probe = pb.RssProbe() + probe = pb.MemoryProbe() probe.mechanism = "none" probe.reason = "forced non-POSIX path for the accounting control" - h2 = pb.Harness(gate=h.gate, session=h.session, rss=probe, tmp=tmp, + h2 = pb.Harness(gate=h.gate, session=h.session, memory=probe, tmp=tmp, candidate=h.candidate, warmup_discards=0, repetitions=1, seed=1) off = h2._run_once([sys.executable, "-c", "pass"], dict(os.environ), ROOT) zeroed = [f for f in pb.ACCOUNTING_FIELDS if off.get(f) is not None] diff --git a/tests/test_round7_apparatus.py b/tests/test_round7_apparatus.py index bf504abc..db7533b0 100644 --- a/tests/test_round7_apparatus.py +++ b/tests/test_round7_apparatus.py @@ -660,8 +660,8 @@ def _run_once(self, argv: list[str], env: dict[str, str], cwd: Path) -> dict[str, object]: rc = self.codes[self.spawns] if self.spawns < len(self.codes) else 0 self.spawns += 1 - return {"elapsed_ns": 1_000_000, "rc": rc, "peak_rss_bytes": 1024, - "rss_unavailable_reason": "", "accounting_unavailable_reason": ""} + return {"elapsed_ns": 1_000_000, "rc": rc, "peak_memory_bytes": 1024, + "memory_unavailable_reason": "", "accounting_unavailable_reason": ""} def control_outcome_contract() -> None: From 5e7215a6f1392b0bc08b0494c6e6d17c90adc805 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 16 Sep 2026 16:18:30 +0500 Subject: [PATCH 2/3] docs(calibration): re-bind steps 4/5/6 to the repaired instrument MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The memory-semantics repair moved `measurement_harness_digest` 562a7f7232dad2f4c79c6adfe0e1e7e25680b4b6f3444d54824bf0405e3c14b3 before 104c384d01bf6060bdec1e7c916053ddb04b97fcbd0b39f8a4fc57b8f139672f after and three controls said so, each by name: `freeze-harness-untouched`, `training-prereg-bindings` and `envcapture-frozen-untouched`. That is the provenance system delivering its inconvenience on purpose, so nothing here tries to preserve the old digest. Re-bound: the digest in the policy freeze, in the ratified design constants, and in the training preregistration's bindings — and with it `design_constants_blob_sha1`, which moved because the design-constants artifact itself was re-bound. A chain of bindings re-bound in the order the chain runs. Not touched: the committed evidence of runs that actually happened. The sizing, calibration and round-7 datasets were produced by the old instrument and record what it produced; rewriting them would be forging a record, not re-accepting a binding. They carry `peak_rss` because that is the field the instrument had when they were taken. This is a re-acceptance of the step-4/5/6 bindings, not a code change; it is a separate commit so it can be reviewed as one. Controls after re-binding: calibration constants 4/4, calibration freeze 7/7, calibration policy 10/10, perf instrument 16/16, round 7 apparatus 10/10, step 7 environment capture 11/11, training preregistration 9/9. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh --- docs/evidence/calibration/p022-263a-design-constants.json | 2 +- docs/evidence/calibration/p022-263a-policy-freeze.json | 2 +- .../calibration/p022-263a-training-preregistration.json | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/evidence/calibration/p022-263a-design-constants.json b/docs/evidence/calibration/p022-263a-design-constants.json index d03c8455..fa02f43b 100644 --- a/docs/evidence/calibration/p022-263a-design-constants.json +++ b/docs/evidence/calibration/p022-263a-design-constants.json @@ -1,6 +1,6 @@ { "artifact": "p022-263a-calibration-design-constants", - "bound_measurement_harness_digest": "562a7f7232dad2f4c79c6adfe0e1e7e25680b4b6f3444d54824bf0405e3c14b3", + "bound_measurement_harness_digest": "104c384d01bf6060bdec1e7c916053ddb04b97fcbd0b39f8a4fc57b8f139672f", "bound_policy_implementation_digest": "c3068ed7fa880a7083866ead25fe8bf65c87889d242d8af1f7eee01582cd5cbf", "constants": { "G": [ diff --git a/docs/evidence/calibration/p022-263a-policy-freeze.json b/docs/evidence/calibration/p022-263a-policy-freeze.json index beebbc77..e206cdf8 100644 --- a/docs/evidence/calibration/p022-263a-policy-freeze.json +++ b/docs/evidence/calibration/p022-263a-policy-freeze.json @@ -1,6 +1,6 @@ { "artifact": "p022-263a-calibration-policy-freeze", - "measurement_harness_digest": "562a7f7232dad2f4c79c6adfe0e1e7e25680b4b6f3444d54824bf0405e3c14b3", + "measurement_harness_digest": "104c384d01bf6060bdec1e7c916053ddb04b97fcbd0b39f8a4fc57b8f139672f", "policy_implementation_digest": "c3068ed7fa880a7083866ead25fe8bf65c87889d242d8af1f7eee01582cd5cbf", "policy_implementation_digest_framing": "sha256 over the source set ordered by the UTF-8 bytes of each repo-relative POSIX path. Each file contributes, with no header and no separator: its path byte length as an 8-byte big-endian unsigned integer, its path's exact UTF-8 bytes, its blob byte length as an 8-byte big-endian unsigned integer, and its exact git blob bytes.", "policy_source_commit": "b4f657a0abdfdfaae199cbc7eee0c47acd8b0057", diff --git a/docs/evidence/calibration/p022-263a-training-preregistration.json b/docs/evidence/calibration/p022-263a-training-preregistration.json index 0becc1f6..a03e85a5 100644 --- a/docs/evidence/calibration/p022-263a-training-preregistration.json +++ b/docs/evidence/calibration/p022-263a-training-preregistration.json @@ -30,8 +30,8 @@ "anchor_commit": "eedf6d3ed44ecf7bda69fa509dcd23b45960cf76", "artifact": "p022-263a-calibration-training-preregistration", "bindings": { - "design_constants_blob_sha1": "d03c8455cbcaf71446f88aeea0957b8e0c56c880", - "measurement_harness_digest": "562a7f7232dad2f4c79c6adfe0e1e7e25680b4b6f3444d54824bf0405e3c14b3", + "design_constants_blob_sha1": "fa02f43bdf795e2f47d8ce781ec3a84f1dee64b5", + "measurement_harness_digest": "104c384d01bf6060bdec1e7c916053ddb04b97fcbd0b39f8a4fc57b8f139672f", "policy_implementation_digest": "c3068ed7fa880a7083866ead25fe8bf65c87889d242d8af1f7eee01582cd5cbf", "training_scope_implementation_digest": "614bf9efe6ba2bb10e26a251c10d6c4a8fdaa99985d43ea03d76c6774447d25b", "training_scope_root": "scripts/training/" From b6c472eeb1f40749626070562e1993f5f00bb99b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 16 Sep 2026 16:20:18 +0500 Subject: [PATCH 3/3] =?UTF-8?q?docs(perf):=20=C2=A79=20names=20the=20quant?= =?UTF-8?q?ity,=20not=20just=20the=20mechanism?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The section was called "Peak RSS" and described two mechanisms as though they produced one number. They do not: POSIX `ru_maxrss` is a peak resident set over the waited-for descendant chain, and the Windows job object's `PeakProcessMemoryUsed` is peak committed memory over the processes of the job. The witness that settles it is recorded here rather than left in a session log — 256 MiB committed and never touched reads as 262.5 MiB on Windows and as nothing resident on Linux — as is the half of the suspicion the same witness refuted: `wait4` does not stop at a wrapping shell. Recorded with it: the strata answer different honest questions and may not be pooled across platforms; Windows resident sampling was considered and refused because this instrument does not sample; and the digest moved 562a7f7232da -> 104c384d01bf, with steps 4/5/6 re-bound in their own commit and the committed evidence of past runs deliberately left alone. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh --- docs/notes/p022-263a-instrument.md | 50 ++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/docs/notes/p022-263a-instrument.md b/docs/notes/p022-263a-instrument.md index d77ff29c..bb066805 100644 --- a/docs/notes/p022-263a-instrument.md +++ b/docs/notes/p022-263a-instrument.md @@ -222,19 +222,49 @@ into. **Both halves of every pair are committed**, and run B records run A's path and sha256, so the verdict can be recomputed from evidence rather than trusted. -## 9. Peak RSS +## 9. Peak memory -By a named mechanism, recorded, never eyeballed: +By a named mechanism **and a named quantity**, recorded, never eyeballed. The +quantity is not the same on both platforms, and the field says which one it is: -- POSIX: `os.wait4` — the kernel's per-child `ru_maxrss`, for exactly the - process spawned. Chosen over `/usr/bin/time -v` as primary because GNU time is - a package that may simply be absent, and "the tool was missing" is not a - memory measurement. `/usr/bin/time -v` remains the documented fallback. -- Windows: a Job Object, `PeakProcessMemoryUsed` via `QueryInformationJobObject`. +| platform | mechanism | `memory_metric` | what it counts | +|---|---|---|---| +| POSIX | `os.wait4` — the kernel's `ru_maxrss` | `max_process_peak_resident` | peak **resident** set, over the child and the descendants it waited for | +| POSIX fallback | `/usr/bin/time -v` | `max_process_peak_resident` | the same quantity | +| Windows | Job Object `PeakProcessMemoryUsed` via `QueryInformationJobObject` | `max_process_peak_commit` | peak **committed** memory of any process ever associated with the job | + +`os.wait4` was chosen over `/usr/bin/time -v` as primary because GNU time is a +package that may simply be absent, and "the tool was missing" is not a memory +measurement. -Where nothing is available the value is `null` **with a reason**, so a silent -absence can never be read as a measured zero. Allocation counts are not yet -captured — recorded as owed on population B's track rather than quietly dropped. +**This used to be one field called `peak_rss_bytes`, and on Windows that was +false.** A witness settles it rather than a manual page: a child that commits +256 MiB and never touches a page is reported as 262.5 MiB through the job +object, while on Linux nothing becomes resident — job and process memory limits +are defined on committed virtual memory. The same witness refuted the other half +of the suspicion: `wait4` on the immediate child does *not* stop at a wrapping +shell. `shell -> heavy child` and `shell -> shell -> heavy child` both returned +the descendant's 260 MiB against 13 MiB for a light control. + +So the two strata answer different, honest questions. The numbers are comparable +Rust-vs-Python **within** a platform and **may not be pooled or compared across +platforms**. The resident analogue on Windows is `PeakWorkingSetSize`, and +obtaining it for a tree of processes that come and go needs handle tracking or +polling; this instrument does not sample, and will not perturb the elapsed-time +interval so that two operating systems can pronounce the same noun. + +A value produced under any metric kind outside that closed set raises rather +than being recorded, and no alias lets a Windows commit number go on being read +as an RSS number. Where nothing is available the value is `null` **with a +reason**, so a silent absence can never be read as a measured zero. Allocation +counts are not yet captured — recorded as owed on population B's track rather +than quietly dropped. + +The repair moved `measurement_harness_digest` from `562a7f7232da` to +`104c384d01bf`. Steps 4, 5 and 6 were re-bound to the new digest in their own +commit; the committed evidence of runs that already happened was **not** rewritten +and still carries the old field name, because it records what the old instrument +produced. On POSIX the same `wait4` call also carries the child's CPU split, fault counts and context-switch counts. Those are now kept rather than discarded — see *The