authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2022-06-13 18:59:52-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2022-06-14 17:15:13-04:00
log22690efcc2378222503cb8aaad26a6f4a539f5aa
tree2dc41347d1db482beccbae41f8ecd078dc3ee9fe
parent47c4d4450281bfddebf89cbad4f9c1fdcc7b0b65

multi-thread `zig build test-cases`

Instead of always using std.testing.allocator, the test harness now follows the same logic as self-hosted for choosing an allocator - that is - it uses C allocator when linking libc, std.testing.allocator otherwise, and respects `-Dforce-gpa` to override the decision. I did this because I found GeneralPurposeAllocator to be prohibitively slow when doing multi-threading, even in the context of a debug build. There is now a second thread pool which is used to spawn each test case. The stage2 tests are passed the first thread pool. If it were only multi-threading the stage1 tests then we could use the same thread pool for everything. However, the problem with this strategy with stage2 is that stage2 wants to spawn tasks and then call wait() on the main thread. If we use the same thread pool for everything, we get a deadlock because all the threads end up all hanging at wait() and nothing is getting done. So we use our second thread pool to simulate a "process pool" of sorts. I spent most of the time working on this commit scratching my head trying to figure out why I was getting ETXTBSY when spawning the test cases. Turns out it's a fundamental Unix design flaw, already a known, unsolved issue by Go and Java maintainers: https://github.com/golang/go/issues/22315 https://bugs.openjdk.org/browse/JDK-8068370 With this change, the following command, executed on my laptop, went from 6m24s to 1m44s: ``` stage1/bin/zig build test-cases -fqemu -fwasmtime -Denable-llvm ``` closes #11818

2 files changed, 137 insertions(+), 63 deletions(-)

