Skip to content

feat: add runtime memory admission for detached compilers - #803

Draft
ZheFeng7110 wants to merge 10 commits into
mcpp-community:mainfrom
ZheFeng7110:feat/runtime-memory-admission
Draft

ZheFeng7110 wants to merge 10 commits into
mcpp-community:mainfrom
ZheFeng7110:feat/runtime-memory-admission

Conversation

@ZheFeng7110

@ZheFeng7110 ZheFeng7110 commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

Summary

Implements the first stage of #802 by adding runtime physical-memory admission control to the GCC bmi_schedule = "on" detached compiler path.

  • Adds runtime memory snapshots to mcpp.platform.capacity.
  • Accounts for Linux MemAvailable and cgroup v2 limits.
  • Adds machine-level build.memory_threshold_percent, defaulting to 80.
  • Adds MCPP_MEMORY_THRESHOLD_PERCENT as an environment override.
  • Keeps detached compiler tokens until the compiler and code generation fully exit.
  • Adds deterministic unit coverage for threshold, hysteresis, and unsupported metrics.

Implementation plan

  1. Extend platform capacity with runtime physical-memory snapshots.
  2. Add machine-level memory threshold configuration and environment override.
  3. Apply admission checks to the existing detached compiler token mechanism.
  4. Preserve configured jobs and compiler caps as hard upper bounds.
  5. Add unit and integration coverage for throttling, recovery, stale-token handling, and detached code generation.
  6. Document the configuration and behavior in English and Chinese documentation.
  7. Extend the same admission controller to ordinary Ninja compilation and Clang two-phase compilation.

Current status

Completed in this PR:

  • Items 1–4 for the GCC detached compiler path.
  • Unit tests for the memory policy.
  • Linux cgroup v2-aware available-memory calculation.
  • Build integration through the generated bmi-compile command.

Not completed in this PR:

  • Ordinary Ninja C/C++ compile edges do not yet use the runtime admission controller.
  • Clang two-phase compilation does not yet use the runtime admission controller.
  • English and Chinese user documentation has not yet been updated.
  • The full cross-platform CI matrix has not yet been run.

This PR is intentionally scoped as the first implementation stage. The remaining items should be addressed before treating #802 as fully implemented.

Tests

  • mcpp build --release passed before the final test rebuild.
  • mcpp test capacity passed: 11 tests passed.
  • mcpp test -p platform passed: 2 tests passed.
  • git diff --check passed.

Related issue

Closes #802


Original plan file: original-plan.md

@ZheFeng7110
ZheFeng7110 marked this pull request as draft October 12, 2026 10:25
An ordinary compile edge runs through `mcpp compile-admit` now, and ninja
echoes that wrapper as the command that failed. `failed_command_in` required a
clang driver as the FIRST word of the line, found none, and the note for a
compiler that does not support C++20 coroutines on its MSVC ABI stopped
appearing — the failure was left unexplained.

That is windows-e2e 892 case C: a C++20 program that uses coroutines, built
for i686-windows-msvc, exits non-zero with clang's two errors and no note
(run 38268962914, windows-e2e 2/3). Cases A and B kept passing because the
std module precompile carries its command in mcpp's own message, which no
wrapper touches.

The driver is now searched for wherever it sits on the line; the target and
the standard read from that line are unchanged by the wrapper. Two tests carry
the CI output as it arrives through the wrapper, and fail without this.
@ZheFeng7110

ZheFeng7110 commented Oct 13, 2026 •

Copy link
Copy Markdown
Member Author

Review: #803 — memory admission control

Verdict: Request changes

The feature is coherent (config → plan → ninja emission → compile-admit wrapper → shared token/memory gate), docs are accurate, and tests cover the new capacity
math. But two defects change existing build behavior unconditionally, and the headline hysteresis is dead code.

Blockers

  1. POSIX BMI restore cascade is dropped whenever admission is on — which is always (ninja_backend.cppm:1992-1997)
    The old cxx_precompile command wrapped the compile in the cp -p "$out" "$out.bak" / bmi-equal / mv dance that keeps a byte-identical BMI rewrite from advancing
    mtime under restat = 1. The admitted branch replaces the whole command with the bare wrapper, so the restore never runs. Since prepare/plan.cpp sets
    memoryCompilerCap = max(1, logicalCores) and a ≥1 GiB baseline unconditionally, memoryAdmission is true for every build — any POSIX bmi_schedule = "on" Clang build
    re-cascades importers on every rebuild. Either preserve the dance inside run_admitted/the wrapper or gate it separately from admission.

  2. memoryCompilerCap silently caps Ninja parallelism at logicalCores (prepare/plan.cpp:905-913)
    The token directory has exactly cap slots, so --cap on every ordinary/two-phase edge is a hard concurrency ceiling, contradicting the adjacent
    comment ("Ninja's resolved job count is the hard ceiling"): mcpp build -j 64 on an 8-core host runs ≤8 compiles, and the default path (ninjaJobs = 0 → ninja's own
    nproc + 2) is quietly reduced to logicalCores. The cap must not bind on count.

