-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathtest_schedule_policy.cpp
More file actions
271 lines (246 loc) · 12.6 KB
/
Copy pathtest_schedule_policy.cpp
File metadata and controls
271 lines (246 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
// The build-shape policy: one table, asserted from both sides.
//
// `decide()` is pure precisely so this file needs no toolchain, no filesystem
// and no compiler — the table can be wrong in a way that only shows up as a
// slower build, which is the kind of wrong that never gets noticed.
#include <gtest/gtest.h>
#include <cstdlib>
import std;
import mcpp.build.schedule.policy;
import mcpp.toolchain.model;
import mcpp.manifest;
using mcpp::build::schedule::Strategy;
using mcpp::build::schedule::decide;
using mcpp::build::schedule::requested_switch;
using mcpp::toolchain::CompilerId;
using mcpp::toolchain::Toolchain;
namespace {
Toolchain with(CompilerId id) {
Toolchain tc;
tc.compiler = id;
return tc;
}
// MCPP_BMI_SCHEDULE outranks the manifest, so a stray one in the developer's
// shell would decide these tests instead of the code under test.
class ScopedVar {
public:
ScopedVar(std::string name, const char* value) : name_(std::move(name)) {
if (const char* old = std::getenv(name_.c_str()); old) { had_ = true; old_ = old; }
apply(value);
}
~ScopedVar() { apply(had_ ? old_.c_str() : nullptr); }
ScopedVar(const ScopedVar&) = delete;
ScopedVar& operator=(const ScopedVar&) = delete;
private:
void apply(const char* v) {
#if defined(_WIN32)
::_putenv_s(name_.c_str(), v ? v : "");
#else
if (v) ::setenv(name_.c_str(), v, 1); else ::unsetenv(name_.c_str());
#endif
}
std::string name_;
bool had_ = false;
std::string old_;
};
mcpp::manifest::Manifest with_schedule(std::string v) {
mcpp::manifest::Manifest m;
m.buildConfig.bmiSchedule = std::move(v);
return m;
}
} // namespace
// The two mechanisms are COMPLEMENTARY, not interchangeable, and getting them
// backwards is silent: clang writes its BMI to the final path with O_TRUNC, so
// detach-codegen would hand importers a half-written file; gcc has no cheap
// BMI-only mode, so two-phase would just compile everything twice.
TEST(SchedulePolicy, EachCompilerGetsItsOwnMechanism) {
EXPECT_EQ(decide(with(CompilerId::Clang), "on", 8).strategy, Strategy::TwoPhase);
EXPECT_EQ(decide(with(CompilerId::GCC), "on", 8).strategy, Strategy::DetachCodegen);
}
// Unmeasured means None. A guess here is not a slow build, it is a miscompile:
// a BMI read while it is still being written is not a diagnostic.
// `auto` is OFF for now — pinned, because it is a decision rather than a gap.
TEST(SchedulePolicy, AutoIsOptInForNow) {
EXPECT_EQ(decide(with(CompilerId::GCC), "auto", 8).strategy, Strategy::None);
EXPECT_NE(decide(with(CompilerId::GCC), "on", 8).strategy, Strategy::None);
}
TEST(SchedulePolicy, UnmeasuredCompilersStayConservative) {
EXPECT_EQ(decide(with(CompilerId::MSVC), "on", 8).strategy, Strategy::None);
EXPECT_EQ(decide(with(CompilerId::Unknown), "on", 8).strategy, Strategy::None);
}
// Asserted from BOTH sides: that "off" disables, and that the same input with
// "auto" does NOT. Checking only the first would pass an implementation that
// never enables anything at all.
TEST(SchedulePolicy, OffDisablesAndAutoDoesNot) {
EXPECT_EQ(decide(with(CompilerId::GCC), "off", 8).strategy, Strategy::None);
EXPECT_NE(decide(with(CompilerId::GCC), "on", 8).strategy, Strategy::None);
}
// HAZARD 2, encoded. Under detach-codegen a compiler stops holding a ninja slot
// the moment it publishes its BMI, so ninja's -j is no longer a bound on how
// many compilers run. With the two equal, ninja's slots fill with edges that
// are merely sleeping, the ready frontier starves, and the schedule degenerates
// to the baseline — which is exactly what the first prototype measured.
TEST(SchedulePolicy, DetachCodegenGivesNinjaMoreSlotsThanCompilers) {
const auto d = decide(with(CompilerId::GCC), "on", 32);
EXPECT_EQ(d.compilerCap, 32);
EXPECT_GT(d.ninjaJobs, d.compilerCap);
}
// Two-phase runs ordinary compilers that hold their slot for the whole compile,
// so inflating -j there would only oversubscribe the machine.
TEST(SchedulePolicy, TwoPhaseLeavesTheJobCountAlone) {
const auto d = decide(with(CompilerId::Clang), "on", 32);
EXPECT_EQ(d.ninjaJobs, d.compilerCap);
}
// A scheduler that silently declines to optimise cannot be debugged: "why is my
// build not using the fast shape?" has to have an answer that ships with the
// build. Every branch, including the ones that choose None.
TEST(SchedulePolicy, EveryDecisionCarriesAReason) {
for (auto id : {CompilerId::GCC, CompilerId::Clang, CompilerId::MSVC,
CompilerId::Unknown}) {
EXPECT_FALSE(decide(with(id), "auto", 8).reason.empty())
<< "no reason for compiler id " << static_cast<int>(id);
EXPECT_FALSE(decide(with(id), "off", 8).reason.empty())
<< "no reason when disabled, compiler id " << static_cast<int>(id);
}
}
// A host that reports nothing must not turn into "-j0" or a negative cap.
TEST(SchedulePolicy, ZeroJobsStaysZeroRatherThanBecomingNonsense) {
const auto d = decide(with(CompilerId::GCC), "on", 0);
EXPECT_EQ(d.compilerCap, 0);
EXPECT_EQ(d.ninjaJobs, 0);
// ...and it must SAY so. Under detach-codegen a cap of 0 disables the
// semaphore, which is the only bound on how many compilers run at once, so
// this state is not a neutral default — it is unbounded concurrency.
EXPECT_NE(d.reason.find("bounded by nothing"), std::string::npos)
<< "a zero cap silently means no bound at all: " << d.reason;
}
// THE DEFAULT CONFIGURATION MUST STILL BE BOUNDED.
//
// `resolve_jobs` returns 0 when the user passed neither `--jobs` nor
// `[build] jobs` — "say nothing, leave the backend's default". For every other
// strategy that is fine, because ninja's -j is a real bound. Under
// DetachCodegen it is NOT: a compiler stops holding its ninja slot the moment
// it publishes a BMI, so the semaphore is the only counter, and a cap of 0
// turns `acquire_token` into a no-op.
//
// This shipped: a plain `mcpp build` with `bmi_schedule = "on"` generated
// `sched_cap = 0`, i.e. ninja starting compiles as fast as BMIs appeared with
// nothing limiting them, on a workload whose single compile peaks near a
// gigabyte. The caller now resolves what the host would pick and hands it in.
TEST(SchedulePolicy, DetachCodegenFallsBackToTheHostWhenNoJobCountWasGiven) {
const auto d = decide(with(CompilerId::GCC), "on", /*hostJobs=*/0, /*autoJobs=*/24);
EXPECT_EQ(d.strategy, Strategy::DetachCodegen);
EXPECT_EQ(d.compilerCap, 24) << "the semaphore would be disabled";
EXPECT_GT(d.ninjaJobs, d.compilerCap) << "hazard 2: -j must exceed the cap";
// An explicit job count still wins over the fallback.
const auto e = decide(with(CompilerId::GCC), "on", /*hostJobs=*/8, /*autoJobs=*/24);
EXPECT_EQ(e.compilerCap, 8);
// The fallback is only for the strategy that needs it; two-phase uses
// ordinary edges, where ninja's -j is already the bound.
const auto c = decide(with(CompilerId::Clang), "on", /*hostJobs=*/0, /*autoJobs=*/24);
EXPECT_EQ(c.ninjaJobs, 0) << "clang must still defer to the backend default";
}
// `auto` is bounded by recommended_jobs' ceiling of 64, but `--jobs N` is only
// checked for `> 0`, so an absurd N reaches decide() intact and `cap * 6` was
// signed overflow — undefined behaviour, with a NEGATIVE `-j` handed to ninja as
// one of the friendlier outcomes. Asserted as "still positive and still greater
// than the cap" rather than against the clamp constant, so tuning the clamp does
// not require editing the test that exists to stop it going negative.
TEST(SchedulePolicy, AnAbsurdJobCountDoesNotOverflowIntoANegativeOne) {
const auto d = decide(with(CompilerId::GCC), "on", 2000000000);
EXPECT_GT(d.ninjaJobs, 0) << "ninja -j went non-positive";
EXPECT_GT(d.ninjaJobs, 1) << "hazard 2: ninja must still outnumber the compilers";
}
// ─── resolve_jobs precedence (#564) ────────────────────────────────────────
//
// A KEY THE GENERATED FILE PROMISED AND NOTHING READ. mcpp writes
// `[build] default_jobs = 0` into `$MCPP_HOME/config.toml` itself; the loader
// parsed it into `GlobalConfig::defaultJobs`, and those were the only two
// mentions of the field in the repository. Setting it to 4 changed nothing:
// `ninja` ran with its own default, which is 10 on an 8-core machine, while a
// single module compile peaks at 0.5-1.0 GB.
//
// THE TEST ASSERTS THE ORDER, NOT THE WIRING. A fixture that sets only the
// global value and reads it back would pass just as well if the parameter had
// been placed ABOVE `MCPP_JOBS` instead of below it -- which would be the
// opposite of correct, because the global value is a property of the machine
// and the environment variable is this invocation. Each of the three levels is
// therefore checked against the level that must beat it.
TEST(SchedulePolicy, ResolveJobsPutsTheMachineBelowTheProjectAndTheInvocation) {
mcpp::manifest::Manifest bare; // no [build] jobs
auto with_jobs = [](std::string v) {
mcpp::manifest::Manifest m;
m.buildConfig.jobs = std::move(v);
return m;
};
using mcpp::build::schedule::resolve_jobs;
{ // The machine's value is used when nothing else says anything, and 0
// still means "say nothing" for everyone who has not written the key.
ScopedVar clear("MCPP_JOBS", nullptr);
EXPECT_EQ(resolve_jobs(bare, {}, 4), 4);
EXPECT_EQ(resolve_jobs(bare, {}, 0), 0);
// A non-positive value reads as absent rather than as a bound: that is
// what the generated template's `0` means, and a negative -j would be
// handed straight to the backend.
EXPECT_EQ(resolve_jobs(bare, {}, -1), 0);
// The project beats the machine. A project that states a number has a
// reason the machine cannot know.
EXPECT_EQ(resolve_jobs(with_jobs("6"), {}, 4), 6);
}
{ // The invocation beats both. Without this leg the parameter could be
// wired in above MCPP_JOBS and every other assertion here would pass.
ScopedVar jobs("MCPP_JOBS", "2");
EXPECT_EQ(resolve_jobs(bare, {}, 4), 2);
EXPECT_EQ(resolve_jobs(with_jobs("6"), {}, 4), 2);
}
}
// ─── requested_switch: a typo is a diagnostic, never a silent "auto" ───────
//
// This is the rule resolve_jobs already followed and this switch did not.
// `bmi_schedule = "ON"` was accepted, meant OFF, and explained itself with
// "the split schedule is opt-in until verified" — which reads as "you did not
// ask for it" to someone who just did.
TEST(SchedulePolicy, RequestedSwitchPassesTheThreeSpellingsThrough) {
ScopedVar clear("MCPP_BMI_SCHEDULE", nullptr);
EXPECT_EQ(requested_switch(with_schedule("on")), "on");
EXPECT_EQ(requested_switch(with_schedule("off")), "off");
EXPECT_EQ(requested_switch(with_schedule("auto")), "auto");
EXPECT_EQ(requested_switch(with_schedule("")), "auto"); // unset
}
TEST(SchedulePolicy, RequestedSwitchReportsATypoInsteadOfSwallowingIt) {
ScopedVar clear("MCPP_BMI_SCHEDULE", nullptr);
for (const char* typo : {"ON", "On", "true", "yes", "1", "enabled"}) {
std::string seen;
const auto v = requested_switch(with_schedule(typo),
[&](std::string_view bad) { seen = bad; });
EXPECT_EQ(v, "auto") << typo << " must fall back to the default";
EXPECT_EQ(seen, typo) << typo << " was accepted silently";
}
}
// Both directions. Checking only that a typo warns would pass an implementation
// that warns about everything, including the spellings that are correct.
TEST(SchedulePolicy, RequestedSwitchStaysQuietForValidValues) {
ScopedVar clear("MCPP_BMI_SCHEDULE", nullptr);
for (const char* ok : {"on", "off", "auto"}) {
bool warned = false;
requested_switch(with_schedule(ok), [&](std::string_view) { warned = true; });
EXPECT_FALSE(warned) << ok << " is valid but was reported as invalid";
}
}
// The environment outranks the manifest — and is validated on the same terms.
// A typo'd MCPP_BMI_SCHEDULE must not fall through to the manifest either:
// silently honouring `[build] bmi_schedule = "on"` when the environment asked
// for something unparseable would make the warning a lie.
TEST(SchedulePolicy, EnvironmentBeatsManifestAndIsValidatedToo) {
{
ScopedVar on("MCPP_BMI_SCHEDULE", "off");
EXPECT_EQ(requested_switch(with_schedule("on")), "off");
}
{
ScopedVar bad("MCPP_BMI_SCHEDULE", "ON");
std::string seen;
EXPECT_EQ(requested_switch(with_schedule("on"),
[&](std::string_view b) { seen = b; }), "auto");
EXPECT_EQ(seen, "ON");
}
}