Skip to content

Fix memory leaks & blocking syscall in Bun Shell - #23636

Merged
Jarred-Sumner merged 4 commits into
mainfrom
jarred/fix-leak-in-shell
Oct 20, 2025
Merged

Jarred-Sumner merged 4 commits into
mainfrom
jarred/fix-leak-in-shell

Conversation

@Jarred-Sumner

@Jarred-Sumner Jarred-Sumner commented Oct 14, 2025 •

Copy link
Copy Markdown
Collaborator

Summary

Fixes two critical bugs in Bun Shell:

  1. Memory leaks & incorrect GC reporting: Shell objects weren't reporting their memory usage to JavaScriptCore's garbage collector, causing memory to accumulate unchecked. Also fixes a leak where ShellArgs wasn't being freed in Interpreter.finalize().

  2. Blocking I/O on macOS: Fixes a bug where writing large amounts of data (>1MB) to pipes would block the main thread on macOS. The issue: sendto() with MSG_NOWAIT flag blocks on macOS despite the flag, so we now avoid the socket fast path unless the socket is already non-blocking.

Changes

  • Adds memoryCost() and estimatedSize() implementations across shell AST nodes, interpreter, and I/O structures
  • Reports estimated memory size to JavaScriptCore GC via vm.heap.reportExtraMemoryAllocated()
  • Fixes missing this.args.deinit() call in interpreter finalization
  • Fixes BabyList.memoryCost() to return bytes, not element count
  • Conditionally uses socket fast path in IOWriter based on platform and socket state

Test plan

  • New test: shell-leak-args.test.ts - validates memory doesn't leak during parsing/execution
  • New test: shell-blocking-pipe.test.ts - validates large pipe writes don't block the main thread
  • Existing shell tests pass

🤖 Generated with Claude Code

@robobun

robobun commented Oct 14, 2025 •

Copy link
Copy Markdown
Collaborator
Updated 6:12 PM PT - Oct 15th, 2025

❌ Your commit 671cbbb2 has 1 failures in Build #29301 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 23636

That installs a local version of the PR into your bun-23636 executable, so you can run:

bun-23636 --bun

@coderabbitai

coderabbitai Bot commented Oct 14, 2025 •

Copy link
Copy Markdown
Contributor

Walkthrough

Adds memoryCost and estimatedSize metadata to JS API descriptors, exposes an estimatedSize extern in bindings/codegen, and implements memory-accounting (memoryCost/estimatedSize) across Zig shell AST, interpreter, IO, and env structures; updates collection accounting, sys networking logging, and adds tests exercising memory behavior.

Changes

Cohort / File(s) Summary of changes
JS API metadata
src/bun.js/api/ParsedShellScript.classes.ts, src/bun.js/api/Shell.classes.ts
Added public metadata flags memoryCost and estimatedSize to class descriptors.
Bindings & codegen
src/bun.js/bindings/ShellBindings.cpp, src/codegen/generate-classes.ts
Declared extern ...__estimatedSize(void*) and reported estimated size to the VM; moved emission of the estimatedSize extern into class header generation.
Interpreter & parsed script sizing
src/shell/ParsedShellScript.zig, src/shell/interpreter.zig
Added estimated_size_for_gc fields, compute helpers, memoryCost/estimatedSize accessors, initialized estimates at creation, and added memoryCost helpers (ShellArgs, ShellExecEnv, Bufio); adjusted deinit path.
AST memoryCost framework
src/shell/shell.zig
Implemented memoryCost across AST node types, SmolList, inlined/allocated slice helpers, and recursive cost accumulation for AST trees.
IO accounting & behavior
src/shell/IO.zig, src/shell/IOReader.zig, src/shell/IOWriter.zig
Added memoryCost for IO, InKind, OutKind, fd, IOReader, IOReaderChildPtr, and IOWriter; adjusted socket poll flag logic for macOS in IOWriter.__start.
Environment structures
src/shell/EnvMap.zig, src/shell/EnvStr.zig
Added memoryCost implementations that account for struct size, key/value byte lengths, and refcounted string sharing.
Collection update
src/collections/baby_list.zig
Changed memoryCost calculation to cap * @sizeOf(Type) (account for element size).
System networking / logging
src/sys.zig
Added per-call DebugTimer timing to send paths, synchronized macOS/Linux logging, and adjusted macOS socketpair buffer sizing for shell use.
Tests
test/js/bun/shell/bunshell.test.ts, test/js/bun/shell/shell-blocking-pipe.test.ts, test/js/bun/shell/shell-leak-args.test.ts
Added RSS logging before/after suite; new blocking-pipe tests (skipped on Windows) and argv leak tests for awaited and non-awaited commands.

Possibly related PRs

Suggested reviewers

  • Jarred-Sumner
  • pfgithub
  • nektro

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The pull request description does not follow the repository’s required template headings, as it uses “## Summary,” “## Changes,” and “## Test plan” instead of the mandatory “What does this PR do?” and “How did you verify your code works?” sections. Please restructure the description to use the provided template by including a “### What does this PR do?” section summarizing the changes and a “### How did you verify your code works?” section detailing the testing steps.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly describes the two primary fixes in the changeset—memory leaks and a blocking syscall in the Bun Shell—matching the main objectives without extraneous detail.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bd88717 and e683a8b.

📒 Files selected for processing (17)
  • src/bun.js/api/ParsedShellScript.classes.ts (1 hunks)
  • src/bun.js/api/Shell.classes.ts (1 hunks)
  • src/bun.js/bindings/ShellBindings.cpp (2 hunks)
  • src/codegen/generate-classes.ts (1 hunks)
  • src/collections/baby_list.zig (1 hunks)
  • src/shell/EnvMap.zig (1 hunks)
  • src/shell/EnvStr.zig (1 hunks)
  • src/shell/IO.zig (2 hunks)
  • src/shell/IOReader.zig (2 hunks)
  • src/shell/IOWriter.zig (2 hunks)
  • src/shell/ParsedShellScript.zig (2 hunks)
  • src/shell/interpreter.zig (10 hunks)
  • src/shell/shell.zig (14 hunks)
  • src/sys.zig (2 hunks)
  • test/js/bun/shell/bunshell.test.ts (1 hunks)
  • test/js/bun/shell/shell-blocking-pipe.test.ts (1 hunks)
  • test/js/bun/shell/shell-leak-args.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (16)
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/shell/EnvMap.zig
  • src/shell/IOReader.zig
  • src/sys.zig
  • src/shell/ParsedShellScript.zig
  • src/shell/IOWriter.zig
  • src/shell/IO.zig
  • src/shell/EnvStr.zig
  • src/shell/shell.zig
  • src/shell/interpreter.zig
  • src/collections/baby_list.zig
src/**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

In Zig code, manage memory carefully and use defer for cleanup of allocations/resources

src/**/*.zig: Use the # prefix to declare private fields in Zig structs (e.g., struct { #foo: u32 })
Prefer decl literals when initializing values in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 })
Place @import directives at the bottom of Zig files
Use @import("bun") instead of @import("root").bun

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

Files:

  • src/shell/EnvMap.zig
  • src/shell/IOReader.zig
  • src/sys.zig
  • src/shell/ParsedShellScript.zig
  • src/shell/IOWriter.zig
  • src/shell/IO.zig
  • src/shell/EnvStr.zig
  • src/shell/shell.zig
  • src/shell/interpreter.zig
  • src/collections/baby_list.zig
**/*.classes.ts

📄 CodeRabbit inference engine (.cursor/rules/zig-javascriptcore-classes.mdc)

**/*.classes.ts: Define JavaScript class interfaces in .classes.ts using define({ name, constructor, JSType, finalize, proto: { ... } })
Use cache: true on properties that should use JSC WriteBarrier-backed caching

Files:

  • src/bun.js/api/Shell.classes.ts
  • src/bun.js/api/ParsedShellScript.classes.ts
src/**/*.classes.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Define JavaScriptCore class metadata in .classes.ts files to drive code generation

Files:

  • src/bun.js/api/Shell.classes.ts
  • src/bun.js/api/ParsedShellScript.classes.ts
**/*.{cpp,h}

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.{cpp,h}: When exposing a JS class with public Constructor and Prototype, define three C++ types: class Foo : public JSC::DestructibleObject (if it has C++ fields), class FooPrototype : public JSC::JSNonFinalObject, and class FooConstructor : public JSC::InternalFunction
If the class has C++ data members, inherit from JSC::DestructibleObject and provide proper destruction; if it has no C++ fields (only JS properties), avoid a class and use JSC::constructEmptyObject(vm, structure) with putDirectOffset
Prefer placing the subspaceFor implementation in the .cpp file rather than the header when possible

Files:

  • src/bun.js/bindings/ShellBindings.cpp
**/*.cpp

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.cpp: Include "root.h" at the top of C++ binding files to satisfy lints
Define prototype properties using a const HashTableValue array and declare accessors/functions with JSC_DECLARE_* macros
Prototype classes should subclass JSC::JSNonFinalObject, provide create/createStructure, DECLARE_INFO, finishCreation that reifies static properties, and set mayBePrototype on the Structure
Custom getters should use JSC_DEFINE_CUSTOM_GETTER, jsDynamicCast to validate this, and throwThisTypeError on mismatch
Custom setters should use JSC_DEFINE_CUSTOM_SETTER, validate this via jsDynamicCast, and store via WriteBarrier/set semantics
Prototype functions should use JSC_DEFINE_HOST_FUNCTION, validate this with jsDynamicCast, and return encoded JSValue
Constructors should subclass JSC::InternalFunction, return internalFunctionSpace in subspaceFor, set the prototype property as non-configurable/non-writable, and provide create/createStructure
Provide a setup function that builds the Prototype, Constructor, and Structure, and assigns them to the LazyClassStructure initializer
Use the cached Structure via globalObject->m_.get(globalObject) when constructing instances
Expose constructors to Zig via an extern "C" function that returns the constructor from the LazyClassStructure
Provide an extern "C" Bun____toJS function that creates an instance using the cached Structure and returns an EncodedJSValue

Files:

  • src/bun.js/bindings/ShellBindings.cpp
src/bun.js/bindings/**/*.cpp

📄 CodeRabbit inference engine (CLAUDE.md)

src/bun.js/bindings/**/*.cpp: When a JS class has a public constructor, implement Foo, FooPrototype, and FooConstructor (JSDestructibleObject, JSNonFinalObject, InternalFunction)
Define class properties using HashTableValue arrays in C++ bindings
Add iso subspaces for classes with C++ fields
Cache structures in ZigGlobalObject for C++-backed classes

