mirror of
https://github.com/oven-sh/bun
synced 2026-02-19 15:21:54 +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>
376 lines
12 KiB
Zig
376 lines
12 KiB
Zig
const log = bun.Output.scoped(.PipeSource, .hidden);
|
|
|
|
pub const Source = union(enum) {
|
|
pipe: *Pipe,
|
|
tty: *Tty,
|
|
file: *File,
|
|
sync_file: *File,
|
|
|
|
const Pipe = uv.Pipe;
|
|
const Tty = uv.uv_tty_t;
|
|
|
|
/// File source for async file I/O operations using libuv.
|
|
///
|
|
/// Manages a single `uv_fs_t` through a state machine that ensures:
|
|
/// - Only one operation uses the `fs` field at a time
|
|
/// - The `fs` is properly deinitialized before reuse
|
|
/// - Cancellation is only attempted when an operation is in-flight
|
|
///
|
|
/// Typical usage pattern:
|
|
/// 1. Check `canStart()` - returns true if ready for a new operation
|
|
/// 2. Call `prepare()` - marks fs as in-use
|
|
/// 3. Set up buffer and call `uv_fs_read()` or `uv_fs_write()`
|
|
/// 4. In callback, call `complete()` first to clean up
|
|
/// 5. Process the result
|
|
///
|
|
/// Cancellation:
|
|
/// - Call `stop()` to cancel an in-flight operation
|
|
/// - The callback will still fire with UV_ECANCELED
|
|
/// - Always call `complete()` in the callback regardless of cancellation
|
|
///
|
|
/// Cleanup:
|
|
/// - Call `detach()` if parent is destroyed before operation completes
|
|
/// - File will automatically close itself after the operation finishes
|
|
pub const File = struct {
|
|
/// The fs_t for I/O operations (reads/writes) and state-machine-managed closes.
|
|
/// State machine ensures this is only used for one operation at a time.
|
|
fs: uv.fs_t,
|
|
|
|
/// Buffer descriptor for the current read operation (unused by writers).
|
|
iov: uv.uv_buf_t,
|
|
|
|
/// The file descriptor.
|
|
file: uv.uv_file,
|
|
|
|
/// Current state of the fs_t request.
|
|
state: enum(u8) {
|
|
deinitialized, // fs.deinit() called, ready for next operation
|
|
operating, // read or write operation in progress
|
|
canceling, // cancel requested, waiting for callback
|
|
closing, // close operation in progress
|
|
} = .deinitialized,
|
|
|
|
/// When true, file will close itself when the current operation completes.
|
|
close_after_operation: bool = false,
|
|
|
|
/// Get the File struct from an fs_t pointer using field offset.
|
|
pub fn fromFS(fs: *uv.fs_t) *File {
|
|
return @fieldParentPtr("fs", fs);
|
|
}
|
|
|
|
/// Returns true if ready to start a new operation.
|
|
pub fn canStart(this: *const File) bool {
|
|
return this.state == .deinitialized and this.fs.data != null;
|
|
}
|
|
|
|
/// Mark the file as in-use for an operation.
|
|
/// Must only be called when canStart() returns true.
|
|
pub fn prepare(this: *File) void {
|
|
bun.assert(this.state == .deinitialized);
|
|
bun.assert(this.fs.data != null);
|
|
this.state = .operating;
|
|
this.close_after_operation = false;
|
|
}
|
|
|
|
/// Request cancellation of the current operation.
|
|
/// If successful, the callback will fire with UV_ECANCELED.
|
|
/// If cancel fails, the operation completes normally.
|
|
pub fn stop(this: *File) void {
|
|
if (this.state != .operating) return;
|
|
|
|
const cancel_result = uv.uv_cancel(@ptrCast(&this.fs));
|
|
if (cancel_result == 0) {
|
|
this.state = .canceling;
|
|
}
|
|
}
|
|
|
|
/// Detach from parent and schedule automatic cleanup.
|
|
/// If an operation is in progress, it will complete and then close the file.
|
|
/// If idle, closes the file immediately.
|
|
pub fn detach(this: *File) void {
|
|
this.fs.data = null;
|
|
this.close_after_operation = true;
|
|
this.stop();
|
|
|
|
if (this.state == .deinitialized) {
|
|
this.close_after_operation = false;
|
|
this.startClose();
|
|
}
|
|
}
|
|
|
|
/// Mark the operation as complete and clean up.
|
|
/// Must be called first in the callback before processing data.
|
|
pub fn complete(this: *File, was_canceled: bool) void {
|
|
bun.assert(this.state == .operating or this.state == .canceling);
|
|
if (was_canceled) {
|
|
bun.assert(this.state == .canceling);
|
|
}
|
|
|
|
this.fs.deinit();
|
|
this.state = .deinitialized;
|
|
|
|
if (this.close_after_operation) {
|
|
this.close_after_operation = false;
|
|
this.startClose();
|
|
}
|
|
}
|
|
|
|
fn startClose(this: *File) void {
|
|
bun.assert(this.state == .deinitialized);
|
|
this.state = .closing;
|
|
_ = uv.uv_fs_close(uv.Loop.get(), &this.fs, this.file, onCloseComplete);
|
|
}
|
|
|
|
fn onCloseComplete(fs: *uv.fs_t) callconv(.c) void {
|
|
const file = File.fromFS(fs);
|
|
bun.assert(file.state == .closing);
|
|
fs.deinit();
|
|
bun.default_allocator.destroy(file);
|
|
}
|
|
};
|
|
|
|
pub fn isClosed(this: Source) bool {
|
|
return switch (this) {
|
|
.pipe => |pipe| pipe.isClosed(),
|
|
.tty => |tty| tty.isClosed(),
|
|
.sync_file, .file => |file| file.file == -1,
|
|
};
|
|
}
|
|
|
|
pub fn isActive(this: Source) bool {
|
|
return switch (this) {
|
|
.pipe => |pipe| pipe.isActive(),
|
|
.tty => |tty| tty.isActive(),
|
|
.sync_file, .file => true,
|
|
};
|
|
}
|
|
|
|
pub fn getHandle(this: Source) *uv.Handle {
|
|
return switch (this) {
|
|
.pipe => @ptrCast(this.pipe),
|
|
.tty => @ptrCast(this.tty),
|
|
.sync_file, .file => unreachable,
|
|
};
|
|
}
|
|
pub fn toStream(this: Source) *uv.uv_stream_t {
|
|
return switch (this) {
|
|
.pipe => this.pipe.asStream(),
|
|
.tty => @ptrCast(this.tty),
|
|
.sync_file, .file => unreachable,
|
|
};
|
|
}
|
|
|
|
pub fn getFd(this: Source) bun.FileDescriptor {
|
|
return switch (this) {
|
|
.pipe => |pipe| pipe.fd(),
|
|
.tty => |tty| tty.fd(),
|
|
.sync_file, .file => |file| .fromUV(file.file),
|
|
};
|
|
}
|
|
|
|
pub fn setData(this: Source, data: ?*anyopaque) void {
|
|
switch (this) {
|
|
.pipe => |pipe| pipe.data = data,
|
|
.tty => |tty| tty.data = data,
|
|
.sync_file, .file => |file| file.fs.data = data,
|
|
}
|
|
}
|
|
|
|
pub fn getData(this: Source) ?*anyopaque {
|
|
return switch (this) {
|
|
.pipe => |pipe| pipe.data,
|
|
.tty => |tty| tty.data,
|
|
.sync_file, .file => |file| file.fs.data,
|
|
};
|
|
}
|
|
|
|
pub fn ref(this: Source) void {
|
|
switch (this) {
|
|
.pipe => this.pipe.ref(),
|
|
.tty => this.tty.ref(),
|
|
.sync_file, .file => return,
|
|
}
|
|
}
|
|
|
|
pub fn unref(this: Source) void {
|
|
switch (this) {
|
|
.pipe => this.pipe.unref(),
|
|
.tty => this.tty.unref(),
|
|
.sync_file, .file => return,
|
|
}
|
|
}
|
|
|
|
pub fn hasRef(this: Source) bool {
|
|
return switch (this) {
|
|
.pipe => |pipe| pipe.hasRef(),
|
|
.tty => |tty| tty.hasRef(),
|
|
.sync_file, .file => false,
|
|
};
|
|
}
|
|
|
|
pub fn openPipe(loop: *uv.Loop, fd: bun.FileDescriptor) bun.sys.Maybe(*Source.Pipe) {
|
|
log("openPipe (fd = {f})", .{fd});
|
|
const pipe = bun.default_allocator.create(Source.Pipe) catch |err| bun.handleOom(err);
|
|
// we should never init using IPC here see ipc.zig
|
|
switch (pipe.init(loop, false)) {
|
|
.err => |err| {
|
|
bun.default_allocator.destroy(pipe);
|
|
return .{ .err = err };
|
|
},
|
|
else => {},
|
|
}
|
|
|
|
switch (pipe.open(fd)) {
|
|
.err => |err| {
|
|
bun.default_allocator.destroy(pipe);
|
|
return .{
|
|
.err = err,
|
|
};
|
|
},
|
|
.result => {},
|
|
}
|
|
|
|
return .{ .result = pipe };
|
|
}
|
|
|
|
pub const StdinTTY = struct {
|
|
var data: uv.uv_tty_t = undefined;
|
|
var lock: bun.Mutex = .{};
|
|
var initialized = std.atomic.Value(bool).init(false);
|
|
const value: *uv.uv_tty_t = &data;
|
|
|
|
pub fn isStdinTTY(tty: *const Source.Tty) bool {
|
|
return tty == StdinTTY.value;
|
|
}
|
|
|
|
fn getStdinTTY(loop: *uv.Loop) bun.sys.Maybe(*Source.Tty) {
|
|
StdinTTY.lock.lock();
|
|
defer StdinTTY.lock.unlock();
|
|
|
|
if (StdinTTY.initialized.swap(true, .monotonic) == false) {
|
|
const rc = uv.uv_tty_init(loop, StdinTTY.value, 0, 0);
|
|
if (rc.toError(.open)) |err| {
|
|
StdinTTY.initialized.store(false, .monotonic);
|
|
return .{ .err = err };
|
|
}
|
|
}
|
|
|
|
return .{ .result = StdinTTY.value };
|
|
}
|
|
};
|
|
|
|
pub fn openTty(loop: *uv.Loop, fd: bun.FileDescriptor) bun.sys.Maybe(*Source.Tty) {
|
|
log("openTTY (fd = {f})", .{fd});
|
|
|
|
const uv_fd = fd.uv();
|
|
|
|
if (uv_fd == 0) {
|
|
return StdinTTY.getStdinTTY(loop);
|
|
}
|
|
|
|
const tty = bun.default_allocator.create(Source.Tty) catch |err| bun.handleOom(err);
|
|
switch (tty.init(loop, uv_fd)) {
|
|
.err => |err| {
|
|
bun.default_allocator.destroy(tty);
|
|
return .{ .err = err };
|
|
},
|
|
.result => {},
|
|
}
|
|
|
|
return .{ .result = tty };
|
|
}
|
|
|
|
pub fn openFile(fd: bun.FileDescriptor) *Source.File {
|
|
bun.assert(fd.isValid() and fd.uv() != -1);
|
|
log("openFile (fd = {f})", .{fd});
|
|
const file = bun.handleOom(bun.default_allocator.create(Source.File));
|
|
|
|
file.* = std.mem.zeroes(Source.File);
|
|
file.file = fd.uv();
|
|
return file;
|
|
}
|
|
|
|
pub fn open(loop: *uv.Loop, fd: bun.FileDescriptor) bun.sys.Maybe(Source) {
|
|
const rc = bun.windows.libuv.uv_guess_handle(fd.uv());
|
|
log("open(fd: {f}, type: {s})", .{ fd, @tagName(rc) });
|
|
|
|
switch (rc) {
|
|
.named_pipe => {
|
|
switch (openPipe(loop, fd)) {
|
|
.result => |pipe| return .{ .result = .{ .pipe = pipe } },
|
|
.err => |err| return .{ .err = err },
|
|
}
|
|
},
|
|
.tty => {
|
|
switch (openTty(loop, fd)) {
|
|
.result => |tty| return .{ .result = .{ .tty = tty } },
|
|
.err => |err| return .{ .err = err },
|
|
}
|
|
},
|
|
.file => {
|
|
return .{
|
|
.result = .{
|
|
.file = openFile(fd),
|
|
},
|
|
};
|
|
},
|
|
else => {
|
|
const errno = bun.windows.getLastErrno();
|
|
|
|
if (errno == .SUCCESS) {
|
|
return .{
|
|
.result = .{
|
|
.file = openFile(fd),
|
|
},
|
|
};
|
|
}
|
|
|
|
return .{ .err = bun.sys.Error.fromCode(errno, .open) };
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn setRawMode(this: Source, value: bool) bun.sys.Maybe(void) {
|
|
return switch (this) {
|
|
.tty => |tty| {
|
|
if (tty
|
|
.setMode(if (value) .raw else .normal)
|
|
.toError(.uv_tty_set_mode)) |err|
|
|
{
|
|
return .{ .err = err };
|
|
} else {
|
|
return .success;
|
|
}
|
|
},
|
|
else => .{
|
|
.err = .{
|
|
.errno = @intFromEnum(bun.sys.E.NOTSUP),
|
|
.syscall = .uv_tty_set_mode,
|
|
.fd = this.getFd(),
|
|
},
|
|
},
|
|
};
|
|
}
|
|
};
|
|
|
|
export fn Source__setRawModeStdin(raw: bool) c_int {
|
|
const tty = switch (Source.openTty(bun.jsc.VirtualMachine.get().uvLoop(), .stdin())) {
|
|
.result => |tty| tty,
|
|
.err => |e| return e.errno,
|
|
};
|
|
// UV_TTY_MODE_RAW_VT is a variant of UV_TTY_MODE_RAW that enables control
|
|
// sequence processing on the TTY implementer side, rather than having libuv
|
|
// translate keypress events into control sequences, aligning behavior more
|
|
// closely with POSIX platforms. This is also required to support some
|
|
// control sequences at all on Windows, such as bracketed paste mode. The
|
|
// Node.js readline implementation handles differences between these modes.
|
|
if (tty.setMode(if (raw) .vt else .normal).toError(.uv_tty_set_mode)) |err| {
|
|
return err.errno;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
const bun = @import("bun");
|
|
const std = @import("std");
|
|
const uv = bun.windows.libuv;
|