diff --git a/CMakeLists.txt b/CMakeLists.txt index 2acc8f3198..242bbeff25 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.22) cmake_policy(SET CMP0091 NEW) cmake_policy(SET CMP0067 NEW) -set(Bun_VERSION "1.0.19") +set(Bun_VERSION "1.0.20") set(WEBKIT_TAG b4de09f41b83e9e5c0e43ef414f1aee5968b6f7c) set(BUN_WORKDIR "${CMAKE_CURRENT_BINARY_DIR}") diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index b1bdcd63ee..8140fb9e4f 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -4144,8 +4144,12 @@ declare module "bun" { /** * Get the resource usage information of the process (max RSS, CPU time, etc) + * + * Only available after the process has exited + * + * If the process hasn't exited yet, this will return `undefined` */ - stats(): ResourceUsage; + resourceUsage(): ResourceUsage | undefined; } /** @@ -4163,6 +4167,10 @@ declare module "bun" { stderr: SpawnOptions.ReadableToSyncIO; exitCode: number; success: boolean; + /** + * Get the resource usage information of the process (max RSS, CPU time, etc) + */ + resourceUsage: ResourceUsage; } /** diff --git a/packages/bun-types/html-rewriter.d.ts b/packages/bun-types/html-rewriter.d.ts index 8cc4e96573..f391180a27 100644 --- a/packages/bun-types/html-rewriter.d.ts +++ b/packages/bun-types/html-rewriter.d.ts @@ -117,5 +117,15 @@ declare class HTMLRewriter { * @param input - The HTML to transform * @returns A new {@link Response} with the transformed HTML */ - transform(input: Response): Response; + transform(input: Response | Blob | Bun.BufferSource): Response; + /** + * @param input - The HTML string to transform + * @returns A new {@link String} containing the transformed HTML + */ + transform(input: string): string; + /** + * @param input - The HTML to transform as a {@link ArrayBuffer} + * @returns A new {@link ArrayBuffer} with the transformed HTML + */ + transform(input: ArrayBuffer): ArrayBuffer; } diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index cdd2b4fb5d..fb25e060db 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -827,14 +827,9 @@ pub const JSBundler = struct { return; } } else { - const buffer_or_string: JSC.Node.SliceOrBuffer = JSC.Node.SliceOrBuffer.fromJS(completion.globalThis, bun.default_allocator, source_code_value) orelse - @panic("expected buffer or string"); - - const source_code = switch (buffer_or_string) { - .buffer => |arraybuffer| bun.default_allocator.dupe(u8, arraybuffer.slice()) catch @panic("Out of memory in onLoad callback"), - .string => |slice| (slice.cloneIfNeeded(bun.default_allocator) catch @panic("Out of memory in onLoad callback")).slice(), - }; - + const source_code = JSC.Node.StringOrBuffer.fromJSToOwnedSlice(completion.globalThis, source_code_value, bun.default_allocator) catch + // TODO: + @panic("Unexpected: source_code is not a string"); this.value = .{ .success = .{ .loader = @as(options.Loader, @enumFromInt(@as(u8, @intCast(loader_as_int.to(i32))))), diff --git a/src/bun.js/api/JSTranspiler.zig b/src/bun.js/api/JSTranspiler.zig index c04095d5e9..dcb7cd8fc5 100644 --- a/src/bun.js/api/JSTranspiler.zig +++ b/src/bun.js/api/JSTranspiler.zig @@ -85,8 +85,7 @@ const TranspilerOptions = struct { // This is going to be hard to not leak pub const TransformTask = struct { - input_code: ZigString = ZigString.init(""), - protected_input_value: JSC.JSValue = @as(JSC.JSValue, @enumFromInt(0)), + input_code: JSC.Node.StringOrBuffer = JSC.Node.StringOrBuffer{ .buffer = .{} }, output_code: ZigString = ZigString.init(""), bundler: Bundler.Bundler = undefined, log: logger.Log, @@ -100,11 +99,10 @@ pub const TransformTask = struct { pub const AsyncTransformTask = JSC.ConcurrentPromiseTask(TransformTask); pub const AsyncTransformEventLoopTask = AsyncTransformTask.EventLoopTask; - pub fn create(transpiler: *Transpiler, protected_input_value: JSC.JSValue, globalThis: *JSGlobalObject, input_code: ZigString, loader: Loader) !*AsyncTransformTask { + pub fn create(transpiler: *Transpiler, input_code: bun.JSC.Node.StringOrBuffer, globalThis: *JSGlobalObject, loader: Loader) !*AsyncTransformTask { var transform_task = try bun.default_allocator.create(TransformTask); transform_task.* = .{ .input_code = input_code, - .protected_input_value = protected_input_value, .bundler = undefined, .global = globalThis, .macro_map = transpiler.transpiler_options.macro_map, @@ -134,6 +132,7 @@ pub const TransformTask = struct { const allocator = arena.allocator(); defer { + this.input_code.deinitAndUnprotect(); JSAst.Stmt.Data.Store.reset(); JSAst.Expr.Data.Store.reset(); arena.deinit(); @@ -222,9 +221,6 @@ pub const TransformTask = struct { finish(this.output_code, this.global, promise); - if (@intFromEnum(this.protected_input_value) != 0) { - this.protected_input_value = @as(JSC.JSValue, @enumFromInt(0)); - } this.deinit(); } @@ -237,9 +233,7 @@ pub const TransformTask = struct { defer if (should_cleanup) bun.Global.mimalloc_cleanup(false); this.log.deinit(); - if (this.input_code.isGloballyAllocated()) { - this.input_code.deinitGlobal(); - } + this.input_code.deinitAndUnprotect(); if (this.output_code.isGloballyAllocated()) { should_cleanup = this.output_code.len > 512_000; @@ -914,13 +908,13 @@ pub fn scan( return .zero; }; - const code_holder = JSC.Node.SliceOrBuffer.fromJS(globalThis, args.arena.allocator(), code_arg) orelse { + const code_holder = JSC.Node.StringOrBuffer.fromJS(globalThis, args.arena.allocator(), code_arg) orelse { globalThis.throwInvalidArgumentType("scan", "code", "string or Uint8Array"); return .zero; }; - + defer code_holder.deinit(); const code = code_holder.slice(); - args.protectEat(); + args.eat(); var exception_ref = [_]JSC.C.JSValueRef{null}; const exception: JSC.C.ExceptionRef = &exception_ref; @@ -1014,12 +1008,11 @@ pub fn transform( return .zero; }; - const code_holder = JSC.Node.StringOrBuffer.fromJS(globalThis, this.arena.allocator(), code_arg, exception) orelse { + var code = JSC.Node.StringOrBuffer.fromJS(globalThis, this.arena.allocator(), code_arg) orelse { globalThis.throwInvalidArgumentType("transform", "code", "string or Uint8Array"); return .zero; }; - const code = code_holder.slice(); args.eat(); const loader: ?Loader = brk: { if (args.next()) |arg| { @@ -1035,15 +1028,11 @@ pub fn transform( return .zero; } - if (code_holder == .string) { - arguments.ptr[0].ensureStillAlive(); - } - + code.toThreadSafe(); var task = TransformTask.create( this, - if (code_holder == .string) arguments.ptr[0] else .zero, + code, globalThis, - ZigString.init(code), loader orelse this.transpiler_options.default_loader, ) catch { globalThis.throw("Out of memory", .{}); @@ -1072,11 +1061,11 @@ pub fn transformSync( var arena = Mimalloc.Arena.init() catch unreachable; defer arena.deinit(); - const code_holder = JSC.Node.StringOrBuffer.fromJS(globalThis, arena.allocator(), code_arg, exception) orelse { + const code_holder = JSC.Node.StringOrBuffer.fromJS(globalThis, arena.allocator(), code_arg) orelse { globalThis.throwInvalidArgumentType("transformSync", "code", "string or Uint8Array"); return .zero; }; - + defer code_holder.deinit(); const code = code_holder.slice(); arguments.ptr[0].ensureStillAlive(); defer arguments.ptr[0].ensureStillAlive(); @@ -1254,7 +1243,7 @@ pub fn scanImports( return .zero; }; - const code_holder = JSC.Node.StringOrBuffer.fromJS(globalThis, args.arena.allocator(), code_arg, exception) orelse { + const code_holder = JSC.Node.StringOrBuffer.fromJS(globalThis, args.arena.allocator(), code_arg) orelse { if (exception.* == null) { globalThis.throwInvalidArgumentType("scanImports", "code", "string or Uint8Array"); } else { @@ -1263,7 +1252,8 @@ pub fn scanImports( return .zero; }; - args.protectEat(); + args.eat(); + defer code_holder.deinit(); const code = code_holder.slice(); var loader: Loader = this.transpiler_options.default_loader; diff --git a/src/bun.js/api/bun.classes.ts b/src/bun.js/api/bun.classes.ts index ecfd44a4e3..8dcecee236 100644 --- a/src/bun.js/api/bun.classes.ts +++ b/src/bun.js/api/bun.classes.ts @@ -83,8 +83,8 @@ export default [ fn: "doUnref", length: 0, }, - stats: { - fn: "stats", + resourceUsage: { + fn: "resourceUsage", length: 0, }, send: { diff --git a/src/bun.js/api/bun.zig b/src/bun.js/api/bun.zig index 00444a07de..79470c9bda 100644 --- a/src/bun.js/api/bun.zig +++ b/src/bun.js/api/bun.zig @@ -956,8 +956,13 @@ fn getImportedStyles(globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) callc return JSValue.createStringArray(globalObject, styles.ptr, styles.len, true); } +extern fn dump_zone_malloc_stats() void; + pub fn dump_mimalloc(globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { globalObject.bunVM().arena.dumpStats(); + if (comptime bun.is_heap_breakdown_enabled) { + dump_zone_malloc_stats(); + } return .undefined; } @@ -1743,23 +1748,18 @@ pub const Crypto = struct { return JSC.JSValue.undefined; } - var string_or_buffer = JSC.Node.SliceOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[0]) orelse { + const password_to_hash = JSC.Node.StringOrBuffer.fromJSToOwnedSlice(globalObject, arguments[0], bun.default_allocator) catch { globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray"); return JSC.JSValue.undefined; }; - if (string_or_buffer.slice().len == 0) { + if (password_to_hash.len == 0) { globalObject.throwInvalidArguments("password must not be empty", .{}); - string_or_buffer.deinit(); + bun.default_allocator.free(password_to_hash); return JSC.JSValue.undefined; } - string_or_buffer.ensureCloned(bun.default_allocator) catch { - globalObject.throwOutOfMemory(); - return JSC.JSValue.undefined; - }; - - return hash(globalObject, string_or_buffer.slice(), algorithm, false); + return hash(globalObject, password_to_hash, algorithm, false); } // Once we have bindings generator, this should be replaced with a generated function @@ -1782,7 +1782,7 @@ pub const Crypto = struct { return JSC.JSValue.undefined; } - var string_or_buffer = JSC.Node.SliceOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[0]) orelse { + var string_or_buffer = JSC.Node.StringOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[0]) orelse { globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray"); return JSC.JSValue.undefined; }; @@ -1793,10 +1793,6 @@ pub const Crypto = struct { return JSC.JSValue.undefined; } - string_or_buffer.ensureCloned(bun.default_allocator) catch { - globalObject.throwOutOfMemory(); - return JSC.JSValue.undefined; - }; defer string_or_buffer.deinit(); return hash(globalObject, string_or_buffer.slice(), algorithm, true); @@ -1920,40 +1916,28 @@ pub const Crypto = struct { }; } - var password = JSC.Node.SliceOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[0]) orelse { - globalObject.throwInvalidArgumentType("verify", "password", "string or TypedArray"); + const owned_password = JSC.Node.StringOrBuffer.fromJSToOwnedSlice(globalObject, arguments[0], bun.default_allocator) catch |err| { + if (err != error.JSError) globalObject.throwInvalidArgumentType("verify", "password", "string or TypedArray"); return JSC.JSValue.undefined; }; - var hash_ = JSC.Node.SliceOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[1]) orelse { - password.deinit(); - globalObject.throwInvalidArgumentType("verify", "hash", "string or TypedArray"); + const owned_hash = JSC.Node.StringOrBuffer.fromJSToOwnedSlice(globalObject, arguments[1], bun.default_allocator) catch |err| { + bun.default_allocator.free(owned_password); + if (err != error.JSError) globalObject.throwInvalidArgumentType("verify", "hash", "string or TypedArray"); return JSC.JSValue.undefined; }; - if (hash_.slice().len == 0) { - password.deinit(); + if (owned_hash.len == 0) { + bun.default_allocator.free(owned_password); return JSC.JSPromise.resolvedPromiseValue(globalObject, JSC.JSValue.jsBoolean(false)); } - if (password.slice().len == 0) { - hash_.deinit(); + if (owned_password.len == 0) { + bun.default_allocator.free(owned_hash); return JSC.JSPromise.resolvedPromiseValue(globalObject, JSC.JSValue.jsBoolean(false)); } - password.ensureCloned(bun.default_allocator) catch { - hash_.deinit(); - globalObject.throwOutOfMemory(); - return JSC.JSValue.undefined; - }; - - hash_.ensureCloned(bun.default_allocator) catch { - password.deinit(); - globalObject.throwOutOfMemory(); - return JSC.JSValue.undefined; - }; - - return verify(globalObject, password.slice(), hash_.slice(), algorithm, false); + return verify(globalObject, owned_password, owned_hash, algorithm, false); } // Once we have bindings generator, this should be replaced with a generated function @@ -1985,12 +1969,12 @@ pub const Crypto = struct { }; } - var password = JSC.Node.SliceOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[0]) orelse { + var password = JSC.Node.StringOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[0]) orelse { globalObject.throwInvalidArgumentType("verify", "password", "string or TypedArray"); return JSC.JSValue.undefined; }; - var hash_ = JSC.Node.SliceOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[1]) orelse { + var hash_ = JSC.Node.StringOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[1]) orelse { password.deinit(); globalObject.throwInvalidArgumentType("verify", "hash", "string or TypedArray"); return JSC.JSValue.undefined; @@ -2046,7 +2030,7 @@ pub const Crypto = struct { fn hashToEncoding( globalThis: *JSGlobalObject, evp: *EVP, - input: JSC.Node.SliceOrBuffer, + input: JSC.Node.StringOrBuffer, encoding: JSC.Node.Encoding, ) JSC.JSValue { var output_digest_buf: Digest = undefined; @@ -2065,7 +2049,7 @@ pub const Crypto = struct { fn hashToBytes( globalThis: *JSGlobalObject, evp: *EVP, - input: JSC.Node.SliceOrBuffer, + input: JSC.Node.StringOrBuffer, output: ?JSC.ArrayBuffer, ) JSC.JSValue { var output_digest_buf: Digest = undefined; @@ -2100,7 +2084,7 @@ pub const Crypto = struct { pub fn hash_( globalThis: *JSGlobalObject, algorithm: ZigString, - input: JSC.Node.SliceOrBuffer, + input: JSC.Node.StringOrBuffer, output: ?JSC.Node.StringOrBuffer, ) JSC.JSValue { var evp = EVP.byName(algorithm, globalThis) orelse { @@ -2111,9 +2095,10 @@ pub const Crypto = struct { if (output) |string_or_buffer| { switch (string_or_buffer) { - .string => |str| { - const encoding = JSC.Node.Encoding.from(str) orelse { - globalThis.throwInvalidArguments("Unknown encoding: {s}", .{str}); + inline else => |*str| { + defer str.deinit(); + const encoding = JSC.Node.Encoding.from(str.slice()) orelse { + globalThis.throwInvalidArguments("Unknown encoding: {s}", .{str.slice()}); return JSC.JSValue.zero; }; @@ -2169,7 +2154,7 @@ pub const Crypto = struct { const arguments = callframe.arguments(2); const input = arguments.ptr[0]; const encoding = arguments.ptr[1]; - const buffer = JSC.Node.SliceOrBuffer.fromJSWithEncoding(globalThis, globalThis.bunVM().allocator, input, encoding) orelse { + const buffer = JSC.Node.StringOrBuffer.fromJSWithEncodingValue(globalThis, globalThis.bunVM().allocator, input, encoding) orelse { globalThis.throwInvalidArguments("expected string or buffer", .{}); return JSC.JSValue.zero; }; @@ -2201,14 +2186,14 @@ pub const Crypto = struct { pub fn digest_( this: *@This(), globalThis: *JSGlobalObject, - output: ?JSC.Node.SliceOrBuffer, + output: ?JSC.Node.StringOrBuffer, ) JSC.JSValue { if (output) |string_or_buffer| { switch (string_or_buffer) { - .string => |str| { + inline else => |*str| { defer str.deinit(); const encoding = JSC.Node.Encoding.from(str.slice()) orelse { - globalThis.throwInvalidArguments("Unknown encoding: {}", .{str}); + globalThis.throwInvalidArguments("Unknown encoding: {}", .{str.*}); return JSC.JSValue.zero; }; @@ -2347,9 +2332,10 @@ pub const Crypto = struct { ) JSC.JSValue { if (output) |string_or_buffer| { switch (string_or_buffer) { - .string => |str| { - const encoding = JSC.Node.Encoding.from(str) orelse { - globalThis.throwInvalidArguments("Unknown encoding: {s}", .{str}); + inline else => |*str| { + defer str.deinit(); + const encoding = JSC.Node.Encoding.from(str.slice()) orelse { + globalThis.throwInvalidArguments("Unknown encoding: {s}", .{str.slice()}); return JSC.JSValue.zero; }; @@ -2381,7 +2367,7 @@ pub const Crypto = struct { pub fn update(this: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { const thisValue = callframe.this(); const input = callframe.argument(0); - const buffer = JSC.Node.SliceOrBuffer.fromJS(globalThis, globalThis.bunVM().allocator, input) orelse { + const buffer = JSC.Node.StringOrBuffer.fromJS(globalThis, globalThis.bunVM().allocator, input) orelse { globalThis.throwInvalidArguments("expected string or buffer", .{}); return JSC.JSValue.zero; }; @@ -2393,11 +2379,11 @@ pub const Crypto = struct { pub fn digest_( this: *@This(), globalThis: *JSGlobalObject, - output: ?JSC.Node.SliceOrBuffer, + output: ?JSC.Node.StringOrBuffer, ) JSC.JSValue { if (output) |string_or_buffer| { switch (string_or_buffer) { - .string => |str| { + inline else => |str| { const encoding = JSC.Node.Encoding.from(str.slice()) orelse { globalThis.throwInvalidArguments("Unknown encoding: \"{s}\"", .{str.slice()}); return JSC.JSValue.zero; diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 6f482e0405..b5e688b624 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -2073,7 +2073,8 @@ fn NewSocket(comptime ssl: bool) type { var exception_ref = [_]JSC.C.JSValueRef{null}; const exception: JSC.C.ExceptionRef = &exception_ref; - if (JSC.Node.StringOrBuffer.fromJS(globalObject, arena.allocator(), session_arg, exception)) |sb| { + if (JSC.Node.StringOrBuffer.fromJS(globalObject, arena.allocator(), session_arg)) |sb| { + defer sb.deinit(); const session_slice = sb.slice(); const ssl_ptr = this.socket.ssl(); var tmp = @as([*c]const u8, @ptrCast(session_slice.ptr)); @@ -2205,7 +2206,8 @@ fn NewSocket(comptime ssl: bool) type { var exception_ref = [_]JSC.C.JSValueRef{null}; const exception: JSC.C.ExceptionRef = &exception_ref; - if (JSC.Node.StringOrBuffer.fromJS(globalObject, arena.allocator(), context_arg, exception)) |sb| { + if (JSC.Node.StringOrBuffer.fromJS(globalObject, arena.allocator(), context_arg)) |sb| { + defer sb.deinit(); const context_slice = sb.slice(); const buffer_size = @as(usize, @intCast(length)); diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig index 2a2c3b11c9..e6ff5f2167 100644 --- a/src/bun.js/api/bun/subprocess.zig +++ b/src/bun.js/api/bun/subprocess.zig @@ -39,13 +39,13 @@ pub const ResourceUsage = struct { ) callconv(.C) JSValue { var cpu = JSC.JSValue.createEmptyObjectWithNullPrototype(globalObject); const rusage = this.rusage; - + const usrTime = JSValue.fromTimevalNoTruncate(globalObject, rusage.utime.tv_usec, rusage.utime.tv_sec); const sysTime = JSValue.fromTimevalNoTruncate(globalObject, rusage.stime.tv_usec, rusage.stime.tv_sec); cpu.put(globalObject, JSC.ZigString.static("user"), usrTime); cpu.put(globalObject, JSC.ZigString.static("system"), sysTime); - cpu.put(globalObject, JSC.ZigString.static("total"), JSValue.bigIntSum(globalObject, usrTime, sysTime)); + cpu.put(globalObject, JSC.ZigString.static("total"), JSValue.bigIntSum(globalObject, usrTime, sysTime)); return cpu; } @@ -179,11 +179,18 @@ pub const Subprocess = struct { // json, }; - pub fn stats( + pub fn resourceUsage( this: *Subprocess, globalObject: *JSGlobalObject, _: *JSC.CallFrame, ) callconv(.C) JSValue { + return this.createResourceUsageObject(globalObject); + } + + pub fn createResourceUsageObject( + this: *Subprocess, + globalObject: *JSGlobalObject, + ) JSValue { if (comptime Environment.isWindows) { //TODO: implement on windows return JSValue.jsUndefined(); @@ -196,14 +203,14 @@ pub const Subprocess = struct { .rusage = pid_rusage, }; - const alloc = globalObject.allocator(); - var result = alloc.create(ResourceUsage) catch { + var result = bun.default_allocator.create(ResourceUsage) catch { globalObject.throwOutOfMemory(); return .zero; }; result.* = resource_usage; return result.toJS(globalObject); } + pub fn hasExited(this: *const Subprocess) bool { return this.exit_code != null or this.waitpid_err != null or this.signal_code != null; } @@ -1664,6 +1671,7 @@ pub const Subprocess = struct { const exitCode = subprocess.exit_code orelse 1; const stdout = subprocess.stdout.toBufferedValue(globalThis); const stderr = subprocess.stderr.toBufferedValue(globalThis); + const resource_usage = subprocess.createResourceUsage(globalThis); subprocess.finalizeSync(); const sync_value = JSC.JSValue.createEmptyObject(globalThis, 4); @@ -1671,6 +1679,7 @@ pub const Subprocess = struct { sync_value.put(globalThis, JSC.ZigString.static("stdout"), stdout); sync_value.put(globalThis, JSC.ZigString.static("stderr"), stderr); sync_value.put(globalThis, JSC.ZigString.static("success"), JSValue.jsBoolean(exitCode == 0)); + sync_value.put(globalThis, JSC.ZigString.static("resourceUsage"), resource_usage); return sync_value; } // POSIX: @@ -2041,6 +2050,7 @@ pub const Subprocess = struct { const exitCode = subprocess.exit_code orelse 1; const stdout = subprocess.stdout.toBufferedValue(globalThis); const stderr = subprocess.stderr.toBufferedValue(globalThis); + const resource_usage = subprocess.createResourceUsageObject(globalThis); subprocess.finalizeSync(); const sync_value = JSC.JSValue.createEmptyObject(globalThis, 4); @@ -2048,7 +2058,7 @@ pub const Subprocess = struct { sync_value.put(globalThis, JSC.ZigString.static("stdout"), stdout); sync_value.put(globalThis, JSC.ZigString.static("stderr"), stderr); sync_value.put(globalThis, JSC.ZigString.static("success"), JSValue.jsBoolean(exitCode == 0)); - + sync_value.put(globalThis, JSC.ZigString.static("resourceUsage"), resource_usage); return sync_value; } diff --git a/src/bun.js/api/html_rewriter.zig b/src/bun.js/api/html_rewriter.zig index e284ca08ae..8ee62c365a 100644 --- a/src/bun.js/api/html_rewriter.zig +++ b/src/bun.js/api/html_rewriter.zig @@ -153,6 +153,7 @@ pub const HTMLRewriter = struct { pub fn finalizeWithoutDestroy(this: *HTMLRewriter) void { this.context.deinit(bun.default_allocator); + this.builder.deinit(); } pub fn beginTransform(this: *HTMLRewriter, global: *JSGlobalObject, response: *Response) JSValue { @@ -161,16 +162,69 @@ pub const HTMLRewriter = struct { return BufferOutputSink.init(new_context, global, response, this.builder); } - pub fn returnEmptyResponse(this: *HTMLRewriter, global: *JSGlobalObject, response: *Response) JSValue { - var result = bun.default_allocator.create(Response) catch unreachable; + pub fn transform_(this: *HTMLRewriter, global: *JSGlobalObject, response_value: JSC.JSValue) JSValue { + if (response_value.as(Response)) |response| { + if (response.body.value == .Used) { + global.throwInvalidArguments("Response body already used", .{}); + return .zero; + } - response.cloneInto(result, getAllocator(global), global); - this.finalizeWithoutDestroy(); - return result.toJS(global); - } + const out = this.beginTransform(global, response); - pub fn transform_(this: *HTMLRewriter, global: *JSGlobalObject, response: *Response) JSValue { - return this.beginTransform(global, response); + if (out != .zero) { + if (out.toError()) |err| { + global.throwValue(err); + return .zero; + } + } + + return out; + } + + const ResponseKind = enum { string, array_buffer, other }; + const kind: ResponseKind = brk: { + if (response_value.isString()) + break :brk .string + else if (response_value.jsType().isTypedArray()) + break :brk .array_buffer + else + break :brk .other; + }; + + if (kind != .other) { + if (JSC.WebCore.Body.extract(global, response_value)) |body_value| { + const resp = bun.new(Response, Response{ + .init = .{ + .status_code = 200, + }, + .body = body_value, + }); + defer resp.finalize(); + const out_response_value = this.beginTransform(global, resp); + out_response_value.ensureStillAlive(); + var out_response = out_response_value.as(Response) orelse return out_response_value; + var blob = out_response.body.value.useAsAnyBlobAllowNonUTF8String(); + + defer { + _ = Response.dangerouslySetPtr(out_response_value, null); + // Manually invoke the finalizer to ensure it does what we want + out_response.finalize(); + } + + return switch (kind) { + .string => brk: { + break :brk blob.toString(global, .transfer); + }, + .array_buffer => brk: { + break :brk blob.toArrayBuffer(global, .transfer); + }, + .other => unreachable, + }; + } + } + + global.throwInvalidArguments("Expected Response or Body", .{}); + return .zero; } pub const on = JSC.wrapInstanceMethod(HTMLRewriter, "on_", false); @@ -337,22 +391,36 @@ pub const HTMLRewriter = struct { pub const BufferOutputSink = struct { global: *JSGlobalObject, bytes: bun.MutableString, - rewriter: *LOLHTML.HTMLRewriter, + rewriter: ?*LOLHTML.HTMLRewriter = null, context: LOLHTMLContext, response: *Response, + response_value: JSC.Strong = .{}, bodyValueBufferer: ?JSC.WebCore.BodyValueBufferer = null, - tmp_sync_error: ?JSC.JSValue = null, + tmp_sync_error: ?*JSC.JSValue = null, // const log = bun.Output.scoped(.BufferOutputSink, false); - pub fn init(context: LOLHTMLContext, global: *JSGlobalObject, original: *Response, builder: *LOLHTML.HTMLRewriter.Builder) JSValue { - var result = bun.default_allocator.create(Response) catch unreachable; - var sink = bun.default_allocator.create(BufferOutputSink) catch unreachable; - sink.* = BufferOutputSink{ + pub fn init(context: LOLHTMLContext, global: *JSGlobalObject, original: *Response, builder: *LOLHTML.HTMLRewriter.Builder) JSC.JSValue { + var sink = bun.new(BufferOutputSink, BufferOutputSink{ .global = global, .bytes = bun.MutableString.initEmpty(bun.default_allocator), - .rewriter = undefined, + .rewriter = null, .context = context, - .response = result, - }; + .response = undefined, + }); + var result = bun.new(Response, .{ + .init = .{ + .status_code = 200, + }, + .body = .{ + .value = .{ + .Locked = .{ + .global = global, + .task = sink, + }, + }, + }, + }); + + sink.response = result; for (sink.context.document_handlers.items) |doc| { doc.ctx = sink; @@ -360,6 +428,7 @@ pub const HTMLRewriter = struct { for (sink.context.element_handlers.items) |doc| { doc.ctx = sink; } + const input_size = original.body.len(); sink.rewriter = builder.build( .UTF8, @@ -377,26 +446,10 @@ pub const HTMLRewriter = struct { BufferOutputSink.done, ) catch { sink.deinit(); - bun.default_allocator.destroy(result); - + result.finalize(); return throwLOLHTMLError(global); }; - result.* = Response{ - .allocator = bun.default_allocator, - .init = .{ - .status_code = 200, - }, - .body = .{ - .value = .{ - .Locked = .{ - .global = global, - .task = sink, - }, - }, - }, - }; - result.init.method = original.init.method; result.init.status_code = original.init.status_code; result.init.status_text = original.init.status_text.clone(); @@ -406,14 +459,21 @@ pub const HTMLRewriter = struct { result.init.headers = headers.cloneThis(global); } + // Hold off on cloning until we're actually done. + const response_js_value = sink.response.toJS(sink.global); + sink.response_value.set(global, response_js_value); + result.url = original.url.clone(); + var sink_error: JSC.JSValue = .zero; + sink.tmp_sync_error = &sink_error; const value = original.getBodyValue(); sink.bodyValueBufferer = JSC.WebCore.BodyValueBufferer.init(sink, onFinishedBuffering, sink.global, bun.default_allocator); + response_js_value.ensureStillAlive(); sink.bodyValueBufferer.?.run(value) catch |buffering_error| { return switch (buffering_error) { error.StreamAlreadyUsed => { var err = JSC.SystemError{ - .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_STREAM_CANNOT_PIPE))), + .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_STREAM_ALREADY_FINISHED))), .message = bun.String.static("Stream already used, please create a new one"), }; return err.toErrorInstance(sink.global); @@ -434,16 +494,18 @@ pub const HTMLRewriter = struct { }, }; }; + // sync error occurs - if (sink.tmp_sync_error) |err| { - err.ensureStillAlive(); - err.unprotect(); - sink.tmp_sync_error = null; - return err; + if (sink_error != .zero) { + sink_error.ensureStillAlive(); + sink_error.unprotect(); + defer sink.deinit(); + + return sink_error; } - // Hold off on cloning until we're actually done. - return sink.response.toJS(sink.global); + response_js_value.ensureStillAlive(); + return response_js_value; } pub fn onFinishedBuffering(ctx: *anyopaque, bytes: []const u8, js_err: ?JSC.JSValue, is_async: bool) void { @@ -467,9 +529,9 @@ pub const HTMLRewriter = struct { var ret_err = throwLOLHTMLError(sink.global); ret_err.ensureStillAlive(); ret_err.protect(); - sink.tmp_sync_error = ret_err; + sink.tmp_sync_error.?.* = ret_err; } - sink.rewriter.end() catch {}; + sink.rewriter.?.end() catch {}; sink.deinit(); return; } @@ -477,7 +539,9 @@ pub const HTMLRewriter = struct { if (sink.runOutputSink(bytes, is_async)) |ret_err| { ret_err.ensureStillAlive(); ret_err.protect(); - sink.tmp_sync_error = ret_err; + sink.tmp_sync_error.?.* = ret_err; + } else { + sink.deinit(); } } @@ -490,9 +554,8 @@ pub const HTMLRewriter = struct { const global = sink.global; var response = sink.response; - sink.rewriter.write(bytes) catch { + sink.rewriter.?.write(bytes) catch { sink.deinit(); - bun.default_allocator.destroy(sink); if (is_async) { response.body.value.toErrorInstance(throwLOLHTMLError(global), global); @@ -503,7 +566,7 @@ pub const HTMLRewriter = struct { } }; - sink.rewriter.end() catch { + sink.rewriter.?.end() catch { if (!is_async) response.finalize(); sink.response = undefined; sink.deinit(); @@ -536,6 +599,7 @@ pub const HTMLRewriter = struct { .capacity = 0, }, }; + prev_value.resolve( &this.response.body.value, this.global, @@ -548,18 +612,17 @@ pub const HTMLRewriter = struct { pub fn deinit(this: *BufferOutputSink) void { this.bytes.deinit(); - if (this.bodyValueBufferer != null) { - var bufferer = this.bodyValueBufferer.?; + if (this.bodyValueBufferer) |*bufferer| { bufferer.deinit(); } - if (this.tmp_sync_error) |ret_err| { - // this should never happens, but still we avoid future leaks - ret_err.unprotect(); - this.tmp_sync_error = null; + this.context.deinit(bun.default_allocator); + this.response_value.deinit(); + if (this.rewriter) |rewriter| { + rewriter.deinit(); } - this.context.deinit(bun.default_allocator); + bun.destroy(this); } }; diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 9ca4da3737..9b0e15cec6 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -359,7 +359,8 @@ pub const ServerConfig = struct { var valid_count: u32 = 0; while (i < count) : (i += 1) { const item = js_obj.getIndex(global, i); - if (JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), item, exception)) |sb| { + if (JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| { + defer sb.deinit(); const sliced = sb.slice(); if (sliced.len > 0) { native_array[valid_count] = bun.default_allocator.dupeZ(u8, sliced) catch unreachable; @@ -407,7 +408,8 @@ pub const ServerConfig = struct { } } else { const native_array = bun.default_allocator.alloc([*c]const u8, 1) catch unreachable; - if (JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj, exception)) |sb| { + if (JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj)) |sb| { + defer sb.deinit(); const sliced = sb.slice(); if (sliced.len > 0) { native_array[0] = bun.default_allocator.dupeZ(u8, sliced) catch unreachable; @@ -442,7 +444,8 @@ pub const ServerConfig = struct { } if (obj.getTruthy(global, "ALPNProtocols")) |protocols| { - if (JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), protocols, exception)) |sb| { + if (JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), protocols)) |sb| { + defer sb.deinit(); const sliced = sb.slice(); if (sliced.len > 0) { result.protos = bun.default_allocator.dupeZ(u8, sliced) catch unreachable; @@ -468,7 +471,8 @@ pub const ServerConfig = struct { while (i < count) : (i += 1) { const item = js_obj.getIndex(global, i); - if (JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), item, exception)) |sb| { + if (JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| { + defer sb.deinit(); const sliced = sb.slice(); if (sliced.len > 0) { native_array[valid_count] = bun.default_allocator.dupeZ(u8, sliced) catch unreachable; @@ -516,7 +520,8 @@ pub const ServerConfig = struct { } } else { const native_array = bun.default_allocator.alloc([*c]const u8, 1) catch unreachable; - if (JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj, exception)) |sb| { + if (JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj)) |sb| { + defer sb.deinit(); const sliced = sb.slice(); if (sliced.len > 0) { native_array[0] = bun.default_allocator.dupeZ(u8, sliced) catch unreachable; @@ -574,7 +579,8 @@ pub const ServerConfig = struct { while (i < count) : (i += 1) { const item = js_obj.getIndex(global, i); - if (JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), item, exception)) |sb| { + if (JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| { + defer sb.deinit(); const sliced = sb.slice(); if (sliced.len > 0) { native_array[valid_count] = bun.default_allocator.dupeZ(u8, sliced) catch unreachable; @@ -622,7 +628,8 @@ pub const ServerConfig = struct { } } else { const native_array = bun.default_allocator.alloc([*c]const u8, 1) catch unreachable; - if (JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj, exception)) |sb| { + if (JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj)) |sb| { + defer sb.deinit(); const sliced = sb.slice(); if (sliced.len > 0) { native_array[0] = bun.default_allocator.dupeZ(u8, sliced) catch unreachable; diff --git a/src/bun.js/base.zig b/src/bun.js/base.zig index 82ae60492e..4976d6553e 100644 --- a/src/bun.js/base.zig +++ b/src/bun.js/base.zig @@ -27,209 +27,77 @@ const TaggedPointerUnion = TaggedPointerTypes.TaggedPointerUnion; pub const ExceptionValueRef = [*c]js.JSValueRef; pub const JSValueRef = js.JSValueRef; -fn ObjectPtrType(comptime Type: type) type { - if (Type == void) return Type; - return *Type; -} - -const Internal = struct { - pub fn toJSWithType(globalThis: *JSC.JSGlobalObject, comptime Type: type, value: Type, exception: JSC.C.ExceptionRef) JSValue { - // TODO: refactor withType to use this instead of the other way around - return JSC.JSValue.c(To.JS.withType(Type, value, globalThis, exception)); - } - - pub fn toJS(globalThis: *JSC.JSGlobalObject, value: anytype, exception: JSC.C.ExceptionRef) JSValue { - return toJSWithType(globalThis, @TypeOf(value), value, exception); - } +pub const Lifetime = enum { + allocated, + temporary, }; - -pub usingnamespace Internal; - -pub const To = struct { - pub const Cpp = struct { - pub fn PropertyGetter( - comptime Type: type, - ) type { - return comptime fn ( - this: ObjectPtrType(Type), - globalThis: *JSC.JSGlobalObject, - ) callconv(.C) JSC.JSValue; - } - - const toJS = Internal.toJSWithType; - - pub fn GetterFn(comptime Type: type, comptime decl: std.meta.DeclEnum(Type)) PropertyGetter(Type) { - return struct { - pub fn getter( - this: ObjectPtrType(Type), - globalThis: *JSC.JSGlobalObject, - ) callconv(.C) JSC.JSValue { - var exception_ref = [_]JSC.C.JSValueRef{null}; - const exception: JSC.C.ExceptionRef = &exception_ref; - const result = toJS(globalThis, @call(.auto, @field(Type, @tagName(decl)), .{this}), exception); - if (exception.* != null) { - globalThis.throwValue(JSC.JSValue.c(exception.*)); - return .zero; - } - - return result; - } - }.getter; +pub fn toJS(globalObject: *JSC.JSGlobalObject, comptime ValueType: type, value: ValueType, comptime lifetime: Lifetime) JSC.JSValue { + const Type = comptime brk: { + var CurrentType = ValueType; + if (@typeInfo(ValueType) == .Optional) { + CurrentType = @typeInfo(ValueType).Optional.child; } + break :brk if (@typeInfo(CurrentType) == .Pointer and @typeInfo(CurrentType).Pointer.size == .One) + @typeInfo(CurrentType).Pointer.child + else + CurrentType; }; - pub const JS = struct { - pub fn withType(comptime Type: type, value: Type, context: JSC.C.JSContextRef, exception: JSC.C.ExceptionRef) JSC.C.JSValueRef { - return withTypeClone(Type, value, context, exception, false); - } - pub fn withTypeClone(comptime Type: type, value: Type, context: JSC.C.JSContextRef, exception: JSC.C.ExceptionRef, clone: bool) JSC.C.JSValueRef { - if (comptime bun.trait.isNumber(Type)) { - return JSC.JSValue.jsNumberWithType(Type, value).asRef(); + if (comptime bun.trait.isNumber(Type)) { + return JSC.JSValue.jsNumberWithType(Type, if (comptime Type != ValueType) value.* else value); + } + + switch (comptime Type) { + void => return .undefined, + bool => return JSC.JSValue.jsBoolean(if (comptime Type != ValueType) value.* else value), + *JSC.JSGlobalObject => return value.toJSValue(), + []const u8, [:0]const u8, [*:0]const u8, []u8, [:0]u8, [*:0]u8 => { + const str = bun.String.create(value); + defer str.deref(); + return str.toJS(globalObject); + }, + []const bun.String => { + defer { + for (value) |out| { + out.deref(); + } + bun.default_allocator.free(value); + } + return bun.String.toJSArray(globalObject, value); + }, + JSC.JSValue => return if (Type != ValueType) value.* else value, + + else => { + + // Recursion can stack overflow here + if (bun.trait.isSlice(Type)) { + const Child = comptime std.meta.Child(Type); + + var array = JSC.JSValue.createEmptyArray(globalObject, value.len); + for (value, 0..) |*item, i| { + const res = toJS(globalObject, *Child, item, lifetime); + if (res == .zero) return .zero; + array.putIndex( + globalObject, + @truncate(i), + res, + ); + } + return array; } - var zig_str: JSC.ZigString = undefined; + if (comptime @hasDecl(Type, "toJSNewlyCreated") and @typeInfo(@TypeOf(@field(Type, "toJSNewlyCreated"))).Fn.params.len == 2) { + return value.toJSNewlyCreated(globalObject); + } - return switch (comptime Type) { - void => JSC.C.JSValueMakeUndefined(context), - bool => JSC.C.JSValueMakeBoolean(context, value), - []const u8, [:0]const u8, [*:0]const u8, []u8, [:0]u8, [*:0]u8 => brk: { - zig_str = ZigString.init(value); - const val = zig_str.toValueAuto(context.ptr()); + if (comptime @hasDecl(Type, "toJS") and @typeInfo(@TypeOf(@field(Type, "toJS"))).Fn.params.len == 2) { + return value.toJS(globalObject); + } - break :brk val.asObjectRef(); - }, - []const JSC.ZigString => { - const array = JSC.JSValue.createStringArray(context.ptr(), value.ptr, value.len, clone).asObjectRef(); - const values: []const JSC.ZigString = value; - defer bun.default_allocator.free(values); - if (clone) { - for (values) |out| { - if (out.isGloballyAllocated()) { - out.deinitGlobal(); - } - } - } - - return array; - }, - []const bun.String => { - defer { - for (value) |out| { - out.deref(); - } - bun.default_allocator.free(value); - } - return bun.String.toJSArray(context, value).asObjectRef(); - }, - []const PathString, []const []const u8, []const []u8, [][]const u8, [][:0]const u8, [][:0]u8 => { - if (value.len == 0) - return JSC.C.JSObjectMakeArray(context, 0, null, exception); - - var stack_fallback = std.heap.stackFallback(512, bun.default_allocator); - var allocator = stack_fallback.get(); - - var zig_strings = allocator.alloc(ZigString, value.len) catch unreachable; - defer if (stack_fallback.fixed_buffer_allocator.end_index >= 511) allocator.free(zig_strings); - - for (value, 0..) |path_string, i| { - if (comptime Type == []const PathString) { - zig_strings[i] = ZigString.init(path_string.slice()); - } else { - zig_strings[i] = ZigString.init(path_string); - } - } - // there is a possible C ABI bug or something here when the ptr is null - // it should not be segfaulting but it is - // that's why we check at the top of this function - const array = JSC.JSValue.createStringArray(context.ptr(), zig_strings.ptr, zig_strings.len, clone).asObjectRef(); - - if (clone and value.len > 0) { - for (value) |path_string| { - if (comptime Type == []const PathString) { - bun.default_allocator.free(path_string.slice()); - } else { - bun.default_allocator.free(path_string); - } - } - bun.default_allocator.free(value); - } - - return array; - }, - - JSC.C.JSValueRef => value, - - else => { - const Info: std.builtin.Type = @typeInfo(Type); - if (Info == .Enum) { - const Enum: std.builtin.Type.Enum = Info.Enum; - if (!bun.trait.isNumber(Enum.tag_type)) { - @compileError("im curious if this ever gets hit. this feels like dead code"); - // zig_str = JSC.ZigString.init(@tagName(value)); - // return zig_str.toValue(context.ptr()).asObjectRef(); - } - } - - // Recursion can stack overflow here - if (bun.trait.isSlice(Type)) { - const Child = comptime std.meta.Child(Type); - - var array = JSC.JSValue.createEmptyArray(context, value.len); - for (value, 0..) |item, i| { - array.putIndex( - context, - @truncate(i), - JSC.JSValue.c(To.JS.withType(Child, item, context, exception)), - ); - - if (exception.* != null) { - return null; - } - } - return array.asObjectRef(); - } - - if (comptime bun.trait.isZigString(Type)) { - zig_str = JSC.ZigString.init(value); - return zig_str.toValue(context.ptr()).asObjectRef(); - } - - if (comptime Info == .Pointer) { - const Child = std.meta.Child(Type); - if (bun.trait.isContainer(Child) and @hasDecl(Child, "Class") and @hasDecl(Child.Class, "isJavaScriptCoreClass")) { - return Child.Class.make(context, value); - } - } - - if (comptime Info == .Struct) { - if (comptime @hasDecl(Type, "Class") and @hasDecl(Type.Class, "isJavaScriptCoreClass")) { - if (comptime !@hasDecl(Type, "finalize")) { - @compileError(std.fmt.comptimePrint("JSC class {s} must implement finalize to prevent memory leaks", .{Type.Class.name})); - } - - if (comptime !@hasDecl(Type, "toJS")) { - return Type.Class.make(context, bun.new(Type, value)); - } - } - } - - if (comptime @hasDecl(Type, "toJS") and @typeInfo(@TypeOf(@field(Type, "toJS"))).Fn.params.len == 2) { - return bun.new(Type, value).toJS(context).asObjectRef(); - } - - const res = value.toJS(context, exception); - - if (@TypeOf(res) == JSC.C.JSValueRef) { - return res; - } else if (@TypeOf(res) == JSC.JSValue) { - return res.asObjectRef(); - } - @compileError("dont know how to convert " ++ @typeName(@TypeOf(res)) ++ " to JS"); - }, - }; - } - }; -}; + @compileError("dont know how to convert " ++ @typeName(ValueType) ++ " to JS"); + }, + } +} pub const Properties = struct { pub const UTF8 = struct { @@ -694,7 +562,7 @@ pub const ArrayBuffer = extern struct { }; pub const MarkedArrayBuffer = struct { - buffer: ArrayBuffer, + buffer: ArrayBuffer = .{}, allocator: ?std.mem.Allocator = null, pub const Stream = ArrayBuffer.Stream; @@ -758,8 +626,8 @@ pub const MarkedArrayBuffer = struct { return container; } - pub fn toNodeBuffer(this: MarkedArrayBuffer, ctx: js.JSContextRef) js.JSObjectRef { - return JSValue.createBufferWithCtx(ctx, this.buffer.byteSlice(), this.buffer.ptr, MarkedArrayBuffer_deallocator).asObjectRef(); + pub fn toNodeBuffer(this: MarkedArrayBuffer, ctx: js.JSContextRef) JSC.JSValue { + return JSValue.createBufferWithCtx(ctx, this.buffer.byteSlice(), this.buffer.ptr, MarkedArrayBuffer_deallocator); } pub fn toJSObjectRef(this: MarkedArrayBuffer, ctx: js.JSContextRef, exception: js.ExceptionRef) js.JSObjectRef { @@ -787,7 +655,18 @@ pub const MarkedArrayBuffer = struct { ); } - pub const toJS = toJSObjectRef; + // TODO: refactor this + pub fn toJS(this: *MarkedArrayBuffer, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + var exception = [_]JSC.C.JSValueRef{null}; + const obj = this.toJSObjectRef(globalObject, &exception); + + if (exception[0] != null) { + globalObject.throwValue(JSC.JSValue.c(exception[0])); + return .zero; + } + + return JSC.JSValue.c(obj); + } }; // expensive heap reference-counted string type @@ -1348,7 +1227,7 @@ pub fn wrapInstanceMethod( iter.deinit(); return JSC.JSValue.zero; }; - args[i] = JSC.Node.StringOrBuffer.fromJS(globalThis.ptr(), iter.arena.allocator(), arg, null) orelse { + args[i] = JSC.Node.StringOrBuffer.fromJS(globalThis.ptr(), iter.arena.allocator(), arg) orelse { globalThis.throwInvalidArguments("expected string or buffer", .{}); iter.deinit(); return JSC.JSValue.zero; @@ -1357,22 +1236,7 @@ pub fn wrapInstanceMethod( ?JSC.Node.StringOrBuffer => { if (iter.nextEat()) |arg| { if (!arg.isEmptyOrUndefinedOrNull()) { - args[i] = JSC.Node.StringOrBuffer.fromJS(globalThis.ptr(), iter.arena.allocator(), arg, null) orelse { - globalThis.throwInvalidArguments("expected string or buffer", .{}); - iter.deinit(); - return JSC.JSValue.zero; - }; - } else { - args[i] = null; - } - } else { - args[i] = null; - } - }, - ?JSC.Node.SliceOrBuffer => { - if (iter.nextEat()) |arg| { - if (!arg.isEmptyOrUndefinedOrNull()) { - args[i] = JSC.Node.SliceOrBuffer.fromJS(globalThis.ptr(), iter.arena.allocator(), arg) orelse { + args[i] = JSC.Node.StringOrBuffer.fromJS(globalThis.ptr(), iter.arena.allocator(), arg) orelse { globalThis.throwInvalidArguments("expected string or buffer", .{}); iter.deinit(); return JSC.JSValue.zero; @@ -1520,7 +1384,7 @@ pub fn wrapStaticMethod( iter.deinit(); return JSC.JSValue.zero; }; - args[i] = JSC.Node.StringOrBuffer.fromJS(globalThis.ptr(), iter.arena.allocator(), arg, null) orelse { + args[i] = JSC.Node.StringOrBuffer.fromJS(globalThis.ptr(), iter.arena.allocator(), arg) orelse { globalThis.throwInvalidArguments("expected string or buffer", .{}); iter.deinit(); return JSC.JSValue.zero; @@ -1528,30 +1392,7 @@ pub fn wrapStaticMethod( }, ?JSC.Node.StringOrBuffer => { if (iter.nextEat()) |arg| { - args[i] = JSC.Node.StringOrBuffer.fromJS(globalThis.ptr(), iter.arena.allocator(), arg, null) orelse { - globalThis.throwInvalidArguments("expected string or buffer", .{}); - iter.deinit(); - return JSC.JSValue.zero; - }; - } else { - args[i] = null; - } - }, - JSC.Node.SliceOrBuffer => { - const arg = iter.nextEat() orelse { - globalThis.throwInvalidArguments("expected string or buffer", .{}); - iter.deinit(); - return JSC.JSValue.zero; - }; - args[i] = JSC.Node.SliceOrBuffer.fromJS(globalThis.ptr(), iter.arena.allocator(), arg) orelse { - globalThis.throwInvalidArguments("expected string or buffer", .{}); - iter.deinit(); - return JSC.JSValue.zero; - }; - }, - ?JSC.Node.SliceOrBuffer => { - if (iter.nextEat()) |arg| { - args[i] = JSC.Node.SliceOrBuffer.fromJS(globalThis.ptr(), iter.arena.allocator(), arg) orelse { + args[i] = JSC.Node.StringOrBuffer.fromJS(globalThis.ptr(), iter.arena.allocator(), arg) orelse { globalThis.throwInvalidArguments("expected string or buffer", .{}); iter.deinit(); return JSC.JSValue.zero; diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index e061f81614..e50c605bee 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -2251,14 +2251,14 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionMemoryUsage, // arrayBuffers: 9386 // } - result->putDirectOffset(vm, 0, JSC::jsNumber(current_rss)); - result->putDirectOffset(vm, 1, JSC::jsNumber(vm.heap.blockBytesAllocated())); + result->putDirectOffset(vm, 0, JSC::jsDoubleNumber(current_rss)); + result->putDirectOffset(vm, 1, JSC::jsDoubleNumber(vm.heap.blockBytesAllocated())); // heap.size() loops through every cell... // TODO: add a binding for heap.sizeAfterLastCollection() - result->putDirectOffset(vm, 2, JSC::jsNumber(vm.heap.sizeAfterLastEdenCollection())); + result->putDirectOffset(vm, 2, JSC::jsDoubleNumber(vm.heap.sizeAfterLastEdenCollection())); - result->putDirectOffset(vm, 3, JSC::jsNumber(vm.heap.externalMemorySize())); + result->putDirectOffset(vm, 3, JSC::jsDoubleNumber(vm.heap.extraMemorySize() + vm.heap.externalMemorySize())); // We report 0 for this because m_arrayBuffers in JSC::Heap is private and we need to add a binding // If we use objectTypeCounts(), it's hideously slow because it loops through every single object in the heap diff --git a/src/bun.js/bindings/BunString.cpp b/src/bun.js/bindings/BunString.cpp index 74db8210fd..b4f26c2271 100644 --- a/src/bun.js/bindings/BunString.cpp +++ b/src/bun.js/bindings/BunString.cpp @@ -28,6 +28,8 @@ #include #include +extern "C" void mi_free(void* ptr); + using namespace JSC; extern "C" BunString BunString__fromBytes(const char* bytes, size_t length); @@ -118,7 +120,6 @@ extern "C" void BunString__toThreadSafe(BunString* str) if (str->tag == BunStringTag::WTFStringImpl) { auto impl = str->impl.wtf->isolatedCopy(); if (impl.ptr() != str->impl.wtf) { - impl->ref(); str->impl.wtf = &impl.leakRef(); } } @@ -507,4 +508,39 @@ WTF::String BunString::toWTFString(ZeroCopyTag) const } return WTF::String(); +} + +extern "C" BunString BunString__createExternalGloballyAllocatedLatin1( + const LChar* bytes, + size_t length) +{ + ASSERT(length > 0); + Ref impl = WTF::ExternalStringImpl::create(bytes, length, nullptr, [](void*, void* ptr, size_t) { + mi_free(ptr); + }); + return { BunStringTag::WTFStringImpl, { .wtf = &impl.leakRef() } }; +} + +extern "C" BunString BunString__createExternalGloballyAllocatedUTF16( + const UChar* bytes, + size_t length) +{ + ASSERT(length > 0); + Ref impl = WTF::ExternalStringImpl::create(bytes, length, nullptr, [](void*, void* ptr, size_t) { + mi_free(ptr); + }); + return { BunStringTag::WTFStringImpl, { .wtf = &impl.leakRef() } }; +} + +extern "C" bool WTFStringImpl__isThreadSafe( + const WTF::StringImpl* wtf) +{ + if (wtf->bufferOwnership() != StringImpl::BufferOwnership::BufferInternal) + return false; + + return !(wtf->isSymbol() || wtf->isAtom()); + // if (wtf->is8Bit()) + // return wtf->characters8() == reinterpret_cast_ptr(reinterpret_cast(wtf) + tailOffset()); + + // return wtf->characters16() == reinterpret_cast_ptr(reinterpret_cast(wtf) + tailOffset()); } \ No newline at end of file diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index a7d34ecbc2..e3b956afe2 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -133,7 +133,7 @@ pub const ZigString = extern struct { } } - pub fn toJS(this: ZigString, ctx: *JSC.JSGlobalObject, _: JSC.C.ExceptionRef) JSValue { + pub fn toJS(this: ZigString, ctx: *JSC.JSGlobalObject) JSValue { if (this.isGloballyAllocated()) { return this.toExternalValue(ctx); } @@ -403,6 +403,15 @@ pub const ZigString = extern struct { ptr: [*]const u8 = undefined, len: u32 = 0, + pub fn reportExtraMemory(this: *const Slice, vm: *JSC.VM) void { + if (this.allocator.get()) |allocator| { + // Don't report it if the memory is actually owned by JSC. + if (!bun.String.isWTFAllocator(allocator)) { + vm.reportExtraMemory(this.len); + } + } + } + pub fn init(allocator: std.mem.Allocator, input: []const u8) Slice { return .{ .ptr = input.ptr, @@ -2537,7 +2546,7 @@ pub const JSGlobalObject = extern struct { return JSGlobalObject__setTimeZone(this, timeZone); } - pub inline fn toJS(globalThis: *JSGlobalObject) JSValue { + pub inline fn toJSValue(globalThis: *JSGlobalObject) JSValue { return @enumFromInt(@as(JSValue.Type, @bitCast(@intFromPtr(globalThis)))); } @@ -2565,6 +2574,10 @@ pub const JSGlobalObject = extern struct { ); } + pub fn toJS(this: *JSC.JSGlobalObject, value: anytype, comptime lifetime: JSC.Lifetime) JSC.JSValue { + return JSC.toJS(this, @TypeOf(value), value, lifetime); + } + pub fn throwInvalidArgumentType( this: *JSGlobalObject, comptime name_: []const u8, @@ -3527,7 +3540,7 @@ pub const JSValue = enum(JSValueReprInt) { return JSC.C.JSObjectCallAsFunctionReturnValue( globalThis, this, - globalThis.toJS(), + globalThis.toJSValue(), args.len, @as(?[*]const JSC.C.JSValueRef, @ptrCast(args.ptr)), ); @@ -4266,7 +4279,7 @@ pub const JSValue = enum(JSValueReprInt) { pub fn bigIntSum(globalObject: *JSGlobalObject, a: JSValue, b: JSValue) JSValue { return cppFn("bigIntSum", .{ globalObject, a, b }); } - + pub fn toUInt64NoTruncate(this: JSValue) u64 { return cppFn("toUInt64NoTruncate", .{ this, diff --git a/src/bun.js/bindings/c-bindings.cpp b/src/bun.js/bindings/c-bindings.cpp index 9015c44e1b..491c3ac8f6 100644 --- a/src/bun.js/bindings/c-bindings.cpp +++ b/src/bun.js/bindings/c-bindings.cpp @@ -96,3 +96,34 @@ extern "C" ssize_t bun_sysconf__SC_CLK_TCK() return 0; #endif } + +#if OS(DARWIN) && BUN_DEBUG +#include + +extern "C" void dump_zone_malloc_stats() +{ + vm_address_t* zones; + unsigned count; + + // Zero out the structures in case a zone is missing + malloc_statistics_t stats; + stats.blocks_in_use = 0; + stats.size_in_use = 0; + stats.max_size_in_use = 0; + stats.size_allocated = 0; + + malloc_get_all_zones(mach_task_self(), 0, &zones, &count); + for (unsigned i = 0; i < count; i++) { + if (const char* name = malloc_get_zone_name(reinterpret_cast(zones[i]))) { + printf("%s:\n", name); + malloc_zone_statistics(reinterpret_cast(zones[i]), &stats); + printf(" blocks_in_use: %u\n", stats.blocks_in_use); + printf(" size_in_use: %zu\n", stats.size_in_use); + printf(" max_size_in_use: %zu\n", stats.max_size_in_use); + printf(" size_allocated: %zu\n", stats.size_allocated); + printf("\n"); + } + } +} + +#endif \ No newline at end of file diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index 84c11348a2..aa4adc6127 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -42,13 +42,12 @@ pub fn ConcurrentPromiseTask(comptime Context: type) type { ref: Async.KeepAlive = .{}, pub fn createOnJSThread(allocator: std.mem.Allocator, globalThis: *JSGlobalObject, value: *Context) !*This { - var this = try allocator.create(This); - this.* = .{ + var this = bun.new(This, .{ .event_loop = VirtualMachine.get().event_loop, .ctx = value, .allocator = allocator, .globalThis = globalThis, - }; + }); var promise = JSC.JSPromise.create(globalThis); this.promise.strong.set(globalThis, promise.asValue(globalThis)); this.ref.ref(this.event_loop.virtual_machine); @@ -80,7 +79,7 @@ pub fn ConcurrentPromiseTask(comptime Context: type) type { } pub fn deinit(this: *This) void { - this.allocator.destroy(this); + bun.destroy(this); } }; } @@ -102,15 +101,14 @@ pub fn WorkTask(comptime Context: type) type { ref: Async.KeepAlive = .{}, pub fn createOnJSThread(allocator: std.mem.Allocator, globalThis: *JSGlobalObject, value: *Context) !*This { - var this = try allocator.create(This); var vm = globalThis.bunVM(); - this.* = .{ + var this = bun.new(This, .{ .event_loop = vm.eventLoop(), .ctx = value, .allocator = allocator, .globalThis = globalThis, .async_task_tracker = JSC.AsyncTaskTracker.init(vm), - }; + }); this.ref.ref(this.event_loop.virtual_machine); return this; @@ -146,10 +144,9 @@ pub fn WorkTask(comptime Context: type) type { } pub fn deinit(this: *This) void { - var allocator = this.allocator; this.ref.unref(this.event_loop.virtual_machine); - allocator.destroy(this); + bun.destroyWithAlloc(this.allocator, this); } }; } diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index c0c59babe4..0c3218a851 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -33,7 +33,6 @@ const PathOrFileDescriptor = JSC.Node.PathOrFileDescriptor; const DirIterator = @import("./dir_iterator.zig"); const Path = @import("../../resolver/resolve_path.zig"); const FileSystem = @import("../../fs.zig").FileSystem; -const StringOrBuffer = JSC.Node.StringOrBuffer; const ArgumentsSlice = JSC.Node.ArgumentsSlice; const TimeLike = JSC.Node.TimeLike; const Mode = bun.Mode; @@ -58,7 +57,7 @@ else // TODO: 0; -const SliceWithUnderlyingStringOrBuffer = JSC.Node.SliceWithUnderlyingStringOrBuffer; +const StringOrBuffer = JSC.Node.StringOrBuffer; const ArrayBuffer = JSC.MarkedArrayBuffer; const Buffer = JSC.Buffer; const FileSystemFlags = JSC.Node.FileSystemFlags; @@ -119,19 +118,23 @@ pub const Async = struct { pub const Task = @This(); + pub const heap_label = "Async" ++ bun.meta.typeBaseName(@typeName(ArgumentType)) ++ "Task"; + pub fn create( globalObject: *JSC.JSGlobalObject, args: ArgumentType, vm: *JSC.VirtualMachine, ) JSC.JSValue { - var task = bun.default_allocator.create(Task) catch @panic("out of memory"); - task.* = Task{ - .promise = JSC.JSPromise.Strong.init(globalObject), - .args = args, - .result = undefined, - .globalObject = globalObject, - .tracker = JSC.AsyncTaskTracker.init(vm), - }; + var task = bun.new( + Task, + Task{ + .promise = JSC.JSPromise.Strong.init(globalObject), + .args = args, + .result = undefined, + .globalObject = globalObject, + .tracker = JSC.AsyncTaskTracker.init(vm), + }, + ); task.ref.ref(vm); task.args.toThreadSafe(); task.tracker.didSchedule(globalObject); @@ -159,14 +162,9 @@ pub const Async = struct { var success = @as(JSC.Maybe(ReturnType).Tag, this.result) == .result; const result = switch (this.result) { .err => |err| err.toJSC(globalObject), - .result => |res| brk: { - var exceptionref: JSC.C.JSValueRef = null; - const out = JSC.JSValue.c(JSC.To.JS.withType(ReturnType, res, globalObject, &exceptionref)); - const exception = JSC.JSValue.c(exceptionref); - if (exception != .zero) { - success = false; - break :brk exception; - } + .result => |*res| brk: { + const out = globalObject.toJS(res, .temporary); + success = out != .zero; break :brk out; }, @@ -202,7 +200,7 @@ pub const Async = struct { this.args.deinit(); } this.promise.strong.deinit(); - bun.default_allocator.destroy(this); + bun.destroy(this); } }; } @@ -234,17 +232,19 @@ pub const AsyncCpTask = struct { return .zero; } - var task = bun.default_allocator.create(AsyncCpTask) catch @panic("out of memory"); - task.* = AsyncCpTask{ - .promise = JSC.JSPromise.Strong.init(globalObject), - .args = cp_args, - .has_result = .{ .raw = false }, - .result = undefined, - .globalObject = globalObject, - .tracker = JSC.AsyncTaskTracker.init(vm), - .arena = arena, - .subtask_count = .{ .raw = 1 }, - }; + var task = bun.new( + AsyncCpTask, + AsyncCpTask{ + .promise = JSC.JSPromise.Strong.init(globalObject), + .args = cp_args, + .has_result = .{ .raw = false }, + .result = undefined, + .globalObject = globalObject, + .tracker = JSC.AsyncTaskTracker.init(vm), + .arena = arena, + .subtask_count = .{ .raw = 1 }, + }, + ); task.ref.ref(vm); task.args.src.toThreadSafe(); task.args.dest.toThreadSafe(); @@ -282,14 +282,9 @@ pub const AsyncCpTask = struct { var success = @as(JSC.Maybe(Return.Cp).Tag, this.result) == .result; const result = switch (this.result) { .err => |err| err.toJSC(globalObject), - .result => |res| brk: { - var exceptionref: JSC.C.JSValueRef = null; - const out = JSC.JSValue.c(JSC.To.JS.withType(Return.Cp, res, globalObject, &exceptionref)); - const exception = JSC.JSValue.c(exceptionref); - if (exception != .zero) { - success = false; - break :brk exception; - } + .result => |*res| brk: { + const out = globalObject.toJS(res, .temporary); + success = out != .zero; break :brk out; }, @@ -318,7 +313,7 @@ pub const AsyncCpTask = struct { this.args.deinit(); this.promise.strong.deinit(); this.arena.deinit(); - bun.default_allocator.destroy(this); + bun.destroy(this); } }; @@ -355,6 +350,8 @@ pub const AsyncReaddirRecursiveTask = struct { pending_err: ?Syscall.Error = null, pending_err_mutex: bun.Lock = bun.Lock.init(), + pub usingnamespace bun.New(@This()); + pub const ResultListEntry = struct { pub const Value = union(Return.Readdir.Tag) { with_file_types: std.ArrayList(Dirent), @@ -396,11 +393,13 @@ pub const AsyncReaddirRecursiveTask = struct { basename: bun.PathString = bun.PathString.empty, task: JSC.WorkPoolTask = .{ .callback = call }, + pub usingnamespace bun.New(@This()); + pub fn call(task: *JSC.WorkPoolTask) void { var this: *Subtask = @fieldParentPtr(Subtask, "task", task); defer { bun.default_allocator.free(this.basename.sliceAssumeZ()); - bun.default_allocator.destroy(this); + this.destroy(); } var buf: [bun.MAX_PATH_BYTES]u8 = undefined; this.readdir_task.performWork(this.basename.sliceAssumeZ(), &buf, false); @@ -411,11 +410,12 @@ pub const AsyncReaddirRecursiveTask = struct { readdir_task: *AsyncReaddirRecursiveTask, basename: [:0]const u8, ) void { - var task = bun.default_allocator.create(Subtask) catch bun.outOfMemory(); - task.* = Subtask{ - .readdir_task = readdir_task, - .basename = bun.PathString.init(bun.default_allocator.dupeZ(u8, basename) catch bun.outOfMemory()), - }; + var task = Subtask.new( + .{ + .readdir_task = readdir_task, + .basename = bun.PathString.init(bun.default_allocator.dupeZ(u8, basename) catch bun.outOfMemory()), + }, + ); std.debug.assert(readdir_task.subtask_count.fetchAdd(1, .Monotonic) > 0); JSC.WorkPool.schedule(&task.task); } @@ -430,8 +430,7 @@ pub const AsyncReaddirRecursiveTask = struct { return .zero; } - var task = bun.default_allocator.create(AsyncReaddirRecursiveTask) catch bun.outOfMemory(); - task.* = AsyncReaddirRecursiveTask{ + var task = AsyncReaddirRecursiveTask.new(.{ .promise = JSC.JSPromise.Strong.init(globalObject), .args = args, .has_result = .{ .raw = false }, @@ -444,7 +443,7 @@ pub const AsyncReaddirRecursiveTask = struct { .with_file_types => .{ .with_file_types = std.ArrayList(Dirent).init(bun.default_allocator) }, .buffers => .{ .buffers = std.ArrayList(Buffer).init(bun.default_allocator) }, }, - }; + }); task.ref.ref(vm); task.args.toThreadSafe(); task.tracker.didSchedule(globalObject); @@ -620,15 +619,12 @@ pub const AsyncReaddirRecursiveTask = struct { .buffers => |*res| Return.Readdir{ .buffers = res.moveToUnmanaged().items }, .files => |*res| Return.Readdir{ .files = res.moveToUnmanaged().items }, }; - var exceptionref: JSC.C.JSValueRef = null; - const out = res.toJS(globalObject, &exceptionref); - const exception = JSC.JSValue.c(exceptionref); - if (exception != .zero) { + const out = res.toJS(globalObject); + if (out == .zero) { success = false; - break :brk exception; } - break :brk out.?.value(); + break :brk out; }; var promise_value = this.promise.value(); var promise = this.promise.get(); @@ -660,8 +656,7 @@ pub const AsyncReaddirRecursiveTask = struct { bun.default_allocator.free(this.root_path.slice()); this.clearResultList(); this.promise.strong.deinit(); - - bun.default_allocator.destroy(this); + this.destroy(); } }; @@ -678,12 +673,11 @@ pub const AsyncCpSingleFileTask = struct { src: [:0]const u8, dest: [:0]const u8, ) void { - var task = bun.default_allocator.create(AsyncCpSingleFileTask) catch @panic("out of memory"); - task.* = AsyncCpSingleFileTask{ + var task = bun.new(AsyncCpSingleFileTask, .{ .cp_task = parent, .src = src, .dest = dest, - }; + }); JSC.WorkPool.schedule(&task.task); } @@ -728,7 +722,7 @@ pub const AsyncCpSingleFileTask = struct { // There is only one path buffer for both paths. 2 extra bytes are the nulls at the end of each bun.default_allocator.free(this.src.ptr[0 .. this.src.len + this.dest.len + 2]); - bun.default_allocator.destroy(this); + bun.destroy(this); } }; @@ -1965,7 +1959,7 @@ pub const Arguments = struct { }; const MkdirTemp = struct { - prefix: JSC.Node.SliceOrBuffer = .{ .buffer = .{ .buffer = JSC.ArrayBuffer.empty } }, + prefix: JSC.Node.StringOrBuffer = .{ .buffer = .{ .buffer = JSC.ArrayBuffer.empty } }, encoding: Encoding = Encoding.utf8, pub fn deinit(this: MkdirTemp) void { @@ -1983,7 +1977,7 @@ pub const Arguments = struct { pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?MkdirTemp { const prefix_value = arguments.next() orelse return MkdirTemp{}; - const prefix = JSC.Node.SliceOrBuffer.fromJS(ctx, bun.default_allocator, prefix_value) orelse { + const prefix = JSC.Node.StringOrBuffer.fromJS(ctx, bun.default_allocator, prefix_value) orelse { if (exception.* == null) { JSC.throwInvalidArguments( "prefix must be a string or TypedArray", @@ -2341,7 +2335,7 @@ pub const Arguments = struct { /// pub const Write = struct { fd: FileDescriptor, - buffer: JSC.Node.SliceWithUnderlyingStringOrBuffer, + buffer: JSC.Node.StringOrBuffer, // buffer_val: JSC.JSValue = JSC.JSValue.zero, offset: u64 = 0, length: u64 = std.math.maxInt(u64), @@ -2387,7 +2381,7 @@ pub const Arguments = struct { if (exception.* != null) return null; - const buffer = SliceWithUnderlyingStringOrBuffer.fromJS(ctx.ptr(), bun.default_allocator, arguments.next() orelse { + const buffer = StringOrBuffer.fromJS(ctx.ptr(), bun.default_allocator, arguments.next() orelse { if (exception.* == null) { JSC.throwInvalidArguments( "data is required", @@ -2397,7 +2391,7 @@ pub const Arguments = struct { ); } return null; - }, exception) orelse { + }) orelse { if (exception.* == null) { JSC.throwInvalidArguments( "data must be a string or TypedArray", @@ -2414,8 +2408,8 @@ pub const Arguments = struct { .fd = fd, .buffer = buffer, .encoding = switch (buffer) { - .SliceWithUnderlyingString => Encoding.utf8, .buffer => Encoding.buffer, + inline else => Encoding.utf8, }, }; @@ -2427,7 +2421,7 @@ pub const Arguments = struct { var current = current_; switch (buffer) { // fs.write(fd, string[, position[, encoding]], callback) - .SliceWithUnderlyingString => { + else => { if (current.isNumber()) { args.position = current.to(i52); arguments.eat(); @@ -2702,22 +2696,29 @@ pub const Arguments = struct { pub const WriteFile = struct { encoding: Encoding = Encoding.utf8, + flag: FileSystemFlags = FileSystemFlags.w, mode: Mode = 0o666, file: PathOrFileDescriptor, + + /// Encoded at the time of construction. data: StringOrBuffer, + dirfd: FileDescriptor, pub fn deinit(self: WriteFile) void { self.file.deinit(); + self.data.deinit(); } pub fn toThreadSafe(self: *WriteFile) void { self.file.toThreadSafe(); + self.data.toThreadSafe(); } pub fn deinitAndUnprotect(self: *WriteFile) void { self.file.deinitAndUnprotect(); + self.data.deinitAndUnprotect(); } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?WriteFile { @@ -2735,7 +2736,8 @@ pub const Arguments = struct { if (exception.* != null) return null; - const data = StringOrBuffer.fromJS(ctx.ptr(), bun.default_allocator, arguments.next() orelse { + const data_value = arguments.nextEat() orelse { + defer file.deinit(); if (exception.* == null) { JSC.throwInvalidArguments( "data is required", @@ -2745,29 +2747,21 @@ pub const Arguments = struct { ); } return null; - }, exception) orelse { - if (exception.* == null) { - JSC.throwInvalidArguments( - "data must be a string or TypedArray", - .{}, - ctx, - exception, - ); - } - return null; }; - if (exception.* != null) return null; - arguments.eat(); - var encoding = Encoding.buffer; var flag = FileSystemFlags.w; var mode: Mode = default_permission; + if (data_value.isString()) { + encoding = Encoding.utf8; + } + if (arguments.next()) |arg| { arguments.eat(); if (arg.isString()) { encoding = Encoding.fromJS(arg, ctx.ptr()) orelse { + defer file.deinit(); if (exception.* == null) { JSC.throwInvalidArguments( "Invalid encoding", @@ -2781,6 +2775,7 @@ pub const Arguments = struct { } else if (arg.isObject()) { if (arg.getTruthy(ctx.ptr(), "encoding")) |encoding_| { encoding = Encoding.fromJS(encoding_, ctx.ptr()) orelse { + defer file.deinit(); if (exception.* == null) { JSC.throwInvalidArguments( "Invalid encoding", @@ -2795,6 +2790,7 @@ pub const Arguments = struct { if (arg.getTruthy(ctx.ptr(), "flag")) |flag_| { flag = FileSystemFlags.fromJS(ctx, flag_, exception) orelse { + defer file.deinit(); if (exception.* == null) { JSC.throwInvalidArguments( "Invalid flag", @@ -2809,6 +2805,7 @@ pub const Arguments = struct { if (arg.getTruthy(ctx.ptr(), "mode")) |mode_| { mode = JSC.Node.modeFromJS(ctx, mode_, exception) orelse { + defer file.deinit(); if (exception.* == null) { JSC.throwInvalidArguments( "Invalid flag", @@ -2823,6 +2820,19 @@ pub const Arguments = struct { } } + const data = StringOrBuffer.fromJSWithEncodingMaybeAsync(ctx.ptr(), bun.default_allocator, data_value, encoding, arguments.will_be_async) orelse { + defer file.deinit(); + if (exception.* == null) { + JSC.throwInvalidArguments( + "data must be a string or TypedArray", + .{}, + ctx, + exception, + ); + } + return null; + }; + // Note: Signal is not implemented return WriteFile{ .file = file, @@ -3527,13 +3537,20 @@ pub const StatOrNotFound = union(enum) { .not_found => JSC.JSValue.undefined, }; } + + pub fn toJSNewlyCreated(this: *const StatOrNotFound, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + return switch (this.*) { + .stats => this.stats.toJSNewlyCreated(globalObject), + .not_found => JSC.JSValue.undefined, + }; + } }; pub const StringOrUndefined = union(enum) { string: bun.String, none: void, - pub fn toJS(this: *StringOrUndefined, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + pub fn toJS(this: *const StringOrUndefined, globalObject: *JSC.JSGlobalObject) JSC.JSValue { return switch (this.*) { .string => this.string.toJS(globalObject), .none => JSC.JSValue.undefined, @@ -3569,8 +3586,8 @@ const Return = struct { pub const Read = struct { bytes_read: u52, - pub fn toJS(this: Read, _: JSC.C.JSContextRef, _: JSC.C.ExceptionRef) JSC.C.JSValueRef { - return JSC.JSValue.jsNumberFromUint64(this.bytes_read).asObjectRef(); + pub fn toJS(this: Read, _: JSC.C.JSContextRef) JSC.JSValue { + return JSC.JSValue.jsNumberFromUint64(this.bytes_read); } }; pub const ReadPromise = struct { @@ -3580,17 +3597,17 @@ const Return = struct { .bytesRead = JSC.ZigString.init("bytesRead"), .buffer = JSC.ZigString.init("buffer"), }; - pub fn toJS(this: Read, ctx: JSC.C.JSContextRef, _: JSC.C.ExceptionRef) JSC.C.JSValueRef { + pub fn toJS(this: *const ReadPromise, ctx: *JSC.JSGlobalObject) JSC.JSValue { defer if (!this.buffer_val.isEmptyOrUndefinedOrNull()) - JSC.C.JSValueUnprotect(ctx, this.buffer_val.asObjectRef()); + this.buffer_val.unprotect(); return JSC.JSValue.createObject2( - ctx.ptr(), + ctx, &fields.bytesRead, &fields.buffer, JSC.JSValue.jsNumberFromUint64(@as(u52, @intCast(@min(std.math.maxInt(u52), this.bytes_read)))), this.buffer_val, - ).asObjectRef(); + ); } }; @@ -3604,20 +3621,20 @@ const Return = struct { }; // Excited for the issue that's like "cannot read file bigger than 2 GB" - pub fn toJS(this: Write, ctx: JSC.C.JSContextRef, exception: JSC.C.ExceptionRef) JSC.C.JSValueRef { - defer if (!this.buffer_val.isEmptyOrUndefinedOrNull() and this.buffer == .buffer) - JSC.C.JSValueUnprotect(ctx, this.buffer_val.asObjectRef()); + pub fn toJS(this: *const WritePromise, globalObject: JSC.C.JSContextRef) JSC.C.JSValueRef { + defer if (!this.buffer_val.isEmptyOrUndefinedOrNull()) + this.buffer_val.unprotect(); return JSC.JSValue.createObject2( - ctx.ptr(), + globalObject, &fields.bytesWritten, &fields.buffer, JSC.JSValue.jsNumberFromUint64(@as(u52, @intCast(@min(std.math.maxInt(u52), this.bytes_written)))), if (this.buffer == .buffer) this.buffer_val else - JSC.JSValue.fromRef(this.buffer.toJS(ctx, exception)), - ).asObjectRef(); + this.buffer.toJS(globalObject), + ); } }; pub const Write = struct { @@ -3627,14 +3644,14 @@ const Return = struct { }; // Excited for the issue that's like "cannot read file bigger than 2 GB" - pub fn toJS(this: Write, _: JSC.C.JSContextRef, _: JSC.C.ExceptionRef) JSC.C.JSValueRef { - return JSC.JSValue.jsNumberFromUint64(this.bytes_written).asObjectRef(); + pub fn toJS(this: *const Write, _: *JSC.JSGlobalObject) JSC.JSValue { + return JSC.JSValue.jsNumberFromUint64(this.bytes_written); } }; pub const Readdir = union(Tag) { with_file_types: []Dirent, - buffers: []const Buffer, + buffers: []Buffer, files: []const bun.String, pub const Tag = enum { @@ -3643,31 +3660,31 @@ const Return = struct { files, }; - pub fn toJS(this: Readdir, ctx: JSC.C.JSContextRef, exception: JSC.C.ExceptionRef) JSC.C.JSValueRef { + pub fn toJS(this: Readdir, globalObject: *JSC.JSGlobalObject) JSC.JSValue { switch (this) { .with_file_types => { defer bun.default_allocator.free(this.with_file_types); - return JSC.To.JS.withType([]const Dirent, this.with_file_types, ctx, exception); + return JSC.toJS(globalObject, []Dirent, this.with_file_types, .temporary); }, .buffers => { defer bun.default_allocator.free(this.buffers); - return JSC.To.JS.withType([]const Buffer, this.buffers, ctx, exception); + return JSC.toJS(globalObject, []Buffer, this.buffers, .temporary); }, .files => { // automatically freed - return JSC.To.JS.withType([]const bun.String, this.files, ctx, exception); + return JSC.toJS(globalObject, []const bun.String, this.files, .temporary); }, } } }; - pub const ReadFile = JSC.Node.StringOrNodeBuffer; + pub const ReadFile = JSC.Node.StringOrBuffer; pub const ReadFileWithOptions = union(enum) { string: string, buffer: JSC.Node.Buffer, null_terminated: [:0]const u8, }; - pub const Readlink = JSC.Node.StringOrBunStringOrBuffer; - pub const Realpath = JSC.Node.StringOrBunStringOrBuffer; + pub const Readlink = JSC.Node.StringOrBuffer; + pub const Realpath = JSC.Node.StringOrBuffer; pub const RealpathNative = Realpath; pub const Rename = void; pub const Rmdir = void; @@ -4998,11 +5015,7 @@ pub const NodeFS = struct { .buffer = ret.result.buffer, }, }, - .string => .{ - .result = .{ - .string = ret.result.string, - }, - }, + .string => .{ .result = .{ .string = bun.SliceWithUnderlyingString.transcodeFromOwnedSlice(@constCast(ret.result.string), args.encoding) } }, else => unreachable, }, }; @@ -5332,11 +5345,11 @@ pub const NodeFS = struct { else => if (args.path == .slice_with_underlying_string and strings.eqlLong(args.path.slice_with_underlying_string.slice(), outbuf[0..len], true)) .{ - .BunString = args.path.slice_with_underlying_string.underlying.dupeRef(), + .string = args.path.slice_with_underlying_string.dupeRef(), } else .{ - .BunString = bun.String.create(outbuf[0..len]), + .string = .{ .utf8 = .{}, .underlying = bun.String.create(outbuf[0..len]) }, }, }, }; @@ -5370,11 +5383,11 @@ pub const NodeFS = struct { else => if (args.path == .slice_with_underlying_string and strings.eqlLong(args.path.slice_with_underlying_string.slice(), buf, true)) .{ - .BunString = args.path.slice_with_underlying_string.underlying.dupeRef(), + .string = args.path.slice_with_underlying_string.dupeRef(), } else .{ - .BunString = bun.String.create(buf), + .string = .{ .utf8 = .{}, .underlying = bun.String.create(buf) }, }, }, }; @@ -5419,11 +5432,11 @@ pub const NodeFS = struct { else => if (args.path == .slice_with_underlying_string and strings.eqlLong(args.path.slice_with_underlying_string.slice(), buf, true)) .{ - .BunString = args.path.slice_with_underlying_string.underlying.dupeRef(), + .string = args.path.slice_with_underlying_string.dupeRef(), } else .{ - .BunString = bun.String.create(buf), + .string = .{ .utf8 = .{}, .underlying = bun.String.create(buf) }, }, }, }; diff --git a/src/bun.js/node/node_fs_binding.zig b/src/bun.js/node/node_fs_binding.zig index 24c0eb25dd..8fe3909593 100644 --- a/src/bun.js/node/node_fs_binding.zig +++ b/src/bun.js/node/node_fs_binding.zig @@ -26,6 +26,7 @@ fn callSync(comptime FunctionEnum: NodeFSFunctionEnum) NodeFSFunction { const Arguments = comptime function.params[1].type.?; const FormattedName = comptime [1]u8{std.ascii.toUpper(@tagName(FunctionEnum)[0])} ++ @tagName(FunctionEnum)[1..]; const Result = comptime JSC.Maybe(@field(JSC.Node.NodeFS.ReturnType, FormattedName)); + _ = Result; const NodeBindingClosure = struct { pub fn bind( @@ -59,33 +60,18 @@ fn callSync(comptime FunctionEnum: NodeFSFunctionEnum) NodeFSFunction { globalObject.throwValue(exception1); return .zero; } - - const result: Result = Function( + var result = Function( &this.node_fs, args, comptime Flavor.sync, ); - switch (result) { .err => |err| { globalObject.throwValue(JSC.JSValue.c(err.toJS(globalObject))); return .zero; }, - .result => |res| { - if (comptime Result.ReturnType != void) { - const out = JSC.JSValue.c(JSC.To.JS.withType(Result.ReturnType, res, globalObject, &exceptionref)); - const exception = JSC.JSValue.c(exceptionref); - if (exception != .zero) { - globalObject.throwValue(exception); - return .zero; - } - - return out; - } else { - return JSC.JSValue.jsUndefined(); - } - - unreachable; + .result => |*res| { + return globalObject.toJS(res, .temporary); }, } } @@ -110,6 +96,7 @@ fn call(comptime FunctionEnum: NodeFSFunctionEnum) NodeFSFunction { var arguments = callframe.arguments(8); var slice = ArgumentsSlice.init(globalObject.bunVM(), arguments.ptr[0..arguments.len]); + slice.will_be_async = true; var exceptionref: JSC.C.JSValueRef = null; const args = if (comptime Arguments != void) (Arguments.fromJS(globalObject, &slice, &exceptionref) orelse { diff --git a/src/bun.js/node/node_fs_stat_watcher.zig b/src/bun.js/node/node_fs_stat_watcher.zig index b4dfda82d2..4451caef50 100644 --- a/src/bun.js/node/node_fs_stat_watcher.zig +++ b/src/bun.js/node/node_fs_stat_watcher.zig @@ -25,9 +25,9 @@ const log = bun.Output.scoped(.StatWatcher, false); fn statToJSStats(globalThis: *JSC.JSGlobalObject, stats: bun.Stat, bigint: bool) JSC.JSValue { if (bigint) { - return StatsBig.initWithAllocator(globalThis.allocator(), stats).toJS(globalThis); + return bun.new(StatsBig, StatsBig.init(stats)).toJS(globalThis); } else { - return StatsSmall.initWithAllocator(globalThis.allocator(), stats).toJS(globalThis); + return bun.new(StatsSmall, StatsSmall.init(stats)).toJS(globalThis); } } diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index db0e54ef8b..fb3cf53ed4 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -188,247 +188,124 @@ pub fn Maybe(comptime ResultType: type) type { }; } -pub const StringOrBuffer = union(Tag) { - string: string, - buffer: Buffer, - - pub const Tag = enum { string, buffer }; - - pub fn slice(this: StringOrBuffer) []const u8 { - return switch (this) { - .string => this.string, - .buffer => this.buffer.slice(), - }; - } - - pub export fn external_string_finalizer(_: ?*anyopaque, _: JSC.C.JSStringRef, buffer: *anyopaque, byteLength: usize) void { - bun.default_allocator.free(@as([*]const u8, @ptrCast(buffer))[0..byteLength]); - } - - pub fn toJS(this: StringOrBuffer, ctx: JSC.C.JSContextRef, exception: JSC.C.ExceptionRef) JSC.C.JSValueRef { - return switch (this) { - .string => { - if (this.string.len == 0) - return JSC.ZigString.Empty.toValue(ctx).asObjectRef(); - - const input = this.string; - if (strings.toUTF16Alloc(bun.default_allocator, input, false) catch null) |utf16| { - bun.default_allocator.free(@constCast(input)); - return JSC.ZigString.toExternalU16(utf16.ptr, utf16.len, ctx.ptr()).asObjectRef(); - } - - return JSC.ZigString.init(input).toExternalValue(ctx.ptr()).asObjectRef(); - }, - .buffer => this.buffer.toJSObjectRef(ctx, exception), - }; - } - - pub fn fromJS(global: *JSC.JSGlobalObject, allocator: std.mem.Allocator, value: JSC.JSValue, exception: JSC.C.ExceptionRef) ?StringOrBuffer { - _ = exception; - return switch (value.jsType()) { - JSC.JSValue.JSType.String, JSC.JSValue.JSType.StringObject, JSC.JSValue.JSType.DerivedStringObject, JSC.JSValue.JSType.Object => { - var zig_str = value.toSlice(global, allocator); - return StringOrBuffer{ .string = zig_str.slice() }; - }, - - .ArrayBuffer, - .Int8Array, - .Uint8Array, - .Uint8ClampedArray, - .Int16Array, - .Uint16Array, - .Int32Array, - .Uint32Array, - .Float32Array, - .Float64Array, - .BigInt64Array, - .BigUint64Array, - .DataView, - => StringOrBuffer{ - .buffer = Buffer.fromArrayBuffer(global, value), - }, - else => null, - }; - } -}; - -pub const StringOrBunStringOrBuffer = union(enum) { - BunString: bun.String, - string: string, - buffer: Buffer, - - pub fn toJS(this: StringOrBunStringOrBuffer, ctx: JSC.C.JSContextRef, exception: JSC.C.ExceptionRef) JSC.C.JSValueRef { - return switch (this) { - .string => { - if (this.string.len == 0) - return JSC.ZigString.Empty.toValue(ctx).asObjectRef(); - - const input = this.string; - if (strings.toUTF16Alloc(bun.default_allocator, input, false) catch null) |utf16| { - bun.default_allocator.free(@constCast(input)); - return JSC.ZigString.toExternalU16(utf16.ptr, utf16.len, ctx.ptr()).asObjectRef(); - } - - return JSC.ZigString.init(input).toExternalValue(ctx.ptr()).asObjectRef(); - }, - .buffer => this.buffer.toJSObjectRef(ctx, exception), - .BunString => { - defer this.BunString.deref(); - return this.BunString.toJS(ctx).asObjectRef(); - }, - }; - } - - pub fn fromJS(global: *JSC.JSGlobalObject, allocator: std.mem.Allocator, value: JSC.JSValue, exception: JSC.C.ExceptionRef) ?StringOrBuffer { - _ = exception; - return switch (value.jsType()) { - JSC.JSValue.JSType.String, JSC.JSValue.JSType.StringObject, JSC.JSValue.JSType.DerivedStringObject, JSC.JSValue.JSType.Object => { - var zig_str = value.toSlice(global, allocator); - return StringOrBuffer{ .string = zig_str.slice() }; - }, - - .ArrayBuffer, - .Int8Array, - .Uint8Array, - .Uint8ClampedArray, - .Int16Array, - .Uint16Array, - .Int32Array, - .Uint32Array, - .Float32Array, - .Float64Array, - .BigInt64Array, - .BigUint64Array, - .DataView, - => StringOrBuffer{ - .buffer = Buffer.fromArrayBuffer(global, value), - }, - else => null, - }; - } -}; - -pub const SliceWithUnderlyingStringOrBuffer = union(enum) { - SliceWithUnderlyingString: bun.SliceWithUnderlyingString, +pub const StringOrBuffer = union(enum) { + string: bun.SliceWithUnderlyingString, + threadsafe_string: bun.SliceWithUnderlyingString, + encoded_slice: JSC.ZigString.Slice, buffer: Buffer, pub fn toThreadSafe(this: *@This()) void { switch (this.*) { - .SliceWithUnderlyingString => this.SliceWithUnderlyingString.toThreadSafe(), - else => {}, + .string => { + this.string.toThreadSafe(); + this.* = .{ + .threadsafe_string = this.string, + }; + }, + .threadsafe_string => {}, + .encoded_slice => {}, + .buffer => {}, } } - pub fn toJS(this: *SliceWithUnderlyingStringOrBuffer, ctx: JSC.C.JSContextRef, exception: JSC.C.ExceptionRef) JSC.C.JSValueRef { - return switch (this) { - .SliceWithUnderlyingStringOrBuffer => { + pub fn fromJSToOwnedSlice(globalObject: *JSC.JSGlobalObject, value: JSC.JSValue, allocator: std.mem.Allocator) ![]u8 { + if (value.asArrayBuffer(globalObject)) |array_buffer| { + defer globalObject.vm().reportExtraMemory(array_buffer.len); + + return try allocator.dupe(u8, array_buffer.byteSlice()); + } + + var str = bun.String.tryFromJS(value, globalObject) orelse return error.JSError; + + const result = try str.toOwnedSlice(allocator); + defer globalObject.vm().reportExtraMemory(result.len); + return result; + } + + pub fn toJS(this: *StringOrBuffer, ctx: JSC.C.JSContextRef) JSC.JSValue { + return switch (this.*) { + inline .threadsafe_string, .string => |*str| { defer { - this.SliceWithUnderlyingString.deinit(); - this.SliceWithUnderlyingString.underlying = bun.String.empty; - this.SliceWithUnderlyingString.utf8 = .{}; + str.deinit(); + str.* = .{}; } - return this.SliceWithUnderlyingString.underlying.toJS(ctx); + return str.toJS(ctx); + }, + .encoded_slice => { + defer { + this.encoded_slice.deinit(); + this.encoded_slice = .{}; + } + + const str = bun.String.create(this.encoded_slice.slice()); + defer str.deref(); + return str.toJS(ctx); + }, + .buffer => { + if (this.buffer.buffer.value != .zero) { + return this.buffer.buffer.value; + } + + return this.buffer.toNodeBuffer(ctx); }, - .buffer => this.buffer.toJSObjectRef(ctx, exception), }; } - pub fn slice(this: *const SliceWithUnderlyingStringOrBuffer) []const u8 { + pub fn slice(this: *const StringOrBuffer) []const u8 { return switch (this.*) { - .SliceWithUnderlyingString => this.SliceWithUnderlyingString.slice(), - .buffer => this.buffer.slice(), + inline else => |*str| str.slice(), }; } - pub fn deinit(this: *const SliceWithUnderlyingStringOrBuffer) void { + pub fn deinit(this: *const StringOrBuffer) void { switch (this.*) { - .SliceWithUnderlyingString => |*str| { + inline .threadsafe_string, .string => |*str| { str.deinit(); }, + .encoded_slice => |*encoded| { + encoded.deinit(); + }, else => {}, } } - pub fn deinitAndUnprotect(this: *const SliceWithUnderlyingStringOrBuffer) void { + pub fn deinitAndUnprotect(this: *const StringOrBuffer) void { switch (this.*) { - .SliceWithUnderlyingString => |*str| { + inline .threadsafe_string, .string => |*str| { str.deinit(); }, .buffer => |buffer| { buffer.buffer.value.unprotect(); }, + .encoded_slice => |*encoded| { + encoded.deinit(); + }, } } - pub fn fromJS(global: *JSC.JSGlobalObject, allocator: std.mem.Allocator, value: JSC.JSValue, exception: JSC.C.ExceptionRef) ?SliceWithUnderlyingStringOrBuffer { - _ = exception; + pub fn fromJSMaybeAsync( + global: *JSC.JSGlobalObject, + allocator: std.mem.Allocator, + value: JSC.JSValue, + is_async: bool, + ) ?StringOrBuffer { return switch (value.jsType()) { JSC.JSValue.JSType.String, JSC.JSValue.JSType.StringObject, JSC.JSValue.JSType.DerivedStringObject, JSC.JSValue.JSType.Object => { var str = bun.String.tryFromJS(value, global) orelse return null; - str.ref(); - return SliceWithUnderlyingStringOrBuffer{ .SliceWithUnderlyingString = str.toSlice(allocator) }; - }, + if (is_async) { + var sliced = str.toThreadSafeSlice(allocator); + sliced.reportExtraMemory(global.vm()); - .ArrayBuffer, - .Int8Array, - .Uint8Array, - .Uint8ClampedArray, - .Int16Array, - .Uint16Array, - .Int32Array, - .Uint32Array, - .Float32Array, - .Float64Array, - .BigInt64Array, - .BigUint64Array, - .DataView, - => SliceWithUnderlyingStringOrBuffer{ - .buffer = Buffer.fromArrayBuffer(global, value), - }, - else => null, - }; - } -}; + if (sliced.underlying.isEmpty()) { + return StringOrBuffer{ .encoded_slice = sliced.utf8 }; + } -/// Like StringOrBuffer but actually returns a Node.js Buffer -pub const StringOrNodeBuffer = union(Tag) { - string: string, - buffer: Buffer, - - pub const Tag = enum { string, buffer }; - - pub fn slice(this: StringOrNodeBuffer) []const u8 { - return switch (this) { - .string => this.string, - .buffer => this.buffer.slice(), - }; - } - - pub fn toJS(this: StringOrNodeBuffer, ctx: JSC.C.JSContextRef, _: JSC.C.ExceptionRef) JSC.C.JSValueRef { - return switch (this) { - .string => { - const input = this.string; - if (this.string.len == 0) - return JSC.ZigString.Empty.toValue(ctx).asObjectRef(); - - if (strings.toUTF16Alloc(bun.default_allocator, input, false) catch null) |utf16| { - bun.default_allocator.free(@constCast(input)); - return JSC.ZigString.toExternalU16(utf16.ptr, utf16.len, ctx.ptr()).asObjectRef(); + return StringOrBuffer{ .threadsafe_string = sliced }; + } else { + str.ref(); + return StringOrBuffer{ .string = str.toSlice(allocator) }; } - - return JSC.ZigString.init(input).toExternalValue(ctx.ptr()).asObjectRef(); - }, - .buffer => this.buffer.toNodeBuffer(ctx), - }; - } - - pub fn fromJS(global: *JSC.JSGlobalObject, allocator: std.mem.Allocator, value: JSC.JSValue, exception: JSC.C.ExceptionRef) ?StringOrBuffer { - _ = exception; - return switch (value.jsType()) { - JSC.JSValue.JSType.String, JSC.JSValue.JSType.StringObject, JSC.JSValue.JSType.DerivedStringObject, JSC.JSValue.JSType.Object => { - var zig_str = value.toSlice(global, allocator); - return StringOrNodeBuffer{ .string = zig_str.slice() }; }, .ArrayBuffer, @@ -450,119 +327,48 @@ pub const StringOrNodeBuffer = union(Tag) { else => null, }; } -}; - -pub const SliceOrBuffer = union(Tag) { - string: JSC.ZigString.Slice, - buffer: Buffer, - - pub fn ensureCloned(this: *SliceOrBuffer, allocator: std.mem.Allocator) !void { - if (this.* == .string) { - this.string = try this.string.cloneIfNeeded(allocator); - return; - } - - const bytes = this.buffer.buffer.byteSlice(); - this.* = .{ - .string = JSC.ZigString.Slice.from(try allocator.dupe(u8, bytes), allocator), - }; + pub fn fromJS(global: *JSC.JSGlobalObject, allocator: std.mem.Allocator, value: JSC.JSValue) ?StringOrBuffer { + return fromJSMaybeAsync(global, allocator, value, false); + } + pub fn fromJSWithEncoding(global: *JSC.JSGlobalObject, allocator: std.mem.Allocator, value: JSC.JSValue, encoding: Encoding) ?StringOrBuffer { + return fromJSWithEncodingMaybeAsync(global, allocator, value, encoding, false); } - pub fn deinit(this: SliceOrBuffer) void { - switch (this) { - .string => { - this.string.deinit(); - }, - .buffer => {}, - } - } - - pub fn toThreadSafe(this: *SliceOrBuffer) void { - var buffer: ?Buffer = null; - if (this.* == .buffer) { - buffer = this.buffer; - this.buffer.buffer.value.ensureStillAlive(); - } - defer { - if (buffer) |buf| { - buf.buffer.value.unprotect(); - } - } - this.ensureCloned(bun.default_allocator) catch unreachable; - } - - pub const Tag = enum { string, buffer }; - - pub fn slice(this: SliceOrBuffer) []const u8 { - return switch (this) { - .string => this.string.slice(), - .buffer => this.buffer.slice(), - }; - } - - pub fn toJS(this: SliceOrBuffer, ctx: JSC.C.JSContextRef, exception: JSC.C.ExceptionRef) JSC.C.JSValueRef { - return switch (this) { - string => { - const input = this.string.slice(); - if (strings.toUTF16Alloc(bun.default_allocator, input, false) catch null) |utf16| { - bun.default_allocator.free(@constCast(input)); - return JSC.ZigString.toExternalU16(utf16.p.tr, utf16.len, ctx.ptr()).asObjectRef(); - } - - return JSC.ZigString.init(input).toExternalValue(ctx.ptr()).asObjectRef(); - }, - .buffer => this.buffer.toJSObjectRef(ctx, exception), - }; - } - - pub fn fromJS(global: *JSC.JSGlobalObject, allocator: std.mem.Allocator, value: JSC.JSValue) ?SliceOrBuffer { - if (!value.isEmpty() and value.isCell() and value.jsType().isTypedArray()) { - return SliceOrBuffer{ - .buffer = JSC.MarkedArrayBuffer{ - .buffer = value.asArrayBuffer(global) orelse return null, - .allocator = null, - }, - }; - } - - if (value.isEmpty()) { - return null; - } - - var str = value.toStringOrNull(global) orelse return null; - return SliceOrBuffer{ .string = str.toSlice(global, allocator) }; - } - - pub fn fromJSWithEncoding(global: *JSC.JSGlobalObject, allocator: std.mem.Allocator, value: JSC.JSValue, encoding_value: JSC.JSValue) ?SliceOrBuffer { + pub fn fromJSWithEncodingMaybeAsync(global: *JSC.JSGlobalObject, allocator: std.mem.Allocator, value: JSC.JSValue, encoding: Encoding, is_async: bool) ?StringOrBuffer { if (value.isCell() and value.jsType().isTypedArray()) { - return SliceOrBuffer{ - .buffer = JSC.MarkedArrayBuffer{ - .buffer = value.asArrayBuffer(global) orelse return null, - .allocator = null, - }, + return StringOrBuffer{ + .buffer = Buffer.fromTypedArray(global, value), }; } + if (encoding == .utf8) { + return fromJSMaybeAsync(global, allocator, value, is_async); + } + + var str = bun.String.tryFromJS(value, global) orelse return null; + if (str.isEmpty()) { + return fromJSMaybeAsync(global, allocator, value, is_async); + } + + const out = str.encode(encoding); + defer global.vm().reportExtraMemory(out.len); + + return .{ + .encoded_slice = JSC.ZigString.Slice.from(out, bun.default_allocator), + }; + } + + pub fn fromJSWithEncodingValue(global: *JSC.JSGlobalObject, allocator: std.mem.Allocator, value: JSC.JSValue, encoding_value: JSC.JSValue) ?StringOrBuffer { const encoding: Encoding = brk: { - if (encoding_value.isEmptyOrUndefinedOrNull()) + if (!encoding_value.isCell()) break :brk .utf8; break :brk Encoding.fromJS(encoding_value, global) orelse .utf8; }; - if (encoding == .utf8) { - return fromJS(global, allocator, value); - } - - var zig_str = value.getZigString(global); - if (zig_str.len == 0) { - return fromJS(global, allocator, value); - } - - const out = zig_str.encode(encoding); - - return .{ .string = JSC.ZigString.Slice.from(out, allocator) }; + return fromJSWithEncoding(global, allocator, value, encoding); } }; + pub const ErrorCode = @import("./nodejs_error_code.zig").Code; // We can't really use Zig's error handling for syscalls because Node.js expects the "real" errno to be returned @@ -713,40 +519,49 @@ pub fn CallbackTask(comptime Result: type) type { }; } -pub const PathLike = union(Tag) { +pub const PathLike = union(enum) { string: bun.PathString, buffer: Buffer, slice_with_underlying_string: bun.SliceWithUnderlyingString, - - pub const Tag = enum { string, buffer, slice_with_underlying_string }; + threadsafe_string: bun.SliceWithUnderlyingString, + encoded_slice: JSC.ZigString.Slice, pub fn estimatedSize(this: *const PathLike) usize { return switch (this.*) { .string => this.string.estimatedSize(), .buffer => this.buffer.slice().len, - .slice_with_underlying_string => 0, + .threadsafe_string, .slice_with_underlying_string => 0, + .encoded_slice => this.encoded_slice.slice().len, }; } pub fn deinit(this: *const PathLike) void { - if (this.* == .slice_with_underlying_string) { - this.slice_with_underlying_string.deinit(); + switch (this.*) { + .string, .buffer => {}, + inline else => |*str| { + str.deinit(); + }, } } pub fn toThreadSafe(this: *PathLike) void { - if (this.* == .slice_with_underlying_string) { - this.slice_with_underlying_string.toThreadSafe(); - } - - if (this.* == .buffer) { - this.buffer.buffer.value.protect(); + switch (this.*) { + .slice_with_underlying_string => { + this.slice_with_underlying_string.toThreadSafe(); + this.* = .{ + .threadsafe_string = this.slice_with_underlying_string, + }; + }, + .buffer => { + this.buffer.buffer.value.protect(); + }, + else => {}, } } pub fn deinitAndUnprotect(this: *const PathLike) void { switch (this.*) { - .slice_with_underlying_string => |val| { + inline .encoded_slice, .threadsafe_string, .slice_with_underlying_string => |*val| { val.deinit(); }, .buffer => |val| { @@ -758,9 +573,7 @@ pub const PathLike = union(Tag) { pub inline fn slice(this: PathLike) string { return switch (this) { - .string => this.string.slice(), - .buffer => this.buffer.slice(), - .slice_with_underlying_string => this.slice_with_underlying_string.slice(), + inline else => |*str| str.slice(), }; } @@ -811,11 +624,21 @@ pub const PathLike = union(Tag) { return sliced.ptr[0..sliced.len :0]; } - pub fn toJS(this: PathLike, ctx: JSC.C.JSContextRef, exception: JSC.C.ExceptionRef) JSC.C.JSValueRef { - return switch (this) { - .string => this.string.toJS(ctx, exception), - .buffer => this.buffer.toJSObjectRef(ctx, exception), - .slice_with_underlying_string => this.slice_with_underlying_string.toJS(ctx).asObjectRef(), + pub fn toJS(this: *const PathLike, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + return switch (this.*) { + .string => this.string.toJS(globalObject, null), + .buffer => this.buffer.toJS(globalObject), + inline .threadsafe_string, .slice_with_underlying_string => |*str| str.toJS(globalObject), + .encoded_slice => |encoded| { + if (this.encoded_slice.allocator.get()) |allocator| { + // Is this a globally-allocated slice? + if (allocator.vtable == bun.default_allocator.vtable) {} + } + + const str = bun.String.create(encoded.slice()); + defer str.deref(); + return str.toJS(globalObject); + }, else => unreachable, }; } @@ -859,27 +682,67 @@ pub const PathLike = union(Tag) { return null; } - str.ref(); + if (arguments.will_be_async) { + var sliced = str.toThreadSafeSlice(allocator); + sliced.reportExtraMemory(ctx.vm()); - return PathLike{ .slice_with_underlying_string = str.toSlice(allocator) }; + if (sliced.underlying.isEmpty()) { + return PathLike{ .encoded_slice = sliced.utf8 }; + } + + return PathLike{ .threadsafe_string = sliced }; + } else { + var sliced = str.toSlice(allocator); + + // Costs nothing to keep both around. + if (sliced.isWTFAllocated()) { + str.ref(); + return PathLike{ .slice_with_underlying_string = sliced }; + } + + sliced.reportExtraMemory(ctx.vm()); + + // It is expensive to keep both around. + return PathLike{ .encoded_slice = sliced.utf8 }; + } }, else => { if (arg.as(JSC.DOMURL)) |domurl| { - const path_str: bun.String = domurl.fileSystemPath(); - defer path_str.deref(); - if (path_str.isEmpty()) { + var str: bun.String = domurl.fileSystemPath(); + defer str.deref(); + if (str.isEmpty()) { JSC.throwInvalidArguments("URL must be a non-empty \"file:\" path", .{}, ctx, exception); return null; } arguments.eat(); - if (!Valid.pathStringLength(path_str.length(), ctx, exception)) { + if (!Valid.pathStringLength(str.length(), ctx, exception)) { return null; } - return PathLike{ - .slice_with_underlying_string = path_str.toSlice(allocator), - }; + if (arguments.will_be_async) { + var sliced = str.toThreadSafeSlice(allocator); + sliced.reportExtraMemory(ctx.vm()); + + if (sliced.underlying.isEmpty()) { + return PathLike{ .encoded_slice = sliced.utf8 }; + } + + return PathLike{ .threadsafe_string = sliced }; + } else { + var sliced = str.toSlice(allocator); + + // Costs nothing to keep both around. + if (sliced.isWTFAllocated()) { + str.ref(); + return PathLike{ .slice_with_underlying_string = sliced }; + } + + sliced.reportExtraMemory(ctx.vm()); + + // It is expensive to keep both around. + return PathLike{ .encoded_slice = sliced.utf8 }; + } } return null; @@ -1012,6 +875,7 @@ pub const ArgumentsSlice = struct { all: []const JSC.JSValue, threw: bool = false, protected: std.bit_set.IntegerBitSet(32) = std.bit_set.IntegerBitSet(32).initEmpty(), + will_be_async: bool = false, pub fn unprotect(this: *ArgumentsSlice) void { var iter = this.protected.iterator(.{}); @@ -1412,11 +1276,10 @@ pub fn StatType(comptime Big: bool) type { const Date = packed struct { value: Float, - pub inline fn toJS(this: @This(), ctx: JSC.C.JSContextRef, exception: JSC.C.ExceptionRef) JSC.C.JSValueRef { + pub inline fn toJS(this: @This(), globalObject: *JSC.JSGlobalObject) JSC.JSValue { const milliseconds = JSC.JSValue.jsNumber(this.value); const array: [1]JSC.C.JSValueRef = .{milliseconds.asObjectRef()}; - const obj = JSC.C.JSObjectMakeDate(ctx, 1, &array, exception); - return obj; + return JSC.JSValue.c(JSC.C.JSObjectMakeDate(globalObject, 1, &array, null)); } }; @@ -1470,25 +1333,27 @@ pub fn StatType(comptime Big: bool) type { } } - fn getter(comptime field: std.meta.FieldEnum(This)) JSC.To.Cpp.PropertyGetter(This) { + const PropertyGetter = fn (this: *This, globalThis: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue; + + fn getter(comptime field: std.meta.FieldEnum(This)) PropertyGetter { return struct { pub fn callback(this: *This, globalThis: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { const value = @field(this, @tagName(field)); if (comptime (Big and @typeInfo(@TypeOf(value)) == .Int)) { return JSC.JSValue.fromInt64NoTruncate(globalThis, @intCast(value)); } - return JSC.toJS(globalThis, value, null); + return globalThis.toJS(value, .temporary); } }.callback; } - fn dateGetter(comptime field: std.meta.FieldEnum(This)) JSC.To.Cpp.PropertyGetter(This) { + fn dateGetter(comptime field: std.meta.FieldEnum(This)) PropertyGetter { return struct { pub fn callback(this: *This, globalThis: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { const value = @field(this, @tagName(field)); // Doing `Date{ ... }` here shouldn't actually change the memory layout of `value` // but it will tell comptime code how to convert the i64/f64 to a JS Date. - return JSC.toJS(globalThis, Date{ .value = value }, null); + return globalThis.toJS(Date{ .value = value }, .temporary); } }.callback; } @@ -1589,7 +1454,7 @@ pub fn StatType(comptime Big: bool) type { // TODO: BigIntStats includes a `_checkModeProperty` but I dont think anyone actually uses it. pub fn finalize(this: *This) callconv(.C) void { - bun.default_allocator.destroy(this); + bun.destroy(this); } pub fn init(stat_: bun.Stat) This { @@ -1637,16 +1502,12 @@ pub fn StatType(comptime Big: bool) type { // dev, mode, nlink, uid, gid, rdev, blksize, ino, size, blocks, atimeMs, mtimeMs, ctimeMs, birthtimeMs var args = callFrame.argumentsPtr()[0..@min(callFrame.argumentsCount(), 14)]; - const this = globalThis.allocator().create(This) catch { - globalThis.throwOutOfMemory(); - return null; - }; - const atime_ms: f64 = if (args.len > 10 and args[10].isNumber()) args[10].asNumber() else 0; const mtime_ms: f64 = if (args.len > 11 and args[11].isNumber()) args[11].asNumber() else 0; const ctime_ms: f64 = if (args.len > 12 and args[12].isNumber()) args[12].asNumber() else 0; const birthtime_ms: f64 = if (args.len > 13 and args[13].isNumber()) args[13].asNumber() else 0; - this.* = .{ + + const this = bun.new(This, .{ .dev = if (args.len > 0 and args[0].isNumber()) @intCast(args[0].toInt32()) else 0, .mode = if (args.len > 1 and args[1].isNumber()) args[1].toInt32() else 0, .nlink = if (args.len > 2 and args[2].isNumber()) args[2].toInt32() else 0, @@ -1661,7 +1522,8 @@ pub fn StatType(comptime Big: bool) type { .mtime_ms = mtime_ms, .ctime_ms = ctime_ms, .birthtime_ms = birthtime_ms, - }; + }); + return this; } @@ -1693,12 +1555,19 @@ pub const Stats = union(enum) { } } - pub inline fn toJS(this: *Stats, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + pub fn toJSNewlyCreated(this: *const Stats, globalObject: *JSC.JSGlobalObject) JSC.JSValue { return switch (this.*) { - .big => this.big.toJS(globalObject), - .small => this.small.toJS(globalObject), + .big => bun.new(StatsBig, this.big).toJS(globalObject), + .small => bun.new(StatsSmall, this.small).toJS(globalObject), }; } + + pub inline fn toJS(this: *Stats, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + _ = this; + _ = globalObject; + + @compileError("Only use Stats.toJSNewlyCreated() or Stats.toJS() directly on a StatsBig or StatsSmall"); + } }; /// A class representing a directory stream. @@ -1733,6 +1602,11 @@ pub const Dirent = struct { return null; } + pub fn toJSNewlyCreated(this: *const Dirent, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + var out = bun.new(Dirent, this.*); + return out.toJS(globalObject); + } + pub fn getName(this: *Dirent, globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { return this.name.toJS(globalObject); } @@ -1789,7 +1663,7 @@ pub const Dirent = struct { pub fn finalize(this: *Dirent) callconv(.C) void { this.name.deref(); - bun.default_allocator.destroy(this); + bun.destroy(this); } }; diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 97d929cc5a..da53f2f9b5 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -299,7 +299,7 @@ pub const Blob = struct { comptime Reader: type, reader: Reader, ) !JSValue { - const allocator = globalThis.allocator(); + const allocator = bun.default_allocator; const version = try reader.readInt(u8, .little); _ = version; @@ -320,8 +320,7 @@ pub const Blob = struct { const bytes = try readSlice(reader, bytes_len, allocator); const blob = Blob.init(bytes, allocator, globalThis); - const blob_ = try allocator.create(Blob); - blob_.* = blob; + const blob_ = bun.new(Blob, blob); break :brk blob_; }, @@ -332,13 +331,12 @@ pub const Blob = struct { .fd => { const fd = try reader.readInt(bun.FileDescriptor, .little); - const blob = try allocator.create(Blob); - blob.* = Blob.findOrCreateFileFromPath( + const blob = bun.new(Blob, Blob.findOrCreateFileFromPath( JSC.Node.PathOrFileDescriptor{ .fd = fd, }, globalThis, - ); + )); break :brk blob; }, @@ -347,15 +345,14 @@ pub const Blob = struct { const path = try readSlice(reader, path_len, default_allocator); - const blob = try allocator.create(Blob); - blob.* = Blob.findOrCreateFileFromPath( + const blob = bun.new(Blob, Blob.findOrCreateFileFromPath( JSC.Node.PathOrFileDescriptor{ .path = .{ .string = bun.PathString.init(path), }, }, globalThis, - ); + )); break :brk blob; }, @@ -364,9 +361,7 @@ pub const Blob = struct { return .zero; }, .empty => brk: { - const blob = try allocator.create(Blob); - blob.* = Blob.initEmpty(globalThis); - break :brk blob; + break :brk bun.new(Blob, Blob.initEmpty(globalThis)); }, }; blob.allocator = allocator; @@ -500,8 +495,7 @@ pub const Blob = struct { export fn Blob__dupe(ptr: *anyopaque) *Blob { var this = bun.cast(*Blob, ptr); - var new = bun.default_allocator.create(Blob) catch unreachable; - new.* = this.dupeWithContentType(true); + var new = bun.new(Blob, this.dupeWithContentType(true)); new.allocator = bun.default_allocator; return new; } @@ -619,7 +613,7 @@ pub const Blob = struct { pub fn run(handler: *@This(), blob_: Store.CopyFile.ResultType) void { var promise = handler.promise; const globalThis = handler.globalThis; - bun.default_allocator.destroy(handler); + bun.destroy(handler); const blob = blob_ catch |err| { var error_string = ZigString.init( std.fmt.allocPrint(bun.default_allocator, "Failed to write file \"{s}\"", .{bun.asByteSlice(@errorName(err))}) catch unreachable, @@ -629,8 +623,7 @@ pub const Blob = struct { promise.reject(globalThis, error_string.toErrorInstance(globalThis)); return; }; - var _blob = bun.default_allocator.create(Blob) catch unreachable; - _blob.* = blob; + var _blob = bun.new(Blob, blob); _blob.allocator = bun.default_allocator; promise.resolve( globalThis, @@ -692,14 +685,14 @@ pub const Blob = struct { file_blob.detach(); _ = value.use(); this.promise.strong.deinit(); - bun.default_allocator.destroy(this); + bun.destroy(this); promise.reject(globalThis, err); }, .Used => { file_blob.detach(); _ = value.use(); this.promise.strong.deinit(); - bun.default_allocator.destroy(this); + bun.destroy(this); promise.reject(globalThis, ZigString.init("Body was used after it was consumed").toErrorInstance(globalThis)); }, .WTFStringImpl, @@ -730,7 +723,7 @@ pub const Blob = struct { file_blob.detach(); this.promise.strong.deinit(); - bun.default_allocator.destroy(this); + bun.destroy(this); }, .Locked => { value.Locked.onReceiveValue = thenWrap; @@ -757,10 +750,9 @@ pub const Blob = struct { const source_type = std.meta.activeTag(source_blob.store.?.data); if (destination_type == .file and source_type == .bytes) { - var write_file_promise = bun.default_allocator.create(WriteFilePromise) catch unreachable; - write_file_promise.* = .{ - .globalThis = ctx.ptr(), - }; + var write_file_promise = bun.new(WriteFilePromise, .{ + .globalThis = ctx, + }); const file_copier = Store.WriteFile.create( bun.default_allocator, @@ -801,8 +793,8 @@ pub const Blob = struct { // eventually, this could be like Buffer.concat var clone = source_blob.dupe(); clone.allocator = bun.default_allocator; - var cloned = bun.default_allocator.create(Blob) catch unreachable; - cloned.* = clone; + const cloned = bun.new(Blob, clone); + cloned.allocator = bun.default_allocator; return JSPromise.resolvedPromiseValue(ctx.ptr(), cloned.toJS(ctx)); } else if (destination_type == .bytes and source_type == .file) { var fake_call_frame: [8]JSC.JSValue = undefined; @@ -1016,13 +1008,12 @@ pub const Blob = struct { return JSC.JSPromise.rejectedPromiseValue(globalThis, err); }, .Locked => { - var task = bun.default_allocator.create(WriteFileWaitFromLockedValueTask) catch unreachable; - task.* = WriteFileWaitFromLockedValueTask{ + var task = bun.new(WriteFileWaitFromLockedValueTask, .{ .globalThis = globalThis, .file_blob = destination_blob, .promise = JSC.JSPromise.Strong.init(globalThis), .mkdirp_if_not_exists = mkdirp_if_not_exists orelse true, - }; + }); response.body.value.Locked.task = task; response.body.value.Locked.onReceiveValue = WriteFileWaitFromLockedValueTask.thenWrap; @@ -1050,13 +1041,12 @@ pub const Blob = struct { return JSC.JSPromise.rejectedPromiseValue(globalThis, err); }, .Locked => { - var task = bun.default_allocator.create(WriteFileWaitFromLockedValueTask) catch unreachable; - task.* = WriteFileWaitFromLockedValueTask{ + var task = bun.new(WriteFileWaitFromLockedValueTask, .{ .globalThis = globalThis, .file_blob = destination_blob, .promise = JSC.JSPromise.Strong.init(globalThis), .mkdirp_if_not_exists = mkdirp_if_not_exists orelse true, - }; + }); request.body.value.Locked.task = task; request.body.value.Locked.onReceiveValue = WriteFileWaitFromLockedValueTask.thenWrap; @@ -1342,8 +1332,7 @@ pub const Blob = struct { blob.content_type_was_set = false; } - var blob_ = allocator.create(Blob) catch unreachable; - blob_.* = blob; + var blob_ = bun.new(Blob, blob); blob_.allocator = allocator; blob_.is_jsdom_file = true; return blob_; @@ -1438,8 +1427,7 @@ pub const Blob = struct { } } - var ptr = bun.default_allocator.create(Blob) catch unreachable; - ptr.* = blob; + var ptr = bun.new(Blob, blob); ptr.allocator = bun.default_allocator; return ptr.toJS(globalObject); } @@ -1525,8 +1513,7 @@ pub const Blob = struct { } pub fn initFile(pathlike: JSC.Node.PathOrFileDescriptor, mime_type: ?http.MimeType, allocator: std.mem.Allocator) !*Store { - const store = try allocator.create(Blob.Store); - store.* = .{ + const store = bun.newWithAlloc(allocator, Blob.Store, .{ .data = .{ .file = FileStore.init( pathlike, @@ -1548,17 +1535,18 @@ pub const Blob = struct { }, .allocator = allocator, .ref_count = 1, - }; + }); return store; } pub fn init(bytes: []u8, allocator: std.mem.Allocator) !*Store { - const store = try allocator.create(Blob.Store); - store.* = .{ - .data = .{ .bytes = ByteStore.init(bytes, allocator) }, + const store = bun.newWithAlloc(allocator, Store, .{ + .data = .{ + .bytes = ByteStore.init(bytes, allocator), + }, .allocator = allocator, .ref_count = 1, - }; + }); return store; } @@ -1591,7 +1579,7 @@ pub const Blob = struct { }, } - allocator.destroy(this); + bun.destroyWithAlloc(allocator, this); } const SerializeTag = enum(u8) { @@ -1858,7 +1846,7 @@ pub const Blob = struct { } pub fn createWithCtx( - allocator: std.mem.Allocator, + _: std.mem.Allocator, store: *Store, onReadFileContext: *anyopaque, onCompleteCallback: OnReadFileCallback, @@ -1868,15 +1856,14 @@ pub const Blob = struct { if (Environment.isWindows) @compileError("dont call this function on windows"); - const read_file = try allocator.create(ReadFile); - read_file.* = ReadFile{ + const read_file = bun.new(ReadFile, ReadFile{ .file_store = store.data.file, .offset = off, .max_length = max_len, .store = store, .onCompleteCtx = onReadFileContext, .onCompleteCallback = onCompleteCallback, - }; + }); store.ref(); return read_file; } @@ -2024,11 +2011,11 @@ pub const Blob = struct { if (this.store == null and this.system_error != null) { const system_error = this.system_error.?; - bun.default_allocator.destroy(this); + bun.destroy(this); cb(cb_ctx, ResultType{ .err = system_error }); return; } else if (this.store == null) { - bun.default_allocator.destroy(this); + bun.destroy(this); if (Environment.isDebug) @panic("assertion failure - store should not be null"); cb(cb_ctx, ResultType{ .err = SystemError{ @@ -2044,13 +2031,16 @@ pub const Blob = struct { const buf = this.buffer.items; defer store.deref(); - defer bun.default_allocator.destroy(this); - if (this.system_error) |err| { + const total_size = this.size; + const system_error = this.system_error; + bun.destroy(this); + + if (system_error) |err| { cb(cb_ctx, ResultType{ .err = err }); return; } - cb(cb_ctx, .{ .result = .{ .buf = buf, .total_size = this.size, .is_temporary = true } }); + cb(cb_ctx, .{ .result = .{ .buf = buf, .total_size = total_size, .is_temporary = true } }); } pub fn run(this: *ReadFile, task: *ReadFileTask) void { @@ -2340,7 +2330,7 @@ pub const Blob = struct { pub fn finalize(this: *ReadFileUV) void { defer { this.store.deref(); - bun.default_allocator.destroy(this); + bun.destroy(this); } const cb = this.on_complete_fn; @@ -2608,18 +2598,18 @@ pub const Blob = struct { onCompleteCallback: OnWriteFileCallback, mkdirp_if_not_exists: bool, ) !*WriteFile { - const read_file = try allocator.create(WriteFile); - read_file.* = WriteFile{ + _ = allocator; + const write_file = bun.new(WriteFile, WriteFile{ .file_blob = file_blob, .bytes_blob = bytes_blob, .onCompleteCtx = onWriteFileContext, .onCompleteCallback = onCompleteCallback, .task = .{ .callback = &doWriteLoopTask }, .mkdirp_if_not_exists = mkdirp_if_not_exists, - }; + }); file_blob.store.?.ref(); bytes_blob.store.?.ref(); - return read_file; + return write_file; } pub fn create( @@ -2704,7 +2694,7 @@ pub const Blob = struct { this.file_blob.store.?.deref(); if (this.system_error) |err| { - bun.default_allocator.destroy(this); + bun.destroy(this); cb(cb_ctx, .{ .err = err, }); @@ -2712,7 +2702,7 @@ pub const Blob = struct { } const wrote = this.total_written; - bun.default_allocator.destroy(this); + bun.destroy(this); cb(cb_ctx, .{ .result = @as(SizeType, @truncate(wrote)) }); } pub fn run(this: *WriteFile, task: *WriteFileTask) void { @@ -2919,8 +2909,7 @@ pub const Blob = struct { globalThis: *JSC.JSGlobalObject, mkdirp_if_not_exists: bool, ) !*CopyFilePromiseTask { - const read_file = try allocator.create(CopyFile); - read_file.* = CopyFile{ + const read_file = bun.new(CopyFile, CopyFile{ .store = store, .source_store = source_store, .offset = off, @@ -2929,7 +2918,7 @@ pub const Blob = struct { .destination_file_store = store.data.file, .source_file_store = source_store.data.file, .mkdirp_if_not_exists = mkdirp_if_not_exists, - }; + }); store.ref(); source_store.ref(); return try CopyFilePromiseTask.createOnJSThread(allocator, globalThis, read_file); @@ -2946,7 +2935,7 @@ pub const Blob = struct { } this.store.?.deref(); - bun.default_allocator.destroy(this); + bun.destroy(this); } pub fn reject(this: *CopyFile, promise: *JSC.JSPromise) void { @@ -3722,16 +3711,13 @@ pub const Blob = struct { globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, ) callconv(.C) JSC.JSValue { - var allocator = globalThis.allocator(); + var allocator = bun.default_allocator; var arguments_ = callframe.arguments(3); var args = arguments_.ptr[0..arguments_.len]; if (this.size == 0) { const empty = Blob.initEmpty(globalThis); - var ptr = allocator.create(Blob) catch { - return JSC.JSValue.jsUndefined(); - }; - ptr.* = empty; + var ptr = bun.new(Blob, empty); ptr.allocator = allocator; return ptr.toJS(globalThis); } @@ -3823,8 +3809,7 @@ pub const Blob = struct { blob.content_type_allocated = content_type_was_allocated; blob.content_type_was_set = this.content_type_was_set or content_type_was_allocated; - var blob_ = allocator.create(Blob) catch unreachable; - blob_.* = blob; + var blob_ = bun.new(Blob, blob); blob_.allocator = allocator; return blob_.toJS(globalThis); } @@ -4031,7 +4016,7 @@ pub const Blob = struct { globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, ) callconv(.C) ?*Blob { - var allocator = globalThis.allocator(); + var allocator = bun.default_allocator; var blob: Blob = undefined; var arguments = callframe.arguments(2); const args = arguments.ptr[0..arguments.len]; @@ -4089,8 +4074,7 @@ pub const Blob = struct { }, } - var blob_ = allocator.create(Blob) catch unreachable; - blob_.* = blob; + var blob_ = bun.new(Blob, blob); blob_.allocator = allocator; return blob_; } @@ -4222,7 +4206,7 @@ pub const Blob = struct { if (this.allocator) |alloc| { this.allocator = null; - alloc.destroy(this); + bun.destroyWithAlloc(alloc, this); } } @@ -4259,7 +4243,7 @@ pub const Blob = struct { var blob = handler.context; blob.allocator = null; const globalThis = handler.globalThis; - bun.default_allocator.destroy(handler); + bun.destroy(handler); switch (maybe_bytes) { .result => |result| { const bytes = result.buf; @@ -4288,7 +4272,7 @@ pub const Blob = struct { pub fn run(handler: *@This(), count: Blob.Store.WriteFile.ResultType) void { var promise = handler.promise.swap(); const globalThis = handler.globalThis; - bun.default_allocator.destroy(handler); + bun.destroy(handler); const value = promise.asValue(globalThis); value.ensureStillAlive(); switch (count) { diff --git a/src/bun.js/webcore/body.zig b/src/bun.js/webcore/body.zig index 7c5a36d5f3..9098d37e2f 100644 --- a/src/bun.js/webcore/body.zig +++ b/src/bun.js/webcore/body.zig @@ -649,8 +649,7 @@ pub const Body = struct { async_form_data.toJS(global, blob.slice(), promise); }, else => { - var ptr = bun.default_allocator.create(Blob) catch unreachable; - ptr.* = new.use(); + var ptr = bun.new(Blob, new.use()); ptr.allocator = bun.default_allocator; promise.resolve(global, ptr.toJS(global)); }, @@ -1107,8 +1106,7 @@ pub fn BodyMixin(comptime Type: type) type { } var blob = value.use(); - var ptr = getAllocator(globalObject).create(Blob) catch unreachable; - ptr.* = blob; + var ptr = bun.new(Blob, blob); blob.allocator = getAllocator(globalObject); if (blob.content_type.len == 0 and blob.store != null) { diff --git a/src/bun.js/webcore/encoding.zig b/src/bun.js/webcore/encoding.zig index b39d1839d7..7c87810c06 100644 --- a/src/bun.js/webcore/encoding.zig +++ b/src/bun.js/webcore/encoding.zig @@ -880,6 +880,81 @@ pub const Encoder = struct { }; } + pub fn toBunStringFromOwnedSlice(input: []u8, encoding: JSC.Node.Encoding) bun.String { + if (input.len == 0) + return bun.String.empty; + + switch (encoding) { + .ascii => { + if (strings.isAllASCII(input)) { + return bun.String.createExternalGloballyAllocated(.latin1, input); + } + + defer bun.default_allocator.free(input); + const str, const chars = bun.String.createUninitialized(.latin1, input.len); + strings.copyLatin1IntoASCII(chars, input); + return str; + }, + .latin1 => { + return bun.String.createExternalGloballyAllocated(.latin1, input); + }, + .buffer, .utf8 => { + const converted = strings.toUTF16Alloc(bun.default_allocator, input, false) catch return bun.String.dead; + if (converted) |utf16| { + defer bun.default_allocator.free(input); + return bun.String.createExternalGloballyAllocated(.utf16, utf16); + } + + // If we get here, it means we can safely assume the string is 100% ASCII characters + return bun.String.createExternalGloballyAllocated(.latin1, input); + }, + .ucs2, .utf16le => { + // Avoid incomplete characters + if (input.len / 2 == 0) { + bun.default_allocator.free(input); + return bun.String.empty; + } + + const as_u16 = std.mem.bytesAsSlice(u16, input); + + return bun.String.createExternalGloballyAllocated(.utf16, @alignCast(as_u16)); + }, + + .hex => { + defer bun.default_allocator.free(input); + const str, const chars = bun.String.createUninitialized(.latin1, input.len * 2); + + const wrote = strings.encodeBytesToHex(chars, input); + + // Return an empty string in this case, just like node. + if (wrote < chars.len) { + str.deref(); + return bun.String.empty; + } + + return str; + }, + + // TODO: this is not right. There is an issue here. But it needs to + // be addressed separately because constructFromU8's base64url also + // appears inconsistent with Node.js. + .base64url => { + defer bun.default_allocator.free(input); + const out, const chars = bun.String.createUninitialized(.latin1, bun.base64.urlSafeEncodeLen(input)); + _ = bun.base64.encodeURLSafe(chars, input); + return out; + }, + + .base64 => { + defer bun.default_allocator.free(input); + const to_len = bun.base64.encodeLen(input); + var to = bun.default_allocator.alloc(u8, to_len) catch return bun.String.dead; + const wrote = bun.base64.encode(to, input); + return bun.String.createExternalGloballyAllocated(.latin1, to[0..wrote]); + }, + } + } + pub fn toString(input_ptr: [*]const u8, len: usize, global: *JSGlobalObject, comptime encoding: JSC.Node.Encoding) JSValue { if (len == 0) return ZigString.Empty.toValue(global); @@ -1140,7 +1215,7 @@ pub const Encoder = struct { pub fn constructFromU8(input: [*]const u8, len: usize, comptime encoding: JSC.Node.Encoding) []u8 { if (len == 0) return &[_]u8{}; - const allocator = VirtualMachine.get().allocator; + const allocator = bun.default_allocator; switch (comptime encoding) { .buffer => { @@ -1192,7 +1267,7 @@ pub const Encoder = struct { pub fn constructFromU16(input: [*]const u16, len: usize, comptime encoding: JSC.Node.Encoding) []u8 { if (len == 0) return &[_]u8{}; - const allocator = VirtualMachine.get().allocator; + const allocator = bun.default_allocator; switch (comptime encoding) { .utf8 => { @@ -1205,7 +1280,7 @@ pub const Encoder = struct { }, // string is already encoded, just need to copy the data .ucs2, .utf16le => { - var to = std.mem.sliceAsBytes(allocator.alloc(u16, len * 2) catch return &[_]u8{}); + var to = std.mem.sliceAsBytes(allocator.alloc(u16, len) catch return &[_]u8{}); const bytes = std.mem.sliceAsBytes(input[0..len]); @memcpy(to[0..bytes.len], bytes); return to; diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index feccc9e395..151a4414ad 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -59,7 +59,6 @@ pub const Response = struct { const ResponseMixin = BodyMixin(@This()); pub usingnamespace JSC.Codegen.JSResponse; - allocator: std.mem.Allocator, body: Body, init: Init, url: bun.String = bun.String.empty, @@ -81,7 +80,7 @@ pub const Response = struct { var content_type_slice: ZigString.Slice = this.getContentType() orelse return null; defer content_type_slice.deinit(); const encoding = bun.FormData.Encoding.get(content_type_slice.slice()) orelse return null; - return bun.FormData.AsyncFormData.init(this.allocator, encoding) catch unreachable; + return bun.FormData.AsyncFormData.init(bun.default_allocator, encoding) catch unreachable; } pub fn estimatedSize(this: *Response) callconv(.C) usize { @@ -251,7 +250,7 @@ pub const Response = struct { globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame, ) callconv(.C) JSValue { - const cloned = this.clone(getAllocator(globalThis), globalThis); + const cloned = this.clone(globalThis); return Response.makeMaybePooled(globalThis, cloned); } @@ -259,14 +258,11 @@ pub const Response = struct { return ptr.toJS(globalObject); } - pub fn cloneInto( + pub fn cloneValue( this: *Response, - new_response: *Response, - allocator: std.mem.Allocator, globalThis: *JSGlobalObject, - ) void { - new_response.* = Response{ - .allocator = allocator, + ) Response { + return Response{ .body = this.body.clone(globalThis), .init = this.init.clone(globalThis), .url = this.url.clone(), @@ -274,10 +270,8 @@ pub const Response = struct { }; } - pub fn clone(this: *Response, allocator: std.mem.Allocator, globalThis: *JSGlobalObject) *Response { - const new_response = allocator.create(Response) catch unreachable; - this.cloneInto(new_response, allocator, globalThis); - return new_response; + pub fn clone(this: *Response, globalThis: *JSGlobalObject) *Response { + return bun.new(Response, this.cloneValue(globalThis)); } pub fn getStatus( @@ -291,13 +285,11 @@ pub const Response = struct { pub fn finalize( this: *Response, ) callconv(.C) void { - var allocator = this.allocator; - - this.init.deinit(allocator); - this.body.deinit(allocator); + this.init.deinit(bun.default_allocator); + this.body.deinit(bun.default_allocator); this.url.deref(); - allocator.destroy(this); + bun.destroy(this); } pub fn getContentType( @@ -324,7 +316,6 @@ pub const Response = struct { const args_list = callframe.arguments(2); // https://github.com/remix-run/remix/blob/db2c31f64affb2095e4286b91306b96435967969/packages/remix-server-runtime/responses.ts#L4 var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), args_list.ptr[0..args_list.len]); - // var response = getAllocator(globalThis).create(Response) catch unreachable; var response = Response{ .body = Body{ @@ -333,7 +324,6 @@ pub const Response = struct { .init = Response.Init{ .status_code = 200, }, - .allocator = getAllocator(globalThis), .url = bun.String.empty, }; @@ -374,10 +364,7 @@ pub const Response = struct { var headers_ref = response.getOrCreateHeaders(globalThis); headers_ref.putDefault("content-type", MimeType.json.value, globalThis); - var ptr = response.allocator.create(Response) catch unreachable; - ptr.* = response; - - return ptr.toJS(globalThis); + return bun.new(Response, response).toJS(globalThis); } pub fn constructRedirect( globalThis: *JSC.JSGlobalObject, @@ -386,7 +373,6 @@ pub const Response = struct { var args_list = callframe.arguments(4); // https://github.com/remix-run/remix/blob/db2c31f64affb2095e4286b91306b96435967969/packages/remix-server-runtime/responses.ts#L4 var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), args_list.ptr[0..args_list.len]); - // var response = getAllocator(globalThis).create(Response) catch unreachable; var response = Response{ .init = Response.Init{ @@ -395,7 +381,6 @@ pub const Response = struct { .body = Body{ .value = .{ .Empty = {} }, }, - .allocator = getAllocator(globalThis), .url = bun.String.empty, }; @@ -422,8 +407,7 @@ pub const Response = struct { response.init.headers = response.getOrCreateHeaders(globalThis); var headers_ref = response.init.headers.?; headers_ref.put("location", url_string_slice.slice(), globalThis); - var ptr = response.allocator.create(Response) catch unreachable; - ptr.* = response; + const ptr = bun.new(Response, response); return ptr.toJS(globalThis); } @@ -431,16 +415,17 @@ pub const Response = struct { globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame, ) callconv(.C) JSValue { - var response = getAllocator(globalThis).create(Response) catch unreachable; - response.* = Response{ - .init = Response.Init{ - .status_code = 0, + const response = bun.new( + Response, + Response{ + .init = Response.Init{ + .status_code = 0, + }, + .body = Body{ + .value = .{ .Empty = {} }, + }, }, - .body = Body{ - .value = .{ .Empty = {} }, - }, - .allocator = getAllocator(globalThis), - }; + ); return response.toJS(globalThis); } @@ -449,8 +434,6 @@ pub const Response = struct { globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, ) callconv(.C) ?*Response { - var allocator = getAllocator(globalThis); - const args_list = brk: { var args = callframe.arguments(2); if (args.len > 1 and args.ptr[1].isEmptyOrUndefinedOrNull()) { @@ -477,7 +460,7 @@ pub const Response = struct { }, else => { if (arguments[1].isObject()) { - break :brk Init.init(allocator, globalThis, arguments[1]) catch null; + break :brk Init.init(bun.default_allocator, globalThis, arguments[1]) catch null; } std.debug.assert(!arguments[1].isEmptyOrUndefinedOrNull()); @@ -504,13 +487,10 @@ pub const Response = struct { unreachable; } orelse return null; - var response = allocator.create(Response) catch unreachable; - - response.* = Response{ + var response = bun.new(Response, Response{ .body = body, .init = init, - .allocator = getAllocator(globalThis), - }; + }); if (response.body.value == .Blob and response.init.headers != null and @@ -620,21 +600,15 @@ pub const Response = struct { return emptyWithStatus(globalThis, 200); } - inline fn emptyWithStatus(globalThis: *JSC.JSGlobalObject, status: u16) Response { - var allocator = getAllocator(globalThis); - const response = allocator.create(Response) catch unreachable; - - response.* = Response{ + inline fn emptyWithStatus(_: *JSC.JSGlobalObject, status: u16) Response { + return bun.new(Response, .{ .body = Body{ .value = Body.Value{ .Null = {} }, }, .init = Init{ .status_code = status, }, - .allocator = getAllocator(globalThis), - }; - - return response; + }); } }; @@ -1408,7 +1382,7 @@ pub const Fetch = struct { return response; } - fn toResponse(this: *FetchTasklet, allocator: std.mem.Allocator) Response { + fn toResponse(this: *FetchTasklet) Response { log("toResponse", .{}); std.debug.assert(this.metadata != null); // at this point we always should have metadata @@ -1416,7 +1390,6 @@ pub const Fetch = struct { const http_response = metadata.response; this.is_waiting_body = this.result.has_more; return Response{ - .allocator = allocator, .url = bun.String.createAtomIfPossible(metadata.url), .redirected = this.result.redirected, .init = .{ @@ -1432,9 +1405,7 @@ pub const Fetch = struct { pub fn onResolve(this: *FetchTasklet) JSValue { log("onResolve", .{}); - const allocator = bun.default_allocator; - const response = allocator.create(Response) catch unreachable; - response.* = this.toResponse(allocator); + const response = bun.new(Response, this.toResponse()); const response_js = Response.makeMaybePooled(@as(js.JSContextRef, this.global_this), response); response_js.ensureStillAlive(); this.response = JSC.Strong.create(response_js, this.global_this); @@ -1664,21 +1635,21 @@ pub const Fetch = struct { blob.content_type_allocated = true; } - var response = allocator.create(Response) catch @panic("out of memory"); - - response.* = Response{ - .body = Body{ - .value = .{ - .Blob = blob, + var response = bun.new( + Response, + Response{ + .body = Body{ + .value = .{ + .Blob = blob, + }, }, + .init = Response.Init{ + .status_code = 200, + .status_text = bun.String.createAtomASCII("OK"), + }, + .url = data_url.url.dupeRef(), }, - .init = Response.Init{ - .status_code = 200, - .status_text = bun.String.createAtomASCII("OK"), - }, - .allocator = allocator, - .url = data_url.url.dupeRef(), - }; + ); return JSPromise.resolvedPromiseValue(globalThis, response.toJS(globalThis)); } @@ -2185,18 +2156,15 @@ pub const Fetch = struct { globalThis, ); - var response = bun.default_allocator.create(Response) catch @panic("out of memory"); - - response.* = Response{ + const response = bun.new(Response, Response{ .body = Body{ .value = .{ .Blob = bun_file }, }, .init = Response.Init{ .status_code = 200, }, - .allocator = bun.default_allocator, .url = file_url_string.clone(), - }; + }); return JSPromise.resolvedPromiseValue(globalThis, response.toJS(globalThis)); } diff --git a/src/bun.zig b/src/bun.zig index 87af417bf9..dd6d2fb80d 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -2800,17 +2800,89 @@ pub noinline fn outOfMemory() noreturn { @panic("Bun ran out of memory!"); } +pub const is_heap_breakdown_enabled = Environment.allow_assert and Environment.isMac; + +const HeapBreakdown = if (is_heap_breakdown_enabled) @import("./heap_breakdown.zig") else struct {}; + +/// Globally-allocate a value on the heap. +/// +/// When used, yuo must call `bun.destroy` to free the memory. +/// default_allocator.destroy should not be used. +/// +/// On macOS, you can use `Bun.DO_NOT_USE_OR_YOU_WILL_BE_FIRED_mimalloc_dump()` +/// to dump the heap. pub inline fn new(comptime T: type, t: T) *T { + if (comptime is_heap_breakdown_enabled) { + const ptr = HeapBreakdown.allocator(T).create(T) catch outOfMemory(); + ptr.* = t; + return ptr; + } + const ptr = default_allocator.create(T) catch outOfMemory(); ptr.* = t; return ptr; } +/// Free a globally-allocated a value +/// +/// On macOS, you can use `Bun.DO_NOT_USE_OR_YOU_WILL_BE_FIRED_mimalloc_dump()` +/// to dump the heap. +pub inline fn destroyWithAlloc(allocator: std.mem.Allocator, t: anytype) void { + if (comptime is_heap_breakdown_enabled) { + if (allocator.vtable == default_allocator.vtable) { + destroy(t); + return; + } + } + + allocator.destroy(t); +} + +pub fn New(comptime T: type) type { + return struct { + pub inline fn destroy(self: *T) void { + if (comptime is_heap_breakdown_enabled) { + HeapBreakdown.allocator(T).destroy(self); + } else { + default_allocator.destroy(self); + } + } + + pub inline fn new(t: T) *T { + if (comptime is_heap_breakdown_enabled) { + const ptr = HeapBreakdown.allocator(T).create(T) catch outOfMemory(); + ptr.* = t; + return ptr; + } + + const ptr = default_allocator.create(T) catch outOfMemory(); + ptr.* = t; + return ptr; + } + }; +} + +/// Free a globally-allocated a value. +/// +/// Must have used `new` to allocate the value. +/// +/// On macOS, you can use `Bun.DO_NOT_USE_OR_YOU_WILL_BE_FIRED_mimalloc_dump()` +/// to dump the heap. pub inline fn destroy(t: anytype) void { - default_allocator.destroy(@TypeOf(t)); + if (comptime is_heap_breakdown_enabled) { + HeapBreakdown.allocator(std.meta.Child(@TypeOf(t))).destroy(t); + } else { + default_allocator.destroy(t); + } } pub inline fn newWithAlloc(allocator: std.mem.Allocator, comptime T: type, t: T) *T { + if (comptime is_heap_breakdown_enabled) { + if (allocator.vtable == default_allocator.vtable) { + return new(T, t); + } + } + const ptr = allocator.create(T) catch outOfMemory(); ptr.* = t; return ptr; diff --git a/src/deps/lol-html.zig b/src/deps/lol-html.zig index 3ddcf90006..ddb315990c 100644 --- a/src/deps/lol-html.zig +++ b/src/deps/lol-html.zig @@ -88,6 +88,11 @@ pub const HTMLRewriter = opaque { strict: bool, ) ?*HTMLRewriter; + pub fn deinit(this: *HTMLRewriter.Builder) void { + auto_disable(); + this.lol_html_rewriter_builder_free(); + } + extern fn lol_html_rewriter_builder_add_document_content_handlers( builder: *HTMLRewriter.Builder, doctype_handler: ?DirectiveFunctionType(DocType), diff --git a/src/fd.zig b/src/fd.zig index ea3c3bc418..df1be892cc 100644 --- a/src/fd.zig +++ b/src/fd.zig @@ -281,7 +281,7 @@ pub const FDImpl = packed struct { } /// After calling, the input file descriptor is no longer valid and must not be used - pub fn toJS(value: FDImpl, _: *JSC.JSGlobalObject, _: JSC.C.ExceptionRef) JSValue { + pub fn toJS(value: FDImpl, _: *JSC.JSGlobalObject) JSValue { return JSValue.jsNumberFromInt32(value.makeLibUVOwned().uv()); } diff --git a/src/heap_breakdown.zig b/src/heap_breakdown.zig new file mode 100644 index 0000000000..3c4a836c05 --- /dev/null +++ b/src/heap_breakdown.zig @@ -0,0 +1,110 @@ +const bun = @import("root").bun; +const std = @import("std"); +const HeapBreakdown = @This(); + +pub fn allocator(comptime T: type) std.mem.Allocator { + const Holder = struct { + pub var zone_t: std.atomic.Value(?*malloc_zone_t) = std.atomic.Value(?*malloc_zone_t).init(null); + pub var zone_t_lock: bun.Lock = bun.Lock.init(); + }; + const zone = Holder.zone_t.load(.Monotonic) orelse brk: { + Holder.zone_t_lock.lock(); + defer Holder.zone_t_lock.unlock(); + + if (Holder.zone_t.load(.Monotonic)) |z| { + break :brk z; + } + + const z = malloc_zone_t.create(T); + Holder.zone_t.store(z, .Monotonic); + break :brk z; + }; + + return zone.getAllocator(); +} + +const malloc_zone_t = opaque { + const Allocator = std.mem.Allocator; + const vm_size_t = usize; + + pub extern fn malloc_default_zone() *malloc_zone_t; + pub extern fn malloc_create_zone(start_size: vm_size_t, flags: c_uint) *malloc_zone_t; + pub extern fn malloc_destroy_zone(zone: *malloc_zone_t) void; + pub extern fn malloc_zone_malloc(zone: *malloc_zone_t, size: usize) ?*anyopaque; + pub extern fn malloc_zone_calloc(zone: *malloc_zone_t, num_items: usize, size: usize) ?*anyopaque; + pub extern fn malloc_zone_valloc(zone: *malloc_zone_t, size: usize) ?*anyopaque; + pub extern fn malloc_zone_free(zone: *malloc_zone_t, ptr: ?*anyopaque) void; + pub extern fn malloc_zone_realloc(zone: *malloc_zone_t, ptr: ?*anyopaque, size: usize) ?*anyopaque; + pub extern fn malloc_zone_from_ptr(ptr: ?*const anyopaque) *malloc_zone_t; + pub extern fn malloc_zone_memalign(zone: *malloc_zone_t, alignment: usize, size: usize) ?*anyopaque; + pub extern fn malloc_zone_batch_malloc(zone: *malloc_zone_t, size: usize, results: [*]?*anyopaque, num_requested: c_uint) c_uint; + pub extern fn malloc_zone_batch_free(zone: *malloc_zone_t, to_be_freed: [*]?*anyopaque, num: c_uint) void; + pub extern fn malloc_default_purgeable_zone() *malloc_zone_t; + pub extern fn malloc_make_purgeable(ptr: ?*anyopaque) void; + pub extern fn malloc_make_nonpurgeable(ptr: ?*anyopaque) c_int; + pub extern fn malloc_zone_register(zone: *malloc_zone_t) void; + pub extern fn malloc_zone_unregister(zone: *malloc_zone_t) void; + pub extern fn malloc_set_zone_name(zone: *malloc_zone_t, name: ?[*:0]const u8) void; + pub extern fn malloc_get_zone_name(zone: *malloc_zone_t) ?[*:0]const u8; + pub extern fn malloc_zone_pressure_relief(zone: *malloc_zone_t, goal: usize) usize; + + fn alignedAlloc(zone: *malloc_zone_t, len: usize, alignment: usize) ?[*]u8 { + // The posix_memalign only accepts alignment values that are a + // multiple of the pointer size + const eff_alignment = @max(alignment, @sizeOf(usize)); + + const ptr = malloc_zone_memalign(zone, eff_alignment, len); + return @as(?[*]u8, @ptrCast(ptr)); + } + + fn alignedAllocSize(ptr: [*]u8) usize { + return std.c.malloc_size(ptr); + } + + fn alloc(ptr: *anyopaque, len: usize, log2_align: u8, _: usize) ?[*]u8 { + const alignment = @as(usize, 1) << @as(Allocator.Log2Align, @intCast(log2_align)); + return alignedAlloc(@ptrCast(ptr), len, alignment); + } + + fn resize(_: *anyopaque, buf: []u8, _: u8, new_len: usize, _: usize) bool { + if (new_len <= buf.len) { + return true; + } + + const full_len = alignedAllocSize(buf.ptr); + if (new_len <= full_len) { + return true; + } + + return false; + } + + fn free(ptr: *anyopaque, buf: [*]u8, _: u8, _: usize) void { + malloc_zone_free(@ptrCast(ptr), @ptrCast(buf)); + } + + pub const VTable = std.mem.Allocator.VTable{ + .alloc = @ptrCast(&alloc), + .resize = @ptrCast(&resize), + .free = @ptrCast(&free), + }; + + pub fn create(comptime T: type) *malloc_zone_t { + const zone = malloc_create_zone(0, 0); + const title = struct { + const base_name = if (@hasDecl(T, "heap_label")) T.heap_label else bun.meta.typeBaseName(@typeName(T)); + pub const title_: []const u8 = "Bun__" ++ base_name ++ .{0}; + pub const title: [:0]const u8 = title_[0 .. title_.len - 1 :0]; + }.title; + malloc_set_zone_name(zone, title.ptr); + + return zone; + } + + pub fn getAllocator(zone: *malloc_zone_t) std.mem.Allocator { + return Allocator{ + .vtable = &VTable, + .ptr = zone, + }; + } +}; diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index 2fbe5660b1..9daecde402 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -1645,7 +1645,7 @@ pub fn saveToDisk(this: *Lockfile, filename: stringZ) void { .fd = bun.toFD(file.handle), }, .dirfd = if (!Environment.isWindows) bun.invalid_fd else @panic("TODO"), - .data = .{ .string = bytes.items }, + .data = .{ .string = .{ .utf8 = bun.JSC.ZigString.Slice.from(bytes.items, bun.default_allocator) } }, }, .sync, )) { diff --git a/src/js/thirdparty/node-fetch.ts b/src/js/thirdparty/node-fetch.ts index 9c634f6f3a..293870cf89 100644 --- a/src/js/thirdparty/node-fetch.ts +++ b/src/js/thirdparty/node-fetch.ts @@ -92,6 +92,14 @@ class Response extends WebResponse { return await super.json(); } + // This is a deprecated function in node-fetch + // but is still used by some libraries and frameworks (like Astro) + async buffer() { + // load the getter + this.body; + return new $Buffer(await super.arrayBuffer()); + } + async text() { // load the getter this.body; diff --git a/src/string.zig b/src/string.zig index 772c52e30e..6e28580399 100644 --- a/src/string.zig +++ b/src/string.zig @@ -4,7 +4,7 @@ const JSC = bun.JSC; const JSValue = bun.JSC.JSValue; const Parent = @This(); -pub const BufferOwnership = enum { +pub const BufferOwnership = enum(u32) { BufferInternal, BufferOwned, BufferSubstring, @@ -54,6 +54,11 @@ pub const WTFStringImplStruct = extern struct { return if (this.is8Bit()) this.m_length else this.m_length * 2; } + extern fn WTFStringImpl__isThreadSafe(WTFStringImpl) bool; + pub fn isThreadSafe(this: WTFStringImpl) bool { + return WTFStringImpl__isThreadSafe(this); + } + pub fn byteSlice(this: WTFStringImpl) []const u8 { return this.m_ptr.latin1[0..this.byteLength()]; } @@ -111,14 +116,18 @@ pub const WTFStringImplStruct = extern struct { std.debug.assert(self.refCount() > current_count or self.isStatic()); } + pub fn toLatin1Slice(this: WTFStringImpl) ZigString.Slice { + this.ref(); + return ZigString.Slice.init(this.refCountAllocator(), this.latin1Slice()); + } + pub fn toUTF8(this: WTFStringImpl, allocator: std.mem.Allocator) ZigString.Slice { if (this.is8Bit()) { if (bun.strings.toUTF8FromLatin1(allocator, this.latin1Slice()) catch bun.outOfMemory()) |utf8| { return ZigString.Slice.init(allocator, utf8.items); } - this.ref(); - return ZigString.Slice.init(this.refCountAllocator(), this.latin1Slice()); + return this.toLatin1Slice(); } return ZigString.Slice.init( @@ -504,6 +513,26 @@ pub const String = extern struct { return BunString__createExternal(bytes.ptr, bytes.len, isLatin1, ctx, callback); } + extern fn BunString__createExternalGloballyAllocatedLatin1( + bytes: [*]u8, + len: usize, + ) String; + + extern fn BunString__createExternalGloballyAllocatedUTF16( + bytes: [*]u16, + len: usize, + ) String; + + pub fn createExternalGloballyAllocated(comptime kind: WTFStringEncoding, bytes: []kind.Byte()) String { + JSC.markBinding(@src()); + std.debug.assert(bytes.len > 0); + + return switch (comptime kind) { + .latin1 => BunString__createExternalGloballyAllocatedLatin1(bytes.ptr, bytes.len), + .utf16 => BunString__createExternalGloballyAllocatedUTF16(bytes.ptr, bytes.len), + }; + } + pub fn fromUTF8(value: []const u8) String { return String.init(ZigString.initUTF8(value)); } @@ -664,6 +693,10 @@ pub const String = extern struct { return JSC.WebCore.Encoder.encodeIntoFrom8(self.latin1(), out, enc); } + pub fn encode(self: String, enc: JSC.Node.Encoding) []u8 { + return self.toZigString().encode(enc); + } + pub inline fn utf8(self: String) []const u8 { if (comptime bun.Environment.allow_assert) std.debug.assert(self.canBeUTF8()); @@ -741,6 +774,66 @@ pub const String = extern struct { }; } + pub fn toThreadSafeSlice(this: *String, allocator: std.mem.Allocator) SliceWithUnderlyingString { + if (this.tag == .WTFStringImpl) { + if (!this.value.WTFStringImpl.isThreadSafe()) { + const slice = this.value.WTFStringImpl.toUTF8WithoutRef(allocator); + + if (slice.allocator.isNull()) { + // this was a WTF-allocated string + // We're going to need to clone it across the threads + // so let's just do that now instead of creating another copy. + return .{ + .utf8 = ZigString.Slice.init(allocator, allocator.dupe(u8, slice.slice()) catch bun.outOfMemory()), + }; + } + + if (comptime bun.Environment.allow_assert) { + std.debug.assert(!isWTFAllocator(slice.allocator.get().?)); // toUTF8WithoutRef() should never return a WTF allocator + std.debug.assert(slice.allocator.get().?.vtable == allocator.vtable); // assert that the allocator is the same + } + + // We've already cloned the string, so let's just return the slice. + return .{ + .utf8 = slice, + .underlying = empty, + }; + } else { + const slice = this.value.WTFStringImpl.toUTF8WithoutRef(allocator); + + // this WTF-allocated string is already thread safe + // and it's ASCII, so we can just use it directly + if (slice.allocator.isNull()) { + // Once for the string + this.ref(); + + // Once for the utf8 slice + this.ref(); + + // We didn't clone anything, so let's conserve memory by re-using the existing WTFStringImpl + return .{ + .utf8 = ZigString.Slice.init(this.value.WTFStringImpl.refCountAllocator(), slice.slice()), + .underlying = this.*, + }; + } + + if (comptime bun.Environment.allow_assert) { + std.debug.assert(!isWTFAllocator(slice.allocator.get().?)); // toUTF8WithoutRef() should never return a WTF allocator + std.debug.assert(slice.allocator.get().?.vtable == allocator.vtable); // assert that the allocator is the same + } + + // We did have to clone the string. Let's avoid keeping the WTFStringImpl around + // for longer than necessary, since the string could potentially have a single + // reference count and that means excess memory usage + return .{ + .utf8 = slice, + }; + } + } + + return this.toSlice(allocator); + } + extern fn BunString__fromJS(globalObject: *JSC.JSGlobalObject, value: bun.JSC.JSValue, out: *String) bool; extern fn BunString__toJS(globalObject: *JSC.JSGlobalObject, in: *const String) JSC.JSValue; extern fn BunString__toJSWithLength(globalObject: *JSC.JSGlobalObject, in: *const String, usize) JSC.JSValue; @@ -923,6 +1016,8 @@ pub const String = extern struct { } extern fn BunString__toThreadSafe(this: *String) void; + + /// Does not increment the reference count unless the StringImpl is cloned. pub fn toThreadSafe(this: *String) void { JSC.markBinding(@src()); @@ -931,6 +1026,21 @@ pub const String = extern struct { } } + /// We don't ref unless the underlying StringImpl is new. + /// + /// This will ref even if it doesn't change. + pub fn toThreadSafeEnsureRef(this: *String) void { + JSC.markBinding(@src()); + + if (this.tag == .WTFStringImpl) { + const orig = this.value.WTFStringImpl; + BunString__toThreadSafe(this); + if (this.value.WTFStringImpl == orig) { + orig.ref(); + } + } + } + pub fn eqlUTF8(this: String, other: []const u8) bool { return this.toZigString().eql(ZigString.initUTF8(other)); } @@ -984,8 +1094,59 @@ pub const String = extern struct { }; pub const SliceWithUnderlyingString = struct { - utf8: ZigString.Slice, - underlying: String, + utf8: ZigString.Slice = ZigString.Slice.empty, + underlying: String = String.dead, + + did_report_extra_memory_debug: bun.DebugOnly(bool) = if (bun.Environment.allow_assert) false else {}, + + pub inline fn reportExtraMemory(this: *SliceWithUnderlyingString, vm: *JSC.VM) void { + if (comptime bun.Environment.allow_assert) { + std.debug.assert(!this.did_report_extra_memory_debug); + this.did_report_extra_memory_debug = true; + } + this.utf8.reportExtraMemory(vm); + } + + pub fn isWTFAllocated(this: *const SliceWithUnderlyingString) bool { + if (this.utf8.allocator.get()) |allocator| { + const is_wtf_allocator = String.isWTFAllocator(allocator); + + return is_wtf_allocator; + } + + return false; + } + + pub fn dupeRef(this: SliceWithUnderlyingString) SliceWithUnderlyingString { + return .{ + .utf8 = ZigString.Slice.empty, + .underlying = this.underlying.dupeRef(), + }; + } + + /// Transcode a byte array to an encoded String, avoiding unnecessary copies. + /// + /// owned_input_bytes ownership is transferred to this function + pub fn transcodeFromOwnedSlice(owned_input_bytes: []u8, encoding: JSC.Node.Encoding) SliceWithUnderlyingString { + if (owned_input_bytes.len == 0) { + return .{ + .utf8 = ZigString.Slice.empty, + .underlying = String.empty, + }; + } + + return .{ + .underlying = JSC.WebCore.Encoder.toBunStringFromOwnedSlice(owned_input_bytes, encoding), + }; + } + + /// Assumes default allocator in use + pub fn fromUTF8(utf8: []const u8) SliceWithUnderlyingString { + return .{ + .utf8 = ZigString.Slice.init(bun.default_allocator, utf8), + .underlying = String.dead, + }; + } pub fn toThreadSafe(this: *SliceWithUnderlyingString) void { if (this.underlying.tag == .WTFStringImpl) { @@ -997,7 +1158,7 @@ pub const SliceWithUnderlyingString = struct { if (this.utf8.allocator.get()) |allocator| { if (String.isWTFAllocator(allocator)) { this.utf8.deinit(); - this.utf8 = this.underlying.toUTF8(bun.default_allocator); + this.utf8 = this.underlying.value.WTFStringImpl.toLatin1Slice(); } } } @@ -1013,7 +1174,42 @@ pub const SliceWithUnderlyingString = struct { return this.utf8.slice(); } - pub fn toJS(this: SliceWithUnderlyingString, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + pub fn format(self: SliceWithUnderlyingString, comptime fmt: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void { + if (self.utf8.len == 0) { + try self.underlying.format(fmt, opts, writer); + return; + } + + try writer.writeAll(self.utf8.slice()); + } + + pub fn toJS(this: *SliceWithUnderlyingString, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + if ((this.underlying.tag == .Dead or this.underlying.tag == .Empty) and this.utf8.length() > 0) { + if (comptime bun.Environment.allow_assert) { + if (this.utf8.allocator.get()) |allocator| { + std.debug.assert(!String.isWTFAllocator(allocator)); // We should never enter this state. + } + } + + if (this.utf8.allocator.get()) |_| { + if (bun.strings.toUTF16Alloc(bun.default_allocator, this.utf8.slice(), false) catch null) |utf16| { + this.utf8.deinit(); + this.utf8 = .{}; + return JSC.ZigString.toExternalU16(utf16.ptr, utf16.len, globalObject); + } else { + const js_value = ZigString.init(this.utf8.slice()).toExternalValue( + globalObject, + ); + this.utf8 = .{}; + return js_value; + } + } + + const out = bun.String.create(this.utf8.slice()); + defer out.deref(); + return out.toJS(globalObject); + } + return this.underlying.toJS(globalObject); } }; diff --git a/src/sys.zig b/src/sys.zig index e4e572ef75..e8f5aa3582 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -771,7 +771,7 @@ pub fn pwritev(fd: bun.FileDescriptor, buffers: []const std.os.iovec_const, posi pub fn readv(fd: bun.FileDescriptor, buffers: []std.os.iovec) Maybe(usize) { if (comptime Environment.allow_assert) { if (buffers.len == 0) { - @panic("readv() called with 0 length buffer"); + bun.Output.debugWarn("readv() called with 0 length buffer", .{}); } } @@ -805,7 +805,7 @@ pub fn readv(fd: bun.FileDescriptor, buffers: []std.os.iovec) Maybe(usize) { pub fn preadv(fd: bun.FileDescriptor, buffers: []std.os.iovec, position: isize) Maybe(usize) { if (comptime Environment.allow_assert) { if (buffers.len == 0) { - @panic("preadv() called with 0 length buffer"); + bun.Output.debugWarn("preadv() called with 0 length buffer", .{}); } } @@ -878,7 +878,7 @@ pub fn pread(fd: bun.FileDescriptor, buf: []u8, offset: i64) Maybe(usize) { if (comptime Environment.allow_assert) { if (adjusted_len == 0) { - @panic("pread() called with 0 length buffer"); + bun.Output.debugWarn("pread() called with 0 length buffer", .{}); } } @@ -902,7 +902,7 @@ else pub fn pwrite(fd: bun.FileDescriptor, bytes: []const u8, offset: i64) Maybe(usize) { if (comptime Environment.allow_assert) { if (bytes.len == 0) { - @panic("pwrite() called with 0 length buffer"); + bun.Output.debugWarn("pwrite() called with 0 length buffer", .{}); } } @@ -925,7 +925,7 @@ pub fn pwrite(fd: bun.FileDescriptor, bytes: []const u8, offset: i64) Maybe(usiz pub fn read(fd: bun.FileDescriptor, buf: []u8) Maybe(usize) { if (comptime Environment.allow_assert) { if (buf.len == 0) { - @panic("read() called with 0 length buffer"); + bun.Output.debugWarn("read() called with 0 length buffer", .{}); } } const debug_timer = bun.Output.DebugTimer.start(); @@ -963,7 +963,7 @@ pub fn recv(fd: bun.FileDescriptor, buf: []u8, flag: u32) Maybe(usize) { const adjusted_len = @min(buf.len, max_count); if (comptime Environment.allow_assert) { if (adjusted_len == 0) { - @panic("recv() called with 0 length buffer"); + bun.Output.debugWarn("recv() called with 0 length buffer", .{}); } } diff --git a/test/cli/run/run_command.test.ts b/test/cli/run/run_command.test.ts index 5ebfb79a0a..4be12784bd 100644 --- a/test/cli/run/run_command.test.ts +++ b/test/cli/run/run_command.test.ts @@ -5,16 +5,16 @@ import { bunEnv, bunExe } from "harness"; let cwd: string; describe("bun", () => { - test("should error with missing script", () => { - const { exitCode, stdout, stderr } = spawnSync({ - cwd, - cmd: [bunExe(), "run", "dev"], - env: bunEnv, - stdout: "pipe", - stderr: "pipe", - }); - expect(stdout.toString()).toBeEmpty(); - expect(stderr.toString()).toMatch(/Script not found/); - expect(exitCode).toBe(1); + test("should error with missing script", () => { + const { exitCode, stdout, stderr } = spawnSync({ + cwd, + cmd: [bunExe(), "run", "dev"], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", }); -}) \ No newline at end of file + expect(stdout.toString()).toBeEmpty(); + expect(stderr.toString()).toMatch(/Script not found/); + expect(exitCode).toBe(1); + }); +}); diff --git a/test/js/bun/http/serve.test.ts b/test/js/bun/http/serve.test.ts index 21df88164a..70e5dbe735 100644 --- a/test/js/bun/http/serve.test.ts +++ b/test/js/bun/http/serve.test.ts @@ -11,11 +11,8 @@ import { tmpdir } from "os"; let renderToReadableStream: any = null; let app_jsx: any = null; -console.log("started"); - type Handler = (req: Request) => Response; afterEach(() => { - console.log("afterEach"); gc(true); }); @@ -23,8 +20,6 @@ const count = 200; let server: Server | undefined; async function runTest({ port, ...serverOptions }: Serve, test: (server: Server) => Promise | void) { - console.trace("runTest"); - console.log("server:", server); if (server) { server.reload({ ...serverOptions, port: 0 }); } else { diff --git a/test/js/bun/io/bun-write.test.js b/test/js/bun/io/bun-write.test.js index ad64a8bb3c..5603e82130 100644 --- a/test/js/bun/io/bun-write.test.js +++ b/test/js/bun/io/bun-write.test.js @@ -299,7 +299,7 @@ it("Bun.write(Bun.stderr, 'new TextEncoder().encode(Bun.write STDERR TEST'))", a // FLAKY TEST // Since Bun.file is resolved lazily, this needs to specifically be checked -it.skip("Bun.write('output.html', HTMLRewriter.transform(Bun.file)))", async done => { +it("Bun.write('output.html', HTMLRewriter.transform(Bun.file)))", async done => { var rewriter = new HTMLRewriter(); rewriter.on("div", { diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index 49ce961c48..290f4dd88c 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "bun:test"; import { dirname, resolve, relative } from "node:path"; import { promisify } from "node:util"; import { bunEnv, bunExe, gc } from "harness"; +import { isAscii } from "node:buffer"; import fs, { closeSync, existsSync, @@ -342,6 +343,243 @@ it("promises.readFile", async () => { } }); +describe("promises.readFile", async () => { + const nodeOutput = [ + { + "encoding": "utf8", + "text": "ascii", + "correct": { + "type": "Buffer", + "data": [97, 115, 99, 105, 105], + }, + "out": "ascii", + }, + { + "encoding": "utf8", + "text": "utf16 🍇 🍈 🍉 🍊 🍋", + "correct": { + "type": "Buffer", + "data": [ + 117, 116, 102, 49, 54, 32, 240, 159, 141, 135, 32, 240, 159, 141, 136, 32, 240, 159, 141, 137, 32, 240, 159, + 141, 138, 32, 240, 159, 141, 139, + ], + }, + "out": "utf16 🍇 🍈 🍉 🍊 🍋", + }, + { + "encoding": "utf8", + "text": "👍", + "correct": { + "type": "Buffer", + "data": [240, 159, 145, 141], + }, + "out": "👍", + }, + { + "encoding": "utf-8", + "text": "ascii", + "correct": { + "type": "Buffer", + "data": [97, 115, 99, 105, 105], + }, + "out": "ascii", + }, + { + "encoding": "utf-8", + "text": "utf16 🍇 🍈 🍉 🍊 🍋", + "correct": { + "type": "Buffer", + "data": [ + 117, 116, 102, 49, 54, 32, 240, 159, 141, 135, 32, 240, 159, 141, 136, 32, 240, 159, 141, 137, 32, 240, 159, + 141, 138, 32, 240, 159, 141, 139, + ], + }, + "out": "utf16 🍇 🍈 🍉 🍊 🍋", + }, + { + "encoding": "utf-8", + "text": "👍", + "correct": { + "type": "Buffer", + "data": [240, 159, 145, 141], + }, + "out": "👍", + }, + { + "encoding": "utf16le", + "text": "ascii", + "correct": { + "type": "Buffer", + "data": [97, 0, 115, 0, 99, 0, 105, 0, 105, 0], + }, + "out": "ascii", + }, + { + "encoding": "utf16le", + "text": "utf16 🍇 🍈 🍉 🍊 🍋", + "correct": { + "type": "Buffer", + "data": [ + 117, 0, 116, 0, 102, 0, 49, 0, 54, 0, 32, 0, 60, 216, 71, 223, 32, 0, 60, 216, 72, 223, 32, 0, 60, 216, 73, + 223, 32, 0, 60, 216, 74, 223, 32, 0, 60, 216, 75, 223, + ], + }, + "out": "utf16 🍇 🍈 🍉 🍊 🍋", + }, + { + "encoding": "utf16le", + "text": "👍", + "correct": { + "type": "Buffer", + "data": [61, 216, 77, 220], + }, + "out": "👍", + }, + { + "encoding": "latin1", + "text": "ascii", + "correct": { + "type": "Buffer", + "data": [97, 115, 99, 105, 105], + }, + "out": "ascii", + }, + { + "encoding": "latin1", + "text": "utf16 🍇 🍈 🍉 🍊 🍋", + "correct": { + "type": "Buffer", + "data": [117, 116, 102, 49, 54, 32, 60, 71, 32, 60, 72, 32, 60, 73, 32, 60, 74, 32, 60, 75], + }, + "out": "utf16 { + const results = []; + for (let encoding of [ + "utf8", + "utf-8", + "utf16le", + "latin1", + "binary", + "base64", + /* TODO: "base64url", */ "hex", + ] as const) { + for (let text of ["ascii", "utf16 🍇 🍈 🍉 🍊 🍋", "👍"]) { + if (encoding === "base64" && !isAscii(Buffer.from(text))) + // TODO: output does not match Node.js, and it's not a problem with readFile specifically. + continue; + const correct = Buffer.from(text, encoding); + const outfile = join( + tmpdir(), + "promises.readFile-" + Date.now() + "-" + Math.random().toString(32) + "-" + encoding + ".txt", + ); + writeFileSync(outfile, correct); + const out = await fs.promises.readFile(outfile, encoding); + { + const { promise, resolve, reject } = Promise.withResolvers(); + + fs.readFile(outfile, encoding, (err, data) => { + if (err) reject(err); + else resolve(data); + }); + + expect(await promise).toEqual(out); + } + + expect(fs.readFileSync(outfile, encoding)).toEqual(out); + await promises.rm(outfile, { force: true }); + + expect(await promises.writeFile(outfile, text, encoding)).toBeUndefined(); + expect(await promises.readFile(outfile, encoding)).toEqual(out); + promises.rm(outfile, { force: true }); + + results.push({ + encoding, + text, + correct, + out, + }); + } + } + + expect(JSON.parse(JSON.stringify(results, null, 2))).toEqual(nodeOutput); + }); +}); + it("promises.readFile - UTF16 file path", async () => { const dest = `/tmp/superduperduperdupduperdupersuperduperduperduperduperduperdupersuperduperduperduperduperduperdupersuperduperduperdupe-Bun-👍-${Date.now()}-${ (Math.random() * 1024000) | 0 diff --git a/test/js/web/timers/setTimeout.test.js b/test/js/web/timers/setTimeout.test.js index 7f2325c55f..2e14f74987 100644 --- a/test/js/web/timers/setTimeout.test.js +++ b/test/js/web/timers/setTimeout.test.js @@ -300,6 +300,6 @@ it("setTimeout CPU usage #7790", async () => { }); const code = await process.exited; expect(code).toBe(0); - const stats = process.stats(); + const stats = process.resourceUsage(); expect(stats.cpuTime.user / BigInt(1e6)).toBeLessThan(1); }); diff --git a/test/js/workerd/html-rewriter.test.js b/test/js/workerd/html-rewriter.test.js index 3f96c22c16..99c484a256 100644 --- a/test/js/workerd/html-rewriter.test.js +++ b/test/js/workerd/html-rewriter.test.js @@ -31,6 +31,11 @@ describe("HTMLRewriter", () => { await gcTick(); }); + it("HTMLRewriter handles Symbol invalid type error", async () => { + expect(() => new HTMLRewriter().transform(new Response(Symbol("ok")))).toThrow(); + expect(() => new HTMLRewriter().transform(Symbol("ok"))).toThrow(); + }); + it("HTMLRewriter: async replacement using fetch + Bun.serve", async () => { await gcTick(); let content; @@ -59,17 +64,18 @@ describe("HTMLRewriter", () => { } }); - it("supports element handlers", async () => { - var rewriter = new HTMLRewriter(); - rewriter.on("div", { - element(element) { - element.setInnerContent("it worked!", { html: true }); - }, + for (let input of [new Response("
hello
"), "
hello
"]) { + it("supports element handlers with input " + input.constructor.name, async () => { + var rewriter = new HTMLRewriter(); + rewriter.on("div", { + element(element) { + element.setInnerContent("it worked!", { html: true }); + }, + }); + var output = rewriter.transform(input); + expect(typeof input === "string" ? output : await output.text()).toBe("
it worked!
"); }); - var input = new Response("
hello
"); - var output = rewriter.transform(input); - expect(await output.text()).toBe("
it worked!
"); - }); + } it("(from file) supports element handlers", async () => { var rewriter = new HTMLRewriter(); @@ -79,8 +85,7 @@ describe("HTMLRewriter", () => { }, }); await Bun.write("/tmp/html-rewriter.txt.js", "
hello
"); - var input = new Response(Bun.file("/tmp/html-rewriter.txt.js")); - var output = rewriter.transform(input); + var output = rewriter.transform(new Response(Bun.file("/tmp/html-rewriter.txt.js"))); expect(await output.text()).toBe("
it worked!
"); }); @@ -539,11 +544,9 @@ afterAll(() => { const request_types = ["/", "/gzip", "/chunked/gzip", "/chunked", "/file", "/file/gzip"]; ["http", "https"].forEach(protocol => { request_types.forEach(url => { - //TODO: change this when Bun.file supports https - const test = url.indexOf("file") !== -1 && protocol === "https" ? it.todo : it; - test(`works with ${protocol} fetch using ${url}`, async () => { + it(`works with ${protocol} fetch using ${url}`, async () => { const server = protocol === "http" ? http_server : https_server; - const server_url = `${protocol}://${server?.hostname}:${server?.port}`; + const server_url = server.url; const res = await fetch(`${server_url}${url}`, { tls: { rejectUnauthorized: false } }); let calls = 0; const rw = new HTMLRewriter();