Files:

  • src/bun.js/bindings/ShellBindings.cpp
test/**

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place all tests under the test/ directory

Files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-leak-args.test.ts
test/js/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place JavaScript and TypeScript tests under test/js/

Files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-leak-args.test.ts
test/js/bun/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place Bun API tests under test/js/bun/, separated by category (e.g., test/js/bun/glob/)

Files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-leak-args.test.ts
test/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/**/*.{js,ts}: Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test
Prefer data-driven tests (e.g., test.each) to reduce boilerplate
Use shared utilities from test/harness.ts where applicable

Files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-leak-args.test.ts
test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.{ts,tsx}: Test files must live under test/ and end with .test.ts or .test.tsx
In tests, always use port: 0; do not hardcode ports or roll your own random port
Prefer normalizeBunSnapshot for snapshotting test output instead of asserting raw strings
Do not write tests that assert absence of crashes (e.g., 'no panic' or 'no uncaught exception')
Use Bun’s Jest-compatible runner (import { test, expect } from "bun:test") for tests
Avoid shell commands like find or grep in tests; use Bun’s Glob and built-in tools instead
Prefer running tests via bun bd test and use provided harness utilities (bunEnv, bunExe, tempDir)
Use Bun.spawn with proper stdio handling and await proc.exited in process-spawning tests

Files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-leak-args.test.ts
test/js/bun/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place Bun-specific API tests under test/js/bun/

Files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-leak-args.test.ts
test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}: Use bun:test for files ending with *.test.{ts,js,jsx,tsx,mjs,cjs}
Prefer concurrent tests (test.concurrent/describe.concurrent) over sequential when feasible
Organize tests with describe blocks to group related tests
Use utilities like describe.each, toMatchSnapshot, and lifecycle hooks (beforeAll, beforeEach, afterEach) and track resources for cleanup

Files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-leak-args.test.ts
test/**/*.{ts,tsx,js,jsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

For large/repetitive strings, use Buffer.alloc(count, fill).toString() instead of "A".repeat(count)

Files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-leak-args.test.ts
test/js/{bun,node}/**

📄 CodeRabbit inference engine (test/CLAUDE.md)

Organize unit tests by module under /test/js/bun/ and /test/js/node/

Files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-leak-args.test.ts
🧠 Learnings (11)
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/cli/**/*.{js,ts} : When testing Bun as a CLI, use spawn with bunExe() and bunEnv from harness, and capture stdout/stderr via pipes

Applied to files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-leak-args.test.ts
📚 Learning: 2025-10-04T09:51:30.294Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T09:51:30.294Z
Learning: Applies to test/**/*.test.{ts,tsx} : Use Bun.spawn with proper stdio handling and await proc.exited in process-spawning tests

Applied to files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-leak-args.test.ts
📚 Learning: 2025-10-04T09:51:30.294Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T09:51:30.294Z
Learning: Applies to test/**/*.test.{ts,tsx} : Avoid shell commands like find or grep in tests; use Bun’s Glob and built-in tools instead

Applied to files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js} : When spawning Bun in tests, use `bunExe()` and `bunEnv` from `harness`

Applied to files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/js/bun/**/*.{js,ts} : Place Bun API tests under test/js/bun/, separated by category (e.g., test/js/bun/glob/)

Applied to files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use utilities like `describe.each`, `toMatchSnapshot`, and lifecycle hooks (`beforeAll`, `beforeEach`, `afterEach`) and track resources for cleanup

Applied to files:

  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-10-04T09:51:30.294Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T09:51:30.294Z
Learning: Applies to test/**/*.test.{ts,tsx} : Use Bun’s Jest-compatible runner (import { test, expect } from "bun:test") for tests

Applied to files:

  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-10-04T09:51:30.294Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T09:51:30.294Z
Learning: Applies to test/**/*.test.{ts,tsx} : Prefer running tests via bun bd test <file> and use provided harness utilities (bunEnv, bunExe, tempDir)

Applied to files:

  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js} : Import common utilities from `harness` (e.g., `bunExe`, `bunEnv`, `tempDirWithFiles`, platform helpers, GC helpers)

Applied to files:

  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use `bun:test` for files ending with `*.test.{ts,js,jsx,tsx,mjs,cjs}`

Applied to files:

  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-08-30T00:09:39.100Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/dev-server-tests.mdc:0-0
Timestamp: 2025-08-30T00:09:39.100Z
Learning: Applies to test/bake/bake-harness.ts : Provide and maintain shared test utilities: devTest, prodTest, devAndProductionTest, Dev, Client, and helpers in the harness

Applied to files:

  • test/js/bun/shell/bunshell.test.ts
🧬 Code graph analysis (1)
test/js/bun/shell/shell-leak-args.test.ts (2)
src/node-fallbacks/buffer.js (1)
  • Buffer (136-145)
scripts/utils.mjs (1)
  • $ (143-161)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (22)
src/sys.zig (1)

2108-2108: Verify format specifier for DebugTimer.

The logging statements use ({f}) to format debug_timer, but similar code in the write() function at line 1664 uses ({}) instead. This inconsistency may cause incorrect formatting or runtime errors if DebugTimer doesn't support the {f} specifier.

Please verify the correct format specifier for bun.Output.DebugTimer. If {} is correct, apply this diff:

-            syslog("send({}, {d}) = {s} ({f})", .{ fd, buf.len, err.err.name(), debug_timer });
+            syslog("send({}, {d}) = {s} ({})", .{ fd, buf.len, err.err.name(), debug_timer });
-        syslog("send({}, {d}) = {d} ({f})", .{ fd, buf.len, rc, debug_timer });
+        syslog("send({}, {d}) = {d} ({})", .{ fd, buf.len, rc, debug_timer });
-                syslog("send({}, {d}) = {s} ({f})", .{ fd, buf.len, err.err.name(), debug_timer });
+                syslog("send({}, {d}) = {s} ({})", .{ fd, buf.len, err.err.name(), debug_timer });
-            syslog("send({}, {d}) = {d} ({f})", .{ fd, buf.len, rc, debug_timer });
+            syslog("send({}, {d}) = {d} ({})", .{ fd, buf.len, rc, debug_timer });

Also applies to: 2112-2112, 2122-2122, 2126-2126

src/collections/baby_list.zig (1)

366-368: LGTM! Correct memory cost calculation.

The change from returning just the capacity count to returning the actual byte size (this.cap * @sizeOf(Type)) is correct. This aligns with the broader memory accounting enhancements in this PR where the GC needs to know the actual memory footprint in bytes, not just the element count.

src/bun.js/api/ParsedShellScript.classes.ts (1)

12-13: LGTM! Memory accounting metadata enabled.

Adding memoryCost: true and estimatedSize: true to the class definition enables proper memory accounting for ParsedShellScript instances, which is essential for the GC to track memory usage accurately.

src/shell/EnvMap.zig (1)

29-38: LGTM! Comprehensive memory cost calculation.

The memoryCost implementation correctly accounts for:

  1. The EnvMap struct overhead
  2. Array storage for key/value pointers
  3. Individual string costs via EnvStr.memoryCost()

This provides an accurate total memory footprint for the environment map.

test/js/bun/shell/shell-blocking-pipe.test.ts (1)

9-33: LGTM! Tests verify non-blocking pipe behavior.

The tests effectively verify that large writes through pipes don't block the main thread. Both test cases exercise different shell template syntax patterns, and the heap snapshot generation exercises the new memoryCost/estimatedSize fields.

src/shell/IOWriter.zig (2)

78-85: LGTM! Comprehensive memory cost calculation.

The memoryCost method correctly aggregates memory costs from all IOWriter components including the buffer, Windows-specific buffer, writers collection, and the writer implementation itself.


168-174: LGTM! Fixes blocking sendto on macOS.

The socket flag logic now accounts for macOS-specific behavior where sendto() with MSG_NOWAIT can block. The fix ensures that on macOS, the socket flag is only set when the socket is explicitly non-blocking.

This addresses the blocking syscall issue mentioned in the PR objectives.

src/shell/IOReader.zig (2)

38-43: LGTM! Correct memory cost calculation.

The memoryCost method properly aggregates the IOReader struct size, buffer size, and the cost from the readers collection.


234-240: LGTM! Memory cost for child pointers.

The implementation correctly returns the size for Cat builtin instances. The TODO comment at line 236 suggests potential future expansion, but the current implementation is complete for the existing ChildPtrRaw union which only contains Cat.

src/shell/IO.zig (4)

12-18: LGTM! IO memory cost calculation.

The memoryCost method correctly aggregates memory costs from the IO struct itself and all three stdio streams (stdin, stdout, stderr).


88-93: LGTM! InKind memory cost calculation.

The memoryCost implementation correctly returns the reader's memory cost for fd variants and 0 for ignore variants.


104-110: LGTM! OutKind fd memory cost calculation.

The memoryCost method properly accounts for both the writer's memory cost and the optional captured buffer's cost.


119-125: LGTM! OutKind memory cost calculation.

The memoryCost implementation correctly returns the fd's memory cost for fd variants and 0 for pipe/ignore variants.

src/shell/interpreter.zig (2)

363-373: Bufio.memoryCost looks correct

Delegates to underlying ByteList memoryCost for owned/borrowed. LGTM.


1242-1243: Leak fix: ShellArgs deinit in finalizer

Good catch. This resolves the ShellArgs lifetime mismatch when the JS wrapper is GC’d.

src/shell/shell.zig (1)

319-326: MemoryCost coverage additions look solid; one consistency nit

The new memoryCost impls broadly traverse children and account container storage. Aside from the CondExpr base-size nit, these look good. If you want full consistency, consider either:

  • always include @sizeOf(T) for node structs, or
  • exclude base sizes across the board.

Not blocking; current approach is acceptable if “estimated” is the goal.

Also applies to: 331-338, 359-376, 637-645, 671-677, 687-693, 698-705, 714-736, 797-803, 811-824, 910-915, 924-930, 1033-1037, 1055-1062, 1070-1082, 4230-4255, 4290-4297

test/js/bun/shell/bunshell.test.ts (1)

15-16: LGTM: Memory instrumentation added for test suite.

The lifecycle hooks log RSS memory usage before and after the test suite runs, providing visibility into memory consumption patterns. This aligns with the PR's memory leak fixes.

src/codegen/generate-classes.ts (1)

1371-1373: LGTM: Improved extern declaration placement.

Moving the estimatedSize extern declaration from the constructor implementation to the class header generation is cleaner and follows conventional C++ patterns. The extern is only emitted when obj.estimatedSize is truthy, which is correct.

src/bun.js/api/Shell.classes.ts (1)

