authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2021-12-06 16:38:12-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2021-12-06 23:30:18-08:00
log274555be21ea756d8480a586f771274a79a58d80
treefccbc4b05398e9ea5d29fe1ac88f0b49a0779689
parenta711fcbbfa41d3f1dedc7ce4be9106e21b6c7ffb

stage2: improve handling of the generated file builtin.zig

All Zig code is eligible to `@import("builtin")` which is mapped to a generated file, build.zig, based on the target and other settings. Zig invocations which share the same target settings will generate the same builtin.zig file and thus the path to builtin.zig is in a shared cache folder, and different projects can sometimes use the same file. Before this commit, this led to race conditions where multiple invocations of `zig` would race to write this file. If one process wanted to *read* the file while the other process *wrote* the file, the reading process could observe a truncated or partially written builtin.zig file. This commit makes the following improvements: - limitations: - avoid clobbering the inode, mtime in the hot path - avoid creating a partially written file - builtin.zig needs to be on disk for debug info / stack trace purposes - don't mark the task as complete until the file is finished being populated (possibly by an external process) - strategy: - create the `@import("builtin")` `Module.File` during the AstGen work, based on generating the contents in memory rather than loading from disk. - write builtin.zig in a separate task that doesn't have to complete until the end of the AstGen work queue so that it can be done in parallel with everything else. - when writing the file, first stat the file path. If it exists, we are done. - otherwise, write the file to a temp file in the same directory and atomically rename it into place (clobbering the inode, mtime in the cold path). - summary: - all limitations respected - hot path: one stat() syscall that happens in a worker thread This required adding a missing function to the standard library: `std.fs.Dir.statFile`. In this commit, it does open() and then fstat() which is two syscalls. It should be improved in a future commit to only make one. Fixes #9439.

3 files changed, 124 insertions(+), 27 deletions(-)

