Files
bun.sh/src/bun.js/api/BunObject.classes.ts
robobun 6580b563b0 Refactor Subprocess to use JSRef instead of hasPendingActivity (#24090)
## Summary

Refactors `Subprocess` to use explicit strong/weak reference management
via `JSRef` instead of the `hasPendingActivity` mechanism that relies on
JSC's internal `WeakHandleOwner`.

## Changes

### Core Refactoring
- **JSRef.zig**: Added `update()` method to update references in-place
- **subprocess.zig**: Changed `this_jsvalue: JSValue` to `this_value:
JSRef`
- **subprocess.zig**: Renamed `hasPendingActivityNonThreadsafe()` to
`computeHasPendingActivity()`
- **subprocess.zig**: Updated `updateHasPendingActivity()` to
upgrade/downgrade `JSRef` based on pending activity
- **subprocess.zig**: Removed `hasPendingActivity()` C callback function
- **subprocess.zig**: Updated `finalize()` to call
`this_value.finalize()`
- **BunObject.classes.ts**: Set `hasPendingActivity: false` for
Subprocess
- **Writable.zig**: Updated references from `this_jsvalue` to
`this_value.tryGet()`
- **ipc.zig**: Updated references from `this_jsvalue` to
`this_value.tryGet()`

## How It Works

**Before**: Used `hasPendingActivity: true` which created a `JSC::Weak`
reference with a `JSC::WeakHandleOwner` that kept the object alive as
long as the C callback returned true.

**After**: Uses `JSRef` with explicit lifecycle management:
1. Starts with a **weak** reference when subprocess is created
2. Immediately calls `updateHasPendingActivity()` after creation
3. **Upgrades to strong** reference when `computeHasPendingActivity()`
returns true:
   - Subprocess hasn't exited
   - Has active stdio streams
   - Has active IPC connection
4. **Downgrades to weak** reference when all activity completes
5. GC can collect the subprocess once it's weak and no other references
exist

## Benefits

- Explicit control over subprocess lifecycle instead of relying on JSC's
internal mechanisms
- Clearer semantics: strong reference = "keep alive", weak reference =
"can be GC'd"
- Removes dependency on `WeakHandleOwner` callback overhead

## Testing

-  `test/js/bun/spawn/spawn.ipc.test.ts` - All 4 tests pass
-  `test/js/bun/spawn/spawn-stress.test.ts` - All tests pass (100
iterations)
- ⚠️ `test/js/bun/spawn/spawnSync.test.ts` - 3/6 pass (3 pre-existing
timing-based failures unrelated to this change)

Manual testing confirms:
- Subprocess is kept alive without user reference while running
- Subprocess can be GC'd after completion
- IPC keeps subprocess alive correctly
- No crashes or memory leaks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2025-10-27 14:19:38 -07:00

128 lines
2.4 KiB
TypeScript

import { define } from "../../codegen/class-definitions";
export default [
define({
name: "ResourceUsage",
construct: true,
noConstructor: true,
finalize: true,
configurable: false,
hasPendingActivity: false,
klass: {},
JSType: "0b11101110",
proto: {
maxRSS: {
getter: "getMaxRSS",
},
shmSize: {
getter: "getSharedMemorySize",
},
swapCount: {
getter: "getSwapCount",
},
messages: {
getter: "getMessages",
},
signalCount: {
getter: "getSignalCount",
},
contextSwitches: {
getter: "getContextSwitches",
cache: true,
},
cpuTime: {
getter: "getCPUTime",
cache: true,
},
ops: {
getter: "getOps",
cache: true,
},
},
values: [],
}),
define({
name: "Subprocess",
construct: true,
noConstructor: true,
finalize: true,
configurable: false,
memoryCost: true,
klass: {},
JSType: "0b11101110",
proto: {
pid: {
getter: "getPid",
},
stdin: {
getter: "getStdin",
cache: true,
},
stdout: {
getter: "getStdout",
cache: true,
},
stderr: {
getter: "getStderr",
cache: true,
},
writable: {
getter: "getStdin",
cache: "stdin",
},
readable: {
getter: "getStdout",
cache: "stdout",
},
ref: {
fn: "doRef",
length: 0,
},
unref: {
fn: "doUnref",
length: 0,
},
resourceUsage: {
fn: "resourceUsage",
length: 0,
},
send: {
fn: "doSend",
length: 1,
},
kill: {
fn: "kill",
length: 1,
},
disconnect: {
fn: "disconnect",
length: 0,
},
connected: {
getter: "getConnected",
},
"@@asyncDispose": {
fn: "asyncDispose",
length: 1,
},
killed: {
getter: "getKilled",
},
exitCode: {
getter: "getExitCode",
},
signalCode: {
getter: "getSignalCode",
},
exited: {
getter: "getExited",
this: true,
},
stdio: {
getter: "getStdio",
},
},
values: ["exitedPromise", "onExitCallback", "onDisconnectCallback", "ipcCallback"],
}),
];