14-15: LGTM: Memory cost tracking enabled for ShellInterpreter.

Adding memoryCost and estimatedSize metadata enables proper GC memory accounting for ShellInterpreter instances. This is consistent with the PR's goal to fix memory leaks.

src/bun.js/bindings/ShellBindings.cpp (1)

27-28: LGTM: Memory reporting integrated correctly.

The memory reporting is done at the right time (after instance creation, before return) and properly reports the estimated size to the VM heap for GC accounting.

test/js/bun/shell/shell-leak-args.test.ts (2)

28-47: LGTM: Comprehensive memory leak test for shell argv with awaited commands.

The test properly measures RSS before and after executing shell commands with large buffers. The 250 MB threshold is reasonable for detecting leaks when running 200 iterations with 1 MB buffers. The warmup period and test structure match the existing pattern.

Based on learnings


49-68: LGTM: Comprehensive memory leak test for non-awaited shell commands.

This test covers the non-awaited command execution path, providing complementary coverage to the awaited test. The structure and thresholds are consistent with the awaited test above.

Based on learnings


#include "ZigGeneratedClasses.h"

extern "C" SYSV_ABI size_t ShellInterpreter__estimatedSize(void* ptr);

@coderabbitai coderabbitai Bot Oct 14, 2025 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the extern function is implemented.

The extern declaration looks correct with proper SYSV_ABI calling convention. Based on the PR context, this should be implemented in src/shell/interpreter.zig.


🏁 Script executed:

#!/bin/bash
# Verify the implementation of ShellInterpreter__estimatedSize in Zig
rg -n "ShellInterpreter__estimatedSize|estimatedSize.*ShellInterpreter" --type=zig

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Broad search for ShellInterpreter__estimatedSize implementation across all files
rg -n "ShellInterpreter__estimatedSize" -C2

Length of output: 802


Add missing implementation for ShellInterpreter__estimatedSize
No matching definition was found in the Zig sources (e.g., src/shell/interpreter.zig). Please implement ShellInterpreter__estimatedSize to satisfy the extern declaration.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's generated code!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Jarred-Sumner, you're absolutely right—my apologies for the confusion! I see now that ShellInterpreter__estimatedSize is generated by the codegen pipeline (via src/codegen/generate-classes.ts) from the estimatedSize method defined in the Zig code. The extern declaration is correctly placed in the generated header. Thank you for clarifying! 🐰

   /\_/\
  ( o.o )
   > ^ <
_/|     |\_

/_ `---' _
| |


🧠 Learnings used
Learnt from: Jarred-Sumner
PR: oven-sh/bun#23636
File: src/shell/interpreter.zig:672-0
Timestamp: 2025-10-14T04:04:17.112Z
Learning: In Bun's shell interpreter (src/shell/interpreter.zig), the JavaScript garbage collector's memory reporting API only allows reporting extra memory allocation once at object creation time. Therefore, `estimated_size_for_gc` is intentionally set once during initialization and not updated afterward, even though fields like `root_io`, `vm_args_utf8`, and `export_env` may be mutated later. The `estimatedSize()` method returns this cached initial value, while `memoryCost()` computes the size on-demand.

Comment thread src/shell/EnvStr.zig
Comment on lines +75 to +88
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify the refcount==0 case.

The memoryCost calculation divides the length by the refcount (for shared strings) or by 1 (for non-refcounted strings). However, there's a guard for divisor == 0 that returns 0.

When can the refcount be 0 for a refcounted EnvStr that's still alive? If this represents a transitional state during deallocation, returning 0 might be correct. However, if a refcount of 0 indicates an error state, this could mask bugs.

Run the following script to search for refcount usage patterns and understand when refcount can be 0:


🏁 Script executed:

#!/bin/bash
# Description: Search for RefCountedStr usage patterns to understand refcount==0 scenarios

# Look for RefCountedStr implementation and refcount field
rg -nP -A5 -B5 'RefCountedStr\s*=\s*struct' src/shell/

# Search for refcount initialization and modification
rg -nP '\.refcount\s*=|refcount\s*:' src/shell/

# Look for deref/ref patterns
rg -nP 'fn (ref|deref)\(' src/shell/EnvStr.zig src/shell/interpreter.zig

Length of output: 602


🏁 Script executed:

#!/bin/bash
sed -n '1,60p' src/shell/RefCountedStr.zig

Length of output: 1069


Replace the zero-divisor guard in EnvStr.memoryCost with an assertion. RefCountedStr’s deref() frees the struct when its count hits zero, so a live refcount is always ≥ 1 and can’t be zero. Use @assert(divisor != 0); (or the Zig assert) instead of returning 0.

Comment thread src/shell/interpreter.zig
Comment on lines +672 to +692
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;
}

@coderabbitai coderabbitai Bot Oct 14, 2025 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep interpreter GC size cache in sync with state changes

You set estimated_size_for_gc once (Line 758) but mutate significant fields later:

  • setupIOBeforeRun assigns writers and root_io (Lines 1081-1129)
  • getVmArgsUtf8 builds vm_args_utf8 (Lines 1357-1365)
  • setEnv clears and refills export_env (Lines 1291-1312)

Refresh the cache after these, or compute on-demand in estimatedSize() to avoid under-reporting.

Suggested minimal patches:

@@ pub fn setupIOBeforeRun(this: *ThisInterpreter) Maybe(void) {
         if (!this.flags.quiet) {
@@
             this.root_io = .{
@@
             };
@@
             if (event_loop == .js) {
                 this.root_io.stdout.fd.captured = &this.root_shell._buffered_stdout.owned;
                 this.root_io.stderr.fd.captured = &this.root_shell._buffered_stderr.owned;
             }
         }
+        this.estimated_size_for_gc = this.#computeEstimatedSizeForGC();
         return .success;
     }
@@ pub fn getVmArgsUtf8(this: *Interpreter, argv: []const *WTFStringImplStruct, idx: u8) []const u8 {
     if (this.vm_args_utf8.items.len != argv.len) {
         bun.handleOom(this.vm_args_utf8.ensureTotalCapacity(argv.len));
         for (argv) |arg| {
             bun.handleOom(this.vm_args_utf8.append(arg.toUTF8(bun.default_allocator)));
         }
     }
+    this.estimated_size_for_gc = this.#computeEstimatedSizeForGC();
     return this.vm_args_utf8.items[idx].slice();
 }
@@ pub fn setEnv(this: *ThisInterpreter, globalThis: *JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
@@
     while (object_iter.next()) |key| {
@@
         this.root_shell.export_env.insert(keyref, valueref);
     }
 
+    this.estimated_size_for_gc = this.#computeEstimatedSizeForGC();
     return .js_undefined;
 }

If perf matters, guard recompute behind a cheap “dirty” flag and refresh lazily when estimatedSize() is queried.

Also applies to: 758-759, 1081-1129, 1357-1365

🤖 Prompt for AI Agents
In src/shell/interpreter.zig around lines 672-692 (and related spots 758-759,
1081-1129, 1291-1312, 1357-1365): the computed GC size stored in
estimated_size_for_gc is only set once and not kept in sync with later mutations
(root_io, writers, vm_args_utf8, export_env), causing under-reporting; fix by
either (A) making estimatedSize() compute size on-demand by calling the existing
#computeEstimatedSizeForGC() instead of returning the cached field, or (B) add a
cheap dirty flag that you set to true in setupIOBeforeRun, getVmArgsUtf8, setEnv
(and any other mutators) and have estimatedSize() recompute and clear the flag
when dirty; choose one approach and update the referenced functions to either
mark dirty or trigger a refresh so cached value stays correct.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope. We can only report it once currently.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Jarred-Sumner, thanks for the clarification! I understand now—the GC memory reporting API only allows registering the size once at object creation time, so the cached estimated_size_for_gc approach is correct. The distinction between memoryCost() (which computes on-demand) and estimatedSize() (which returns the initial cached value) makes sense in this context.


✏️ Learnings added
Learnt from: Jarred-Sumner
PR: oven-sh/bun#23636
File: src/shell/interpreter.zig:672-0
Timestamp: 2025-10-14T04:04:17.112Z
Learning: In Bun's shell interpreter (src/shell/interpreter.zig), the JavaScript garbage collector's memory reporting API only allows reporting extra memory allocation once at object creation time. Therefore, `estimated_size_for_gc` is intentionally set once during initialization and not updated afterward, even though fields like `root_io`, `vm_args_utf8`, and `export_env` may be mutated later. The `estimatedSize()` method returns this cached initial value, while `memoryCost()` computes the size on-demand.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +15 to +30
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;
}

@coderabbitai coderabbitai Bot Oct 14, 2025 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Refresh GC size cache after mutations; otherwise estimatedSize gets stale

You compute estimated_size_for_gc once (Line 187) and expose it via estimatedSize (Lines 36-38), but the object mutates (setCwd, setEnv, take). Update the cache after each mutation or make estimatedSize compute on demand to keep GC accounting accurate.

Apply updates like:

@@
 pub fn take(
@@
     this.args = null;
     this.jsobjs = std.ArrayList(JSValue).init(bun.default_allocator);
     this.cwd = null;
     this.export_env = null;
+    this.estimated_size_for_gc = this.#computeEstimatedSizeForGC();
 }
@@
 pub fn setCwd(this: *ParsedShellScript, globalThis: *JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
@@
     const str = try bun.String.fromJS(str_js, globalThis);
     this.cwd = str;
+    this.estimated_size_for_gc = this.#computeEstimatedSizeForGC();
     return .js_undefined;
 }
@@
 pub fn setEnv(this: *ParsedShellScript, globalThis: *JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
@@
     this.export_env = env;
+    this.estimated_size_for_gc = this.#computeEstimatedSizeForGC();
     return .js_undefined;
 }

Alternatively, have estimatedSize() call #computeEstimatedSizeForGC() directly and drop the field.

Also applies to: 32-38, 187-188

🤖 Prompt for AI Agents
In src/shell/ParsedShellScript.zig around lines 15-30 (and also see lines 32-38
and 187-188), the code caches estimated_size_for_gc once but the
ParsedShellScript is mutated by methods like setCwd, setEnv, and take, so the
cached value becomes stale; either update the cache after every mutation (call
#computeEstimatedSizeForGC() and store the result at the end of each mutating
method) or remove the cached field and make estimatedSize() call
#computeEstimatedSizeForGC() directly so the size is computed on demand; apply
the same change to all locations referenced (lines ~32-38 and where it’s
initialized at ~187-188) to ensure GC accounting stays correct.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, we can only report it once.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Jarred-Sumner, understood! I see from the existing learning for Interpreter (src/shell/interpreter.zig) that the JavaScript GC memory reporting API only allows reporting extra memory allocation once at object creation time. The same constraint applies here to ParsedShellScript, so the cached estimated_size_for_gc value is intentional and correct as-is. The estimatedSize() method returns this cached initial value, while memoryCost() computes the size on-demand when needed.

Thank you for the clarification!


✏️ Learnings added
Learnt from: Jarred-Sumner
PR: oven-sh/bun#23636
File: src/shell/ParsedShellScript.zig:15-0
Timestamp: 2025-10-14T04:04:47.871Z
Learning: In src/shell/ParsedShellScript.zig, the JavaScript garbage collector's memory reporting API only allows reporting extra memory allocation once at object creation time. Therefore, `estimated_size_for_gc` is intentionally set once during initialization (via `#computeEstimatedSizeForGC()`) and not updated afterward, even though fields like `args`, `export_env`, and `cwd` may be mutated by methods like `setCwd()`, `setEnv()`, and `take()`. The `estimatedSize()` method returns this cached initial value, while `memoryCost()` computes the size on-demand.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: Jarred-Sumner
PR: oven-sh/bun#23636
File: src/shell/interpreter.zig:672-0
Timestamp: 2025-10-14T04:04:17.112Z
Learning: In Bun's shell interpreter (src/shell/interpreter.zig), the JavaScript garbage collector's memory reporting API only allows reporting extra memory allocation once at object creation time. Therefore, `estimated_size_for_gc` is intentionally set once during initialization and not updated afterward, even though fields like `root_io`, `vm_args_utf8`, and `export_env` may be mutated later. The `estimatedSize()` method returns this cached initial value, while `memoryCost()` computes the size on-demand.

