-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(fs): WriteStream
pending write fastpath
#16856
Merged
+7,037
−247
Merged
Changes from 15 commits
Commits
Show all changes
41 commits
Select commit
Hold shift + click to select a range
6ef67d3
dont write out of order
dylan-conway dc04802
update
dylan-conway 21a4816
update
dylan-conway ce66af7
fixup
dylan-conway b8ff702
one more
dylan-conway 135c13b
auto flush
dylan-conway 07feacb
progress
dylan-conway 9086433
buffered stream
dylan-conway 08e8588
missing onWrite
dylan-conway e80764b
Merge branch 'main' into dylan/fix-pending-write
dylan-conway 991871a
fix windows zig build
dylan-conway 3ff9aa2
update ref
dylan-conway 1242201
clear capacity
dylan-conway a21c44c
test
dylan-conway 5c13864
fix windows build
dylan-conway 7dce37a
Update src/bun.js/webcore/streams.zig
dylan-conway d33699b
runPending update
dylan-conway 07b1dfb
mini eventloop
dylan-conway 17b5d04
stderr
dylan-conway 6c1520c
update
dylan-conway d34465b
Don't spin loop
Jarred-Sumner a707cd9
Update sys.zig
Jarred-Sumner 3aa413c
Update streams.zig
Jarred-Sumner 4040d9e
fix
Jarred-Sumner 11e477c
Update sys.zig
Jarred-Sumner 3d2bdcf
Update sys.zig
Jarred-Sumner ee815bd
Match node's behavior with force sync
Jarred-Sumner 0f410dd
Update streams.zig
Jarred-Sumner d561ea3
Ensure we actually set blocking
Jarred-Sumner 1a6181b
Workaround for zig std lib decision
Jarred-Sumner b397a20
Update PipeWriter.zig
Jarred-Sumner 0f36adb
Delete the function that is labeled as not being correct
Jarred-Sumner 4309c75
Update tty.ts
Jarred-Sumner c87cb34
update
dylan-conway 5401b5f
update
dylan-conway f9d5d91
write fast
dylan-conway 6bef911
oops
dylan-conway 5690100
update
dylan-conway f6b96ce
Merge branch 'main' into dylan/fix-pending-write
dylan-conway fe2ae42
block
dylan-conway 2de9cc2
fix it!!
dylan-conway File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1563,7 +1563,7 @@ pub const ArrayBufferSink = struct { | |
pub const JSSink = NewJSSink(@This(), "ArrayBufferSink"); | ||
}; | ||
|
||
const AutoFlusher = struct { | ||
pub const AutoFlusher = struct { | ||
registered: bool = false, | ||
|
||
pub fn registerDeferredMicrotaskWithType(comptime Type: type, this: *Type, vm: *JSC.VirtualMachine) void { | ||
|
@@ -2894,7 +2894,8 @@ pub const NetworkSink = struct { | |
return .{ .owned = len }; | ||
} | ||
|
||
this.buffer.writeLatin1(bytes) catch { | ||
const check_ascii = false; | ||
this.buffer.writeLatin1(bytes, check_ascii) catch { | ||
return .{ .err = Syscall.Error.fromCode(.NOMEM, .write) }; | ||
}; | ||
|
||
|
@@ -2905,15 +2906,18 @@ pub const NetworkSink = struct { | |
} else if (this.buffer.size() + len >= this.getHighWaterMark()) { | ||
// kinda fast path: | ||
// - combined chunk is large enough to flush automatically | ||
this.buffer.writeLatin1(bytes) catch { | ||
|
||
const check_ascii = true; | ||
this.buffer.writeLatin1(bytes, check_ascii) catch { | ||
return .{ .err = Syscall.Error.fromCode(.NOMEM, .write) }; | ||
}; | ||
_ = this.internalFlush() catch { | ||
return .{ .err = Syscall.Error.fromCode(.NOMEM, .write) }; | ||
}; | ||
return .{ .owned = len }; | ||
} else { | ||
this.buffer.writeLatin1(bytes) catch { | ||
const check_ascii = true; | ||
this.buffer.writeLatin1(bytes, check_ascii) catch { | ||
return .{ .err = Syscall.Error.fromCode(.NOMEM, .write) }; | ||
}; | ||
} | ||
|
@@ -3421,10 +3425,12 @@ pub const FileSink = struct { | |
// we should not duplicate these fields... | ||
pollable: bool = false, | ||
nonblocking: bool = false, | ||
force_sync_on_windows: bool = false, | ||
force_sync: bool = false, | ||
|
||
is_socket: bool = false, | ||
fd: bun.FileDescriptor = bun.invalid_fd, | ||
has_js_called_unref: bool = false, | ||
|
||
auto_flusher: AutoFlusher = .{}, | ||
|
||
const log = Output.scoped(.FileSink, false); | ||
|
||
|
@@ -3438,17 +3444,16 @@ pub const FileSink = struct { | |
return this.writer.memoryCost(); | ||
} | ||
|
||
fn Bun__ForceFileSinkToBeSynchronousOnWindows(globalObject: *JSC.JSGlobalObject, jsvalue: JSC.JSValue) callconv(.C) void { | ||
comptime bun.assert(Environment.isWindows); | ||
|
||
fn Bun__ForceFileSinkToBeSynchronousForProcessObjectStdio(globalObject: *JSC.JSGlobalObject, jsvalue: JSC.JSValue) callconv(.C) void { | ||
var this: *FileSink = @alignCast(@ptrCast(JSSink.fromJS(globalObject, jsvalue) orelse return)); | ||
this.force_sync_on_windows = true; | ||
this.force_sync = true; | ||
if (comptime !Environment.isWindows) { | ||
this.writer.force_sync = true; | ||
} | ||
} | ||
|
||
comptime { | ||
if (Environment.isWindows) { | ||
@export(Bun__ForceFileSinkToBeSynchronousOnWindows, .{ .name = "Bun__ForceFileSinkToBeSynchronousOnWindows" }); | ||
} | ||
@export(Bun__ForceFileSinkToBeSynchronousForProcessObjectStdio, .{ .name = "Bun__ForceFileSinkToBeSynchronousForProcessObjectStdio" }); | ||
} | ||
|
||
pub fn onAttachedProcessExit(this: *FileSink) void { | ||
|
@@ -3488,6 +3493,10 @@ pub const FileSink = struct { | |
// If there's no pending write, no need to keep the event loop ref'd. | ||
this.writer.updateRef(this.eventLoop(), has_pending_data); | ||
|
||
if (has_pending_data) { | ||
AutoFlusher.registerDeferredMicrotaskWithType(@This(), this, JSC.VirtualMachine.get()); | ||
} | ||
|
||
// if we are not done yet and has pending data we just wait so we do not runPending twice | ||
if (status == .pending and has_pending_data) { | ||
if (this.pending.state == .pending) { | ||
|
@@ -3589,22 +3598,36 @@ pub const FileSink = struct { | |
pub fn setup(this: *FileSink, options: *const StreamStart.FileSinkOptions) JSC.Maybe(void) { | ||
// TODO: this should be concurrent. | ||
var isatty: ?bool = null; | ||
var is_nonblocking_tty = false; | ||
var is_nonblocking = false; | ||
const fd = switch (switch (options.input_path) { | ||
.path => |path| bun.sys.openA(path.slice(), options.flags(), options.mode), | ||
.path => |path| brk: { | ||
is_nonblocking = true; | ||
break :brk bun.sys.openA(path.slice(), options.flags(), options.mode); | ||
}, | ||
.fd => |fd_| brk: { | ||
if (comptime Environment.isPosix and FeatureFlags.nonblocking_stdout_and_stderr_on_posix) { | ||
if (bun.FDTag.get(fd_) != .none) { | ||
const rc = bun.C.open_as_nonblocking_tty(@intCast(fd_.cast()), bun.O.WRONLY); | ||
if (rc > -1) { | ||
isatty = true; | ||
is_nonblocking_tty = true; | ||
is_nonblocking = true; | ||
break :brk JSC.Maybe(bun.FileDescriptor){ .result = bun.toFD(rc) }; | ||
} | ||
} | ||
} | ||
|
||
break :brk bun.sys.dupWithFlags(fd_, if (bun.FDTag.get(fd_) == .none and !this.force_sync_on_windows) bun.O.NONBLOCK else 0); | ||
const duped = bun.sys.dupWithFlags(fd_, 0); | ||
|
||
if (comptime Environment.isPosix) { | ||
if (bun.FDTag.get(fd_) == .none and !this.force_sync and duped == .result) { | ||
is_nonblocking = switch (bun.sys.getFcntlFlags(duped.result)) { | ||
.result => |flags| (flags & bun.O.NONBLOCK) != 0, | ||
.err => false, | ||
}; | ||
} | ||
} | ||
|
||
break :brk duped; | ||
}, | ||
}) { | ||
.err => |err| return .{ .err = err }, | ||
|
@@ -3630,21 +3653,21 @@ pub const FileSink = struct { | |
|
||
this.fd = fd; | ||
this.is_socket = std.posix.S.ISSOCK(stat.mode); | ||
this.nonblocking = is_nonblocking_tty or (this.pollable and switch (options.input_path) { | ||
this.nonblocking = is_nonblocking and this.pollable and switch (options.input_path) { | ||
.path => true, | ||
.fd => |fd_| bun.FDTag.get(fd_) == .none, | ||
}); | ||
}; | ||
}, | ||
} | ||
} else if (comptime Environment.isWindows) { | ||
this.pollable = (bun.windows.GetFileType(fd.cast()) & bun.windows.FILE_TYPE_PIPE) != 0 and !this.force_sync_on_windows; | ||
this.pollable = (bun.windows.GetFileType(fd.cast()) & bun.windows.FILE_TYPE_PIPE) != 0 and !this.force_sync; | ||
this.fd = fd; | ||
} else { | ||
@compileError("TODO: implement for this platform"); | ||
} | ||
|
||
if (comptime Environment.isWindows) { | ||
if (this.force_sync_on_windows) { | ||
if (this.force_sync) { | ||
switch (this.writer.startSync( | ||
fd, | ||
this.pollable, | ||
|
@@ -3721,6 +3744,33 @@ pub const FileSink = struct { | |
return .{ .result = {} }; | ||
} | ||
|
||
pub fn onAutoFlush(this: *FileSink) bool { | ||
if (this.done or !this.writer.hasPendingData()) { | ||
this.updateRef(false); | ||
this.auto_flusher.registered = false; | ||
return false; | ||
} | ||
|
||
const amt = switch (this.writer.flush()) { | ||
.done, .wrote, .pending => |amt| amt, | ||
else => 0, | ||
}; | ||
|
||
if (amt == 0) { | ||
this.updateRef(false); | ||
} else { | ||
this.runPending(); | ||
} | ||
|
||
if (!this.writer.hasPendingData()) { | ||
this.updateRef(false); | ||
this.auto_flusher.registered = false; | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
pub fn flush(_: *FileSink) JSC.Maybe(void) { | ||
return .{ .result = {} }; | ||
} | ||
|
@@ -3841,6 +3891,7 @@ pub const FileSink = struct { | |
pub fn deinit(this: *FileSink) void { | ||
this.pending.deinit(); | ||
this.writer.deinit(); | ||
AutoFlusher.unregisterDeferredMicrotaskWithType(@This(), this, JSC.VirtualMachine.get()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This needs to not run when it's the shell |
||
} | ||
|
||
pub fn toJS(this: *FileSink, globalThis: *JSGlobalObject) JSValue { | ||
|
@@ -3892,7 +3943,6 @@ pub const FileSink = struct { | |
} | ||
|
||
pub fn updateRef(this: *FileSink, value: bool) void { | ||
this.has_js_called_unref = !value; | ||
if (value) { | ||
this.writer.enableKeepingProcessAlive(this.event_loop_handle); | ||
} else { | ||
|
@@ -3961,7 +4011,6 @@ pub const FileReader = struct { | |
buffered: std.ArrayListUnmanaged(u8) = .{}, | ||
read_inside_on_pull: ReadDuringJSOnPullResult = .{ .none = {} }, | ||
highwater_mark: usize = 16384, | ||
has_js_called_unref: bool = false, | ||
|
||
pub const IOReader = bun.io.BufferedReader; | ||
pub const Poll = IOReader; | ||
|
@@ -3989,37 +4038,43 @@ pub const FileReader = struct { | |
pub fn openFileBlob(file: *Blob.FileStore) JSC.Maybe(OpenedFileBlob) { | ||
var this = OpenedFileBlob{ .fd = bun.invalid_fd }; | ||
var file_buf: bun.PathBuffer = undefined; | ||
var is_nonblocking_tty = false; | ||
var is_nonblocking = false; | ||
|
||
const fd = if (file.pathlike == .fd) | ||
if (file.pathlike.fd.isStdio()) brk: { | ||
if (comptime Environment.isPosix) { | ||
if (comptime Environment.isPosix and FeatureFlags.nonblocking_stdout_and_stderr_on_posix) { | ||
dylan-conway marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const rc = bun.C.open_as_nonblocking_tty(file.pathlike.fd.int(), bun.O.RDONLY); | ||
if (rc > -1) { | ||
is_nonblocking_tty = true; | ||
is_nonblocking = true; | ||
file.is_atty = true; | ||
break :brk bun.toFD(rc); | ||
} | ||
} | ||
break :brk file.pathlike.fd; | ||
} else switch (Syscall.dupWithFlags(file.pathlike.fd, brk: { | ||
} else brk: { | ||
const duped = Syscall.dupWithFlags(file.pathlike.fd, 0); | ||
|
||
if (duped != .result) { | ||
return .{ .err = duped.err.withFd(file.pathlike.fd) }; | ||
} | ||
|
||
const fd = duped.result; | ||
|
||
if (comptime Environment.isPosix) { | ||
if (bun.FDTag.get(file.pathlike.fd) == .none and !(file.is_atty orelse false)) { | ||
break :brk bun.O.NONBLOCK; | ||
if (bun.FDTag.get(fd) == .none) { | ||
is_nonblocking = switch (bun.sys.getFcntlFlags(fd)) { | ||
.result => |flags| (flags & bun.O.NONBLOCK) != 0, | ||
.err => false, | ||
}; | ||
} | ||
} | ||
|
||
break :brk 0; | ||
})) { | ||
.result => |fd| switch (bun.sys.toLibUVOwnedFD(fd, .dup, .close_on_fail)) { | ||
break :brk switch (bun.sys.toLibUVOwnedFD(fd, .dup, .close_on_fail)) { | ||
.result => |owned_fd| owned_fd, | ||
.err => |err| { | ||
return .{ .err = err }; | ||
}, | ||
}, | ||
.err => |err| { | ||
return .{ .err = err.withFd(file.pathlike.fd) }; | ||
}, | ||
}; | ||
} | ||
else switch (Syscall.open(file.pathlike.path.sliceZ(&file_buf), bun.O.RDONLY | bun.O.NONBLOCK | bun.O.CLOEXEC, 0)) { | ||
.result => |fd| fd, | ||
|
@@ -4055,7 +4110,7 @@ pub const FileReader = struct { | |
return .{ .err = Syscall.Error.fromCode(.ISDIR, .fstat) }; | ||
} | ||
|
||
this.pollable = bun.sys.isPollable(stat.mode) or is_nonblocking_tty or (file.is_atty orelse false); | ||
this.pollable = bun.sys.isPollable(stat.mode) or is_nonblocking or (file.is_atty orelse false); | ||
this.file_type = if (bun.S.ISFIFO(stat.mode)) | ||
.pipe | ||
else if (bun.S.ISSOCK(stat.mode)) | ||
|
@@ -4064,11 +4119,11 @@ pub const FileReader = struct { | |
.file; | ||
|
||
// pretend it's a non-blocking pipe if it's a TTY | ||
if (is_nonblocking_tty and this.file_type != .socket) { | ||
if (is_nonblocking and this.file_type != .socket) { | ||
this.file_type = .nonblocking_pipe; | ||
} | ||
|
||
this.nonblocking = is_nonblocking_tty or (this.pollable and !(file.is_atty orelse false)); | ||
this.nonblocking = is_nonblocking or (this.pollable and !(file.is_atty orelse false)); | ||
|
||
if (this.nonblocking and this.file_type == .pipe) { | ||
this.file_type = .nonblocking_pipe; | ||
|
@@ -4507,7 +4562,6 @@ pub const FileReader = struct { | |
|
||
pub fn setRefOrUnref(this: *FileReader, enable: bool) void { | ||
if (this.done) return; | ||
this.has_js_called_unref = !enable; | ||
this.reader.updateRef(enable); | ||
} | ||
|
||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want to run pending when:
done
Instead, we should runPending when
!this.writer.hasPendingData()
If this leads to a timeout in tests, it's likely due to not draining microtasks since this is being called after the microtask queue is over. If the developer immediately calls write every time the write Promise fulfills, that could maybe lead to an infinite loop but we will have to see.
I think both of these branches
if (amt == 0)
and this one should be removed. And instead