Major

  1. Admission baseline is bucketed downward — the gate is nearly inert on small machines (prepare/plan.cpp:918-924)
    max(1 GiB, (available / 4 GiB) * 4 GiB) never exceeds the observation and can be 4 GiB below it. On any host with <4 GiB available, baseline = 1 GiB, so at the
    default 80% the gate admits compilers until only ~205 MiB stays free — far under the per-compile peak the module header documents. Flooring makes admission more
    permissive, not "conservative" as the comment claims. Quantize upward (or derive from total memory).

  2. Hysteresis is dead code (capacity.cppm:346-349)
    memory_allows implements a stricter recovering margin, but the only caller (acquire_token) never passes recovering = true, so admission resumes exactly at the base
    threshold — the oscillation the hysteresis exists to damp. Either carry a "was denied" bool across loop iterations in acquire_token or remove the parameter.

  3. Baseline bytes are embedded in every admitted command line (ninja_backend.cppm:1847-1851)
    admission_prefix() interpolates plan.memoryBaselineBytes into the emitted command (and --mem-baseline $sched_mem_baseline for detached edges). Ninja re-runs edges
    whose command changed, so the first build after the bucketed baseline shifts recompiles the whole graph — precisely the false invalidation the bucket comment says
    it avoids. The value is only needed at admission time; pass it via edge variable ($sched_mem_baseline-style) or env, not literal command text.

  4. The 2-hour bounded-wait escape is unreachable under memory denial (detach_codegen.cppm:235-240)
    The new continue skips past the kLimit timeout check, so while memory_allows keeps refusing (external memory pressure), the build hangs silently with no diagnostic
    — contradicting the function's own "the wait is bounded anyway" contract. Include the memory wait in the bounded-wait accounting.

Minor

  • Hysteresis test passes with the hysteresis removed (test_capacity_jobs.cpp:94-99): at 1 GiB and 3 GiB both the 20% and 25% margins give the same verdicts. Use
    e.g. 2 GiB + 1: allowed when recovering = false, denied when true. (Moot if §4 removes the parameter.)
  • Silent exit 1 on depfile conversion failure (detach_codegen.cppm:722-730): compile succeeds, wrapper returns 1 with no output — indistinguishable from a compiler
    failure and names no file. The replaced depfile-filter path at least printed the missing file. Print an error naming the path.
  • from_chars results unchecked (cli.cppm compile-admit, cli/cmd_build.cppm): empty/garbage --cap/--mem-baseline/--mem-threshold silently fall back to defaults
    (0/0/80). Empty-string from_chars is safe (leaves the value), but a malformed explicit value is indistinguishable from an omitted one. Low risk since the emitters
    are in-repo, but worth an ec check + error message.
  • --sem .mcpp-sched hardcoded in admission_prefix() vs $sched_sem on the bmi rule — fine while the constant is fixed, but it's a second spelling of the same fact.
  • Stray whitespace change in the bmi-compile option block (cli.cppm:1007-1009): leading-space shift on cap/mem-baseline/mem-threshold lines, unrelated noise in the
    diff.

Sound

  • memory_allows math (threshold = consumed portion, clamping 1–100, hysteresis direction is correct where used), cgroup v2 handling (max sentinel, current >= max,
    min with host reading), and supported semantics — the "unsupported metrics leave configured caps in force" claim holds.
  • The anyToken gate in acquire_token (first token always admitted) correctly avoids self-deadlock of a single runnable edge.
  • Config plumbing, env override validation, range clamping, template file, and both language docs are consistent with the implementation.
  • Incremental-token cleanup and the token release paths in run_admitted cover the failure branches.

Fix §1–§3 before merge; §4–§6 should follow in the same PR since they're all in the touched code paths.

Address review item §1 by backing up the BMI before compilation and restoring its original timestamp when the successful output is equivalent. Cover the wrapper and downstream Ninja restat behavior with regression tests.
Address review item §2 by removing counting slots from ordinary and two-phase compile admission. Retain memory gating with a shared single-slot fallback, include memory waits in timeout accounting, and reclaim stale fallback tokens for every build strategy. Update regression coverage and both language manuals.
Address review item §3 by rounding observed available memory up to a 4 GiB bucket, retaining the unknown-memory fallback and saturating at the integer limit. Add boundary coverage and document upward quantization in both language manuals.

This branch has not been deployed

No deployments
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.

feat: add runtime memory-aware build concurrency control

1 participant