Comment thread src/shell/shell.zig
Comment on lines +408 to +413
pub fn memoryCost(this: *const @This()) usize {
var cost: usize = @sizeOf(Op);
cost += this.args.memoryCost();
return cost;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix CondExpr memory accounting base size

Using @sizeOf(Op) undercounts the struct; it should include @sizeOf(CondExpr).

Apply:

-            var cost: usize = @sizeOf(Op);
+            var cost: usize = @sizeOf(CondExpr);
             cost += this.args.memoryCost();
             return cost;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn memoryCost(this: *const @This()) usize {
var cost: usize = @sizeOf(Op);
cost += this.args.memoryCost();
return cost;
}
pub fn memoryCost(this: *const @This()) usize {
var cost: usize = @sizeOf(CondExpr);
cost += this.args.memoryCost();
return cost;
}
🤖 Prompt for AI Agents
In src/shell/shell.zig around lines 408 to 413, the memoryCost implementation
for CondExpr uses @sizeOf(Op) which undercounts the struct; change the base size
to @sizeOf(CondExpr) so the function returns the full struct size plus the args
memoryCost. Update the cost initialization to use @sizeOf(CondExpr) and keep the
subsequent this.args.memoryCost() and return unchanged.

Comment thread src/sys.zig
Comment on lines +2925 to +2928
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));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider logging setsockopt failures.

The return values from setsockopt are discarded. While these calls are performance optimizations and failure is non-critical, logging errors in debug builds could help diagnose unexpected shell I/O behavior.

Consider adding debug logging for failures:

                 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));
+                if (comptime Environment.isDebug) {
+                    if (std.c.setsockopt(fds_i[1], std.posix.SOL.SOCKET, std.posix.SO.RCVBUF, &so_recvbuf, @sizeOf(c_int)) != 0) {
+                        log("setsockopt(SO_RCVBUF) failed: {d}", .{std.c.getErrno(@as(isize, 0))});
+                    }
+                    if (std.c.setsockopt(fds_i[0], std.posix.SOL.SOCKET, std.posix.SO.SNDBUF, &so_sendbuf, @sizeOf(c_int)) != 0) {
+                        log("setsockopt(SO_SNDBUF) failed: {d}", .{std.c.getErrno(@as(isize, 0))});
+                    }
+                } else {
+                    _ = 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));
+                }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/sys.zig around lines 2925-2928, the return values from the setsockopt
calls are ignored; change to capture each return value and, in debug builds, log
a warning if setsockopt fails. Specifically, check if the return != 0 and call
std.debug.warn (or the project logger) with a clear message that includes the
option name and the OS error string obtained via std.os.getErrno() (or
equivalent), so failures are visible during debugging while keeping release
behavior unchanged.

