diff --git a/src/bun.js/api/ShellArgs.classes.ts b/src/bun.js/api/ParsedShellScript.classes.ts similarity index 91% rename from src/bun.js/api/ShellArgs.classes.ts rename to src/bun.js/api/ParsedShellScript.classes.ts index 1a028e55e7..f6cac6c756 100644 --- a/src/bun.js/api/ShellArgs.classes.ts +++ b/src/bun.js/api/ParsedShellScript.classes.ts @@ -9,6 +9,8 @@ export default [ hasPendingActivity: false, configurable: false, valuesArray: true, + memoryCost: true, + estimatedSize: true, klass: {}, proto: { setCwd: { diff --git a/src/bun.js/api/Shell.classes.ts b/src/bun.js/api/Shell.classes.ts index 570c06584d..c806f04593 100644 --- a/src/bun.js/api/Shell.classes.ts +++ b/src/bun.js/api/Shell.classes.ts @@ -11,6 +11,8 @@ export default [ klass: {}, values: ["resolve", "reject"], valuesArray: true, + memoryCost: true, + estimatedSize: true, proto: { run: { fn: "runFromJS", diff --git a/src/bun.js/bindings/ShellBindings.cpp b/src/bun.js/bindings/ShellBindings.cpp index 52a767f66e..5d8cc9d692 100644 --- a/src/bun.js/bindings/ShellBindings.cpp +++ b/src/bun.js/bindings/ShellBindings.cpp @@ -2,6 +2,8 @@ #include "ZigGeneratedClasses.h" +extern "C" SYSV_ABI size_t ShellInterpreter__estimatedSize(void* ptr); + namespace Bun { using namespace JSC; @@ -21,6 +23,9 @@ extern "C" SYSV_ABI EncodedJSValue Bun__createShellInterpreter(Zig::GlobalObject ASSERT(structure); auto* result = WebCore::JSShellInterpreter::create(vm, globalObject, structure, ptr, WTFMove(args), resolveFn, rejectFn); + + size_t size = ShellInterpreter__estimatedSize(ptr); + vm.heap.reportExtraMemoryAllocated(result, size); return JSValue::encode(result); } diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index a5622f8e60..1c18c4738c 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -627,9 +627,6 @@ function generateConstructorImpl(typeName, obj: ClassDefinition) { Object.keys(fields).length > 0 ? generateHashTable(name, classSymbolName, typeName, obj, fields, false) : ""; const hashTableIdentifier = hashTable.length ? `${name}TableValues` : ""; - if (obj.estimatedSize) { - externs += `extern JSC_CALLCONV size_t ${symbolName(typeName, "estimatedSize")}(void* ptr);` + "\n"; - } return ( ` @@ -1371,6 +1368,10 @@ function generateClassHeader(typeName, obj: ClassDefinition) { const name = className(typeName); + if (obj.estimatedSize) { + externs += `extern JSC_CALLCONV size_t ${symbolName(typeName, "estimatedSize")}(void* ptr);` + "\n"; + } + const DECLARE_VISIT_CHILDREN = values.length || obj.estimatedSize || diff --git a/src/collections/baby_list.zig b/src/collections/baby_list.zig index 0ecf3b9df2..0a1c0badcc 100644 --- a/src/collections/baby_list.zig +++ b/src/collections/baby_list.zig @@ -364,7 +364,7 @@ pub fn BabyList(comptime Type: type) type { } pub fn memoryCost(this: Self) usize { - return this.cap; + return this.cap * @sizeOf(Type); } /// This method is available only for `BabyList(u8)`. diff --git a/src/shell/EnvMap.zig b/src/shell/EnvMap.zig index 46d4baa812..8d8c5210e2 100644 --- a/src/shell/EnvMap.zig +++ b/src/shell/EnvMap.zig @@ -26,6 +26,17 @@ pub fn init(alloc: Allocator) EnvMap { return .{ .map = MapType.init(alloc) }; } +pub fn memoryCost(this: *const EnvMap) usize { + var size: usize = @sizeOf(EnvMap); + size += std.mem.sliceAsBytes(this.map.keys()).len; + size += std.mem.sliceAsBytes(this.map.values()).len; + for (this.map.keys(), this.map.values()) |key, value| { + size += key.memoryCost(); + size += value.memoryCost(); + } + return size; +} + pub fn initWithCapacity(alloc: Allocator, cap: usize) EnvMap { var map = MapType.init(alloc); bun.handleOom(map.ensureTotalCapacity(cap)); diff --git a/src/shell/EnvStr.zig b/src/shell/EnvStr.zig index 37aff365e1..42e9155906 100644 --- a/src/shell/EnvStr.zig +++ b/src/shell/EnvStr.zig @@ -72,6 +72,21 @@ pub const EnvStr = packed struct(u128) { }; } + pub fn memoryCost(this: EnvStr) usize { + const divisor: usize = brk: { + if (this.asRefCounted()) |refc| { + break :brk refc.refcount; + } + break :brk 1; + }; + if (divisor == 0) { + @branchHint(.unlikely); + return 0; + } + + return this.len / divisor; + } + pub fn ref(this: EnvStr) void { if (this.asRefCounted()) |refc| { refc.ref(); diff --git a/src/shell/IO.zig b/src/shell/IO.zig index 6be1a9d886..2fd484d244 100644 --- a/src/shell/IO.zig +++ b/src/shell/IO.zig @@ -9,6 +9,14 @@ pub fn format(this: IO, comptime _: []const u8, _: std.fmt.FormatOptions, writer try writer.print("stdin: {}\nstdout: {}\nstderr: {}", .{ this.stdin, this.stdout, this.stderr }); } +pub fn memoryCost(this: *const IO) usize { + var size: usize = @sizeOf(IO); + size += this.stdin.memoryCost(); + size += this.stdout.memoryCost(); + size += this.stderr.memoryCost(); + return size; +} + pub fn deinit(this: *IO) void { this.stdin.close(); this.stdout.close(); @@ -76,6 +84,13 @@ pub const InKind = union(enum) { }, } } + + pub fn memoryCost(this: InKind) usize { + switch (this) { + .fd => return this.fd.memoryCost(), + .ignore => return 0, + } + } }; pub const OutKind = union(enum) { @@ -83,7 +98,17 @@ pub const OutKind = union(enum) { /// If `captured` is non-null, it will write to std{out,err} and also buffer it. /// The pointer points to the `buffered_stdout`/`buffered_stdin` fields /// in the Interpreter struct - fd: struct { writer: *Interpreter.IOWriter, captured: ?*bun.ByteList = null }, + fd: struct { + writer: *Interpreter.IOWriter, + captured: ?*bun.ByteList = null, + pub fn memoryCost(this: *const @This()) usize { + var cost: usize = this.writer.memoryCost(); + if (this.captured) |captured| { + cost += captured.memoryCost(); + } + return cost; + } + }, /// Buffers the output (handled in Cmd.BufferedIoClosed.close()) /// /// This is set when the shell is called with `.quiet()` @@ -91,6 +116,14 @@ pub const OutKind = union(enum) { /// Discards output ignore, + pub fn memoryCost(this: *const OutKind) usize { + return switch (this.*) { + .fd => |*fd| fd.memoryCost(), + .pipe => 0, + .ignore => 0, + }; + } + // fn dupeForSubshell(this: *ShellExecEnv, pub fn format(this: OutKind, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { switch (this) { diff --git a/src/shell/IOReader.zig b/src/shell/IOReader.zig index 4049e49075..582c50deb6 100644 --- a/src/shell/IOReader.zig +++ b/src/shell/IOReader.zig @@ -35,6 +35,13 @@ pub fn refSelf(this: *IOReader) *IOReader { return this; } +pub fn memoryCost(this: *const IOReader) usize { + var size: usize = @sizeOf(IOReader); + size += this.buf.allocatedSlice().len; + size += this.readers.memoryCost(); + return size; +} + pub fn eventLoop(this: *IOReader) jsc.EventLoopHandle { return this.evtloop; } @@ -224,6 +231,14 @@ pub const IOReaderChildPtr = struct { }; } + pub fn memoryCost(this: IOReaderChildPtr) usize { + if (this.ptr.is(Interpreter.Builtin.Cat)) { + // TODO: + return @sizeOf(Interpreter.Builtin.Cat); + } + return 0; + } + /// Return true if the child should be deleted pub fn onReadChunk(this: IOReaderChildPtr, chunk: []const u8, remove: *bool) Yield { return this.ptr.call("onIOReaderChunk", .{ chunk, remove }, Yield); diff --git a/src/shell/IOWriter.zig b/src/shell/IOWriter.zig index 26d0bb9f9b..debc800af8 100644 --- a/src/shell/IOWriter.zig +++ b/src/shell/IOWriter.zig @@ -75,6 +75,15 @@ pub fn refSelf(this: *IOWriter) *IOWriter { return this; } +pub fn memoryCost(this: *const IOWriter) usize { + var cost: usize = @sizeOf(IOWriter); + cost += this.buf.allocatedSlice().len; + cost += if (comptime bun.Environment.isWindows) this.winbuf.allocatedSlice().len else 0; + cost += this.writers.memoryCost(); + cost += this.writer.memoryCost(); + return cost; +} + pub const Flags = packed struct(u8) { pollable: bool = false, nonblocking: bool = false, @@ -156,7 +165,9 @@ pub fn __start(this: *IOWriter) Maybe(void) { this.writer.getPoll().?.flags.insert(.nonblocking); } - if (this.flags.is_socket) { + const sendto_MSG_NOWAIT_blocks = bun.Environment.isMac; + + if (this.flags.is_socket and (!sendto_MSG_NOWAIT_blocks or this.flags.nonblocking)) { this.writer.getPoll().?.flags.insert(.socket); } else if (this.flags.pollable) { this.writer.getPoll().?.flags.insert(.fifo); diff --git a/src/shell/ParsedShellScript.zig b/src/shell/ParsedShellScript.zig index 809b29d3c1..4871fead1d 100644 --- a/src/shell/ParsedShellScript.zig +++ b/src/shell/ParsedShellScript.zig @@ -12,6 +12,30 @@ export_env: ?EnvMap = null, quiet: bool = false, cwd: ?bun.String = null, this_jsvalue: JSValue = .zero, +estimated_size_for_gc: usize = 0, + +fn #computeEstimatedSizeForGC(this: *const ParsedShellScript) usize { + var size: usize = @sizeOf(ParsedShellScript); + if (this.args) |args| { + size += args.memoryCost(); + } + if (this.export_env) |*env| { + size += env.memoryCost(); + } + if (this.cwd) |*cwd| { + size += cwd.estimatedSize(); + } + size += std.mem.sliceAsBytes(this.jsobjs.allocatedSlice()).len; + return size; +} + +pub fn memoryCost(this: *const ParsedShellScript) usize { + return this.#computeEstimatedSizeForGC(); +} + +pub fn estimatedSize(this: *const ParsedShellScript) usize { + return this.estimated_size_for_gc; +} pub fn take( this: *ParsedShellScript, @@ -161,6 +185,7 @@ fn createParsedShellScriptImpl(globalThis: *jsc.JSGlobalObject, callframe: *jsc. .args = shargs, .jsobjs = jsobjs, }); + parsed_shell_script.estimated_size_for_gc = parsed_shell_script.#computeEstimatedSizeForGC(); const this_jsvalue = jsc.Codegen.JSParsedShellScript.toJSWithValues(parsed_shell_script, globalThis, marked_argument_buffer); parsed_shell_script.this_jsvalue = this_jsvalue; diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 6a5efa110c..e90b06988a 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -232,6 +232,10 @@ pub const ShellArgs = struct { .script_ast = undefined, }); } + + pub fn memoryCost(this: *const ShellArgs) usize { + return @sizeOf(ShellArgs) + this.script_ast.memoryCost(); + } }; pub const AssignCtx = Interpreter.Assigns.AssignCtx; @@ -277,6 +281,7 @@ pub const Interpreter = struct { this_jsvalue: JSValue = .zero, __alloc_scope: if (bun.Environment.enableAllocScopes) bun.AllocationScope else void, + estimated_size_for_gc: usize = 0, // Here are all the state nodes: pub const State = @import("./states/Base.zig"); @@ -355,7 +360,16 @@ pub const Interpreter = struct { const pid_t = if (bun.Environment.isPosix) std.posix.pid_t else uv.uv_pid_t; - const Bufio = union(enum) { owned: bun.ByteList, borrowed: *bun.ByteList }; + const Bufio = union(enum) { + owned: bun.ByteList, + borrowed: *bun.ByteList, + pub fn memoryCost(this: *const @This()) usize { + return switch (this.*) { + .owned => |*owned| owned.memoryCost(), + .borrowed => |borrowed| borrowed.memoryCost(), + }; + } + }; const Kind = enum { normal, @@ -369,6 +383,19 @@ pub const Interpreter = struct { return bun.default_allocator; } + pub fn memoryCost(this: *const ShellExecEnv) usize { + var size: usize = @sizeOf(ShellExecEnv); + size += this.shell_env.memoryCost(); + size += this.cmd_local_env.memoryCost(); + size += this.export_env.memoryCost(); + size += this.__cwd.allocatedSlice().len; + size += this.__prev_cwd.allocatedSlice().len; + size += this._buffered_stderr.memoryCost(); + size += this._buffered_stdout.memoryCost(); + size += this.async_pids.memoryCost(); + return size; + } + pub fn buffered_stdout(this: *ShellExecEnv) *bun.ByteList { return switch (this._buffered_stdout) { .owned => &this._buffered_stdout.owned, @@ -567,7 +594,7 @@ pub const Interpreter = struct { this.__cwd.clearRetainingCapacity(); bun.handleOom(this.__cwd.appendSlice(new_cwd[0 .. new_cwd.len + 1])); - if (comptime bun.Environment.allow_assert) { + if (comptime bun.Environment.isDebug) { assert(this.__cwd.items[this.__cwd.items.len -| 1] == 0); assert(this.__prev_cwd.items[this.__prev_cwd.items.len -| 1] == 0); } @@ -642,6 +669,27 @@ pub const Interpreter = struct { } }; + fn #computeEstimatedSizeForGC(this: *const ThisInterpreter) usize { + var size: usize = @sizeOf(ThisInterpreter); + size += this.args.memoryCost(); + size += this.root_shell.memoryCost(); + size += this.root_io.memoryCost(); + size += this.jsobjs.len * @sizeOf(JSValue); + for (this.vm_args_utf8.items) |arg| { + size += arg.byteSlice().len; + } + size += this.vm_args_utf8.allocatedSlice().len * @sizeOf(jsc.ZigString.Slice); + return size; + } + + pub fn memoryCost(this: *const ThisInterpreter) usize { + return this.#computeEstimatedSizeForGC(); + } + + pub fn estimatedSize(this: *const ThisInterpreter) usize { + return this.estimated_size_for_gc; + } + pub fn createShellInterpreter(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { const allocator = bun.default_allocator; const arguments_ = callframe.arguments_old(3); @@ -707,6 +755,7 @@ pub const Interpreter = struct { interpreter.flags.quiet = quiet; interpreter.globalThis = globalThis; + interpreter.estimated_size_for_gc = interpreter.#computeEstimatedSizeForGC(); const js_value = Bun__createShellInterpreter( globalThis, @@ -716,7 +765,6 @@ pub const Interpreter = struct { reject, ); interpreter.this_jsvalue = js_value; - interpreter.keep_alive.ref(globalThis.bunVM()); bun.analytics.Features.shell += 1; return js_value; @@ -748,7 +796,7 @@ pub const Interpreter = struct { return shell.ParseError.Lex; } - if (comptime bun.Environment.allow_assert) { + if (comptime bun.Environment.isDebug) { const debug = bun.Output.scoped(.ShellTokens, .hidden); var test_tokens = std.ArrayList(shell.Test.TestToken).initCapacity(arena_allocator, lex_result.tokens.len) catch @panic("OOPS"); defer test_tokens.deinit(); @@ -824,7 +872,7 @@ pub const Interpreter = struct { var cwd_arr = bun.handleOom(std.ArrayList(u8).initCapacity(bun.default_allocator, cwd.len + 1)); bun.handleOom(cwd_arr.appendSlice(cwd[0 .. cwd.len + 1])); - if (comptime bun.Environment.allow_assert) { + if (comptime bun.Environment.isDebug) { assert(cwd_arr.items[cwd_arr.items.len -| 1] == 0); } @@ -1191,6 +1239,7 @@ pub const Interpreter = struct { this.root_shell._buffered_stdout.owned.deinit(bun.default_allocator); } this.this_jsvalue = .zero; + this.args.deinit(); this.allocator.destroy(this); } diff --git a/src/shell/shell.zig b/src/shell/shell.zig index e4110869a9..248a89471d 100644 --- a/src/shell/shell.zig +++ b/src/shell/shell.zig @@ -315,10 +315,26 @@ pub const GlobalMini = struct { pub const AST = struct { pub const Script = struct { stmts: []Stmt, + + pub fn memoryCost(this: *const @This()) usize { + var cost: usize = 0; + for (this.stmts) |*stmt| { + cost += stmt.memoryCost(); + } + return cost; + } }; pub const Stmt = struct { exprs: []Expr, + + pub fn memoryCost(this: *const @This()) usize { + var cost: usize = 0; + for (this.exprs) |*expr| { + cost += expr.memoryCost(); + } + return cost; + } }; pub const Expr = union(Expr.Tag) { @@ -340,6 +356,25 @@ pub const AST = struct { /// could probably find a more efficient way to encode this information. @"async": *Expr, + pub fn memoryCost(this: *const @This()) usize { + return switch (this.*) { + .assign => |assign| brk: { + var cost: usize = 0; + for (assign) |*expr| { + cost += expr.memoryCost(); + } + break :brk cost; + }, + .binary => |binary| binary.memoryCost(), + .pipeline => |pipeline| pipeline.memoryCost(), + .cmd => |cmd| cmd.memoryCost(), + .subshell => |subshell| subshell.memoryCost(), + .@"if" => |@"if"| @"if".memoryCost(), + .condexpr => |condexpr| condexpr.memoryCost(), + .@"async" => |@"async"| @"async".memoryCost(), + }; + } + pub fn asPipelineItem(this: *Expr) ?PipelineItem { return switch (this.*) { .assign => .{ .assigns = this.assign }, @@ -370,6 +405,12 @@ pub const AST = struct { const ArgList = SmolList(Atom, 2); + pub fn memoryCost(this: *const @This()) usize { + var cost: usize = @sizeOf(Op); + cost += this.args.memoryCost(); + return cost; + } + // args: SmolList(1, comptime INLINED_MAX: comptime_int) pub const Op = enum { /// -a file @@ -592,6 +633,15 @@ pub const AST = struct { script: Script, redirect: ?Redirect = null, redirect_flags: RedirectFlags = .{}, + + pub fn memoryCost(this: *const @This()) usize { + var cost: usize = @sizeOf(Subshell); + cost += this.script.memoryCost(); + if (this.redirect) |*redirect| { + cost += redirect.memoryCost(); + } + return cost; + } }; /// TODO: If we know cond/then/elif/else is just a single command we don't need to store the stmt @@ -617,6 +667,14 @@ pub const AST = struct { .@"if" = @"if", }; } + + pub fn memoryCost(this: *const @This()) usize { + var cost: usize = @sizeOf(If); + cost += this.cond.memoryCost(); + cost += this.then.memoryCost(); + cost += this.else_parts.memoryCost(); + return cost; + } }; pub const Binary = struct { @@ -625,10 +683,25 @@ pub const AST = struct { right: Expr, const Op = enum { And, Or }; + + pub fn memoryCost(this: *const @This()) usize { + var cost: usize = @sizeOf(Binary); + cost += this.left.memoryCost(); + cost += this.right.memoryCost(); + return cost; + } }; pub const Pipeline = struct { items: []PipelineItem, + + pub fn memoryCost(this: *const @This()) usize { + var cost: usize = 0; + for (this.items) |*item| { + cost += item.memoryCost(); + } + return cost; + } }; pub const PipelineItem = union(enum) { @@ -637,6 +710,30 @@ pub const AST = struct { subshell: *Subshell, @"if": *If, condexpr: *CondExpr, + + pub fn memoryCost(this: *const @This()) usize { + var cost: usize = 0; + switch (this.*) { + .cmd => |cmd| { + cost += cmd.memoryCost(); + }, + .assigns => |assigns| { + for (assigns) |*assign| { + cost += assign.memoryCost(); + } + }, + .subshell => |subshell| { + cost += subshell.memoryCost(); + }, + .@"if" => |@"if"| { + cost += @"if".memoryCost(); + }, + .condexpr => |condexpr| { + cost += condexpr.memoryCost(); + }, + } + return cost; + } }; pub const CmdOrAssigns = union(CmdOrAssigns.Tag) { @@ -696,6 +793,13 @@ pub const AST = struct { .value = value, }; } + + pub fn memoryCost(this: *const @This()) usize { + var cost: usize = @sizeOf(Assign); + cost += this.label.len; + cost += this.value.memoryCost(); + return cost; + } }; pub const Cmd = struct { @@ -703,6 +807,21 @@ pub const AST = struct { name_and_args: []Atom, redirect: RedirectFlags = .{}, redirect_file: ?Redirect = null, + + pub fn memoryCost(this: *const @This()) usize { + var cost: usize = @sizeOf(Cmd); + for (this.assigns) |*assign| { + cost += assign.memoryCost(); + } + for (this.name_and_args) |*atom| { + cost += atom.memoryCost(); + } + + if (this.redirect_file) |*redirect_file| { + cost += redirect_file.memoryCost(); + } + return cost; + } }; /// Bit flags for redirects: @@ -787,6 +906,13 @@ pub const AST = struct { pub const Redirect = union(enum) { atom: Atom, jsbuf: JSBuf, + + pub fn memoryCost(this: *const @This()) usize { + return switch (this.*) { + .atom => |*atom| atom.memoryCost(), + .jsbuf => @sizeOf(JSBuf), + }; + } }; pub const Atom = union(Atom.Tag) { @@ -795,6 +921,13 @@ pub const AST = struct { pub const Tag = enum(u8) { simple, compound }; + pub fn memoryCost(this: *const @This()) usize { + return switch (this.*) { + .simple => |*simple| simple.memoryCost(), + .compound => |*compound| compound.memoryCost(), + }; + } + pub fn merge(this: Atom, right: Atom, allocator: Allocator) !Atom { if (this == .simple and right == .simple) { var atoms = try allocator.alloc(SimpleAtom, 2); @@ -896,6 +1029,12 @@ pub const AST = struct { cmd_subst: struct { script: Script, quoted: bool = false, + + pub fn memoryCost(this: *const @This()) usize { + var cost: usize = @sizeOf(@This()); + cost += this.script.memoryCost(); + return cost; + } }, pub fn glob_hint(this: SimpleAtom) bool { @@ -912,12 +1051,35 @@ pub const AST = struct { .tilde => false, }; } + + pub fn memoryCost(this: *const @This()) usize { + return switch (this.*) { + .Var => this.Var.len, + .Text => this.Text.len, + .cmd_subst => this.cmd_subst.memoryCost(), + else => 0, + } + @sizeOf(SimpleAtom); + } }; pub const CompoundAtom = struct { atoms: []SimpleAtom, brace_expansion_hint: bool = false, glob_hint: bool = false, + + pub fn memoryCost(this: *const @This()) usize { + var cost: usize = @sizeOf(CompoundAtom); + cost += this.#atomsMemoryCost(); + return cost; + } + + fn #atomsMemoryCost(this: *const @This()) usize { + var cost: usize = 0; + for (this.atoms) |*atom| { + cost += atom.memoryCost(); + } + return cost; + } }; }; @@ -4065,6 +4227,33 @@ pub fn SmolList(comptime T: type, comptime INLINED_MAX: comptime_int) type { return this; } + pub fn memoryCost(this: *const @This()) usize { + var cost: usize = @sizeOf(@This()); + switch (this.*) { + .inlined => |*inlined| { + if (comptime bun.trait.isContainer(T) and @hasDecl(T, "memoryCost")) { + for (inlined.slice()) |*item| { + cost += item.memoryCost(); + } + } else { + cost += std.mem.sliceAsBytes(inlined.allocatedSlice()).len; + } + }, + .heap => { + if (comptime bun.trait.isContainer(T) and @hasDecl(T, "memoryCost")) { + for (this.heap.slice()) |*item| { + cost += item.memoryCost(); + } + cost += this.heap.memoryCost(); + } else { + cost += std.mem.sliceAsBytes(this.heap.allocatedSlice()).len; + } + }, + } + + return cost; + } + pub fn initWithSlice(vals: []const T) @This() { if (bun.Environment.allow_assert) assert(vals.len <= std.math.maxInt(u32)); if (vals.len <= INLINED_MAX) { @@ -4098,6 +4287,14 @@ pub fn SmolList(comptime T: type, comptime INLINED_MAX: comptime_int) type { items: [INLINED_MAX]T = undefined, len: u32 = 0, + pub fn slice(this: *const Inlined) []const T { + return this.items[0..this.len]; + } + + pub fn allocatedSlice(this: *const Inlined) []const T { + return &this.items; + } + pub fn promote(this: *Inlined, n: usize, new: T) bun.BabyList(T) { var list = bun.handleOom(bun.BabyList(T).initCapacity(bun.default_allocator, n)); bun.handleOom(list.appendSlice(bun.default_allocator, this.items[0..INLINED_MAX])); diff --git a/src/sys.zig b/src/sys.zig index c80d767f3a..e34ebf055b 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -2101,27 +2101,29 @@ pub fn sendNonBlock(fd: bun.FileDescriptor, buf: []const u8) Maybe(usize) { pub fn send(fd: bun.FileDescriptor, buf: []const u8, flag: u32) Maybe(usize) { if (comptime Environment.isMac) { + const debug_timer = bun.Output.DebugTimer.start(); const rc = darwin_nocancel.@"sendto$NOCANCEL"(fd.cast(), buf.ptr, buf.len, flag, null, 0); if (Maybe(usize).errnoSysFd(rc, .send, fd)) |err| { - syslog("send({}, {d}) = {s}", .{ fd, buf.len, err.err.name() }); + syslog("send({}, {d}) = {s} ({f})", .{ fd, buf.len, err.err.name(), debug_timer }); return err; } - syslog("send({}, {d}) = {d}", .{ fd, buf.len, rc }); + syslog("send({}, {d}) = {d} ({f})", .{ fd, buf.len, rc, debug_timer }); return Maybe(usize){ .result = @as(usize, @intCast(rc)) }; } else { + const debug_timer = bun.Output.DebugTimer.start(); while (true) { const rc = linux.sendto(fd.cast(), buf.ptr, buf.len, flag, null, 0); if (Maybe(usize).errnoSysFd(rc, .send, fd)) |err| { if (err.getErrno() == .INTR) continue; - syslog("send({}, {d}) = {s}", .{ fd, buf.len, err.err.name() }); + syslog("send({}, {d}) = {s} ({f})", .{ fd, buf.len, err.err.name(), debug_timer }); return err; } - syslog("send({}, {d}) = {d}", .{ fd, buf.len, rc }); + syslog("send({}, {d}) = {d} ({f})", .{ fd, buf.len, rc, debug_timer }); return Maybe(usize){ .result = @as(usize, @intCast(rc)) }; } } @@ -2914,7 +2916,16 @@ pub fn socketpairImpl(domain: socketpair_t, socktype: socketpair_t, protocol: so if (comptime Environment.isMac) { if (for_shell) { // see the comment on `socketpairForShell` for why we don't - // set SO_NOSIGPIPE here + // set SO_NOSIGPIPE here. + + // macOS seems to default to around 8 + // KB for the buffer size this is comically small. for + // processes normally, we do about 512 KB. for this we do + // 128 KB since you might have a lot of them at once. + const so_recvbuf: c_int = 1024 * 128; + const so_sendbuf: c_int = 1024 * 128; + _ = std.c.setsockopt(fds_i[1], std.posix.SOL.SOCKET, std.posix.SO.RCVBUF, &so_recvbuf, @sizeOf(c_int)); + _ = std.c.setsockopt(fds_i[0], std.posix.SOL.SOCKET, std.posix.SO.SNDBUF, &so_sendbuf, @sizeOf(c_int)); } else { inline for (0..2) |i| { switch (setNoSigpipe(.fromNative(fds_i[i]))) { diff --git a/test/js/bun/shell/bunshell.test.ts b/test/js/bun/shell/bunshell.test.ts index 290399a1a2..8c1d2ba8d4 100644 --- a/test/js/bun/shell/bunshell.test.ts +++ b/test/js/bun/shell/bunshell.test.ts @@ -12,6 +12,9 @@ import { join, sep } from "path"; import { createTestBuilder, sortedShellOutput } from "./util"; const TestBuilder = createTestBuilder(import.meta.path); +afterAll(() => console.error("After all RSS", process.memoryUsage.rss() / 1024 / 1024)); +beforeAll(() => console.error("Before all RSS", process.memoryUsage.rss() / 1024 / 1024)); + export const bunEnv: NodeJS.ProcessEnv = { ...process.env, GITHUB_ACTIONS: "false", diff --git a/test/js/bun/shell/shell-blocking-pipe.test.ts b/test/js/bun/shell/shell-blocking-pipe.test.ts new file mode 100644 index 0000000000..3249b2caa3 --- /dev/null +++ b/test/js/bun/shell/shell-blocking-pipe.test.ts @@ -0,0 +1,33 @@ +import { $, generateHeapSnapshot } from "bun"; + +import { test } from "bun:test"; +import { isWindows } from "harness"; + +// We skip this test on Windows becasue: +// 1. Windows didn't have this problem to begin with +// 2. We need system cat. +test.skipIf(isWindows)("writing > send buffer size doesn't block the main thread", async () => { + const expected = Buffer.alloc(1024 * 1024, "bun!").toString(); + const massiveComamnd = "echo " + expected + " | " + Bun.which("cat"); + const pendingResult = $`${{ + raw: massiveComamnd, + }}`.text(); + + // Ensure that heap snapshot works, to excercise the memoryCost & estimated fields. + generateHeapSnapshot("v8"); + + const result = await pendingResult; + + if (result !== expected + "\n") { + throw new Error("Expected " + expected + "\n but got " + result); + } +}); + +test.skipIf(isWindows)("writing > send buffer size (with a variable) doesn't block the main thread", async () => { + const expected = Buffer.alloc(1024 * 1024, "bun!").toString(); + const result = await $`echo ${expected} | ${Bun.which("cat")}`.text(); + + if (result !== expected + "\n") { + throw new Error("Expected " + expected + "\n but got " + result); + } +}); diff --git a/test/js/bun/shell/shell-leak-args.test.ts b/test/js/bun/shell/shell-leak-args.test.ts index cd05212788..170fdd8c42 100644 --- a/test/js/bun/shell/shell-leak-args.test.ts +++ b/test/js/bun/shell/shell-leak-args.test.ts @@ -24,3 +24,45 @@ test("shell parsing error does not leak emmory", async () => { // Received: 0.25 expect(after - before).toBeLessThan(100); }); + +test("shell execution doesn't leak argv", async () => { + const buffer = Buffer.alloc(1024 * 1024, "bun!").toString(); + const cmd = `echo ${buffer}`; + for (let i = 0; i < 5; i++) { + await $`${{ raw: cmd }}`.quiet(); + } + const rss = process.memoryUsage.rss(); + for (let i = 0; i < 200; i++) { + await $`${{ raw: cmd }}`.quiet(); + } + const after = process.memoryUsage.rss() / 1024 / 1024; + const before = rss / 1024 / 1024; + // In Bun v1.3.0 on macOS arm64: + // Expected: < 250 + // Received: 588.515625 + // In Bun v1.3.1 on macOS arm64: + // Expected: < 250 + // Received: 93.875 + expect(after - before).toBeLessThan(250); +}); + +test("non-awaited shell command does not leak argv", async () => { + const buffer = Buffer.alloc(1024 * 1024, "bun!").toString(); + const cmd = `echo ${buffer}`; + for (let i = 0; i < 5; i++) { + $`${{ raw: cmd }}`.quiet(); + } + const rss = process.memoryUsage.rss(); + for (let i = 0; i < 200; i++) { + $`${{ raw: cmd }}`.quiet(); + } + const after = process.memoryUsage.rss() / 1024 / 1024; + const before = rss / 1024 / 1024; + // In Bun v1.3.0 on macOS arm64: + // Expected: < 250 + // Received: 588.515625 + // In Bun v1.3.1 on macOS arm64: + // Expected: < 250 + // Received: 93.875 + expect(after - before).toBeLessThan(250); +});