authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-02-14 00:18:47-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-03-15 10:48:12-07:00
log1fa1484288dc7431f73facb8c423b71670d6914e
tree22a09907edad86dc053a5c6fe49b7c57445772ed
parentcff86cf7a17e038db44fa1f72ee5919eea6a6cae

build runner: proper threaded dependency management

After sorting the step stack so that dependencies can be popped before their dependants are popped, there is still a situation left to handle correctly: Example: A depends on: B C D depends on: E F They will be ordered like this: A B C D E F If there are 6+ cores, then all of them will be evaluated at once, incorrectly evaluating A and D before their dependencies. Starting evaluation of F and then E is correct, but waiting until they are done is not correct because it should start working on B and C as well. This commit solves the problem by computing dependants in the dependency loop checking logic, and then having workers queue up their dependants when they finish their own work.

2 files changed, 98 insertions(+), 42 deletions(-)

lib/build_runner.zig+78-31
......@@ -7,6 +7,7 @@ const mem = std.mem;
77const process = std.process;
88const ArrayList = std.ArrayList;
99const File = std.fs.File;
10const Step = std.Build.Step;
1011
1112pub const dependencies = @import("@dependencies");
1213
......@@ -258,7 +259,7 @@ pub fn main() !void {
258259}
259260
260261fn runStepNames(b: *std.Build, step_names: []const []const u8) !void {
261 var step_stack = ArrayList(*std.Build.Step).init(b.allocator);
262 var step_stack = ArrayList(*Step).init(b.allocator);
262263 defer step_stack.deinit();
263264
264265 if (step_names.len == 0) {
......@@ -290,28 +291,35 @@ fn runStepNames(b: *std.Build, step_names: []const []const u8) !void {
290291 {
291292 var wait_group: std.Thread.WaitGroup = .{};
292293 defer wait_group.wait();
293 var i = step_stack.items.len;
294294
295 // Here we spawn the initial set of tasks with a nice heuristic -
296 // dependency order. Each worker when it finishes a step will then
297 // check whether it should run any dependants.
298 var i = step_stack.items.len;
295299 while (i > 0) {
296300 i -= 1;
297301 const step = step_stack.items[i];
298302
299303 wait_group.start();
300 thread_pool.spawn(workerMakeOneStep, .{ &wait_group, b, step }) catch
301 @panic("unhandled error");
304 thread_pool.spawn(workerMakeOneStep, .{ &wait_group, &thread_pool, b, step }) catch
305 @panic("OOM");
302306 }
303307 }
304308
305309 var any_failed = false;
306310
307311 for (step_stack.items) |s| {
308 switch (s.result) {
309 .not_done => unreachable,
312 switch (s.state) {
313 .precheck_unstarted => unreachable,
314 .precheck_started => unreachable,
315 .precheck_done => unreachable,
316 .running => unreachable,
317 .dependency_failure => continue,
310318 .success => continue,
311 .failure => |f| {
319 .failure => {
312320 any_failed = true;
313321 std.debug.print("{s}: {s}\n", .{
314 s.name, @errorName(f.err_code),
322 s.name, @errorName(s.result.err_code),
315323 });
316324 },
317325 }
......@@ -324,20 +332,20 @@ fn runStepNames(b: *std.Build, step_names: []const []const u8) !void {
324332
325333fn checkForDependencyLoop(
326334 b: *std.Build,
327 s: *std.Build.Step,
328 step_stack: *ArrayList(*std.Build.Step),
335 s: *Step,
336 step_stack: *ArrayList(*Step),
329337) !void {
330 switch (s.loop_tag) {
331 .started => {
338 switch (s.state) {
339 .precheck_started => {
332340 std.debug.print("dependency loop detected:\n {s}\n", .{s.name});
333341 return error.DependencyLoopDetected;
334342 },
335 .unstarted => {
336 s.loop_tag = .started;
337
338 try step_stack.append(s);
343 .precheck_unstarted => {
344 s.state = .precheck_started;
339345
340346 for (s.dependencies.items) |dep| {
347 try step_stack.append(dep);
348 try dep.dependants.append(b.allocator, s);
341349 checkForDependencyLoop(b, dep, step_stack) catch |err| {
342350 if (err == error.DependencyLoopDetected) {
343351 std.debug.print(" {s}\n", .{s.name});
......@@ -346,31 +354,70 @@ fn checkForDependencyLoop(
346354 };
347355 }
348356
349 s.loop_tag = .done;
357 s.state = .precheck_done;
350358 },
351 .done => {},
359 .precheck_done => {},
360
361 // These don't happen until we actually run the step graph.
362 .dependency_failure => unreachable,
363 .running => unreachable,
364 .success => unreachable,
365 .failure => unreachable,
352366 }
353367}
354368
355fn workerMakeOneStep(wg: *std.Thread.WaitGroup, b: *std.Build, s: *std.Build.Step) void {
369fn workerMakeOneStep(
370 wg: *std.Thread.WaitGroup,
371 thread_pool: *std.Thread.Pool,
372 b: *std.Build,
373 s: *Step,
374) void {
356375 defer wg.finish();
357376
358 _ = b;
377 // First, check the conditions for running this step. If they are not met,
378 // then we return without doing the step, relying on another worker to
379 // queue this step up again when dependencies are met.
380 for (s.dependencies.items) |dep| {
381 switch (@atomicLoad(Step.State, &dep.state, .SeqCst)) {
382 .success => continue,
383 .failure, .dependency_failure => {
384 @atomicStore(Step.State, &s.state, .dependency_failure, .SeqCst);
385 return;
386 },
387 .precheck_done, .running => {
388 // dependency is not finished yet.
389 return;
390 },
391 .precheck_unstarted => unreachable,
392 .precheck_started => unreachable,
393 }
394 }
359395
360 if (s.make()) |_| {
361 s.result = .success;
362 } else |err| {
363 s.result = .{ .failure = .{
364 .err_code = err,
365 } };
396 // Avoid running steps twice.
397 if (@cmpxchgStrong(Step.State, &s.state, .precheck_done, .running, .SeqCst, .SeqCst) != null) {
398 // Another worker got the job.
399 return;
366400 }
367}
368401
369fn makeOneStep(b: *std.Build, s: *std.Build.Step) anyerror!void {
370 for (s.dependencies.items) |dep| {
371 try makeOneStep(b, dep);
402 // I suspect we will want to pass `b` to make() in a future modification.
403 // For example, CompileStep does some sus things with modifying the saved
404 // *Build object in install header steps that might be able to be removed
405 // by passing the *Build object through the make() functions.
406 s.make() catch |err| {
407 s.result = .{
408 .err_code = err,
409 };
410 @atomicStore(Step.State, &s.state, .failure, .SeqCst);
411 return;
412 };
413
414 @atomicStore(Step.State, &s.state, .success, .SeqCst);
415
416 // Successful completion of a step, so we queue up its dependants as well.
417 for (s.dependants.items) |dep| {
418 wg.start();
419 thread_pool.spawn(workerMakeOneStep, .{ wg, thread_pool, b, dep }) catch @panic("OOM");
372420 }
373 try s.make();
374421}
375422
376423fn steps(builder: *std.Build, already_ran_build: bool, out_stream: anytype) !void {
lib/std/Build/Step.zig+20-11
......@@ -2,16 +2,25 @@ id: Id,
22name: []const u8,
33makeFn: *const fn (self: *Step) anyerror!void,
44dependencies: std.ArrayList(*Step),
5/// Used only during a pre-check for dependency loops.
6loop_tag: enum { unstarted, started, done },
7result: union(enum) {
8 not_done,
9 success,
10 failure: struct {
11 err_code: anyerror,
12 },
5/// This field is empty during execution of the user's build script, and
6/// then populated during dependency loop checking in the build runner.
7dependants: std.ArrayListUnmanaged(*Step),
8state: State,
9/// Populated only if state is success.
10result: struct {
11 err_code: anyerror,
1312},
1413
14pub const State = enum {
15 precheck_unstarted,
16 precheck_started,
17 precheck_done,
18 running,
19 dependency_failure,
20 success,
21 failure,
22};
23
1524pub const Id = enum {
1625 top_level,
1726 compile,
......@@ -67,8 +76,9 @@ pub fn init(
6776 .name = allocator.dupe(u8, name) catch @panic("OOM"),
6877 .makeFn = makeFn,
6978 .dependencies = std.ArrayList(*Step).init(allocator),
70 .loop_tag = .unstarted,
71 .result = .not_done,
79 .dependants = .{},
80 .state = .precheck_unstarted,
81 .result = undefined,
7282 };
7383}
7484
......@@ -77,7 +87,6 @@ pub fn initNoOp(id: Id, name: []const u8, allocator: Allocator) Step {
7787}
7888
7989pub fn make(self: *Step) !void {
80 assert(self.result == .not_done);
8190 try self.makeFn(self);
8291}
8392