Skip to content

Commit 028d353

Browse files
committed
std.Build: add --seed argument to randomize step dependencies spawning
help detect possibly hidden dependencies on the running order of steps, especially in -j1 mode
1 parent a327d8b commit 028d353

File tree

2 files changed

+53
-5
lines changed

2 files changed

+53
-5
lines changed

lib/build_runner.zig

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ pub fn main() !void {
9393
var summary: ?Summary = null;
9494
var max_rss: usize = 0;
9595
var color: Color = .auto;
96+
var seed: u32 = 0;
9697

9798
const stderr_stream = io.getStdErr().writer();
9899
const stdout_stream = io.getStdOut().writer();
@@ -192,6 +193,15 @@ pub fn main() !void {
192193
std.debug.print("Expected argument after {s}\n\n", .{arg});
193194
usageAndErr(builder, false, stderr_stream);
194195
} };
196+
} else if (mem.eql(u8, arg, "--seed")) {
197+
const next_arg = nextArg(args, &arg_idx) orelse {
198+
std.debug.print("Expected u32 after {s}\n\n", .{arg});
199+
usageAndErr(builder, false, stderr_stream);
200+
};
201+
seed = std.fmt.parseUnsigned(u32, next_arg, 10) catch |err| {
202+
std.debug.print("unable to parse seed '{s}' as u32: {s}", .{ next_arg, @errorName(err) });
203+
process.exit(1);
204+
};
195205
} else if (mem.eql(u8, arg, "--debug-log")) {
196206
const next_arg = nextArg(args, &arg_idx) orelse {
197207
std.debug.print("Expected argument after {s}\n\n", .{arg});
@@ -324,6 +334,7 @@ pub fn main() !void {
324334
main_progress_node,
325335
thread_pool_options,
326336
&run,
337+
seed,
327338
) catch |err| switch (err) {
328339
error.UncleanExit => process.exit(1),
329340
else => return err,
@@ -349,6 +360,7 @@ fn runStepNames(
349360
parent_prog_node: *std.Progress.Node,
350361
thread_pool_options: std.Thread.Pool.Options,
351362
run: *Run,
363+
seed: u32,
352364
) !void {
353365
const gpa = b.allocator;
354366
var step_stack: std.AutoArrayHashMapUnmanaged(*Step, void) = .{};
@@ -369,8 +381,13 @@ fn runStepNames(
369381
}
370382

371383
const starting_steps = try arena.dupe(*Step, step_stack.keys());
384+
385+
var rng = std.rand.DefaultPrng.init(seed);
386+
const rand = rng.random();
387+
rand.shuffle(*Step, starting_steps);
388+
372389
for (starting_steps) |s| {
373-
checkForDependencyLoop(b, s, &step_stack) catch |err| switch (err) {
390+
constructGraphAndCheckForDependencyLoop(b, s, &step_stack, rand) catch |err| switch (err) {
374391
error.DependencyLoopDetected => return error.UncleanExit,
375392
else => |e| return e,
376393
};
@@ -498,7 +515,9 @@ fn runStepNames(
498515
stderr.writeAll(" (disable with --summary none)") catch {};
499516
ttyconf.setColor(stderr, .reset) catch {};
500517
}
501-
stderr.writeAll("\n") catch {};
518+
ttyconf.setColor(stderr, .dim) catch {};
519+
stderr.writer().print("\nseed is {}\n", .{seed}) catch {};
520+
ttyconf.setColor(stderr, .reset) catch {};
502521
const failures_only = run.summary != Summary.all;
503522

504523
// Print a fancy tree with build results.
@@ -731,10 +750,22 @@ fn printTreeStep(
731750
}
732751
}
733752

734-
fn checkForDependencyLoop(
753+
/// Traverse the dependency graph depth-first and make it undirected by having
754+
/// steps know their dependants (they only know dependencies at start).
755+
/// Along the way, check that there is no dependency loop, and record the steps
756+
/// in traversal order in `step_stack`.
757+
/// Each step has its dependencies traversed in random order, this accomplishes
758+
/// two things:
759+
/// - `step_stack` will be in randomized-depth-first order, so the build runner
760+
/// spawns steps in a random (but optimized) order
761+
/// - each step's `dependants` list is also filled in a random order, so that
762+
/// when it finishes executing in `workerMakeOneStep`, it spawns next steps
763+
/// to run in random order
764+
fn constructGraphAndCheckForDependencyLoop(
735765
b: *std.Build,
736766
s: *Step,
737767
step_stack: *std.AutoArrayHashMapUnmanaged(*Step, void),
768+
rand: std.rand.Random,
738769
) !void {
739770
switch (s.state) {
740771
.precheck_started => {
@@ -745,10 +776,16 @@ fn checkForDependencyLoop(
745776
s.state = .precheck_started;
746777

747778
try step_stack.ensureUnusedCapacity(b.allocator, s.dependencies.items.len);
748-
for (s.dependencies.items) |dep| {
779+
780+
// We dupe to avoid shuffling the steps in the summary, it depends
781+
// on s.dependencies' order.
782+
const deps = b.allocator.dupe(*Step, s.dependencies.items) catch @panic("OOM");
783+
rand.shuffle(*Step, deps);
784+
785+
for (deps) |dep| {
749786
try step_stack.put(b.allocator, dep, {});
750787
try dep.dependants.append(b.allocator, s);
751-
checkForDependencyLoop(b, dep, step_stack) catch |err| {
788+
constructGraphAndCheckForDependencyLoop(b, dep, step_stack, rand) catch |err| {
752789
if (err == error.DependencyLoopDetected) {
753790
std.debug.print(" {s}\n", .{s.name});
754791
}
@@ -1014,6 +1051,7 @@ fn usage(builder: *std.Build, already_ran_build: bool, out_stream: anytype) !voi
10141051
\\ --global-cache-dir [path] Override path to global Zig cache directory
10151052
\\ --zig-lib-dir [arg] Override path to Zig lib directory
10161053
\\ --build-runner [file] Override path to build runner
1054+
\\ --seed [integer] For shuffling dependency traversal order (default: random)
10171055
\\ --debug-log [scope] Enable debugging the compiler
10181056
\\ --debug-pkg-config Fail if unknown pkg-config flags encountered
10191057
\\ --verbose-link Enable compiler debug output for linking

src/main.zig

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4389,6 +4389,8 @@ pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
43894389
var child_argv = std.ArrayList([]const u8).init(arena);
43904390
var reference_trace: ?u32 = null;
43914391
var debug_compile_errors = false;
4392+
const micros: u32 = @truncate(@as(u64, @bitCast(std.time.microTimestamp())));
4393+
var seed: []const u8 = try std.fmt.allocPrint(arena, "{}", .{micros});
43924394

43934395
const argv_index_exe = child_argv.items.len;
43944396
_ = try child_argv.addOne();
@@ -4404,6 +4406,9 @@ pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
44044406
const argv_index_global_cache_dir = child_argv.items.len;
44054407
_ = try child_argv.addOne();
44064408

4409+
try child_argv.appendSlice(&[_][]const u8{ "--seed", seed });
4410+
const argv_index_seed = child_argv.items.len - 1;
4411+
44074412
{
44084413
var i: usize = 0;
44094414
while (i < args.len) : (i += 1) {
@@ -4450,6 +4455,11 @@ pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
44504455
} else if (mem.eql(u8, arg, "--debug-compile-errors")) {
44514456
try child_argv.append(arg);
44524457
debug_compile_errors = true;
4458+
} else if (mem.eql(u8, arg, "--seed")) {
4459+
if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
4460+
i += 1;
4461+
child_argv.items[argv_index_seed] = args[i];
4462+
continue;
44534463
}
44544464
}
44554465
try child_argv.append(arg);

0 commit comments

Comments
 (0)