lib/std/fs.zig+10
...@@ -2170,6 +2170,16 @@ pub const Dir = struct {...@@ -2170,6 +2170,16 @@ pub const Dir = struct {
2170 return file.stat();2170 return file.stat();
2171 }2171 }
21722172
2173 pub const StatFileError = File.OpenError || StatError;
2174
2175 // TODO: improve this to use the fstatat syscall instead of making 2 syscalls here.
2176 pub fn statFile(self: Dir, sub_path: []const u8) StatFileError!File.Stat {
2177 var file = try self.openFile(sub_path, .{});
2178 defer file.close();
2179
2180 return file.stat();
2181 }
2182
2173 pub const ChmodError = File.ChmodError;2183 pub const ChmodError = File.ChmodError;
21742184
2175 /// Changes the mode of the directory.2185 /// Changes the mode of the directory.
src/Compilation.zig+42-27
...@@ -2152,15 +2152,6 @@ pub fn performAllTheWork(self: *Compilation) error{ TimerUnsupported, OutOfMemor...@@ -2152,15 +2152,6 @@ pub fn performAllTheWork(self: *Compilation) error{ TimerUnsupported, OutOfMemor
2152 defer main_progress_node.end();2152 defer main_progress_node.end();
2153 if (self.color == .off) progress.terminal = null;2153 if (self.color == .off) progress.terminal = null;
21542154
2155 // If we need to write out builtin.zig, it needs to be done before starting
2156 // the AstGen tasks.
2157 if (self.bin_file.options.module) |mod| {
2158 if (mod.job_queued_update_builtin_zig) {
2159 mod.job_queued_update_builtin_zig = false;
2160 try self.updateBuiltinZigFile(mod);
2161 }
2162 }
2163
2164 // Here we queue up all the AstGen tasks first, followed by C object compilation.2155 // Here we queue up all the AstGen tasks first, followed by C object compilation.
2165 // We wait until the AstGen tasks are all completed before proceeding to the2156 // We wait until the AstGen tasks are all completed before proceeding to the
2166 // (at least for now) single-threaded main work queue. However, C object compilation2157 // (at least for now) single-threaded main work queue. However, C object compilation
...@@ -2185,6 +2176,21 @@ pub fn performAllTheWork(self: *Compilation) error{ TimerUnsupported, OutOfMemor...@@ -2185,6 +2176,21 @@ pub fn performAllTheWork(self: *Compilation) error{ TimerUnsupported, OutOfMemor
2185 self.astgen_wait_group.reset();2176 self.astgen_wait_group.reset();
2186 defer self.astgen_wait_group.wait();2177 defer self.astgen_wait_group.wait();
21872178
2179 // builtin.zig is handled specially for two reasons:
2180 // 1. to avoid race condition of zig processes truncating each other's builtin.zig files
2181 // 2. optimization; in the hot path it only incurs a stat() syscall, which happens
2182 // in the `astgen_wait_group`.
2183 if (self.bin_file.options.module) |mod| {
2184 if (mod.job_queued_update_builtin_zig) {
2185 mod.job_queued_update_builtin_zig = false;
2186
2187 self.astgen_wait_group.start();
2188 try self.thread_pool.spawn(workerUpdateBuiltinZigFile, .{
2189 self, mod, &self.astgen_wait_group,
2190 });
2191 }
2192 }
2193
2188 while (self.astgen_work_queue.readItem()) |file| {2194 while (self.astgen_work_queue.readItem()) |file| {
2189 self.astgen_wait_group.start();2195 self.astgen_wait_group.start();
2190 try self.thread_pool.spawn(workerAstGenFile, .{2196 try self.thread_pool.spawn(workerAstGenFile, .{
...@@ -2748,6 +2754,8 @@ fn workerAstGenFile(...@@ -2748,6 +2754,8 @@ fn workerAstGenFile(
2748 extra_index = item.end;2754 extra_index = item.end;
27492755
2750 const import_path = file.zir.nullTerminatedString(item.data.name);2756 const import_path = file.zir.nullTerminatedString(item.data.name);
2757 // `@import("builtin")` is handled specially.
2758 if (mem.eql(u8, import_path, "builtin")) continue;
27512759
2752 const import_result = blk: {2760 const import_result = blk: {
2753 comp.mutex.lock();2761 comp.mutex.lock();
...@@ -2775,6 +2783,29 @@ fn workerAstGenFile(...@@ -2775,6 +2783,29 @@ fn workerAstGenFile(
2775 }2783 }
2776}2784}
27772785
2786fn workerUpdateBuiltinZigFile(
2787 comp: *Compilation,
2788 mod: *Module,
2789 wg: *WaitGroup,
2790) void {
2791 defer wg.finish();
2792
2793 mod.populateBuiltinFile() catch |err| {
2794 const dir_path: []const u8 = mod.zig_cache_artifact_directory.path orelse ".";
2795
2796 comp.mutex.lock();
2797 defer comp.mutex.unlock();
2798
2799 comp.setMiscFailure(.write_builtin_zig, "unable to write builtin.zig to {s}: {s}", .{
2800 dir_path, @errorName(err),
2801 }) catch |oom| switch (oom) {
2802 error.OutOfMemory => log.err("unable to write builtin.zig to {s}: {s}", .{
2803 dir_path, @errorName(err),
2804 }),
2805 };
2806 };
2807}
2808
2778fn workerCheckEmbedFile(2809fn workerCheckEmbedFile(
2779 comp: *Compilation,2810 comp: *Compilation,
2780 embed_file: *Module.EmbedFile,2811 embed_file: *Module.EmbedFile,
...@@ -4046,22 +4077,6 @@ fn wantBuildLibUnwindFromSource(comp: *Compilation) bool {...@@ -4046,22 +4077,6 @@ fn wantBuildLibUnwindFromSource(comp: *Compilation) bool {
4046 comp.bin_file.options.object_format != .c;4077 comp.bin_file.options.object_format != .c;
4047}4078}
40484079
4049fn updateBuiltinZigFile(comp: *Compilation, mod: *Module) Allocator.Error!void {
4050 const tracy_trace = trace(@src());
4051 defer tracy_trace.end();
4052
4053 const source = try comp.generateBuiltinZigSource(comp.gpa);
4054 defer comp.gpa.free(source);
4055
4056 mod.zig_cache_artifact_directory.handle.writeFile("builtin.zig", source) catch |err| {
4057 const dir_path: []const u8 = mod.zig_cache_artifact_directory.path orelse ".";
4058 try comp.setMiscFailure(.write_builtin_zig, "unable to write builtin.zig to {s}: {s}", .{
4059 dir_path,
4060 @errorName(err),
4061 });
4062 };
4063}
4064
4065fn setMiscFailure(4080fn setMiscFailure(
4066 comp: *Compilation,4081 comp: *Compilation,
4067 tag: MiscTask,4082 tag: MiscTask,
...@@ -4084,7 +4099,7 @@ pub fn dump_argv(argv: []const []const u8) void {...@@ -4084,7 +4099,7 @@ pub fn dump_argv(argv: []const []const u8) void {
4084 std.debug.print("{s}\n", .{argv[argv.len - 1]});4099 std.debug.print("{s}\n", .{argv[argv.len - 1]});
4085}4100}
40864101
4087pub fn generateBuiltinZigSource(comp: *Compilation, allocator: Allocator) Allocator.Error![]u8 {4102pub fn generateBuiltinZigSource(comp: *Compilation, allocator: Allocator) Allocator.Error![:0]u8 {
4088 const tracy_trace = trace(@src());4103 const tracy_trace = trace(@src());
4089 defer tracy_trace.end();4104 defer tracy_trace.end();
40904105
...@@ -4290,7 +4305,7 @@ pub fn generateBuiltinZigSource(comp: *Compilation, allocator: Allocator) Alloca...@@ -4290,7 +4305,7 @@ pub fn generateBuiltinZigSource(comp: *Compilation, allocator: Allocator) Alloca
4290 }4305 }
4291 }4306 }
42924307
4293 return buffer.toOwnedSlice();4308 return buffer.toOwnedSliceSentinel(0);
4294}4309}
42954310
4296pub fn updateSubCompilation(sub_compilation: *Compilation) !void {4311pub fn updateSubCompilation(sub_compilation: *Compilation) !void {
src/Module.zig+72
...@@ -2967,6 +2967,78 @@ fn updateZirRefs(gpa: Allocator, file: *File, old_zir: Zir) !void {...@@ -2967,6 +2967,78 @@ fn updateZirRefs(gpa: Allocator, file: *File, old_zir: Zir) !void {
2967 }2967 }
2968}2968}
29692969
2970pub fn populateBuiltinFile(mod: *Module) !void {
2971 const tracy = trace(@src());
2972 defer tracy.end();
2973
2974 const comp = mod.comp;
2975 const pkg_and_file = blk: {
2976 comp.mutex.lock();
2977 defer comp.mutex.unlock();
2978
2979 const builtin_pkg = mod.main_pkg.table.get("builtin").?;
2980 const result = try mod.importPkg(builtin_pkg);
2981 break :blk .{
2982 .file = result.file,
2983 .pkg = builtin_pkg,
2984 };
2985 };
2986 const file = pkg_and_file.file;
2987 const builtin_pkg = pkg_and_file.pkg;
2988 const gpa = mod.gpa;
2989 file.source = try comp.generateBuiltinZigSource(gpa);
2990 file.source_loaded = true;
2991
2992 if (builtin_pkg.root_src_directory.handle.statFile(builtin_pkg.root_src_path)) |stat| {
2993 if (stat.size != file.source.len) {
2994 const full_path = try builtin_pkg.root_src_directory.join(gpa, &.{
2995 builtin_pkg.root_src_path,
2996 });
2997 defer gpa.free(full_path);
2998
2999 log.warn(
3000 "the cached file '{s}' had the wrong size. Expected {d}, found {d}. " ++
3001 "Overwriting with correct file contents now",
3002 .{ full_path, file.source.len, stat.size },
3003 );
3004
3005 try writeBuiltinFile(file, builtin_pkg);
3006 } else {
3007 file.stat_size = stat.size;
3008 file.stat_inode = stat.inode;
3009 file.stat_mtime = stat.mtime;
3010 }
3011 } else |err| switch (err) {
3012 error.BadPathName => unreachable, // it's always "builtin.zig"
3013 error.NameTooLong => unreachable, // it's always "builtin.zig"
3014 error.PipeBusy => unreachable, // it's not a pipe
3015 error.WouldBlock => unreachable, // not asking for non-blocking I/O
3016
3017 error.FileNotFound => try writeBuiltinFile(file, builtin_pkg),
3018
3019 else => |e| return e,
3020 }
3021
3022 file.tree = try std.zig.parse(gpa, file.source);
3023 file.tree_loaded = true;
3024 assert(file.tree.errors.len == 0); // builtin.zig must parse
3025
3026 file.zir = try AstGen.generate(gpa, file.tree);
3027 file.zir_loaded = true;
3028 file.status = .success_zir;
3029}
3030
3031pub fn writeBuiltinFile(file: *File, builtin_pkg: *Package) !void {
3032 var af = try builtin_pkg.root_src_directory.handle.atomicFile(builtin_pkg.root_src_path, .{});
3033 defer af.deinit();
3034 try af.file.writeAll(file.source);
3035 try af.finish();
3036
3037 file.stat_size = file.source.len;
3038 file.stat_inode = 0; // dummy value
3039 file.stat_mtime = 0; // dummy value
3040}
3041
2970pub fn mapOldZirToNew(3042pub fn mapOldZirToNew(
2971 gpa: Allocator,3043 gpa: Allocator,
2972 old_zir: Zir,3044 old_zir: Zir,