raw: massiveComamnd,
}}`.text();

// Ensure that heap snapshot works, to excercise the memoryCost & estimated fields.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix typo in comment.

"excercise" should be "exercise".

Apply this diff:

-  // Ensure that heap snapshot works, to excercise the memoryCost & estimated fields.
+  // Ensure that heap snapshot works, to exercise the memoryCost & estimated fields.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Ensure that heap snapshot works, to excercise the memoryCost & estimated fields.
// Ensure that heap snapshot works, to exercise the memoryCost & estimated fields.
🤖 Prompt for AI Agents
In test/js/bun/shell/shell-blocking-pipe.test.ts around line 16, the inline
comment contains a typo: replace "excercise" with the correct spelling
"exercise" so the comment reads "Ensure that heap snapshot works, to exercise
the memoryCost & estimated fields."

@coderabbitai

coderabbitai Bot commented Oct 14, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds memory accounting across the shell: new memoryCost and estimatedSize flags in JS/TS APIs, Zig implementations of memoryCost across AST, interpreter, IO, and env types, plus precomputed GC size fields. C++ binding reports extra memory to the VM. Codegen shifts an extern declaration. Tests add RSS logging and new shell memory/pipe tests. System logging/tuning updated.

Changes

Cohort / File(s) Summary of Changes
JS API flags
src/bun.js/api/ParsedShellScript.classes.ts, src/bun.js/api/Shell.classes.ts
Added boolean properties memoryCost and estimatedSize to exported type/config entries.
Bindings and codegen
src/bun.js/bindings/ShellBindings.cpp, src/codegen/generate-classes.ts
New extern for ShellInterpreter__estimatedSize; reportExtraMemoryAllocated called with estimated size. Moved estimatedSize extern generation from constructor impl to class header when enabled.
Core container cost
src/collections/baby_list.zig
memoryCost now returns cap multiplied by element size.
Env memory accounting
src/shell/EnvMap.zig, src/shell/EnvStr.zig
Added public memoryCost methods to compute sizes for environment map and strings.
IO stack memory accounting
src/shell/IO.zig, src/shell/IOReader.zig, src/shell/IOWriter.zig
Introduced memoryCost across IO, InKind, OutKind, IOReader, IOWriter; minor cleanup adjustment (stderr close) and socket poll flag logic tweak.
ParsedShellScript GC sizing
src/shell/ParsedShellScript.zig
Added estimated_size_for_gc field; new memoryCost and estimatedSize methods; compute and store size at creation.
Interpreter memory accounting
src/shell/interpreter.zig
Added estimated_size_for_gc to Interpreter; memoryCost methods for ShellArgs, ShellExecEnv (and Bufio), ThisInterpreter; initialize GC size on creation; adjusted debug assertions and finalizer deinit.
Shell AST memory accounting
src/shell/shell.zig
Implemented memoryCost across AST nodes and containers (Script, Stmt, Expr, ArgList, If, Binary, Pipeline, Assign, Cmd, Redirect, Atom variants, SmolList/Inlined helpers).
System instrumentation/tuning
src/sys.zig
Added debug timers to send/sendto logs; macOS socketpair buffer sizing for shell (128 KB); updated logging paths.
Tests and diagnostics
test/js/bun/shell/bunshell.test.ts, test/js/bun/shell/shell-blocking-pipe.test.ts, test/js/bun/shell/shell-leak-args.test.ts
Added beforeAll/afterAll RSS logs; new pipe non-blocking test (with heap snapshot); added argv leak tests for awaited and non-awaited shell commands.

Suggested reviewers

  • pfgithub
  • Jarred-Sumner

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The description includes useful sections labeled “## Summary,” “## Changes,” and “## Test plan,” but it does not follow the repository’s required template headings of “### What does this PR do?” and “### How did you verify your code works?”. Please update the description to include the exact template sections: a “### What does this PR do?” section summarizing the change and a “### How did you verify your code works?” section detailing testing or verification steps.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly describes the two primary issues addressed by this changeset—memory leaks in the shell and blocking system calls—matching the core bug fixes implemented in the pull request.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/shell/ParsedShellScript.zig (1)

40-59: Keep estimatedSize in sync after take()

After transferring args/jsobjs/cwd/export_env, the precomputed size is stale. Recompute to avoid over/under-reporting.

Apply this diff:

     this.args = null;
     this.jsobjs = std.ArrayList(JSValue).init(bun.default_allocator);
     this.cwd = null;
     this.export_env = null;
+    // keep precomputed estimate consistent with current state
+    this.estimated_size_for_gc = this.computeEstimatedSizeForGC();

Based on learnings

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bd88717 and 5e92a2f.

📒 Files selected for processing (17)
  • src/bun.js/api/ParsedShellScript.classes.ts (1 hunks)
  • src/bun.js/api/Shell.classes.ts (1 hunks)
  • src/bun.js/bindings/ShellBindings.cpp (2 hunks)
  • src/codegen/generate-classes.ts (1 hunks)
  • src/collections/baby_list.zig (1 hunks)
  • src/shell/EnvMap.zig (1 hunks)
  • src/shell/EnvStr.zig (1 hunks)
  • src/shell/IO.zig (2 hunks)
  • src/shell/IOReader.zig (2 hunks)
  • src/shell/IOWriter.zig (2 hunks)
  • src/shell/ParsedShellScript.zig (2 hunks)
  • src/shell/interpreter.zig (10 hunks)
  • src/shell/shell.zig (14 hunks)
  • src/sys.zig (2 hunks)
  • test/js/bun/shell/bunshell.test.ts (1 hunks)
  • test/js/bun/shell/shell-blocking-pipe.test.ts (1 hunks)
  • test/js/bun/shell/shell-leak-args.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (16)
test/**

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place all tests under the test/ directory

Files:

  • test/js/bun/shell/shell-leak-args.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-blocking-pipe.test.ts
test/js/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place JavaScript and TypeScript tests under test/js/

Files:

  • test/js/bun/shell/shell-leak-args.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-blocking-pipe.test.ts
test/js/bun/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place Bun API tests under test/js/bun/, separated by category (e.g., test/js/bun/glob/)

Files:

  • test/js/bun/shell/shell-leak-args.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-blocking-pipe.test.ts
test/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/**/*.{js,ts}: Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test
Prefer data-driven tests (e.g., test.each) to reduce boilerplate
Use shared utilities from test/harness.ts where applicable

Files:

  • test/js/bun/shell/shell-leak-args.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-blocking-pipe.test.ts
test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.{ts,tsx}: Test files must live under test/ and end with .test.ts or .test.tsx
In tests, always use port: 0; do not hardcode ports or roll your own random port
Prefer normalizeBunSnapshot for snapshotting test output instead of asserting raw strings
Do not write tests that assert absence of crashes (e.g., 'no panic' or 'no uncaught exception')
Use Bun’s Jest-compatible runner (import { test, expect } from "bun:test") for tests
Avoid shell commands like find or grep in tests; use Bun’s Glob and built-in tools instead
Prefer running tests via bun bd test and use provided harness utilities (bunEnv, bunExe, tempDir)
Use Bun.spawn with proper stdio handling and await proc.exited in process-spawning tests

Files:

  • test/js/bun/shell/shell-leak-args.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-blocking-pipe.test.ts
test/js/bun/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place Bun-specific API tests under test/js/bun/

Files:

  • test/js/bun/shell/shell-leak-args.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-blocking-pipe.test.ts
test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}: Use bun:test for files ending with *.test.{ts,js,jsx,tsx,mjs,cjs}
Prefer concurrent tests (test.concurrent/describe.concurrent) over sequential when feasible
Organize tests with describe blocks to group related tests
Use utilities like describe.each, toMatchSnapshot, and lifecycle hooks (beforeAll, beforeEach, afterEach) and track resources for cleanup

Files:

  • test/js/bun/shell/shell-leak-args.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-blocking-pipe.test.ts
test/**/*.{ts,tsx,js,jsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

For large/repetitive strings, use Buffer.alloc(count, fill).toString() instead of "A".repeat(count)

Files:

  • test/js/bun/shell/shell-leak-args.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-blocking-pipe.test.ts
test/js/{bun,node}/**

📄 CodeRabbit inference engine (test/CLAUDE.md)

Organize unit tests by module under /test/js/bun/ and /test/js/node/

Files:

  • test/js/bun/shell/shell-leak-args.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-blocking-pipe.test.ts
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/shell/IOWriter.zig
  • src/collections/baby_list.zig
  • src/shell/IOReader.zig
  • src/shell/EnvMap.zig
  • src/shell/ParsedShellScript.zig
  • src/shell/EnvStr.zig
  • src/shell/interpreter.zig
  • src/sys.zig
  • src/shell/shell.zig
  • src/shell/IO.zig
src/**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

In Zig code, manage memory carefully and use defer for cleanup of allocations/resources

src/**/*.zig: Use the # prefix to declare private fields in Zig structs (e.g., struct { #foo: u32 })
Prefer decl literals when initializing values in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 })
Place @import directives at the bottom of Zig files
Use @import("bun") instead of @import("root").bun

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

Files:

  • src/shell/IOWriter.zig
  • src/collections/baby_list.zig
  • src/shell/IOReader.zig
  • src/shell/EnvMap.zig
  • src/shell/ParsedShellScript.zig
  • src/shell/EnvStr.zig
  • src/shell/interpreter.zig
  • src/sys.zig
  • src/shell/shell.zig
  • src/shell/IO.zig
**/*.classes.ts

📄 CodeRabbit inference engine (.cursor/rules/zig-javascriptcore-classes.mdc)

**/*.classes.ts: Define JavaScript class interfaces in .classes.ts using define({ name, constructor, JSType, finalize, proto: { ... } })
Use cache: true on properties that should use JSC WriteBarrier-backed caching

Files:

  • src/bun.js/api/Shell.classes.ts
  • src/bun.js/api/ParsedShellScript.classes.ts
src/**/*.classes.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Define JavaScriptCore class metadata in .classes.ts files to drive code generation

Files:

  • src/bun.js/api/Shell.classes.ts
  • src/bun.js/api/ParsedShellScript.classes.ts
**/*.{cpp,h}

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.{cpp,h}: When exposing a JS class with public Constructor and Prototype, define three C++ types: class Foo : public JSC::DestructibleObject (if it has C++ fields), class FooPrototype : public JSC::JSNonFinalObject, and class FooConstructor : public JSC::InternalFunction
If the class has C++ data members, inherit from JSC::DestructibleObject and provide proper destruction; if it has no C++ fields (only JS properties), avoid a class and use JSC::constructEmptyObject(vm, structure) with putDirectOffset
Prefer placing the subspaceFor implementation in the .cpp file rather than the header when possible

Files:

  • src/bun.js/bindings/ShellBindings.cpp
**/*.cpp

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.cpp: Include "root.h" at the top of C++ binding files to satisfy lints
Define prototype properties using a const HashTableValue array and declare accessors/functions with JSC_DECLARE_* macros
Prototype classes should subclass JSC::JSNonFinalObject, provide create/createStructure, DECLARE_INFO, finishCreation that reifies static properties, and set mayBePrototype on the Structure
Custom getters should use JSC_DEFINE_CUSTOM_GETTER, jsDynamicCast to validate this, and throwThisTypeError on mismatch
Custom setters should use JSC_DEFINE_CUSTOM_SETTER, validate this via jsDynamicCast, and store via WriteBarrier/set semantics
Prototype functions should use JSC_DEFINE_HOST_FUNCTION, validate this with jsDynamicCast, and return encoded JSValue
Constructors should subclass JSC::InternalFunction, return internalFunctionSpace in subspaceFor, set the prototype property as non-configurable/non-writable, and provide create/createStructure
Provide a setup function that builds the Prototype, Constructor, and Structure, and assigns them to the LazyClassStructure initializer
Use the cached Structure via globalObject->m_.get(globalObject) when constructing instances
Expose constructors to Zig via an extern "C" function that returns the constructor from the LazyClassStructure
Provide an extern "C" Bun____toJS function that creates an instance using the cached Structure and returns an EncodedJSValue

Files:

  • src/bun.js/bindings/ShellBindings.cpp
src/bun.js/bindings/**/*.cpp

📄 CodeRabbit inference engine (CLAUDE.md)

src/bun.js/bindings/**/*.cpp: When a JS class has a public constructor, implement Foo, FooPrototype, and FooConstructor (JSDestructibleObject, JSNonFinalObject, InternalFunction)
Define class properties using HashTableValue arrays in C++ bindings
Add iso subspaces for classes with C++ fields
Cache structures in ZigGlobalObject for C++-backed classes

Files:

  • src/bun.js/bindings/ShellBindings.cpp
🧠 Learnings (11)
📚 Learning: 2025-10-04T09:51:30.294Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T09:51:30.294Z
Learning: Applies to test/**/*.test.{ts,tsx} : Use Bun.spawn with proper stdio handling and await proc.exited in process-spawning tests

Applied to files:

  • test/js/bun/shell/shell-leak-args.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-blocking-pipe.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/cli/**/*.{js,ts} : When testing Bun as a CLI, use spawn with bunExe() and bunEnv from harness, and capture stdout/stderr via pipes

Applied to files:

  • test/js/bun/shell/shell-leak-args.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-blocking-pipe.test.ts
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use utilities like `describe.each`, `toMatchSnapshot`, and lifecycle hooks (`beforeAll`, `beforeEach`, `afterEach`) and track resources for cleanup

Applied to files:

  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js} : When spawning Bun in tests, use `bunExe()` and `bunEnv` from `harness`

Applied to files:

  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/shell-blocking-pipe.test.ts
📚 Learning: 2025-10-04T09:51:30.294Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T09:51:30.294Z
Learning: Applies to test/**/*.test.{ts,tsx} : Use Bun’s Jest-compatible runner (import { test, expect } from "bun:test") for tests

Applied to files:

  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-10-04T09:51:30.294Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T09:51:30.294Z
Learning: Applies to test/**/*.test.{ts,tsx} : Prefer running tests via bun bd test <file> and use provided harness utilities (bunEnv, bunExe, tempDir)

Applied to files:

  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js} : Import common utilities from `harness` (e.g., `bunExe`, `bunEnv`, `tempDirWithFiles`, platform helpers, GC helpers)

Applied to files:

  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use `bun:test` for files ending with `*.test.{ts,js,jsx,tsx,mjs,cjs}`

Applied to files:

  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-08-30T00:09:39.100Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/dev-server-tests.mdc:0-0
Timestamp: 2025-08-30T00:09:39.100Z
Learning: Applies to test/bake/bake-harness.ts : Provide and maintain shared test utilities: devTest, prodTest, devAndProductionTest, Dev, Client, and helpers in the harness

Applied to files:

  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-10-04T09:51:30.294Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T09:51:30.294Z
Learning: Applies to test/**/*.test.{ts,tsx} : Avoid shell commands like find or grep in tests; use Bun’s Glob and built-in tools instead

Applied to files:

  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-09-08T00:41:12.052Z
Learnt from: CR
PR: oven-sh/bun#0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-09-08T00:41:12.052Z
Learning: Applies to src/bun.js/bindings/v8/test/v8/v8.test.ts : Add a corresponding test case in test/v8/v8.test.ts that invokes checkSameOutput with the new function

Applied to files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
🧬 Code graph analysis (2)
test/js/bun/shell/shell-leak-args.test.ts (1)
scripts/utils.mjs (1)
  • $ (143-161)
test/js/bun/shell/shell-blocking-pipe.test.ts (1)
scripts/utils.mjs (1)
  • $ (143-161)
🔇 Additional comments (21)
src/collections/baby_list.zig (1)

366-368: LGTM! Critical fix for GC memory accounting.

The method now correctly returns allocated memory in bytes rather than element count, which aligns with the PR objective of fixing incorrect GC reporting. This ensures JavaScriptCore receives accurate memory cost information for shell objects.

src/shell/ParsedShellScript.zig (2)

15-16: LGTM: precomputed size field

Field addition makes sense for JSC extra memory accounting.


17-31: Fix invalid Zig identifiers (‘#...’) and update call sites

Zig identifiers cannot start with ‘#’. #computeEstimatedSizeForGC and its uses won’t compile. Rename and update references.

Apply this diff:

-fn #computeEstimatedSizeForGC(this: *const ParsedShellScript) usize {
+fn computeEstimatedSizeForGC(this: *const ParsedShellScript) usize {
@@
-pub fn memoryCost(this: *const ParsedShellScript) usize {
-    return this.#computeEstimatedSizeForGC();
+pub fn memoryCost(this: *const ParsedShellScript) usize {
+    return this.computeEstimatedSizeForGC();
@@
-    parsed_shell_script.estimated_size_for_gc = parsed_shell_script.#computeEstimatedSizeForGC();
+    parsed_shell_script.estimated_size_for_gc = parsed_shell_script.computeEstimatedSizeForGC();

As per coding guidelines

Also applies to: 32-34, 187-188

⛔ Skipped due to learnings
Learnt from: Jarred-Sumner
PR: oven-sh/bun#23636
File: src/shell/ParsedShellScript.zig:15-0
Timestamp: 2025-10-14T04:04:47.871Z
Learning: In src/shell/ParsedShellScript.zig, the JavaScript garbage collector's memory reporting API only allows reporting extra memory allocation once at object creation time. Therefore, `estimated_size_for_gc` is intentionally set once during initialization (via `#computeEstimatedSizeForGC()`) and not updated afterward, even though fields like `args`, `export_env`, and `cwd` may be mutated by methods like `setCwd()`, `setEnv()`, and `take()`. The `estimatedSize()` method returns this cached initial value, while `memoryCost()` computes the size on-demand.
Learnt from: Jarred-Sumner
PR: oven-sh/bun#23636
File: src/shell/interpreter.zig:672-0
Timestamp: 2025-10-14T04:04:17.112Z
Learning: In Bun's shell interpreter (src/shell/interpreter.zig), the JavaScript garbage collector's memory reporting API only allows reporting extra memory allocation once at object creation time. Therefore, `estimated_size_for_gc` is intentionally set once during initialization and not updated afterward, even though fields like `root_io`, `vm_args_utf8`, and `export_env` may be mutated later. The `estimatedSize()` method returns this cached initial value, while `memoryCost()` computes the size on-demand.
src/shell/interpreter.zig (6)

