Files
bun.sh/src/bake/DevServer/DirectoryWatchStore.zig
pfg 05d0475c6c Update to zig 0.15.2 (#24204)
Fixes ENG-21287

Build times, from `bun run build && echo '//' >> src/main.zig && time
bun run build`

|Platform|0.14.1|0.15.2|Speedup|
|-|-|-|-|
|macos debug asan|126.90s|106.27s|1.19x|
|macos debug noasan|60.62s|50.85s|1.19x|
|linux debug asan|292.77s|241.45s|1.21x|
|linux debug noasan|146.58s|130.94s|1.12x|
|linux debug use_llvm=false|n/a|78.27s|1.87x|
|windows debug asan|177.13s|142.55s|1.24x|

Runtime performance:

- next build memory usage may have gone up by 5%. Otherwise seems the
same. Some code with writers may have gotten slower, especially one
instance of a counting writer and a few instances of unbuffered writers
that now have vtable overhead.
- File size reduced by 800kb (from 100.2mb to 99.4mb)

Improvements:

- `@export` hack is no longer needed for watch
- native x86_64 backend for linux builds faster. to use it, set use_llvm
false and no_link_obj false. also set `ASAN_OPTIONS=detect_leaks=0`
otherwise it will spam the output with tens of thousands of lines of
debug info errors. may need to use the zig lldb fork for debugging.
- zig test-obj, which we will be able to use for zig unit tests

Still an issue:

- false 'dependency loop' errors remain in watch mode
- watch mode crashes observed

Follow-up:

- [ ] search `comptime Writer: type` and `comptime W: type` and remove
- [ ] remove format_mode in our zig fork
- [ ] remove deprecated.zig autoFormatLabelFallback
- [ ] remove deprecated.zig autoFormatLabel
- [ ] remove deprecated.BufferedWriter and BufferedReader
- [ ] remove override_no_export_cpp_apis as it is no longer needed
- [ ] css Parser(W) -> Parser, and remove all the comptime writer: type
params
- [ ] remove deprecated writer fully

Files that add lines:

```
649     src/deprecated.zig
167     scripts/pack-codegen-for-zig-team.ts
54      scripts/cleartrace-impl.js
46      scripts/cleartrace.ts
43      src/windows.zig
18      src/fs.zig
17      src/bun.js/ConsoleObject.zig
16      src/output.zig
12      src/bun.js/test/debug.zig
12      src/bun.js/node/node_fs.zig
8       src/env_loader.zig
7       src/css/printer.zig
7       src/cli/init_command.zig
7       src/bun.js/node.zig
6       src/string/escapeRegExp.zig
6       src/install/PnpmMatcher.zig
5       src/bun.js/webcore/Blob.zig
4       src/crash_handler.zig
4       src/bun.zig
3       src/install/lockfile/bun.lock.zig
3       src/cli/update_interactive_command.zig
3       src/cli/pack_command.zig
3       build.zig
2       src/Progress.zig
2       src/install/lockfile/lockfile_json_stringify_for_debugging.zig
2       src/css/small_list.zig
2       src/bun.js/webcore/prompt.zig
1       test/internal/ban-words.test.ts
1       test/internal/ban-limits.json
1       src/watcher/WatcherTrace.zig
1       src/transpiler.zig
1       src/shell/builtin/cp.zig
1       src/js_printer.zig
1       src/io/PipeReader.zig
1       src/install/bin.zig
1       src/css/selectors/selector.zig
1       src/cli/run_command.zig
1       src/bun.js/RuntimeTranspilerStore.zig
1       src/bun.js/bindings/JSRef.zig
1       src/bake/DevServer.zig
```

Files that remove lines:

```
-1      src/test/recover.zig
-1      src/sql/postgres/SocketMonitor.zig
-1      src/sql/mysql/MySQLRequestQueue.zig
-1      src/sourcemap/CodeCoverage.zig
-1      src/css/values/color_js.zig
-1      src/compile_target.zig
-1      src/bundler/linker_context/convertStmtsForChunk.zig
-1      src/bundler/bundle_v2.zig
-1      src/bun.js/webcore/blob/read_file.zig
-1      src/ast/base.zig
-2      src/sql/postgres/protocol/ArrayList.zig
-2      src/shell/builtin/mkdir.zig
-2      src/install/PackageManager/patchPackage.zig
-2      src/install/PackageManager/PackageManagerDirectories.zig
-2      src/fmt.zig
-2      src/css/declaration.zig
-2      src/css/css_parser.zig
-2      src/collections/baby_list.zig
-2      src/bun.js/bindings/ZigStackFrame.zig
-2      src/ast/E.zig
-3      src/StandaloneModuleGraph.zig
-3      src/deps/picohttp.zig
-3      src/deps/libuv.zig
-3      src/btjs.zig
-4      src/threading/Futex.zig
-4      src/shell/builtin/touch.zig
-4      src/meta.zig
-4      src/install/lockfile.zig
-4      src/css/selectors/parser.zig
-5      src/shell/interpreter.zig
-5      src/css/error.zig
-5      src/bun.js/web_worker.zig
-5      src/bun.js.zig
-6      src/cli/test_command.zig
-6      src/bun.js/VirtualMachine.zig
-6      src/bun.js/uuid.zig
-6      src/bun.js/bindings/JSValue.zig
-9      src/bun.js/test/pretty_format.zig
-9      src/bun.js/api/BunObject.zig
-14     src/install/install_binding.zig
-14     src/fd.zig
-14     src/bun.js/node/path.zig
-14     scripts/pack-codegen-for-zig-team.sh
-17     src/bun.js/test/diff_format.zig
```

`git diff --numstat origin/main...HEAD | awk '{ print ($1-$2)"\t"$3 }' |
sort -rn`

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
Co-authored-by: Meghan Denny <meghan@bun.com>
Co-authored-by: tayor.fish <contact@taylor.fish>
2025-11-10 14:38:26 -08:00

270 lines
9.5 KiB
Zig

const DirectoryWatchStore = @This();
/// When a file fails to import a relative path, directory watchers are added so
/// that when a matching file is created, the dependencies can be rebuilt. This
/// handles HMR cases where a user writes an import before creating the file,
/// or moves files around. This structure is not thread-safe.
///
/// This structure manages those watchers, including releasing them once
/// import resolution failures are solved.
// TODO: when a file fixes its resolution, there is no code specifically to remove the watchers.
/// List of active watchers. Can be re-ordered on removal
watches: bun.StringArrayHashMapUnmanaged(Entry),
dependencies: ArrayListUnmanaged(Dep),
/// Dependencies cannot be re-ordered. This list tracks what indexes are free.
dependencies_free_list: ArrayListUnmanaged(Dep.Index),
pub const empty: DirectoryWatchStore = .{
.watches = .{},
.dependencies = .{},
.dependencies_free_list = .{},
};
pub fn owner(store: *DirectoryWatchStore) *DevServer {
return @alignCast(@fieldParentPtr("directory_watchers", store));
}
pub fn trackResolutionFailure(store: *DirectoryWatchStore, import_source: []const u8, specifier: []const u8, renderer: bake.Graph, loader: bun.options.Loader) bun.OOM!void {
// When it does not resolve to a file path, there is nothing to track.
if (specifier.len == 0) return;
if (!std.fs.path.isAbsolute(import_source)) return;
switch (loader) {
.tsx, .ts, .jsx, .js => {
if (!(bun.strings.startsWith(specifier, "./") or
bun.strings.startsWith(specifier, "../"))) return;
},
// Imports in CSS can resolve to relative files without './'
// Imports in HTML can resolve to project-relative paths by
// prefixing with '/', but that is done in HTMLScanner.
.css, .html => {},
// Multiple parts of DevServer rely on the fact that these
// loaders do not depend on importing other files.
.file,
.json,
.jsonc,
.toml,
.yaml,
.wasm,
.napi,
.base64,
.dataurl,
.text,
.bunsh,
.sqlite,
.sqlite_embedded,
=> bun.debugAssert(false),
}
const buf = bun.path_buffer_pool.get();
defer bun.path_buffer_pool.put(buf);
const joined = bun.path.joinAbsStringBuf(bun.path.dirname(import_source, .auto), buf, &.{specifier}, .auto);
const dir = bun.path.dirname(joined, .auto);
// The `import_source` parameter is not a stable string. Since the
// import source will be added to IncrementalGraph anyways, this is a
// great place to share memory.
const dev = store.owner();
dev.graph_safety_lock.lock();
defer dev.graph_safety_lock.unlock();
const owned_file_path = switch (renderer) {
.client => (try dev.client_graph.insertEmpty(import_source, .unknown)).key,
.server, .ssr => (try dev.server_graph.insertEmpty(import_source, .unknown)).key,
};
store.insert(dir, owned_file_path, specifier) catch |err| switch (err) {
error.Ignore => {}, // ignoring watch errors.
error.OutOfMemory => |e| return e,
};
}
/// `dir_name_to_watch` is cloned
/// `file_path` must have lifetime that outlives the watch
/// `specifier` is cloned
fn insert(
store: *DirectoryWatchStore,
dir_name_to_watch: []const u8,
file_path: []const u8,
specifier: []const u8,
) !void {
assert(specifier.len > 0);
// TODO: watch the parent dir too.
const dev = store.owner();
debug.log("DirectoryWatchStore.insert({f}, {f}, {f})", .{
bun.fmt.quote(dir_name_to_watch),
bun.fmt.quote(file_path),
bun.fmt.quote(specifier),
});
if (store.dependencies_free_list.items.len == 0)
try store.dependencies.ensureUnusedCapacity(dev.allocator(), 1);
const gop = try store.watches.getOrPut(dev.allocator(), bun.strings.withoutTrailingSlashWindowsPath(dir_name_to_watch));
const specifier_cloned = if (specifier[0] == '.' or std.fs.path.isAbsolute(specifier))
try dev.allocator().dupe(u8, specifier)
else
try std.fmt.allocPrint(dev.allocator(), "./{s}", .{specifier});
errdefer dev.allocator().free(specifier_cloned);
if (gop.found_existing) {
const dep = store.appendDepAssumeCapacity(.{
.next = gop.value_ptr.first_dep.toOptional(),
.source_file_path = file_path,
.specifier = specifier_cloned,
});
gop.value_ptr.first_dep = dep;
return;
}
errdefer store.watches.swapRemoveAt(gop.index);
// Try to use an existing open directory handle
const cache_fd = if (dev.server_transpiler.resolver.readDirInfo(dir_name_to_watch) catch null) |cache|
cache.getFileDescriptor().unwrapValid()
else
null;
const fd, const owned_fd = if (Watcher.requires_file_descriptors) if (cache_fd) |fd|
.{ fd, false }
else switch (bun.sys.open(
&(std.posix.toPosixPath(dir_name_to_watch) catch |err| switch (err) {
error.NameTooLong => return error.Ignore, // wouldn't be able to open, ignore
}),
// O_EVTONLY is the flag to indicate that only watches will be used.
bun.O.DIRECTORY | bun.c.O_EVTONLY,
0,
)) {
.result => |fd| .{ fd, true },
.err => |err| switch (err.getErrno()) {
// If this directory doesn't exist, a watcher should be placed
// on the parent directory. Then, if this directory is later
// created, the watcher can be properly initialized. This would
// happen if a specifier like `./dir/whatever/hello.tsx` and
// `dir` does not exist, Bun must place a watcher on `.`, see
// the creation of `dir`, and repeat until it can open a watcher
// on `whatever` to see the creation of `hello.tsx`
.NOENT => {
// TODO: implement that. for now it ignores (BUN-10968)
return error.Ignore;
},
.NOTDIR => return error.Ignore, // ignore
else => {
bun.todoPanic(@src(), "log watcher error", .{});
},
},
} else .{ bun.invalid_fd, false };
errdefer if (Watcher.requires_file_descriptors) if (owned_fd) fd.close();
if (Watcher.requires_file_descriptors)
debug.log("-> fd: {f} ({s})", .{
fd,
if (owned_fd) "from dir cache" else "owned fd",
});
const dir_name = try dev.allocator().dupe(u8, dir_name_to_watch);
errdefer dev.allocator().free(dir_name);
gop.key_ptr.* = bun.strings.withoutTrailingSlashWindowsPath(dir_name);
const watch_index = switch (dev.bun_watcher.addDirectory(fd, dir_name, bun.Watcher.getHash(dir_name), false)) {
.err => return error.Ignore,
.result => |id| id,
};
const dep = store.appendDepAssumeCapacity(.{
.next = .none,
.source_file_path = file_path,
.specifier = specifier_cloned,
});
store.watches.putAssumeCapacity(dir_name, .{
.dir = fd,
.dir_fd_owned = owned_fd,
.first_dep = dep,
.watch_index = watch_index,
});
}
/// Caller must detach the dependency from the linked list it is in.
pub fn freeDependencyIndex(store: *DirectoryWatchStore, alloc: Allocator, index: Dep.Index) !void {
alloc.free(store.dependencies.items[index.get()].specifier);
if (Environment.isDebug) {
store.dependencies.items[index.get()] = undefined;
}
if (index.get() == (store.dependencies.items.len - 1)) {
store.dependencies.items.len -= 1;
} else {
try store.dependencies_free_list.append(alloc, index);
}
}
/// Expects dependency list to be already freed
pub fn freeEntry(store: *DirectoryWatchStore, alloc: Allocator, entry_index: usize) void {
const entry = store.watches.values()[entry_index];
debug.log("DirectoryWatchStore.freeEntry({d}, {f})", .{
entry_index,
entry.dir,
});
store.owner().bun_watcher.removeAtIndex(entry.watch_index, 0, &.{}, .file);
defer if (entry.dir_fd_owned) entry.dir.close();
alloc.free(store.watches.keys()[entry_index]);
store.watches.swapRemoveAt(entry_index);
if (store.watches.entries.len == 0) {
assert(store.dependencies.items.len == 0);
store.dependencies_free_list.clearRetainingCapacity();
}
}
fn appendDepAssumeCapacity(store: *DirectoryWatchStore, dep: Dep) Dep.Index {
if (store.dependencies_free_list.pop()) |index| {
store.dependencies.items[index.get()] = dep;
return index;
}
const index = Dep.Index.init(@intCast(store.dependencies.items.len));
store.dependencies.appendAssumeCapacity(dep);
return index;
}
pub const Entry = struct {
/// The directory handle the watch is placed on
dir: bun.FileDescriptor,
dir_fd_owned: bool,
/// Files which request this import index
first_dep: Dep.Index,
/// To pass to Watcher.remove
watch_index: u16,
};
pub const Dep = struct {
next: Index.Optional,
/// The file used
source_file_path: []const u8,
/// The specifier that failed. Before running re-build, it is resolved for, as
/// creating an unrelated file should not re-emit another error. Allocated memory
specifier: []u8,
pub const Index = bun.GenericIndex(u32, Dep);
};
const bun = @import("bun");
const Environment = bun.Environment;
const Watcher = bun.Watcher;
const assert = bun.assert;
const bake = bun.bake;
const DevServer = bake.DevServer;
const debug = DevServer.debug;
const std = @import("std");
const ArrayListUnmanaged = std.ArrayListUnmanaged;
const Allocator = std.mem.Allocator;