build.zig+1
......@@ -398,6 +398,7 @@ pub fn build(b: *Builder) !void {
398398 test_cases_options.addOption(bool, "llvm_has_csky", llvm_has_csky);
399399 test_cases_options.addOption(bool, "llvm_has_ve", llvm_has_ve);
400400 test_cases_options.addOption(bool, "llvm_has_arc", llvm_has_arc);
401 test_cases_options.addOption(bool, "force_gpa", force_gpa);
401402 test_cases_options.addOption(bool, "enable_qemu", b.enable_qemu);
402403 test_cases_options.addOption(bool, "enable_wine", b.enable_wine);
403404 test_cases_options.addOption(bool, "enable_wasmtime", b.enable_wasmtime);
src/test.zig+136-63
......@@ -1,11 +1,19 @@
11const std = @import("std");
22const builtin = @import("builtin");
3const Allocator = std.mem.Allocator;
4const CrossTarget = std.zig.CrossTarget;
5const print = std.debug.print;
6const assert = std.debug.assert;
7
38const link = @import("link.zig");
49const Compilation = @import("Compilation.zig");
5const Allocator = std.mem.Allocator;
610const Package = @import("Package.zig");
711const introspect = @import("introspect.zig");
812const build_options = @import("build_options");
13const ThreadPool = @import("ThreadPool.zig");
14const WaitGroup = @import("WaitGroup.zig");
15const zig_h = link.File.C.zig_h;
16
917const enable_qemu: bool = build_options.enable_qemu;
1018const enable_wine: bool = build_options.enable_wine;
1119const enable_wasmtime: bool = build_options.enable_wasmtime;
......@@ -13,12 +21,6 @@ const enable_darling: bool = build_options.enable_darling;
1321const enable_rosetta: bool = build_options.enable_rosetta;
1422const glibc_runtimes_dir: ?[]const u8 = build_options.glibc_runtimes_dir;
1523const skip_stage1 = build_options.skip_stage1;
16const ThreadPool = @import("ThreadPool.zig");
17const CrossTarget = std.zig.CrossTarget;
18const print = std.debug.print;
19const assert = std.debug.assert;
20
21const zig_h = link.File.C.zig_h;
2224
2325const hr = "=" ** 80;
2426
......@@ -27,11 +29,24 @@ test {
2729 @import("stage1.zig").os_init();
2830 }
2931
30 var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
32 const use_gpa = build_options.force_gpa or !builtin.link_libc;
33 const gpa = gpa: {
34 if (use_gpa) {
35 break :gpa std.testing.allocator;
36 }
37 // We would prefer to use raw libc allocator here, but cannot
38 // use it if it won't support the alignment we need.
39 if (@alignOf(std.c.max_align_t) < @alignOf(i128)) {
40 break :gpa std.heap.c_allocator;
41 }
42 break :gpa std.heap.raw_c_allocator;
43 };
44
45 var arena_allocator = std.heap.ArenaAllocator.init(gpa);
3146 defer arena_allocator.deinit();
3247 const arena = arena_allocator.allocator();
3348
34 var ctx = TestContext.init(std.testing.allocator, arena);
49 var ctx = TestContext.init(gpa, arena);
3550 defer ctx.deinit();
3651
3752 {
......@@ -536,6 +551,7 @@ fn sortTestFilenames(filenames: [][]const u8) void {
536551}
537552
538553pub const TestContext = struct {
554 gpa: Allocator,
539555 arena: Allocator,
540556 cases: std.ArrayList(Case),
541557
......@@ -604,6 +620,8 @@ pub const TestContext = struct {
604620
605621 files: std.ArrayList(File),
606622
623 result: anyerror!void = {},
624
607625 pub fn addSourceFile(case: *Case, name: []const u8, src: [:0]const u8) void {
608626 case.files.append(.{ .path = name, .src = src }) catch @panic("out of memory");
609627 }
......@@ -1185,6 +1203,7 @@ pub const TestContext = struct {
11851203
11861204 fn init(gpa: Allocator, arena: Allocator) TestContext {
11871205 return .{
1206 .gpa = gpa,
11881207 .cases = std.ArrayList(Case).init(gpa),
11891208 .arena = arena,
11901209 };
......@@ -1204,19 +1223,23 @@ pub const TestContext = struct {
12041223 }
12051224
12061225 fn run(self: *TestContext) !void {
1207 const host = try std.zig.system.NativeTargetInfo.detect(std.testing.allocator, .{});
1226 const host = try std.zig.system.NativeTargetInfo.detect(self.gpa, .{});
12081227
12091228 var progress = std.Progress{};
12101229 const root_node = progress.start("compiler", self.cases.items.len);
12111230 defer root_node.end();
12121231
1213 var zig_lib_directory = try introspect.findZigLibDir(std.testing.allocator);
1232 var zig_lib_directory = try introspect.findZigLibDir(self.gpa);
12141233 defer zig_lib_directory.handle.close();
1215 defer std.testing.allocator.free(zig_lib_directory.path.?);
1234 defer self.gpa.free(zig_lib_directory.path.?);
1235
1236 var aux_thread_pool: ThreadPool = undefined;
1237 try aux_thread_pool.init(self.gpa);
1238 defer aux_thread_pool.deinit();
12161239
1217 var thread_pool: ThreadPool = undefined;
1218 try thread_pool.init(std.testing.allocator);
1219 defer thread_pool.deinit();
1240 var case_thread_pool: ThreadPool = undefined;
1241 try case_thread_pool.init(self.gpa);
1242 defer case_thread_pool.deinit();
12201243
12211244 // Use the same global cache dir for all the tests, such that we for example don't have to
12221245 // rebuild musl libc for every case (when LLVM backend is enabled).
......@@ -1225,60 +1248,90 @@ pub const TestContext = struct {
12251248
12261249 var cache_dir = try global_tmp.dir.makeOpenPath("zig-cache", .{});
12271250 defer cache_dir.close();
1228 const tmp_dir_path = try std.fs.path.join(std.testing.allocator, &[_][]const u8{ ".", "zig-cache", "tmp", &global_tmp.sub_path });
1229 defer std.testing.allocator.free(tmp_dir_path);
1251 const tmp_dir_path = try std.fs.path.join(self.gpa, &[_][]const u8{ ".", "zig-cache", "tmp", &global_tmp.sub_path });
1252 defer self.gpa.free(tmp_dir_path);
12301253
12311254 const global_cache_directory: Compilation.Directory = .{
12321255 .handle = cache_dir,
1233 .path = try std.fs.path.join(std.testing.allocator, &[_][]const u8{ tmp_dir_path, "zig-cache" }),
1256 .path = try std.fs.path.join(self.gpa, &[_][]const u8{ tmp_dir_path, "zig-cache" }),
12341257 };
1235 defer std.testing.allocator.free(global_cache_directory.path.?);
1236
1237 var fail_count: usize = 0;
1258 defer self.gpa.free(global_cache_directory.path.?);
1259
1260 {
1261 var wait_group: WaitGroup = .{};
1262 defer wait_group.wait();
1263
1264 for (self.cases.items) |*case| {
1265 if (build_options.skip_non_native) {
1266 if (case.target.getCpuArch() != builtin.cpu.arch)
1267 continue;
1268 if (case.target.getObjectFormat() != builtin.object_format)
1269 continue;
1270 }
12381271
1239 for (self.cases.items) |case| {
1240 if (build_options.skip_non_native) {
1241 if (case.target.getCpuArch() != builtin.cpu.arch)
1272 // Skip tests that require LLVM backend when it is not available
1273 if (!build_options.have_llvm and case.backend == .llvm)
12421274 continue;
1243 if (case.target.getObjectFormat() != builtin.object_format)
1244 continue;
1245 }
12461275
1247 // Skip tests that require LLVM backend when it is not available
1248 if (!build_options.have_llvm and case.backend == .llvm)
1249 continue;
1276 if (build_options.test_filter) |test_filter| {
1277 if (std.mem.indexOf(u8, case.name, test_filter) == null) continue;
1278 }
12501279
1251 if (build_options.test_filter) |test_filter| {
1252 if (std.mem.indexOf(u8, case.name, test_filter) == null) continue;
1280 wait_group.start();
1281 try case_thread_pool.spawn(workerRunOneCase, .{
1282 self.gpa,
1283 root_node,
1284 case,
1285 zig_lib_directory,
1286 &aux_thread_pool,
1287 global_cache_directory,
1288 host,
1289 &wait_group,
1290 });
12531291 }
1254 var prg_node = root_node.start(case.name, case.updates.items.len);
1255 prg_node.activate();
1256 defer prg_node.end();
1257
1258 // So that we can see which test case failed when the leak checker goes off,
1259 // or there's an internal error
1260 progress.initial_delay_ns = 0;
1261 progress.refresh_rate_ns = 0;
1262
1263 runOneCase(
1264 std.testing.allocator,
1265 &prg_node,
1266 case,
1267 zig_lib_directory,
1268 &thread_pool,
1269 global_cache_directory,
1270 host,
1271 ) catch |err| {
1292 }
1293
1294 var fail_count: usize = 0;
1295 for (self.cases.items) |*case| {
1296 case.result catch |err| {
12721297 fail_count += 1;
1273 print("test '{s}' failed: {s}\n\n", .{ case.name, @errorName(err) });
1298 print("{s} failed: {s}\n", .{ case.name, @errorName(err) });
12741299 };
12751300 }
1301
12761302 if (fail_count != 0) {
12771303 print("{d} tests failed\n", .{fail_count});
12781304 return error.TestFailed;
12791305 }
12801306 }
12811307
1308 fn workerRunOneCase(
1309 gpa: Allocator,
1310 root_node: *std.Progress.Node,
1311 case: *Case,
1312 zig_lib_directory: Compilation.Directory,
1313 thread_pool: *ThreadPool,
1314 global_cache_directory: Compilation.Directory,
1315 host: std.zig.system.NativeTargetInfo,
1316 wait_group: *WaitGroup,
1317 ) void {
1318 defer wait_group.finish();
1319
1320 var prg_node = root_node.start(case.name, case.updates.items.len);
1321 prg_node.activate();
1322 defer prg_node.end();
1323
1324 case.result = runOneCase(
1325 gpa,
1326 &prg_node,
1327 case.*,
1328 zig_lib_directory,
1329 thread_pool,
1330 global_cache_directory,
1331 host,
1332 );
1333 }
1334
12821335 fn runOneCase(
12831336 allocator: Allocator,
12841337 root_node: *std.Progress.Node,
......@@ -1368,6 +1421,11 @@ pub const TestContext = struct {
13681421 try zig_args.append("-O");
13691422 try zig_args.append(@tagName(case.optimize_mode));
13701423
1424 // Prevent sub-process progress bar from interfering with the
1425 // one in this parent process.
1426 try zig_args.append("--color");
1427 try zig_args.append("off");
1428
13711429 const result = try std.ChildProcess.exec(.{
13721430 .allocator = arena,
13731431 .argv = zig_args.items,
......@@ -1529,6 +1587,8 @@ pub const TestContext = struct {
15291587 .use_llvm = use_llvm,
15301588 .use_stage1 = null, // We already handled stage1 tests
15311589 .self_exe_path = std.testing.zig_exe_path,
1590 // TODO instead of turning off color, pass in a std.Progress.Node
1591 .color = .off,
15321592 });
15331593 defer comp.destroy();
15341594
......@@ -1820,18 +1880,31 @@ pub const TestContext = struct {
18201880
18211881 try comp.makeBinFileExecutable();
18221882
1823 break :x std.ChildProcess.exec(.{
1824 .allocator = allocator,
1825 .argv = argv.items,
1826 .cwd_dir = tmp.dir,
1827 .cwd = tmp_dir_path,
1828 }) catch |err| {
1829 print("\nupdate_index={d} The following command failed with {s}:\n", .{
1830 update_index, @errorName(err),
1831 });
1832 dumpArgs(argv.items);
1833 return error.ChildProcessExecution;
1834 };
1883 while (true) {
1884 break :x std.ChildProcess.exec(.{
1885 .allocator = allocator,
1886 .argv = argv.items,
1887 .cwd_dir = tmp.dir,
1888 .cwd = tmp_dir_path,
1889 }) catch |err| switch (err) {
1890 error.FileBusy => {
1891 // There is a fundamental design flaw in Unix systems with how
1892 // ETXTBSY interacts with fork+exec.
1893 // https://github.com/golang/go/issues/22315
1894 // https://bugs.openjdk.org/browse/JDK-8068370
1895 // Unfortunately, this could be a real error, but we can't
1896 // tell the difference here.
1897 continue;
1898 },
1899 else => {
1900 print("\n{s}.{d} The following command failed with {s}:\n", .{
1901 case.name, update_index, @errorName(err),
1902 });
1903 dumpArgs(argv.items);
1904 return error.ChildProcessExecution;
1905 },
1906 };
1907 }
18351908 };
18361909 var test_node = update_node.start("test", 0);
18371910 test_node.activate();