236-238: LGTM: ShellArgs.memoryCost

Approach is correct and keeps accounting simple.


363-372: LGTM: Bufio.memoryCost

Covers both owned and borrowed variants as expected.


386-397: LGTM: ShellExecEnv.memoryCost

Accounts for env maps, cwd buffers, buffered IO, and async pids. Looks complete.


597-600: LGTM: Debug-only asserts in changeCwdImpl

Safer bounds checks without runtime cost in release.


285-285: LGTM: precomputed size field on Interpreter

Matches the new API surface and C++ reporting.


1242-1243: LGTM: Free ShellArgs in finalizer

This addresses the leak when Interpreter is GC’d from JS.

src/bun.js/api/ParsedShellScript.classes.ts (1)

12-14: LGTM: expose memoryCost and estimatedSize

Matches new Zig API; enables codegen to wire these up.

src/bun.js/api/Shell.classes.ts (1)

14-16: LGTM: expose memoryCost and estimatedSize on ShellInterpreter

Consistent with runtime changes and GC reporting.

src/bun.js/bindings/ShellBindings.cpp (1)

27-28: Ensure symmetric GC accounting (deallocate on finalize) and verify size semantics

Good to report external memory on creation. Please also call vm.heap.reportExtraMemoryDeallocated(result, size) (or equivalent) in the interpreter’s finalize/destructor using the same size source, otherwise GC will permanently over-account. Also confirm ShellInterpreter__estimatedSize(ptr) excludes JSCell/JSObject size to avoid double-counting.

Also applies to: 5-6

test/js/bun/shell/shell-leak-args.test.ts (1)

28-47: Stabilize measurement for non-awaited commands

Consider awaiting a microtask tick before measuring RSS to let background cleanup settle, e.g., await Bun.sleep(0), especially in the non-awaited case, to reduce flakiness. Otherwise looks good.

Also applies to: 49-68

test/js/bun/shell/shell-blocking-pipe.test.ts (1)

16-18: Verify generateHeapSnapshot("v8") support

Confirm passing "v8" is supported on this platform/runtime; otherwise call without an engine name or use the supported identifier.

src/shell/EnvMap.zig (1)

29-38: Memory accounting approach looks reasonable

Includes struct size, backing storage for keys/values, and per-entry costs. With EnvStr.slice returning 0, this avoids double-counting borrowed strings. LGTM.

If MapType stores any extra side buffers beyond keys/values in this configuration, consider including them as well.

src/codegen/generate-classes.ts (1)

1371-1374: Extern for estimatedSize moved to class header generation

Change looks fine; ensures symbol is declared regardless of constructor path.

Please confirm no duplicate externs remain elsewhere and that all classes with estimatedSize set generate the Zig export in generateZig() (they do at lines ~2148).

src/shell/IOWriter.zig (2)

78-86: Account for writer buffers and children

The memoryCost implementation covers the struct, buffers, writer list, and writer. LGTM.


168-174: Mac socket fast-path gating is correct

On macOS, only mark as socket if already nonblocking; otherwise fall back to fifo. Matches the MSG_NOWAIT behavior and avoids blocking.

Ensure call sites set .nonblocking = true when you intentionally want the socket path on macOS.

src/shell/IOReader.zig (1)

38-44: Reader memoryCost aggregation looks good

Counts struct, buffer, and readers list. LGTM.

src/shell/IO.zig (1)

12-19: Comprehensive IO memoryCost coverage

  • IO sums stdin/stdout/stderr.
  • InKind delegates to reader.
  • OutKind.fd includes writer and captured buffers; switch-based aggregator for OutKind.

Looks consistent and cohesive.

Also applies to: 88-94, 101-111, 119-126

test/js/bun/shell/bunshell.test.ts (1)

15-16: Fix incorrect usage of process.memoryUsage

Call the function before accessing rss; otherwise this throws at test startup.

-afterAll(() => console.error("After all RSS", process.memoryUsage.rss() / 1024 / 1024));
-beforeAll(() => console.error("Before all RSS", process.memoryUsage.rss() / 1024 / 1024));
+afterAll(() => console.error("After all RSS", process.memoryUsage().rss / 1024 / 1024));
+beforeAll(() => console.error("Before all RSS", process.memoryUsage().rss / 1024 / 1024));
⛔ Skipped due to learnings
Learnt from: Jarred-Sumner
PR: oven-sh/bun#22279
File: test/js/web/structured-clone-fastpath.test.ts:12-12
Timestamp: 2025-08-31T09:08:12.104Z
Learning: In Bun, process.memoryUsage.rss() is called as a direct function, not as process.memoryUsage().rss like in Node.js. This is the correct API shape for Bun.
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T09:51:30.294Z
Learning: Applies to test/**/*.test.{ts,tsx} : Use Bun.spawn with proper stdio handling and await proc.exited in process-spawning tests
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js} : When spawning Bun in tests, use `bunExe()` and `bunEnv` from `harness`

Comment thread src/shell/EnvStr.zig
Comment on lines +75 to +88
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Correct memoryCost for refcounted vs borrowed slices

  • For .refcounted created via initRefCounted, this.len is 0, so memoryCost wrongly returns 0.
  • For .slice, memory is owned elsewhere; returning len double-counts.

Compute by tag: use byteSlice().len / refcount for .refcounted, return 0 for .slice, and 0 for .empty.

-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 memoryCost(this: EnvStr) usize {
+    return switch (this.tag) {
+        .empty => 0,
+        .slice => 0, // memory owned elsewhere
+        .refcounted => brk: {
+            const refc = this.castRefCounted();
+            const rc = refc.refcount;
+            if (rc == 0) break :brk 0;
+            break :brk refc.byteSlice().len / rc;
+        },
+    };
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 memoryCost(this: EnvStr) usize {
return switch (this.tag) {
.empty => 0,
.slice => 0, // memory owned elsewhere
.refcounted => brk: {
const refc = this.castRefCounted();
const rc = refc.refcount;
if (rc == 0) break :brk 0;
break :brk refc.byteSlice().len / rc;
},
};
}

Comment thread src/shell/interpreter.zig
Comment thread src/shell/IOReader.zig
Comment on lines +234 to +241
pub fn memoryCost(this: IOReaderChildPtr) usize {
if (this.ptr.is(Interpreter.Builtin.Cat)) {
// TODO:
return @sizeOf(Interpreter.Builtin.Cat);
}
return 0;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Avoid hardcoding Cat size; delegate to child memoryCost if available

Returning @sizeOf(Interpreter.Builtin.Cat) is a placeholder and likely inaccurate for dynamic allocations.

Consider exposing a memoryCost() on Interpreter.Builtin.Cat and calling it here; otherwise return 0 to avoid misleading totals.

🤖 Prompt for AI Agents
In src/shell/IOReader.zig around lines 234 to 241, the memoryCost function
currently returns a hardcoded @sizeOf(Interpreter.Builtin.Cat) for Cat children
which is a placeholder and can under/over-report dynamic allocations; instead
add or call a memoryCost() method on Interpreter.Builtin.Cat (expose it on that
type) and invoke that here to compute actual child memory usage, falling back to
0 if the child type does not implement memoryCost; update types/signatures as
needed so IOReaderChildPtr can call the child's memoryCost without assuming a
fixed struct size.

Comment thread src/shell/shell.zig
Comment thread src/shell/shell.zig
Comment on lines +4230 to +4255
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

SmolList.memoryCost double-counts inlined storage and may over-count heap

  • In the .inlined branch, you add std.mem.sliceAsBytes(inlined.allocatedSlice()).len on top of @sizeOf(@This()); this double-counts the inlined array (already included in the union size).
  • For .heap, when T has memoryCost, you add both per-item costs and this.heap.memoryCost(); ensure BabyList.memoryCost() only counts the backing store bytes, not the elements’ deep costs, to avoid double-counting.

Refactor to:

  • Count @sizeOf(@This()) always.
  • For .inlined: only add per-item memoryCost when available; don’t add inlined storage bytes again.
  • For .heap: add backing store bytes (allocatedSlice or this.heap.memoryCost() if it returns bytes), plus per-item memoryCost when available.

Apply this diff:

-        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 memoryCost(this: *const @This()) usize {
+            var cost: usize = @sizeOf(@This());
+            switch (this.*) {
+                .inlined => |*inlined| {
+                    // Inlined storage is already accounted for by @sizeOf(@This()).
+                    if (comptime @hasDecl(T, "memoryCost")) {
+                        for (inlined.slice()) |*item| {
+                            cost += item.memoryCost();
+                        }
+                    }
+                },
+                .heap => |*list| {
+                    // Add backing store bytes for the heap list.
+                    // Prefer BabyList.memoryCost() if it returns bytes; otherwise compute from allocatedSlice.
+                    cost += if (comptime @hasDecl(@TypeOf(list.*), "memoryCost"))
+                        list.memoryCost()
+                    else
+                        std.mem.sliceAsBytes(list.allocatedSlice()).len;
+                    if (comptime @hasDecl(T, "memoryCost")) {
+                        for (list.slice()) |*item| {
+                            cost += item.memoryCost();
+                        }
+                    }
+                },
+            }
+            return cost;
+        }

Based on learnings

Also applies to: 4291-4296

🤖 Prompt for AI Agents
In src/shell/shell.zig around lines 4230-4255 and 4291-4296, SmolList.memoryCost
is double-counting storage: keep @sizeOf(@This()) always, remove adding the
inlined storage bytes in the .inlined branch (only iterate and add per-item
memoryCost when bun.trait.isContainer(T) and @hasDecl(T, "memoryCost")), and in
the .heap branch add the backing-store byte count (use
this.heap.allocatedSlice().len or this.heap.memoryCost() only if that function
is documented/implemented to return raw bytes) plus per-item memoryCost when
available—do not add both element deep-costs and a backing-store that already
includes element sizes; ensure BabyList.memoryCost (or the heap backing
memoryCost) returns only the backing byte count so you only sum element deep
costs separately.

Comment thread src/sys.zig
Comment on lines +2104 to 2113
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 });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Incorrect format specifier {f} for DebugTimer in logs

