Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/evidence/calibration/p022-263a-design-constants.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down
2 changes: 1 addition & 1 deletion docs/evidence/calibration/p022-263a-policy-freeze.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
Expand Down
50 changes: 40 additions & 10 deletions docs/notes/p022-263a-instrument.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
95 changes: 67 additions & 28 deletions scripts/perf_baseline.py
Original file line number Diff line number Diff line change
Expand Up @@ -1112,34 +1112,63 @@ 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.
"""

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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = ""
Expand All @@ -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),
Expand All @@ -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}

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand All @@ -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,
}
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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]
Expand Down
8 changes: 4 additions & 4 deletions tests/test_perf_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions tests/test_round7_apparatus.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading