From 410ed76bf22d0f6ad49df74fd912ff103d24da62 Mon Sep 17 00:00:00 2001 From: Sinity Date: Mon, 20 Jul 2026 07:38:13 +0200 Subject: [PATCH] perf(daemon): scale parse-stage whale budget with physical RAM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: the fixed 64 MiB prefetch inflight budget starves bulk-scale warm on whale corpora — measured live on the 50K-raw archive: a 2000-raw page warmed 139 raws in 376s (0.37 raws/s, workers stalled on cache admission) under 64 MiB, vs 500 raws in 8.8s (56.7 raws/s) with an adequate budget. The bound exists to cap transient memory beside a live daemon, so a one-size constant is wrong on both ends. What changed: the default budget is physical RAM / 16 clamped to [64 MiB, 2 GiB]; unknown RAM falls back to the 64 MiB floor; the explicit env override still always wins. Verification: devtools test tests/unit/daemon/test_parse_prefetch.py (7 passed, including the new adaptive/clamp/override cases); devtools verify --quick green. --- polylogue/daemon/parse_prefetch.py | 32 ++++++++++++++++++++--- tests/unit/daemon/test_parse_prefetch.py | 33 ++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/polylogue/daemon/parse_prefetch.py b/polylogue/daemon/parse_prefetch.py index 54eb75ddf8..9d0da72378 100644 --- a/polylogue/daemon/parse_prefetch.py +++ b/polylogue/daemon/parse_prefetch.py @@ -48,7 +48,17 @@ logger = get_logger(__name__) -_DEFAULT_MAX_INFLIGHT_BYTES = 64 * 1024 * 1024 # 64 MiB +# Floor/ceiling for the adaptive whale-memory budget below. The original +# fixed 64 MiB default starved bulk-scale warm on whale corpora: measured +# live 2026-07-20 on the 50K-raw archive, a 2000-raw page warmed 139 raws in +# 376s (0.37 raws/s, pool stalled on cache admission) under 64 MiB versus +# 500 raws in 8.8s (56.7 raws/s) with the budget raised — the workers were +# blocked on `try_admit`, not on parsing. The budget's purpose is bounding +# transient memory beside a live daemon, so it scales with the machine +# instead of a one-size constant: 1/16 of physical RAM, clamped to +# [64 MiB, 2 GiB]. +_MIN_MAX_INFLIGHT_BYTES = 64 * 1024 * 1024 # 64 MiB +_MAX_MAX_INFLIGHT_BYTES = 2 * 1024 * 1024 * 1024 # 2 GiB # CodeRabbit (PR #3168): as_completed()/future.result() had no timeout, so one # hung worker (e.g. an unresponsive filesystem read) would block warm() @@ -86,10 +96,23 @@ def daemon_parse_stage_worker_count() -> int: return max(1, (os.cpu_count() or 2) - 1) +def _physical_memory_bytes() -> int | None: + try: + pages = os.sysconf("SC_PHYS_PAGES") + page_size = os.sysconf("SC_PAGE_SIZE") + except (ValueError, OSError, AttributeError): + return None + if pages <= 0 or page_size <= 0: + return None + return pages * page_size + + def daemon_parse_stage_max_inflight_bytes() -> int: """Whale-memory budget for parsed sessions held in the prefetch cache. - Override with ``POLYLOGUE_DAEMON_PARSE_STAGE_MAX_INFLIGHT_BYTES``. + Adaptive: 1/16 of physical RAM clamped to [64 MiB, 2 GiB] (see the + constants above for the measured starvation the old fixed 64 MiB default + caused). Override with ``POLYLOGUE_DAEMON_PARSE_STAGE_MAX_INFLIGHT_BYTES``. """ raw = os.environ.get("POLYLOGUE_DAEMON_PARSE_STAGE_MAX_INFLIGHT_BYTES") if raw is not None: @@ -99,7 +122,10 @@ def daemon_parse_stage_max_inflight_bytes() -> int: value = 0 if value > 0: return value - return _DEFAULT_MAX_INFLIGHT_BYTES + physical = _physical_memory_bytes() + if physical is None: + return _MIN_MAX_INFLIGHT_BYTES + return max(_MIN_MAX_INFLIGHT_BYTES, min(_MAX_MAX_INFLIGHT_BYTES, physical // 16)) def daemon_parse_stage_warm_timeout_seconds() -> float: diff --git a/tests/unit/daemon/test_parse_prefetch.py b/tests/unit/daemon/test_parse_prefetch.py index 3bec8235b5..0f1075773e 100644 --- a/tests/unit/daemon/test_parse_prefetch.py +++ b/tests/unit/daemon/test_parse_prefetch.py @@ -222,3 +222,36 @@ def hanging_worker(*args: object, **kwargs: object) -> object: # warm() returned close to its own timeout, not after the hung worker's # sleep -- proving the wait is genuinely bounded, not merely reordered. assert elapsed < 0.3 + + +def test_max_inflight_bytes_default_is_adaptive_and_clamped(monkeypatch: pytest.MonkeyPatch) -> None: + """The whale-memory budget scales with physical RAM, clamped to [64MiB, 2GiB]. + + The old fixed 64 MiB default starved bulk-scale warm on whale corpora + (measured live: 0.37 raws/s stalled on cache admission vs 56.7 raws/s + with an adequate budget) — the budget must grow on capable machines + while keeping the 64 MiB floor semantics on small ones. + """ + from polylogue.daemon import parse_prefetch as pp + + monkeypatch.delenv("POLYLOGUE_DAEMON_PARSE_STAGE_MAX_INFLIGHT_BYTES", raising=False) + + # 32 GiB machine -> hits the 2 GiB ceiling (32 GiB / 16 = 2 GiB). + monkeypatch.setattr(pp, "_physical_memory_bytes", lambda: 32 * 1024**3) + assert pp.daemon_parse_stage_max_inflight_bytes() == 2 * 1024**3 + + # 512 MiB machine -> clamped up to the 64 MiB floor. + monkeypatch.setattr(pp, "_physical_memory_bytes", lambda: 512 * 1024**2) + assert pp.daemon_parse_stage_max_inflight_bytes() == 64 * 1024**2 + + # 8 GiB machine -> proportional (8 GiB / 16 = 512 MiB). + monkeypatch.setattr(pp, "_physical_memory_bytes", lambda: 8 * 1024**3) + assert pp.daemon_parse_stage_max_inflight_bytes() == 512 * 1024**2 + + # Unknown physical memory -> conservative floor. + monkeypatch.setattr(pp, "_physical_memory_bytes", lambda: None) + assert pp.daemon_parse_stage_max_inflight_bytes() == 64 * 1024**2 + + # Explicit env override always wins. + monkeypatch.setenv("POLYLOGUE_DAEMON_PARSE_STAGE_MAX_INFLIGHT_BYTES", "123456789") + assert pp.daemon_parse_stage_max_inflight_bytes() == 123456789