{f} expects a float; DebugTimer should be formatted with {} or {any} like elsewhere in this file. Current code risks a compile-time error.

Apply this diff:

-        syslog("send({}, {d}) = {s} ({f})", .{ fd, buf.len, err.err.name(), debug_timer });
+        syslog("send({}, {d}) = {s} ({})", .{ fd, buf.len, err.err.name(), debug_timer });
@@
-        syslog("send({}, {d}) = {d} ({f})", .{ fd, buf.len, rc, debug_timer });
+        syslog("send({}, {d}) = {d} ({})", .{ fd, buf.len, rc, debug_timer });
@@
-                syslog("send({}, {d}) = {s} ({f})", .{ fd, buf.len, err.err.name(), debug_timer });
+                syslog("send({}, {d}) = {s} ({})", .{ fd, buf.len, err.err.name(), debug_timer });
@@
-            syslog("send({}, {d}) = {d} ({f})", .{ fd, buf.len, rc, debug_timer });
+            syslog("send({}, {d}) = {d} ({})", .{ fd, buf.len, rc, debug_timer });

Also applies to: 2116-2129

🤖 Prompt for AI Agents
In src/sys.zig around lines 2104-2113 (and similarly 2116-2129), the log calls
use the incorrect format specifier "{f}" for DebugTimer which expects "{}" or
"{any}"; update those syslog format strings to use "{}" (or "{any}") instead of
"{f}" and keep the same arguments so the DebugTimer value is formatted correctly
and compiles.

Comment thread src/sys.zig
Comment on lines 2918 to 2929
// 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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Mac shell socket buffers: consider handling setsockopt errors

Setting 128 KB RCVBUF/SNDBUF is reasonable. If practical, log failures (only in debug) to aid diagnosis on constrained systems. No behavior change needed.

🤖 Prompt for AI Agents
In src/sys.zig around lines 2918 to 2929, the setsockopt calls that set 128 KB
for SO_RCVBUF/SO_SNDBUF ignore errors; update the code to check the return value
of std.c.setsockopt and, in debug builds only, log a non-fatal diagnostic when
it fails (include the errno or error string for context) without changing
functionality on failure. Ensure the logging path is compiled only in debug
(e.g., std.debug or build flag), does not abort or alter behavior, and keeps the
existing calls otherwise.

Comment on lines +6 to +14
// 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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Fix typos in comments/variables

  • “becasue” → “because”
  • “massiveComamnd” → “massiveCommand”
  • “excercise” → “exercise”
🤖 Prompt for AI Agents
In test/js/bun/shell/shell-blocking-pipe.test.ts around lines 6 to 14, fix
several typos: change the comment "becasue" to "because", rename the variable
"massiveComamnd" to "massiveCommand" everywhere it's used, and search the file
for the misspelling "excercise" and correct it to "exercise"; update any
corresponding references or uses to match the corrected identifier and comment
text so the code and comments remain consistent.

Comment on lines +21 to +24
if (result !== expected + "\n") {
throw new Error("Expected " + expected + "\n but got " + result);
}
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Prefer expect() over manual throw in tests

Replace manual throws with expect(result).toBe(expected + "\n") for consistency with bun:test.

Also applies to: 30-33

🤖 Prompt for AI Agents
In test/js/bun/shell/shell-blocking-pipe.test.ts around lines 21-24 (and
similarly at 30-33), the test uses a manual throw when the result mismatches;
replace the manual throw with bun:test assertions by calling
expect(result).toBe(expected + "\n") so the test integrates with the test runner
and produces consistent failure output. Update both occurrences to use
expect(...).toBe(...) and remove the manual if/throw blocks.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (4)
src/sys.zig (2)

2104-2129: Incorrect format specifier {f} for DebugTimer in logs

DebugTimer should be formatted with {} or {any}, not {f}. The format specifier {f} expects a float, which will cause a compile-time error with DebugTimer.

Apply this diff to fix the format specifiers:

-        syslog("send({}, {d}) = {s} ({f})", .{ fd, buf.len, err.err.name(), debug_timer });
+        syslog("send({}, {d}) = {s} ({})", .{ fd, buf.len, err.err.name(), debug_timer });
@@
-        syslog("send({}, {d}) = {d} ({f})", .{ fd, buf.len, rc, debug_timer });
+        syslog("send({}, {d}) = {d} ({})", .{ fd, buf.len, rc, debug_timer });
@@
-                syslog("send({}, {d}) = {s} ({f})", .{ fd, buf.len, err.err.name(), debug_timer });
+                syslog("send({}, {d}) = {s} ({})", .{ fd, buf.len, err.err.name(), debug_timer });
@@
-            syslog("send({}, {d}) = {d} ({f})", .{ fd, buf.len, rc, debug_timer });
+            syslog("send({}, {d}) = {d} ({})", .{ fd, buf.len, rc, debug_timer });

2918-2928: Consider logging setsockopt failures in debug builds

The return values from setsockopt are discarded. While these socket buffer adjustments are optimizations and failures are non-critical, logging errors in debug builds could help diagnose unexpected shell I/O behavior.

Consider adding debug logging:

+                if (comptime Environment.isDebug) {
+                    if (std.c.setsockopt(fds_i[1], std.posix.SOL.SOCKET, std.posix.SO.RCVBUF, &so_recvbuf, @sizeOf(c_int)) != 0) {
+                        syslog("setsockopt(SO_RCVBUF) failed: {d}", .{std.c.getErrno(@as(isize, 0))});
+                    }
+                    if (std.c.setsockopt(fds_i[0], std.posix.SOL.SOCKET, std.posix.SO.SNDBUF, &so_sendbuf, @sizeOf(c_int)) != 0) {
+                        syslog("setsockopt(SO_SNDBUF) failed: {d}", .{std.c.getErrno(@as(isize, 0))});
+                    }
+                } else {
                     _ = 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));
+                }
test/js/bun/shell/shell-blocking-pipe.test.ts (2)

6-14: Fix typos and variable name.

Correct “becasue”→“because”, “massiveComamnd”→“massiveCommand”, and “excercise”→“exercise”.

-// We skip this test on Windows becasue:
+// We skip this test on Windows because:
@@
-  const massiveComamnd = "echo " + expected + " | " + Bun.which("cat");
+  const massiveCommand = "echo " + expected + " | " + Bun.which("cat");
@@
-  const pendingResult = $`${{
-    raw: massiveComamnd,
+  const pendingResult = $`${{
+    raw: massiveCommand,
   }}`.text();
@@
-  // Ensure that heap snapshot works, to excercise the memoryCost & estimated fields.
+  // Ensure that heap snapshot works, to exercise the memoryCost & estimated fields.

Also applies to: 16-16


21-24: Use bun:test assertions instead of manual throw.

Replace manual if/throw with expect(...).toBe(...).

-  if (result !== expected + "\n") {
-    throw new Error("Expected " + expected + "\n but got " + result);
-  }
+  expect(result).toBe(expected + "\n");
@@
-  if (result !== expected + "\n") {
-    throw new Error("Expected " + expected + "\n but got " + result);
-  }
+  expect(result).toBe(expected + "\n");

Also applies to: 30-33

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e683a8b and 671cbbb.

📒 Files selected for processing (6)
  • src/bun.js/api/ParsedShellScript.classes.ts (1 hunks)
  • src/codegen/generate-classes.ts (1 hunks)
  • src/shell/ParsedShellScript.zig (2 hunks)
  • src/sys.zig (2 hunks)
  • test/js/bun/shell/bunshell.test.ts (1 hunks)
  • test/js/bun/shell/shell-blocking-pipe.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (13)
test/**

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place all tests under the test/ directory

Files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
test/js/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place JavaScript and TypeScript tests under test/js/

Files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
test/js/bun/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place Bun API tests under test/js/bun/, separated by category (e.g., test/js/bun/glob/)

Files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
test/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/**/*.{js,ts}: Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test
Prefer data-driven tests (e.g., test.each) to reduce boilerplate
Use shared utilities from test/harness.ts where applicable

Files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.{ts,tsx}: Test files must live under test/ and end with .test.ts or .test.tsx
In tests, always use port: 0; do not hardcode ports or roll your own random port
Prefer normalizeBunSnapshot for snapshotting test output instead of asserting raw strings
Do not write tests that assert absence of crashes (e.g., 'no panic' or 'no uncaught exception')
Use Bun’s Jest-compatible runner (import { test, expect } from "bun:test") for tests
Avoid shell commands like find or grep in tests; use Bun’s Glob and built-in tools instead
Prefer running tests via bun bd test and use provided harness utilities (bunEnv, bunExe, tempDir)
Use Bun.spawn with proper stdio handling and await proc.exited in process-spawning tests

Files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
test/js/bun/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place Bun-specific API tests under test/js/bun/

Files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}: Use bun:test for files ending with *.test.{ts,js,jsx,tsx,mjs,cjs}
Prefer concurrent tests (test.concurrent/describe.concurrent) over sequential when feasible
Organize tests with describe blocks to group related tests
Use utilities like describe.each, toMatchSnapshot, and lifecycle hooks (beforeAll, beforeEach, afterEach) and track resources for cleanup

Files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
test/**/*.{ts,tsx,js,jsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

For large/repetitive strings, use Buffer.alloc(count, fill).toString() instead of "A".repeat(count)

Files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
test/js/{bun,node}/**

📄 CodeRabbit inference engine (test/CLAUDE.md)

Organize unit tests by module under /test/js/bun/ and /test/js/node/

Files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/shell/ParsedShellScript.zig
  • src/sys.zig
src/**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

In Zig code, manage memory carefully and use defer for cleanup of allocations/resources

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

src/**/*.zig: Use private fields in Zig with the # prefix (e.g., struct { #foo: u32 };)
Prefer decl literals in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 };)
Prefer placing @import statements at the bottom of the Zig file (formatter may reorder automatically)
Prefer @import("bun") rather than @import("root").bun or @import("../bun.zig")

Files:

  • src/shell/ParsedShellScript.zig
  • src/sys.zig
**/*.classes.ts

📄 CodeRabbit inference engine (.cursor/rules/zig-javascriptcore-classes.mdc)

**/*.classes.ts: Define JavaScript class interfaces in .classes.ts using define({ name, constructor, JSType, finalize, proto: { ... } })
Use cache: true on properties that should use JSC WriteBarrier-backed caching

Files:

  • src/bun.js/api/ParsedShellScript.classes.ts
src/**/*.classes.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Define JavaScriptCore class metadata in .classes.ts files to drive code generation

Files:

  • src/bun.js/api/ParsedShellScript.classes.ts
🧠 Learnings (19)
📓 Common learnings
Learnt from: Jarred-Sumner
PR: oven-sh/bun#23636
File: src/shell/interpreter.zig:672-0
Timestamp: 2025-10-14T04:04:17.132Z
Learning: In Bun's shell interpreter (src/shell/interpreter.zig), the JavaScript garbage collector's memory reporting API only allows reporting extra memory allocation once at object creation time. Therefore, `estimated_size_for_gc` is intentionally set once during initialization and not updated afterward, even though fields like `root_io`, `vm_args_utf8`, and `export_env` may be mutated later. The `estimatedSize()` method returns this cached initial value, while `memoryCost()` computes the size on-demand.
Learnt from: Jarred-Sumner
PR: oven-sh/bun#23636
File: src/shell/ParsedShellScript.zig:15-0
Timestamp: 2025-10-14T04:04:47.894Z
Learning: In src/shell/ParsedShellScript.zig, the JavaScript garbage collector's memory reporting API only allows reporting extra memory allocation once at object creation time. Therefore, `estimated_size_for_gc` is intentionally set once during initialization (via `#computeEstimatedSizeForGC()`) and not updated afterward, even though fields like `args`, `export_env`, and `cwd` may be mutated by methods like `setCwd()`, `setEnv()`, and `take()`. The `estimatedSize()` method returns this cached initial value, while `memoryCost()` computes the size on-demand.
📚 Learning: 2025-10-04T09:51:30.294Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T09:51:30.294Z
Learning: Applies to test/**/*.test.{ts,tsx} : Use Bun.spawn with proper stdio handling and await proc.exited in process-spawning tests

