diff options
author | 2022-09-07 19:54:51 -0700 | |
---|---|---|
committer | 2022-09-07 19:54:51 -0700 | |
commit | 25e4fcf5c82af1e3af5bb103c68d5e110cfbc30f (patch) | |
tree | f92f75b941e3d5dfe68e912eae66aee389d1e30e /src | |
parent | ce382788b07d90f5f14b2182e0539f440af727d4 (diff) | |
download | bun-25e4fcf5c82af1e3af5bb103c68d5e110cfbc30f.tar.gz bun-25e4fcf5c82af1e3af5bb103c68d5e110cfbc30f.tar.zst bun-25e4fcf5c82af1e3af5bb103c68d5e110cfbc30f.zip |
Fast path for `Bun.write` with short-ish strings & typed arrays
Helps with https://github.com/oven-sh/bun/issues/646 but does not fully fix
Diffstat (limited to 'src')
-rw-r--r-- | src/bun.js/webcore/response.zig | 236 |
1 files changed, 225 insertions, 11 deletions
diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index 4e23c3d15..7d3d3785a 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -884,6 +884,26 @@ const PathOrBlob = union(enum) { return null; } + + pub fn fromJSNoCopy(ctx: js.JSContextRef, args: *JSC.Node.ArgumentsSlice, exception: js.ExceptionRef) ?PathOrBlob { + if (JSC.Node.PathOrFileDescriptor.fromJS(ctx, args, exception)) |path| { + return PathOrBlob{ .path = .{ + .path = .{ + .string = bun.PathString.init(path.path.slice()), + }, + } }; + } + + const arg = args.nextEat() orelse return null; + + if (arg.as(Blob)) |blob| { + return PathOrBlob{ + .blob = blob.*, + }; + } + + return null; + } }; pub const Blob = struct { @@ -1174,11 +1194,58 @@ pub const Blob = struct { var args = JSC.Node.ArgumentsSlice.from(ctx.bunVM(), arguments); defer args.deinit(); // accept a path or a blob - var path_or_blob = PathOrBlob.fromJS(ctx, &args, exception) orelse { + var path_or_blob = PathOrBlob.fromJSNoCopy(ctx, &args, exception) orelse { exception.* = JSC.toInvalidArguments("Bun.write expects a path, file descriptor or a blob", .{}, ctx).asObjectRef(); return null; }; + var data = args.nextEat() orelse { + exception.* = JSC.toInvalidArguments("Bun.write(pathOrFdOrBlob, blob) expects a Blob-y thing to write", .{}, ctx).asObjectRef(); + return null; + }; + + if (data.isEmptyOrUndefinedOrNull()) { + exception.* = JSC.toInvalidArguments("Bun.write(pathOrFdOrBlob, blob) expects a Blob-y thing to write", .{}, ctx).asObjectRef(); + return null; + } + + if (data.isString()) { + const len = data.getLengthOfArray(ctx); + if (len == 0) + return JSC.JSPromise.resolvedPromiseValue(ctx, JSC.JSValue.jsNumber(0)).asObjectRef(); + + if (len < 256 * 1024) { + const str = data.getZigString(ctx); + + const pathlike: JSC.Node.PathOrFileDescriptor = if (path_or_blob == .path) + path_or_blob.path + else + path_or_blob.blob.store.?.data.file.pathlike; + + if (pathlike == .path) { + return writeStringToFileFast(ctx, pathlike, str, true).asObjectRef(); + } else { + return writeStringToFileFast(ctx, pathlike, str, false).asObjectRef(); + } + } + } else if (data.asArrayBuffer(ctx)) |buffer_view| { + if (buffer_view.byte_len == 0) + return JSC.JSPromise.resolvedPromiseValue(ctx, JSC.JSValue.jsNumber(0)).asObjectRef(); + + if (buffer_view.byte_len < 256 * 1024) { + const pathlike: JSC.Node.PathOrFileDescriptor = if (path_or_blob == .path) + path_or_blob.path + else + path_or_blob.blob.store.?.data.file.pathlike; + + if (pathlike == .path) { + return writeBytesToFileFast(ctx, pathlike, buffer_view.byteSlice(), true).asObjectRef(); + } else { + return writeBytesToFileFast(ctx, pathlike, buffer_view.byteSlice(), false).asObjectRef(); + } + } + } + // if path_or_blob is a path, convert it into a file blob var destination_blob: Blob = if (path_or_blob == .path) Blob.findOrCreateFileFromPath(path_or_blob.path, ctx.ptr()) @@ -1190,16 +1257,6 @@ pub const Blob = struct { return null; } - var data = args.nextEat() orelse { - exception.* = JSC.toInvalidArguments("Bun.write(pathOrFdOrBlob, blob) expects a Blob-y thing to write", .{}, ctx).asObjectRef(); - return null; - }; - - if (data.isUndefinedOrNull() or data.isEmpty()) { - exception.* = JSC.toInvalidArguments("Bun.write(pathOrFdOrBlob, blob) expects a Blob-y thing to write", .{}, ctx).asObjectRef(); - return null; - } - // TODO: implement a writeev() fast path var source_blob: Blob = brk: { if (data.as(Response)) |response| { @@ -1287,6 +1344,159 @@ pub const Blob = struct { return writeFileWithSourceDestination(ctx, &source_blob, &destination_blob); } + fn writeStringToFileFast( + globalThis: *JSC.JSGlobalObject, + pathlike: JSC.Node.PathOrFileDescriptor, + str: ZigString, + comptime needs_open: bool, + ) JSC.JSValue { + var fd: JSC.Node.FileDescriptor = if (comptime !needs_open) pathlike.fd else std.math.maxInt(JSC.Node.FileDescriptor); + + if (needs_open) { + var file_path: [bun.MAX_PATH_BYTES]u8 = undefined; + switch (JSC.Node.Syscall.open(pathlike.path.sliceZ(&file_path), std.os.O.WRONLY | std.os.O.CREAT | std.os.O.NONBLOCK, 0o644)) { + .result => |result| { + fd = result; + }, + .err => |err| { + return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)); + }, + } + } + + var truncate = true; + var jsc_vm = globalThis.bunVM(); + var written: usize = 0; + + defer { + if (truncate) { + _ = JSC.Node.Syscall.system.ftruncate(fd, @intCast(i64, written)); + } + + if (needs_open) { + _ = JSC.Node.Syscall.close(fd); + } + } + + if (str.is16Bit()) { + var decoded = str.toSlice(jsc_vm.allocator); + defer decoded.deinit(); + + var remain = decoded.slice(); + const end = remain.ptr + remain.len; + + while (remain.ptr != end) { + const result = JSC.Node.Syscall.write(fd, remain); + switch (result) { + .result => |res| { + written += res; + remain = remain[res..]; + if (res == 0) break; + }, + .err => |err| { + truncate = false; + return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)); + }, + } + } + } else if (str.isUTF8() or strings.isAllASCII(str.slice())) { + var remain = str.slice(); + const end = remain.ptr + remain.len; + + while (remain.ptr != end) { + const result = JSC.Node.Syscall.write(fd, remain); + switch (result) { + .result => |res| { + written += res; + remain = remain[res..]; + if (res == 0) break; + }, + .err => |err| { + truncate = false; + return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)); + }, + } + } + } else { + var decoded = str.toOwnedSlice(jsc_vm.allocator) catch { + globalThis.vm().throwError(globalThis, ZigString.static("Out of memory").toErrorInstance(globalThis)); + return JSC.JSValue.jsUndefined(); + }; + defer jsc_vm.allocator.free(decoded); + var remain = decoded; + const end = remain.ptr + remain.len; + while (remain.ptr != end) { + const result = JSC.Node.Syscall.write(fd, remain); + switch (result) { + .result => |res| { + written += res; + remain = remain[res..]; + if (res == 0) break; + }, + .err => |err| { + truncate = false; + return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)); + }, + } + } + } + + return JSC.JSPromise.resolvedPromiseValue(globalThis, JSC.JSValue.jsNumber(written)); + } + + fn writeBytesToFileFast( + globalThis: *JSC.JSGlobalObject, + pathlike: JSC.Node.PathOrFileDescriptor, + bytes: []const u8, + comptime needs_open: bool, + ) JSC.JSValue { + var fd: JSC.Node.FileDescriptor = if (comptime !needs_open) pathlike.fd else std.math.maxInt(JSC.Node.FileDescriptor); + + if (needs_open) { + var file_path: [bun.MAX_PATH_BYTES]u8 = undefined; + switch (JSC.Node.Syscall.open(pathlike.path.sliceZ(&file_path), std.os.O.WRONLY | std.os.O.CREAT | std.os.O.NONBLOCK, 0644)) { + .result => |result| { + fd = result; + }, + .err => |err| { + return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)); + }, + } + } + + var truncate = true; + var written: usize = 0; + defer { + if (truncate) { + _ = JSC.Node.Syscall.system.ftruncate(fd, @intCast(i64, written)); + } + + if (needs_open) { + _ = JSC.Node.Syscall.close(fd); + } + } + + var remain = bytes; + const end = remain.ptr + remain.len; + + while (remain.ptr != end) { + const result = JSC.Node.Syscall.write(fd, remain); + switch (result) { + .result => |res| { + written += res; + remain = remain[res..]; + if (res == 0) break; + }, + .err => |err| { + truncate = false; + return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)); + }, + } + } + + return JSC.JSPromise.resolvedPromiseValue(globalThis, JSC.JSValue.jsNumber(written)); + } + pub fn constructFile( _: void, ctx: js.JSContextRef, @@ -3198,6 +3408,10 @@ pub const Blob = struct { if (lifetime != .temporary) this.setIsASCIIFlag(true); } + if (buf.len == 0) { + return ZigString.Empty.toValue(global); + } + switch (comptime lifetime) { // strings are immutable // we don't need to clone |