mirror of
https://github.com/oven-sh/bun
synced 2026-02-14 21:01:52 +00:00
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>
453 lines
14 KiB
Zig
453 lines
14 KiB
Zig
//! This is a similar API to std.fs.File, except it:
|
|
//! - Preserves errors from the operating system
|
|
//! - Supports normalizing BOM to UTF-8
|
|
//! - Has several optimizations somewhat specific to Bun
|
|
//! - Potentially goes through libuv on Windows
|
|
//! - Does not use unreachable in system calls.
|
|
|
|
const File = @This();
|
|
|
|
// "handle" matches std.fs.File
|
|
handle: bun.FileDescriptor,
|
|
|
|
pub fn openat(dir: bun.FileDescriptor, path: [:0]const u8, flags: i32, mode: bun.Mode) Maybe(File) {
|
|
return switch (sys.openat(dir, path, flags, mode)) {
|
|
.result => |fd| .{ .result = .{ .handle = fd } },
|
|
.err => |err| .{ .err = err },
|
|
};
|
|
}
|
|
|
|
pub fn open(path: [:0]const u8, flags: i32, mode: bun.Mode) Maybe(File) {
|
|
return File.openat(bun.FD.cwd(), path, flags, mode);
|
|
}
|
|
|
|
pub fn makeOpen(path: [:0]const u8, flags: i32, mode: bun.Mode) Maybe(File) {
|
|
return File.makeOpenat(bun.FD.cwd(), path, flags, mode);
|
|
}
|
|
|
|
pub fn makeOpenat(other: bun.FD, path: [:0]const u8, flags: i32, mode: bun.Mode) Maybe(File) {
|
|
const fd = switch (sys.openat(other, path, flags, mode)) {
|
|
.result => |fd| fd,
|
|
.err => |err| fd: {
|
|
if (std.fs.path.dirname(path)) |dir_path| {
|
|
bun.makePath(other.stdDir(), dir_path) catch {};
|
|
break :fd switch (sys.openat(other, path, flags, mode)) {
|
|
.result => |fd| fd,
|
|
.err => |err2| return .{ .err = err2 },
|
|
};
|
|
}
|
|
|
|
return .{ .err = err };
|
|
},
|
|
};
|
|
|
|
return .{ .result = .{ .handle = fd } };
|
|
}
|
|
|
|
pub fn openatOSPath(other: bun.FD, path: bun.OSPathSliceZ, flags: i32, mode: bun.Mode) Maybe(File) {
|
|
return switch (sys.openatOSPath(other, path, flags, mode)) {
|
|
.result => |fd| .{ .result = .{ .handle = fd } },
|
|
.err => |err| .{ .err = err },
|
|
};
|
|
}
|
|
|
|
pub fn from(other: anytype) File {
|
|
const T = @TypeOf(other);
|
|
|
|
if (T == File) {
|
|
return other;
|
|
}
|
|
|
|
if (T == std.posix.fd_t) {
|
|
return .{ .handle = .fromNative(other) };
|
|
}
|
|
|
|
if (T == bun.FileDescriptor) {
|
|
return .{ .handle = other };
|
|
}
|
|
|
|
if (T == std.fs.File) {
|
|
return .{ .handle = .fromStdFile(other) };
|
|
}
|
|
|
|
if (T == std.fs.Dir) {
|
|
return File{ .handle = .fromStdDir(other) };
|
|
}
|
|
|
|
if (comptime Environment.isLinux) {
|
|
if (T == u64) {
|
|
return File{ .handle = .fromNative(@intCast(other)) };
|
|
}
|
|
}
|
|
|
|
@compileError("Unsupported type " ++ bun.meta.typeName(T));
|
|
}
|
|
|
|
pub fn write(self: File, buf: []const u8) Maybe(usize) {
|
|
return sys.write(self.handle, buf);
|
|
}
|
|
|
|
pub fn read(self: File, buf: []u8) Maybe(usize) {
|
|
return sys.read(self.handle, buf);
|
|
}
|
|
|
|
pub fn readAll(self: File, buf: []u8) Maybe(usize) {
|
|
return sys.readAll(self.handle, buf);
|
|
}
|
|
|
|
pub fn writeAll(self: File, buf: []const u8) Maybe(void) {
|
|
var remain = buf;
|
|
while (remain.len > 0) {
|
|
const rc = sys.write(self.handle, remain);
|
|
switch (rc) {
|
|
.err => |err| return .{ .err = err },
|
|
.result => |amt| {
|
|
if (amt == 0) {
|
|
return .success;
|
|
}
|
|
remain = remain[amt..];
|
|
},
|
|
}
|
|
}
|
|
|
|
return .success;
|
|
}
|
|
|
|
pub fn writeFile(
|
|
relative_dir_or_cwd: anytype,
|
|
path: bun.OSPathSliceZ,
|
|
data: []const u8,
|
|
) Maybe(void) {
|
|
const file = switch (File.openatOSPath(relative_dir_or_cwd, path, bun.O.WRONLY | bun.O.CREAT | bun.O.TRUNC, 0o664)) {
|
|
.err => |err| return .{ .err = err },
|
|
.result => |fd| fd,
|
|
};
|
|
defer file.close();
|
|
switch (file.writeAll(data)) {
|
|
.err => |err| return .{ .err = err },
|
|
.result => {},
|
|
}
|
|
return .success;
|
|
}
|
|
|
|
pub const ReadError = anyerror;
|
|
|
|
pub fn closeAndMoveTo(this: File, src: [:0]const u8, dest: [:0]const u8) !void {
|
|
// On POSIX, close the file after moving it.
|
|
defer if (Environment.isPosix) this.close();
|
|
// On Windows, close the file before moving it.
|
|
if (Environment.isWindows) this.close();
|
|
const cwd = bun.FD.cwd();
|
|
try bun.sys.moveFileZWithHandle(this.handle, cwd, src, cwd, dest);
|
|
}
|
|
|
|
fn stdIoRead(this: File, buf: []u8) ReadError!usize {
|
|
return try this.read(buf).unwrap();
|
|
}
|
|
|
|
pub const Reader = std.Io.GenericReader(File, anyerror, stdIoRead);
|
|
|
|
pub fn reader(self: File) Reader {
|
|
return Reader{ .context = self };
|
|
}
|
|
|
|
pub const WriteError = anyerror;
|
|
fn stdIoWrite(this: File, bytes: []const u8) WriteError!usize {
|
|
try this.writeAll(bytes).unwrap();
|
|
|
|
return bytes.len;
|
|
}
|
|
|
|
fn stdIoWriteQuietDebug(this: File, bytes: []const u8) WriteError!usize {
|
|
bun.Output.disableScopedDebugWriter();
|
|
defer bun.Output.enableScopedDebugWriter();
|
|
try this.writeAll(bytes).unwrap();
|
|
|
|
return bytes.len;
|
|
}
|
|
|
|
pub const Writer = std.Io.GenericWriter(File, anyerror, stdIoWrite);
|
|
pub const QuietWriter = if (Environment.isDebug) std.Io.GenericWriter(File, anyerror, stdIoWriteQuietDebug) else Writer;
|
|
|
|
pub fn writer(self: File) Writer {
|
|
return Writer{ .context = self };
|
|
}
|
|
|
|
pub fn quietWriter(self: File) QuietWriter {
|
|
return QuietWriter{ .context = self };
|
|
}
|
|
|
|
pub fn isTty(self: File) bool {
|
|
return std.posix.isatty(self.handle.cast());
|
|
}
|
|
|
|
/// Asserts in debug that this File object is valid
|
|
pub fn close(self: File) void {
|
|
self.handle.close();
|
|
}
|
|
|
|
pub fn getEndPos(self: File) Maybe(usize) {
|
|
return getFileSize(self.handle);
|
|
}
|
|
|
|
pub fn stat(self: File) Maybe(bun.Stat) {
|
|
return fstat(self.handle);
|
|
}
|
|
|
|
/// Be careful about using this on Linux or macOS.
|
|
///
|
|
/// File calls stat() internally.
|
|
pub fn kind(self: File) Maybe(std.fs.File.Kind) {
|
|
if (Environment.isWindows) {
|
|
const rt = windows.GetFileType(self.handle.cast());
|
|
if (rt == windows.FILE_TYPE_UNKNOWN) {
|
|
switch (windows.GetLastError()) {
|
|
.SUCCESS => {},
|
|
else => |err| {
|
|
return .{ .err = Error.fromCode((SystemErrno.init(err) orelse SystemErrno.EUNKNOWN).toE(), .fstat) };
|
|
},
|
|
}
|
|
}
|
|
|
|
return .{
|
|
.result = switch (rt) {
|
|
windows.FILE_TYPE_CHAR => .character_device,
|
|
windows.FILE_TYPE_REMOTE, windows.FILE_TYPE_DISK => .file,
|
|
windows.FILE_TYPE_PIPE => .named_pipe,
|
|
windows.FILE_TYPE_UNKNOWN => .unknown,
|
|
else => .file,
|
|
},
|
|
};
|
|
}
|
|
|
|
const st = switch (self.stat()) {
|
|
.err => |err| return .{ .err = err },
|
|
.result => |s| s,
|
|
};
|
|
|
|
const m = st.mode & posix.S.IFMT;
|
|
switch (m) {
|
|
posix.S.IFBLK => return .{ .result = .block_device },
|
|
posix.S.IFCHR => return .{ .result = .character_device },
|
|
posix.S.IFDIR => return .{ .result = .directory },
|
|
posix.S.IFIFO => return .{ .result = .named_pipe },
|
|
posix.S.IFLNK => return .{ .result = .sym_link },
|
|
posix.S.IFREG => return .{ .result = .file },
|
|
posix.S.IFSOCK => return .{ .result = .unix_domain_socket },
|
|
else => {
|
|
return .{ .result = .file };
|
|
},
|
|
}
|
|
}
|
|
|
|
pub const ReadToEndResult = struct {
|
|
bytes: std.array_list.Managed(u8) = std.array_list.Managed(u8).init(default_allocator),
|
|
err: ?Error = null,
|
|
|
|
pub fn unwrap(self: *const ReadToEndResult) ![]u8 {
|
|
if (self.err) |err| {
|
|
try (bun.sys.Maybe(void){ .err = err }).unwrap();
|
|
}
|
|
return self.bytes.items;
|
|
}
|
|
};
|
|
|
|
pub fn readFillBuf(this: File, buf: []u8) Maybe([]u8) {
|
|
var read_amount: usize = 0;
|
|
while (read_amount < buf.len) {
|
|
switch (if (comptime Environment.isPosix)
|
|
pread(this.handle, buf[read_amount..], @intCast(read_amount))
|
|
else
|
|
sys.read(this.handle, buf[read_amount..])) {
|
|
.err => |err| {
|
|
return .{ .err = err };
|
|
},
|
|
.result => |bytes_read| {
|
|
if (bytes_read == 0) {
|
|
break;
|
|
}
|
|
|
|
read_amount += bytes_read;
|
|
},
|
|
}
|
|
}
|
|
|
|
return .{ .result = buf[0..read_amount] };
|
|
}
|
|
|
|
pub fn readToEndWithArrayList(this: File, list: *std.array_list.Managed(u8), size_guess: enum { probably_small, unknown_size }) Maybe(usize) {
|
|
if (size_guess == .probably_small) {
|
|
bun.handleOom(list.ensureUnusedCapacity(64));
|
|
} else {
|
|
list.ensureTotalCapacityPrecise(
|
|
switch (this.getEndPos()) {
|
|
.err => |err| {
|
|
return .{ .err = err };
|
|
},
|
|
.result => |s| s,
|
|
} + 16,
|
|
) catch |err| bun.handleOom(err);
|
|
}
|
|
|
|
var total: i64 = 0;
|
|
while (true) {
|
|
if (list.unusedCapacitySlice().len == 0) {
|
|
bun.handleOom(list.ensureUnusedCapacity(16));
|
|
}
|
|
|
|
switch (if (comptime Environment.isPosix)
|
|
pread(this.handle, list.unusedCapacitySlice(), total)
|
|
else
|
|
sys.read(this.handle, list.unusedCapacitySlice())) {
|
|
.err => |err| {
|
|
return .{ .err = err };
|
|
},
|
|
.result => |bytes_read| {
|
|
if (bytes_read == 0) {
|
|
break;
|
|
}
|
|
|
|
list.items.len += bytes_read;
|
|
total += @intCast(bytes_read);
|
|
},
|
|
}
|
|
}
|
|
|
|
return .{ .result = @intCast(total) };
|
|
}
|
|
|
|
/// Use this function on potentially large files.
|
|
/// Calls fstat() on the file to get the size of the file and avoids reallocations + extra read() calls.
|
|
pub fn readToEnd(this: File, allocator: std.mem.Allocator) ReadToEndResult {
|
|
var list = std.array_list.Managed(u8).init(allocator);
|
|
return switch (readToEndWithArrayList(this, &list, .unknown_size)) {
|
|
.err => |err| .{ .err = err, .bytes = list },
|
|
.result => .{ .err = null, .bytes = list },
|
|
};
|
|
}
|
|
|
|
/// Use this function on small files <= 1024 bytes.
|
|
/// File will skip the fstat() call, preallocating 64 bytes instead of the file's size.
|
|
pub fn readToEndSmall(this: File, allocator: std.mem.Allocator) ReadToEndResult {
|
|
var list = std.array_list.Managed(u8).init(allocator);
|
|
return switch (readToEndWithArrayList(this, &list, .probably_small)) {
|
|
.err => |err| .{ .err = err, .bytes = list },
|
|
.result => .{ .err = null, .bytes = list },
|
|
};
|
|
}
|
|
|
|
pub fn getPath(this: File, out_buffer: *bun.PathBuffer) Maybe([]u8) {
|
|
return getFdPath(this.handle, out_buffer);
|
|
}
|
|
|
|
/// 1. Normalize the file path
|
|
/// 2. Open a file for reading
|
|
/// 2. Read the file to a buffer
|
|
/// 3. Return the File handle and the buffer
|
|
pub fn readFromUserInput(dir_fd: anytype, input_path: anytype, allocator: std.mem.Allocator) Maybe([]u8) {
|
|
var buf: bun.PathBuffer = undefined;
|
|
const normalized = bun.path.joinAbsStringBufZ(
|
|
bun.fs.FileSystem.instance.top_level_dir,
|
|
&buf,
|
|
&.{input_path},
|
|
.loose,
|
|
);
|
|
return readFrom(dir_fd, normalized, allocator);
|
|
}
|
|
|
|
/// 1. Open a file for reading
|
|
/// 2. Read the file to a buffer
|
|
/// 3. Return the File handle and the buffer
|
|
pub fn readFileFrom(dir_fd: anytype, path: anytype, allocator: std.mem.Allocator) Maybe(struct { File, []u8 }) {
|
|
const ElementType = std.meta.Elem(@TypeOf(path));
|
|
|
|
const rc = brk: {
|
|
if (comptime Environment.isWindows and ElementType == u16) {
|
|
break :brk openatWindowsTMaybeNormalize(u16, from(dir_fd).handle, path, O.RDONLY, false);
|
|
}
|
|
|
|
if (comptime ElementType == u8 and std.meta.sentinel(@TypeOf(path)) == null) {
|
|
break :brk sys.openatA(from(dir_fd).handle, path, O.RDONLY, 0);
|
|
}
|
|
|
|
break :brk sys.openat(from(dir_fd).handle, path, O.RDONLY, 0);
|
|
};
|
|
|
|
const this = switch (rc) {
|
|
.err => |err| return .{ .err = err },
|
|
.result => |fd| from(fd),
|
|
};
|
|
|
|
var result = this.readToEnd(allocator);
|
|
|
|
if (result.err) |err| {
|
|
this.close();
|
|
result.bytes.deinit();
|
|
return .{ .err = err };
|
|
}
|
|
|
|
if (result.bytes.items.len == 0) {
|
|
// Don't allocate an empty string.
|
|
// We won't be modifying an empty slice, anyway.
|
|
return .{ .result = .{ this, @ptrCast(@constCast("")) } };
|
|
}
|
|
|
|
return .{ .result = .{ this, result.bytes.items } };
|
|
}
|
|
|
|
/// 1. Open a file for reading relative to a directory
|
|
/// 2. Read the file to a buffer
|
|
/// 3. Close the file
|
|
/// 4. Return the buffer
|
|
pub fn readFrom(dir_fd: anytype, path: anytype, allocator: std.mem.Allocator) Maybe([]u8) {
|
|
const file, const bytes = switch (readFileFrom(dir_fd, path, allocator)) {
|
|
.err => |err| return .{ .err = err },
|
|
.result => |result| result,
|
|
};
|
|
|
|
file.close();
|
|
return .{ .result = bytes };
|
|
}
|
|
|
|
const ToSourceOptions = struct {
|
|
convert_bom: bool = false,
|
|
};
|
|
|
|
pub fn toSourceAt(dir_fd: anytype, path: anytype, allocator: std.mem.Allocator, opts: ToSourceOptions) Maybe(bun.logger.Source) {
|
|
var bytes = switch (readFrom(dir_fd, path, allocator)) {
|
|
.err => |err| return .{ .err = err },
|
|
.result => |bytes| bytes,
|
|
};
|
|
|
|
if (opts.convert_bom) {
|
|
if (bun.strings.BOM.detect(bytes)) |bom| {
|
|
bytes = bun.handleOom(bom.removeAndConvertToUTF8AndFree(allocator, bytes));
|
|
}
|
|
}
|
|
|
|
return .{ .result = bun.logger.Source.initPathString(path, bytes) };
|
|
}
|
|
|
|
pub fn toSource(path: anytype, allocator: std.mem.Allocator, opts: ToSourceOptions) Maybe(bun.logger.Source) {
|
|
return toSourceAt(std.fs.cwd(), path, allocator, opts);
|
|
}
|
|
|
|
const bun = @import("bun");
|
|
const Environment = bun.Environment;
|
|
const default_allocator = bun.default_allocator;
|
|
const windows = bun.windows;
|
|
|
|
const sys = bun.sys;
|
|
const Error = bun.sys.Error;
|
|
const Maybe = bun.sys.Maybe;
|
|
const O = sys.O;
|
|
const SystemErrno = bun.sys.SystemErrno;
|
|
const fstat = sys.fstat;
|
|
const getFdPath = sys.getFdPath;
|
|
const getFileSize = sys.getFileSize;
|
|
const openatWindowsTMaybeNormalize = sys.openatWindowsTMaybeNormalize;
|
|
const pread = sys.pread;
|
|
|
|
const std = @import("std");
|
|
const posix = std.posix;
|