Applied to files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/cli/**/*.{js,ts} : When testing Bun as a CLI, use spawn with bunExe() and bunEnv from harness, and capture stdout/stderr via pipes

Applied to files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/js/bun/**/*.{js,ts} : Place Bun API tests under test/js/bun/, separated by category (e.g., test/js/bun/glob/)

Applied to files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
📚 Learning: 2025-10-04T09:51:30.294Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T09:51:30.294Z
Learning: Applies to test/**/*.test.{ts,tsx} : Avoid shell commands like find or grep in tests; use Bun’s Glob and built-in tools instead

Applied to files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-10-12T02:22:34.373Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.373Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js} : When spawning Bun in tests, use `bunExe()` and `bunEnv` from `harness`

Applied to files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-10-04T09:51:30.294Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T09:51:30.294Z
Learning: Applies to test/**/*.test.{ts,tsx} : Prefer running tests via bun bd test <file> and use provided harness utilities (bunEnv, bunExe, tempDir)

Applied to files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-10-08T13:48:02.430Z
Learnt from: Jarred-Sumner
PR: oven-sh/bun#23373
File: test/js/bun/tarball/extract.test.ts:107-111
Timestamp: 2025-10-08T13:48:02.430Z
Learning: In Bun's test runner, use `expect(async () => { await ... }).toThrow()` to assert async rejections. Unlike Jest/Vitest, Bun does not require `await expect(...).rejects.toThrow()` - the async function wrapper with `.toThrow()` is the correct pattern for async error assertions in Bun tests.

Applied to files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
📚 Learning: 2025-08-30T00:09:39.100Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/dev-server-tests.mdc:0-0
Timestamp: 2025-08-30T00:09:39.100Z
Learning: Applies to test/bake/{dev/*.test.ts,dev-and-prod.ts} : Explicitly assert client console logs with c.expectMessage; unasserted logs should not appear

Applied to files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
📚 Learning: 2025-10-12T02:22:34.373Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.373Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js,test/**/*-fixture.ts} : Use `using`/`await using` for resource cleanup with Bun APIs (e.g., `Bun.spawn`, `Bun.listen`, `Bun.serve`)

Applied to files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test

Applied to files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
📚 Learning: 2025-10-12T02:22:34.373Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.373Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js} : Always check exit codes and error scenarios in tests (e.g., spawned processes should assert non-zero on failure)

Applied to files:

  • test/js/bun/shell/shell-blocking-pipe.test.ts
📚 Learning: 2025-10-14T04:04:47.894Z
Learnt from: Jarred-Sumner
PR: oven-sh/bun#23636
File: src/shell/ParsedShellScript.zig:15-0
Timestamp: 2025-10-14T04:04:47.894Z
Learning: In src/shell/ParsedShellScript.zig, the JavaScript garbage collector's memory reporting API only allows reporting extra memory allocation once at object creation time. Therefore, `estimated_size_for_gc` is intentionally set once during initialization (via `#computeEstimatedSizeForGC()`) and not updated afterward, even though fields like `args`, `export_env`, and `cwd` may be mutated by methods like `setCwd()`, `setEnv()`, and `take()`. The `estimatedSize()` method returns this cached initial value, while `memoryCost()` computes the size on-demand.

Applied to files:

  • src/shell/ParsedShellScript.zig
📚 Learning: 2025-10-14T04:04:17.132Z
Learnt from: Jarred-Sumner
PR: oven-sh/bun#23636
File: src/shell/interpreter.zig:672-0
Timestamp: 2025-10-14T04:04:17.132Z
Learning: In Bun's shell interpreter (src/shell/interpreter.zig), the JavaScript garbage collector's memory reporting API only allows reporting extra memory allocation once at object creation time. Therefore, `estimated_size_for_gc` is intentionally set once during initialization and not updated afterward, even though fields like `root_io`, `vm_args_utf8`, and `export_env` may be mutated later. The `estimatedSize()` method returns this cached initial value, while `memoryCost()` computes the size on-demand.

Applied to files:

  • src/shell/ParsedShellScript.zig
📚 Learning: 2025-10-12T02:22:34.373Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.373Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use utilities like `describe.each`, `toMatchSnapshot`, and lifecycle hooks (`beforeAll`, `beforeEach`, `afterEach`) and track resources for cleanup

Applied to files:

  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-10-04T09:51:30.294Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T09:51:30.294Z
Learning: Applies to test/**/*.test.{ts,tsx} : Use Bun’s Jest-compatible runner (import { test, expect } from "bun:test") for tests

Applied to files:

  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-10-12T02:22:34.373Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.373Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js} : Import common utilities from `harness` (e.g., `bunExe`, `bunEnv`, `tempDirWithFiles`, platform helpers, GC helpers)

Applied to files:

  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-10-12T02:22:34.373Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.373Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use `bun:test` for files ending with `*.test.{ts,js,jsx,tsx,mjs,cjs}`

Applied to files:

  • test/js/bun/shell/bunshell.test.ts
📚 Learning: 2025-08-30T00:09:39.100Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/dev-server-tests.mdc:0-0
Timestamp: 2025-08-30T00:09:39.100Z
Learning: Applies to test/bake/bake-harness.ts : Provide and maintain shared test utilities: devTest, prodTest, devAndProductionTest, Dev, Client, and helpers in the harness

Applied to files:

  • test/js/bun/shell/bunshell.test.ts
🧬 Code graph analysis (1)
test/js/bun/shell/shell-blocking-pipe.test.ts (1)
scripts/utils.mjs (1)
  • $ (143-161)
🔇 Additional comments (3)
src/shell/ParsedShellScript.zig (1)

15-38: GC-size caching + on-demand memoryCost look correct.

  • Cached estimated_size_for_gc set once; estimatedSize returns it.
  • memoryCost computes current size (args/env/cwd/jsobjs).
  • Initialization before JS exposure is correct.

Based on learnings

Also applies to: 188-188

src/bun.js/api/ParsedShellScript.classes.ts (1)

12-13: Expose memoryCost and estimatedSize: LGTM.

Matches Zig methods and enables GC accounting hooks.

src/codegen/generate-classes.ts (1)

1371-1374: Approve centralized estimatedSize extern

The extern declaration is emitted only once in generate-classes.ts; no duplicates remain.

Comment on lines +15 to +17
afterAll(() => console.error("After all RSS", process.memoryUsage.rss() / 1024 / 1024));
beforeAll(() => console.error("Before all RSS", process.memoryUsage.rss() / 1024 / 1024));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Gate debug RSS logs behind an env flag.

These stderr logs can spam CI. Suggest guarding with if (process.env.DEBUG_RSS) to keep test output clean.

-afterAll(() => console.error("After all RSS", process.memoryUsage.rss() / 1024 / 1024));
-beforeAll(() => console.error("Before all RSS", process.memoryUsage.rss() / 1024 / 1024));
+afterAll(() => { if (process.env.DEBUG_RSS) console.error("After all RSS", process.memoryUsage.rss() / 1024 / 1024); });
+beforeAll(() => { if (process.env.DEBUG_RSS) console.error("Before all RSS", process.memoryUsage.rss() / 1024 / 1024); });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
afterAll(() => console.error("After all RSS", process.memoryUsage.rss() / 1024 / 1024));
beforeAll(() => console.error("Before all RSS", process.memoryUsage.rss() / 1024 / 1024));
afterAll(() => { if (process.env.DEBUG_RSS) console.error("After all RSS", process.memoryUsage.rss() / 1024 / 1024); });
beforeAll(() => { if (process.env.DEBUG_RSS) console.error("Before all RSS", process.memoryUsage.rss() / 1024 / 1024); });
🤖 Prompt for AI Agents
In test/js/bun/shell/bunshell.test.ts around lines 15-17, the unconditional
console.error calls printing RSS can clutter CI output; guard these debug logs
with a check like if (process.env.DEBUG_RSS) before calling console.error so
they only run when the DEBUG_RSS environment variable is set, leaving the
existing messages intact.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants