Skip to content

Commit 39f7b54

Browse files
committed
build: the status row counts the work of the build
A clean build of xlings counted 1195 steps on its status row, of which 503 were placements of the cache pass and 460 were dependency scans, and read 967/1195 when its first compile began. Ninja passes now have a kind. The cache pass is a placement pass: its Cached lines are written as before and its steps are not counted. The scans that wait on no action run in a pass of their own, before the main pass, shown as Scanning f/t; a scan that waits on a package's prepare or check action stays in the main pass, which keeps such an action from holding back every compile. Building f/t is the main pass: 0/232 at the first compile of the same build. The fast path runs the same passes. A failed scan pass ends the build with its own output. A build of named goals scans in its main pass. Measured on xlings: the scan pass takes 20 ms on an edit and about 0.3 s on a clean build, whose scans all ended in the first quarter second of the main pass before.
1 parent cf844c9 commit 39f7b54

4 files changed

Lines changed: 185 additions & 20 deletions

File tree

‎src/build/execute.cppm‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1290,10 +1290,29 @@ std::optional<int> run_ninja_fast(const std::string& ninjaProgram,
12901290
int status = 0;
12911291
bool reported = false;
12921292
const auto prefixes = read_ninja_command_prefixes(ninjaPath);
1293+
// The scan pass comes first here as on the full path (build wall-time
1294+
// plan, W2): the same passes, so the same counts.
1295+
std::optional<std::vector<std::string>> scanArgv;
1296+
{
1297+
std::ifstream in(ninjaPath, std::ios::binary);
1298+
std::string text{std::istreambuf_iterator<char>(in), {}};
1299+
if (text.find("\nbuild " + std::string(mcpp::build::kScannedGoal) + " : phony")
1300+
!= std::string::npos) {
1301+
scanArgv = argv;
1302+
scanArgv->push_back(std::string(mcpp::build::kScannedGoal));
1303+
}
1304+
}
12931305
if (reporting) {
12941306
mcpp::build::progress::Build report(outputDir);
1295-
auto run = mcpp::build::run_ninja_reporting(argv, childEnv, std::chrono::milliseconds{0},
1296-
report, verbose, prefixes);
1307+
mcpp::build::NinjaRun run;
1308+
if (scanArgv)
1309+
run = mcpp::build::run_ninja_reporting(*scanArgv, childEnv, std::chrono::milliseconds{0},
1310+
report, verbose, prefixes,
1311+
mcpp::build::progress::PassKind::Scan);
1312+
// A failed scan ends the build with its own output.
1313+
if (run.exitCode == 0 && !run.timedOut)
1314+
run = mcpp::build::run_ninja_reporting(argv, childEnv, std::chrono::milliseconds{0},
1315+
report, verbose, prefixes);
12971316
out = std::move(run.output);
12981317
status = run.exitCode;
12991318
reported = run.reported;
@@ -1305,7 +1324,9 @@ std::optional<int> run_ninja_fast(const std::string& ninjaProgram,
13051324
// Nobody reads this ninja's progress: it reports no action start
13061325
// (build progress design 2026-09-29, §6.4).
13071326
childEnv.emplace_back(std::string(mcpp::build::progress::kStartsEnv), "");
1308-
auto r = mcpp::platform::process::capture_exec(argv, childEnv);
1327+
mcpp::platform::process::RunResult r;
1328+
if (scanArgv) r = mcpp::platform::process::capture_exec(*scanArgv, childEnv);
1329+
if (r.exit_code == 0) r = mcpp::platform::process::capture_exec(argv, childEnv);
13091330
out = std::move(r.output);
13101331
status = r.exit_code;
13111332
}

‎src/build/ninja_backend.cppm‎

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,14 @@ NinjaRun run_ninja_reporting(const std::vector<std::string>& argv,
9595
std::chrono::milliseconds deadline,
9696
mcpp::build::progress::Build& progress,
9797
bool verbose,
98-
std::span<const std::string> commandPrefixes);
98+
std::span<const std::string> commandPrefixes,
99+
mcpp::build::progress::PassKind kind =
100+
mcpp::build::progress::PassKind::Work);
101+
102+
// The goal of the scan pass (build wall-time plan, W2): the dyndep file of
103+
// every unit whose scan waits on no action. Present in build.ninja when the
104+
// graph scans such a unit.
105+
inline constexpr std::string_view kScannedGoal = "_mcpp_scanned";
99106

100107
// The step record of a plan (design §6.3): how the plan names its packages,
101108
// and which package each statement of `attribution` is for.
@@ -1175,10 +1182,11 @@ NinjaRun run_ninja_reporting(const std::vector<std::string>& argv,
11751182
std::chrono::milliseconds deadline,
11761183
mcpp::build::progress::Build& progress,
11771184
bool verbose,
1178-
std::span<const std::string> commandPrefixes) {
1185+
std::span<const std::string> commandPrefixes,
1186+
mcpp::build::progress::PassKind kind) {
11791187
NinjaRun run;
11801188
for (auto& kv : progress.environment()) env.push_back(std::move(kv));
1181-
progress.pass_begin();
1189+
progress.pass_begin(kind);
11821190
// The lines after `FAILED:` up to the next status line are the failed
11831191
// step's command and output; the lines after a status line alone are a
11841192
// successful step's output, which only --verbose shows (as before).
@@ -2692,6 +2700,22 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements,
26922700
}
26932701
append("\n");
26942702

2703+
// THE SCAN PASS'S GOAL (build wall-time plan, W2): the dyndep file of
2704+
// every unit whose scan waits on no action. A scan waits on its
2705+
// package's actions that precede compilation (`order_only_for`), and
2706+
// a pass over those scans would hold every compile behind the longest
2707+
// such action; they stay in the main pass.
2708+
{
2709+
std::string goal;
2710+
for (auto& ddi : ddi_paths) {
2711+
auto it = actionOutputsByPackage.find(ddiOwner[ddi]);
2712+
if (it != actionOutputsByPackage.end() && !it->second.empty()) continue;
2713+
goal += " " + ddi + ".dd";
2714+
}
2715+
if (!goal.empty())
2716+
append(std::format("build {} : phony{}\n\n", kScannedGoal, goal));
2717+
}
2718+
26952719
// ── Phase 3: compile edges with per-file dyndep. ────────────────
26962720
// Each compile edge references its OWN .dd file instead of a global one.
26972721
// P2: module compile edges get a $bmi_out variable for BMI preservation.
@@ -4286,7 +4310,8 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
42864310
std::vector<std::string> pre{ninjaProgram, "-C", plan.outputDir.string(),
42874311
std::string(kStagedCacheGoal)};
42884312
(void)run_ninja_reporting(pre, nenv, preDeadline, *opts.progress, opts.verbose,
4289-
command_prefixes(flags, plan));
4313+
command_prefixes(flags, plan),
4314+
mcpp::build::progress::PassKind::Placement);
42904315
} else {
42914316
std::vector<std::string> pre{ninjaProgram, "--quiet", "-C", plan.outputDir.string(),
42924317
std::string(kStagedCacheGoal)};
@@ -4299,13 +4324,62 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
42994324
stage("ninja-staged-cache");
43004325
}
43014326

4327+
// THE SCANS THAT WAIT ON NO ACTION RUN IN A PASS OF THEIR OWN, after the
4328+
// cache pass and before the main pass (build wall-time plan, W2). The
4329+
// main pass's count then states the work of the build, its compiles,
4330+
// links, archives and actions, instead of 460 scans of which all end in
4331+
// the first quarter second; and every dyndep file of those units is
4332+
// current when the main pass loads them, as the cache pass arranges for
4333+
// its placements (ninja-build/ninja#2662). A build of named goals (a test,
4334+
// a target) scans in its main pass: a pass over every scan would scan
4335+
// units its goals never compile.
43024336
bool buildTimedOut = false;
43034337
bool reported = false;
43044338
std::string out;
43054339
int ninjaExit = 0;
4340+
bool scanFailed = false;
4341+
if (goalArg.empty()
4342+
&& manifest.find("\nbuild " + std::string(kScannedGoal) + " : phony") != std::string::npos) {
4343+
const auto scanDeadline =
4344+
std::chrono::milliseconds(static_cast<long long>(opts.buildTimeoutSecs) * 1000);
4345+
std::vector<std::string> scan{ninjaProgram};
4346+
if (!opts.verbose && !opts.progress) scan.push_back("--quiet");
4347+
scan.insert(scan.end(), {std::string("-C"), plan.outputDir.string()});
4348+
if (opts.verbose) scan.push_back("-v");
4349+
scan.push_back(std::string(kScannedGoal));
4350+
if (opts.parallelJobs) scan.push_back(std::format("-j{}", opts.parallelJobs));
4351+
if (opts.progress) {
4352+
auto run = run_ninja_reporting(scan, nenv, scanDeadline, *opts.progress, opts.verbose,
4353+
command_prefixes(flags, plan),
4354+
mcpp::build::progress::PassKind::Scan);
4355+
if (run.exitCode != 0 || run.timedOut) {
4356+
out = std::move(run.output);
4357+
ninjaExit = run.exitCode;
4358+
buildTimedOut = run.timedOut;
4359+
reported = run.reported;
4360+
scanFailed = true;
4361+
}
4362+
} else {
4363+
auto scanEnv = nenv;
4364+
scanEnv.emplace_back(std::string(mcpp::build::progress::kStartsEnv), "");
4365+
auto cap = mcpp::platform::process::capture_exec_deadline(
4366+
scan, scanEnv, scanDeadline, &buildTimedOut);
4367+
if (cap.exit_code != 0 || buildTimedOut) {
4368+
out = std::move(cap.output);
4369+
ninjaExit = cap.exit_code;
4370+
scanFailed = true;
4371+
}
4372+
}
4373+
stage("ninja-scan");
4374+
}
4375+
4376+
// A failed scan ends the build with its own output: the main pass would
4377+
// run the failed step again and state its diagnostics a second time.
43064378
const auto deadline =
43074379
std::chrono::milliseconds(static_cast<long long>(opts.buildTimeoutSecs) * 1000);
4308-
if (opts.progress) {
4380+
if (scanFailed) {
4381+
// `out`, `ninjaExit`, `buildTimedOut` and `reported` are the scan's.
4382+
} else if (opts.progress) {
43094383
auto run = run_ninja_reporting(nargv, nenv, deadline, *opts.progress, opts.verbose,
43104384
command_prefixes(flags, plan));
43114385
out = std::move(run.output);

‎src/build/progress.cppm‎

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ enum class ProgramOutcome { Ran, Cached, Failed };
190190
void open(bool verbose);
191191
// Closes it: the region is erased. Idempotent.
192192
void close();
193+
// The status row as the region would draw it now.
194+
std::string status_row();
193195
// The number of configurations the command builds: with more than one, a
194196
// package line names its configuration.
195197
void configurations(std::size_t n);
@@ -215,6 +217,16 @@ void defer_finished();
215217
void finish_deferred();
216218

217219
// One build directory's ninja runs within this command.
220+
// WHAT A NINJA PASS IS FOR, AND WHETHER ITS STEPS ARE COUNTED (build wall-time
221+
// plan, W2). `Building f/t` states the work of the build: its compiles, links,
222+
// archives and actions. The cache pass places files the global cache serves,
223+
// which its `Cached` lines report; counted, its 503 placements and the 460
224+
// dependency scans of a clean build of xlings put the count at 81% when the
225+
// first compile began. A placement pass is therefore read but not counted, and
226+
// the scans that wait on no action run in a pass of their own, shown as
227+
// `Scanning f/t`.
228+
enum class PassKind { Placement, Scan, Work };
229+
218230
class Build {
219231
public:
220232
// Opaque: its definition is this module's own.
@@ -234,8 +246,8 @@ public:
234246

235247
// The environment ninja runs with: NINJA_STATUS and the start file.
236248
std::vector<std::pair<std::string, std::string>> environment() const;
237-
// One ninja invocation.
238-
void pass_begin();
249+
// One ninja invocation, of the kind `kind` (see PassKind).
250+
void pass_begin(PassKind kind = PassKind::Work);
239251
void status(const StatusLine& line);
240252
// A `FAILED: <outputs>` line. For the command's first failure, returns
241253
// the failed step's package as a line names it (empty when the step is
@@ -634,7 +646,7 @@ void record_action_start(std::string_view stamp) {
634646
// ─── The model ───────────────────────────────────────────────────────────
635647

636648
// Not exported, and not TU-local either: `Build::Impl` holds them.
637-
enum class Phase { Planning, Programs, Building, Stopping, Checking };
649+
enum class Phase { Planning, Programs, Scanning, Building, Stopping, Checking };
638650

639651
struct Program {
640652
std::string name;
@@ -672,6 +684,7 @@ struct Build::Impl {
672684
std::size_t finished = 0, total = 0; // this pass
673685
std::optional<std::size_t> unstarted; // this pass, from `%u`
674686
bool inPass = false;
687+
PassKind kind = PassKind::Work; // this pass; only Work passes count
675688
// The log of this pass.
676689
std::filesystem::path logPath;
677690
std::string logTail; // the file's last bytes when the pass began
@@ -991,12 +1004,18 @@ std::string phase_status(Report& r, std::string_view cells) {
9911004
std::string current;
9921005
long long oldest = std::numeric_limits<long long>::max();
9931006
std::size_t done = 0, total = 0, remaining = 0;
1007+
std::size_t scanDone = 0, scanTotal = 0;
9941008
bool tail = true; // every build in a pass has started its last step
9951009
bool anyPass = false;
9961010
for (auto const& b : live_builds(r)) {
997-
done += b->doneBefore + b->finished;
998-
total += b->totalBefore + b->total;
999-
if (b->inPass) {
1011+
const bool work = b->kind == PassKind::Work;
1012+
done += b->doneBefore + (work ? b->finished : 0);
1013+
total += b->totalBefore + (work ? b->total : 0);
1014+
if (b->inPass && b->kind == PassKind::Scan) {
1015+
scanDone += b->finished;
1016+
scanTotal += b->total;
1017+
}
1018+
if (b->inPass && work) {
10001019
anyPass = true;
10011020
if (!b->unstarted || *b->unstarted > 0) tail = false;
10021021
else remaining += b->total > b->finished ? b->total - b->finished : 0;
@@ -1026,6 +1045,10 @@ std::string phase_status(Report& r, std::string_view cells) {
10261045
counts = std::format("{}/{}", finishedPrograms, r.programs.size());
10271046
break;
10281047
}
1048+
case Phase::Scanning:
1049+
phase = "Scanning";
1050+
if (scanTotal > 0) counts = std::format("{}/{}", scanDone, scanTotal);
1051+
break;
10291052
case Phase::Building:
10301053
case Phase::Stopping:
10311054
phase = r.phase == Phase::Building ? "Building" : "Stopping";
@@ -1066,8 +1089,9 @@ mcpp::ui::Frame frame() {
10661089
} else if (r.animation) {
10671090
std::size_t done = 0, total = 0;
10681091
for (auto const& b : live_builds(r)) {
1069-
done += b->doneBefore + b->finished;
1070-
total += b->totalBefore + b->total;
1092+
const bool work = b->kind == PassKind::Work;
1093+
done += b->doneBefore + (work ? b->finished : 0);
1094+
total += b->totalBefore + (work ? b->total : 0);
10711095
}
10721096
const auto now = now_ms();
10731097
screen::Input in;
@@ -1281,6 +1305,10 @@ void checking() {
12811305
mcpp::ui::touch_region();
12821306
}
12831307

1308+
std::string status_row() {
1309+
return frame().status;
1310+
}
1311+
12841312
void defer_finished() {
12851313
auto& r = report();
12861314
std::lock_guard lock(r.m);
@@ -1394,18 +1422,23 @@ std::vector<std::pair<std::string, std::string>> Build::environment() const {
13941422
{std::string(kStartsEnv), (impl_->dir / kStartsFile).string()}};
13951423
}
13961424

1397-
void Build::pass_begin() {
1425+
void Build::pass_begin(PassKind kind) {
13981426
auto& r = report();
13991427
{
14001428
std::lock_guard lock(r.m);
14011429
auto& b = *impl_;
1402-
b.doneBefore += b.finished;
1403-
b.totalBefore += b.total;
1430+
if (b.kind == PassKind::Work) {
1431+
b.doneBefore += b.finished;
1432+
b.totalBefore += b.total;
1433+
}
1434+
b.kind = kind;
14041435
b.finished = b.total = 0;
14051436
b.unstarted.reset();
14061437
b.passStart = now_ms();
14071438
if (!r.buildStart) r.buildStart = b.passStart;
1408-
r.phase = Phase::Building;
1439+
// A placement pass keeps the phase it found (`Planning`).
1440+
if (kind == PassKind::Work) r.phase = Phase::Building;
1441+
else if (kind == PassKind::Scan) r.phase = Phase::Scanning;
14091442
b.inPass = true;
14101443
b.ends.clear();
14111444
b.logPath = b.dir / ".ninja_log";

‎tests/unit/test_build_progress.cpp‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,43 @@ TEST(ProgressModel, APackageTheCacheServesIsNamedCachedWithItsUnits) {
390390
EXPECT_EQ(count(out, "compat.ftxui"), 1u) << out;
391391
}
392392

393+
// ─── What the count counts (build wall-time plan, W2) ────────────────────
394+
//
395+
// `Building f/t` states the work of the build. A clean build of xlings counted
396+
// 1195 steps, 503 of them placements of the cache pass and 460 dependency
397+
// scans, and read 967/1195 when its first compile began. A placement pass is
398+
// read but not counted, a scan pass shows as `Scanning f/t`, and the main pass
399+
// alone is `Building f/t`.
400+
TEST(ProgressModel, OnlyTheMainPassIsCountedAsBuilding) {
401+
mcpp::ui::disable_color();
402+
Tmp tmp;
403+
Record rec;
404+
rec.packages = {{"app", true, "app", 0, 1, "v0.1.0 (.)", "project"}};
405+
rec.steps = 1;
406+
Build b(tmp.path);
407+
b.set_record(rec);
408+
testing::internal::CaptureStdout();
409+
b.pass_begin(mcpp::build::progress::PassKind::Placement);
410+
b.status({503, 503, 1, 0, {}});
411+
const auto placing = mcpp::build::progress::status_row();
412+
b.pass_end();
413+
b.pass_begin(mcpp::build::progress::PassKind::Scan);
414+
b.status({200, 460, 2, 100, {}});
415+
const auto scanning = mcpp::build::progress::status_row();
416+
b.pass_end();
417+
b.pass_begin(mcpp::build::progress::PassKind::Work);
418+
b.status({1, 232, 3, 200, {}});
419+
const auto building = mcpp::build::progress::status_row();
420+
b.pass_end();
421+
b.finish(true);
422+
testing::internal::GetCapturedStdout();
423+
EXPECT_EQ(placing.find("503"), std::string::npos) << placing;
424+
EXPECT_NE(scanning.find("Scanning"), std::string::npos) << scanning;
425+
EXPECT_NE(scanning.find("200/460"), std::string::npos) << scanning;
426+
EXPECT_NE(building.find("Building"), std::string::npos) << building;
427+
EXPECT_NE(building.find(" 1/232"), std::string::npos) << building;
428+
}
429+
393430
TEST(ProgressModel, AFailureNamesItsPackageOnce) {
394431
mcpp::ui::disable_color();
395432
Tmp tmp;

0 commit comments

Comments
 (0)