|
7 | 7 | // whatever ninja defaulted to. Deriving the same decision in two places is how |
8 | 8 | // the two halves drifted apart. This module is the only place it is derived. |
9 | 9 | // |
10 | | -// PURE. No filesystem, no processes, no environment reads — a caller passes the |
11 | | -// facts in and gets a Decision plus the sentence explaining it. That is what |
12 | | -// makes the policy unit-testable without a toolchain, and what lets the reason |
13 | | -// be printed, logged and written into build.ninja unchanged. |
| 10 | +// `decide()` IS PURE. No filesystem, no processes, no environment — a caller |
| 11 | +// hands it facts and gets a Decision plus the sentence explaining it, so the |
| 12 | +// table is unit-testable without a toolchain and the reason can be printed, |
| 13 | +// logged and written into build.ninja unchanged. `requested_switch()` is the |
| 14 | +// one impure function here, and it is impure on purpose: the switch has to be |
| 15 | +// read somewhere, and two callers each doing env-then-manifest in their own |
| 16 | +// order is exactly the duplicate derivation this module exists to prevent. |
14 | 17 | // |
15 | 18 | // THE MEASUREMENTS BEHIND THE TABLE (2026-08-13, mcpp building itself: 138 |
16 | 19 | // module interface units, 57k lines, i9-13900K, gcc@16.1.0 / llvm@22.1.8): |
@@ -49,6 +52,8 @@ export module mcpp.build.schedule.policy; |
49 | 52 |
|
50 | 53 | import std; |
51 | 54 | import mcpp.toolchain.model; |
| 55 | +import mcpp.manifest; |
| 56 | +import mcpp.platform.capacity; |
52 | 57 |
|
53 | 58 | export namespace mcpp::build::schedule { |
54 | 59 |
|
@@ -84,6 +89,32 @@ struct Decision { |
84 | 89 | int ninjaJobs = 0; |
85 | 90 | }; |
86 | 91 |
|
| 92 | +// The one place the switch is READ. `decide` above stays pure — a caller hands |
| 93 | +// it facts — but the switch itself has to come from somewhere, and having two |
| 94 | +// callers each read env-then-manifest in their own order is precisely the |
| 95 | +// duplicate-derivation this module exists to prevent. |
| 96 | +// |
| 97 | +// Precedence matches every other mcpp switch: environment beats manifest. |
| 98 | +std::string requested_switch(const manifest::Manifest& m); |
| 99 | + |
| 100 | +// How many compilers this machine should run at once. |
| 101 | +// |
| 102 | +// Precedence: MCPP_JOBS (where `--jobs` lands) > `[build] jobs` > 0, meaning |
| 103 | +// "say nothing" and leave the backend's own default. The default is unchanged |
| 104 | +// on purpose: altering everyone's concurrency is a behaviour change. |
| 105 | +// |
| 106 | +// `auto` is resolved HERE, against the machine doing the build, never frozen |
| 107 | +// into a manifest. Measured on this repository: the cold self-build takes 81.0s |
| 108 | +// at -j8 and 79.9s at -j32 — 4x the workers for 1.4%, because the build is |
| 109 | +// latency-bound — while a single module compile peaks at 0.5–1.0 GB, so the |
| 110 | +// extra jobs are pure memory pressure. On a high-core, modest-RAM machine the |
| 111 | +// backend default swaps. |
| 112 | +// |
| 113 | +// `onInvalid` is called with the offending text instead of warning directly, so |
| 114 | +// this stays free of any UI dependency and remains testable. |
| 115 | +int resolve_jobs(const manifest::Manifest& m, |
| 116 | + const std::function<void(std::string_view)>& onInvalid = {}); |
| 117 | + |
87 | 118 | // `requested` is the user's switch: "auto" (default), "on", "off". `hostJobs` is |
88 | 119 | // the already-resolved parallelism (`--jobs`, `[build] jobs`, or the backend |
89 | 120 | // default), i.e. how many compilers this machine should run at once. |
@@ -137,4 +168,35 @@ Decision decide(const toolchain::Toolchain& tc, std::string_view requested, int |
137 | 168 | return d; |
138 | 169 | } |
139 | 170 |
|
| 171 | +int resolve_jobs(const manifest::Manifest& m, |
| 172 | + const std::function<void(std::string_view)>& onInvalid) { |
| 173 | + auto from_text = [&](std::string_view v) -> std::optional<int> { |
| 174 | + if (v.empty()) return std::nullopt; |
| 175 | + if (v == "auto") { |
| 176 | + const auto cap = platform::capacity::host_capacity(); |
| 177 | + return platform::capacity::recommended_jobs(cap); |
| 178 | + } |
| 179 | + int n = 0; |
| 180 | + const auto* first = v.data(); |
| 181 | + const auto* last = v.data() + v.size(); |
| 182 | + if (auto [p, ec] = std::from_chars(first, last, n); |
| 183 | + ec == std::errc{} && p == last && n > 0) |
| 184 | + return n; |
| 185 | + // A malformed value must not silently become "use the default" — that |
| 186 | + // is how a typo turns into a build that is mysteriously slower. |
| 187 | + if (onInvalid) onInvalid(v); |
| 188 | + return std::nullopt; |
| 189 | + }; |
| 190 | + if (const char* e = std::getenv("MCPP_JOBS")) |
| 191 | + if (auto n = from_text(e)) return *n; |
| 192 | + if (auto n = from_text(m.buildConfig.jobs)) return *n; |
| 193 | + return 0; |
| 194 | +} |
| 195 | + |
| 196 | +std::string requested_switch(const manifest::Manifest& m) { |
| 197 | + if (const char* e = std::getenv("MCPP_BMI_SCHEDULE"); e && *e) return std::string(e); |
| 198 | + if (!m.buildConfig.schedule.empty()) return m.buildConfig.schedule; |
| 199 | + return "auto"; |
| 200 | +} |
| 201 | + |
140 | 202 | } // namespace mcpp::build::schedule |
0 commit comments