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;...@@ -7,6 +7,7 @@ const mem = std.mem;
7const process = std.process;7const process = std.process;
8const ArrayList = std.ArrayList;8const ArrayList = std.ArrayList;
9const File = std.fs.File;9const File = std.fs.File;
10const Step = std.Build.Step;
1011
11pub const dependencies = @import("@dependencies");12pub const dependencies = @import("@dependencies");
1213
...@@ -258,7 +259,7 @@ pub fn main() !void {...@@ -258,7 +259,7 @@ pub fn main() !void {
258}259}
259260
260fn runStepNames(b: *std.Build, step_names: []const []const u8) !void {261fn 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);
262 defer step_stack.deinit();263 defer step_stack.deinit();
263264
264 if (step_names.len == 0) {265 if (step_names.len == 0) {
...@@ -290,28 +291,35 @@ fn runStepNames(b: *std.Build, step_names: []const []const u8) !void {...@@ -290,28 +291,35 @@ fn runStepNames(b: *std.Build, step_names: []const []const u8) !void {
290 {291 {
291 var wait_group: std.Thread.WaitGroup = .{};292 var wait_group: std.Thread.WaitGroup = .{};
292 defer wait_group.wait();293 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;
295 while (i > 0) {299 while (i > 0) {
296 i -= 1;300 i -= 1;
297 const step = step_stack.items[i];301 const step = step_stack.items[i];
298302
299 wait_group.start();303 wait_group.start();
300 thread_pool.spawn(workerMakeOneStep, .{ &wait_group, b, step }) catch304 thread_pool.spawn(workerMakeOneStep, .{ &wait_group, &thread_pool, b, step }) catch
301 @panic("unhandled error");305 @panic("OOM");
302 }306 }
303 }307 }
304308
305 var any_failed = false;309 var any_failed = false;
306310
307 for (step_stack.items) |s| {311 for (step_stack.items) |s| {
308 switch (s.result) {312 switch (s.state) {
309 .not_done => unreachable,313 .precheck_unstarted => unreachable,
314 .precheck_started => unreachable,
315 .precheck_done => unreachable,
316 .running => unreachable,
317 .dependency_failure => continue,
310 .success => continue,318 .success => continue,
311 .failure => |f| {319 .failure => {
312 any_failed = true;320 any_failed = true;
313 std.debug.print("{s}: {s}\n", .{321 std.debug.print("{s}: {s}\n", .{
314 s.name, @errorName(f.err_code),322 s.name, @errorName(s.result.err_code),
315 });323 });
316 },324 },
317 }325 }
...@@ -324,20 +332,20 @@ fn runStepNames(b: *std.Build, step_names: []const []const u8) !void {...@@ -324,20 +332,20 @@ fn runStepNames(b: *std.Build, step_names: []const []const u8) !void {
324332
325fn checkForDependencyLoop(333fn checkForDependencyLoop(
326 b: *std.Build,334 b: *std.Build,
327 s: *std.Build.Step,335 s: *Step,
328 step_stack: *ArrayList(*std.Build.Step),336 step_stack: *ArrayList(*Step),
329) !void {337) !void {
330 switch (s.loop_tag) {338 switch (s.state) {
331 .started => {339 .precheck_started => {
332 std.debug.print("dependency loop detected:\n {s}\n", .{s.name});340 std.debug.print("dependency loop detected:\n {s}\n", .{s.name});
333 return error.DependencyLoopDetected;341 return error.DependencyLoopDetected;
334 },342 },
335 .unstarted => {343 .precheck_unstarted => {
336 s.loop_tag = .started;344 s.state = .precheck_started;
337
338 try step_stack.append(s);
339345
340 for (s.dependencies.items) |dep| {346 for (s.dependencies.items) |dep| {
347 try step_stack.append(dep);
348 try dep.dependants.append(b.allocator, s);
341 checkForDependencyLoop(b, dep, step_stack) catch |err| {349 checkForDependencyLoop(b, dep, step_stack) catch |err| {
342 if (err == error.DependencyLoopDetected) {350 if (err == error.DependencyLoopDetected) {
343 std.debug.print(" {s}\n", .{s.name});351 std.debug.print(" {s}\n", .{s.name});
...@@ -346,31 +354,70 @@ fn checkForDependencyLoop(...@@ -346,31 +354,70 @@ fn checkForDependencyLoop(
346 };354 };
347 }355 }
348356
349 s.loop_tag = .done;357 s.state = .precheck_done;
350 },358 },
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,
352 }366 }
353}367}
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 {
356 defer wg.finish();375 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()) |_| {396 // Avoid running steps twice.
361 s.result = .success;397 if (@cmpxchgStrong(Step.State, &s.state, .precheck_done, .running, .SeqCst, .SeqCst) != null) {
362 } else |err| {398 // Another worker got the job.
363 s.result = .{ .failure = .{399 return;
364 .err_code = err,
365 } };
366 }400 }
367}
368401
369fn makeOneStep(b: *std.Build, s: *std.Build.Step) anyerror!void {402 // I suspect we will want to pass `b` to make() in a future modification.
370 for (s.dependencies.items) |dep| {403 // For example, CompileStep does some sus things with modifying the saved
371 try makeOneStep(b, dep);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");
372 }420 }
373 try s.make();
374}421}
375422
376fn steps(builder: *std.Build, already_ran_build: bool, out_stream: anytype) !void {423fn 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,...@@ -2,16 +2,25 @@ id: Id,
2name: []const u8,2name: []const u8,
3makeFn: *const fn (self: *Step) anyerror!void,3makeFn: *const fn (self: *Step) anyerror!void,
4dependencies: std.ArrayList(*Step),4dependencies: std.ArrayList(*Step),
5/// Used only during a pre-check for dependency loops.5/// This field is empty during execution of the user's build script, and
6loop_tag: enum { unstarted, started, done },6/// then populated during dependency loop checking in the build runner.
7result: union(enum) {7dependants: std.ArrayListUnmanaged(*Step),
8 not_done,8state: State,
9 success,9/// Populated only if state is success.
10 failure: struct {10result: struct {
11 err_code: anyerror,11 err_code: anyerror,
12 },
13},12},
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
15pub const Id = enum {24pub const Id = enum {
16 top_level,25 top_level,
17 compile,26 compile,
...@@ -67,8 +76,9 @@ pub fn init(...@@ -67,8 +76,9 @@ pub fn init(
67 .name = allocator.dupe(u8, name) catch @panic("OOM"),76 .name = allocator.dupe(u8, name) catch @panic("OOM"),
68 .makeFn = makeFn,77 .makeFn = makeFn,
69 .dependencies = std.ArrayList(*Step).init(allocator),78 .dependencies = std.ArrayList(*Step).init(allocator),
70 .loop_tag = .unstarted,79 .dependants = .{},
71 .result = .not_done,80 .state = .precheck_unstarted,
81 .result = undefined,
72 };82 };
73}83}
7484
...@@ -77,7 +87,6 @@ pub fn initNoOp(id: Id, name: []const u8, allocator: Allocator) Step {...@@ -77,7 +87,6 @@ pub fn initNoOp(id: Id, name: []const u8, allocator: Allocator) Step {
77}87}
7888
79pub fn make(self: *Step) !void {89pub fn make(self: *Step) !void {
80 assert(self.result == .not_done);
81 try self.makeFn(self);90 try self.makeFn(self);
82}91}
8392