Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/javascriptcore-garbage-collector/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ visitor.reportExtraMemoryVisited(thisObject->wrapped().byteSize());
- If the size changes over time, report the delta on growth (`reportExtraMemoryAllocated(cell, newSize - oldSize)`) and report the current size in `visitChildren`.
- `deprecatedReportExtraMemory` exists for callers that can't satisfy the visit-side half — avoid it.

In `.classes.ts`, `estimatedSize: true` generates the `reportExtraMemoryVisited` side; you implement `estimated_size()` in Rust. You still call `reportExtraMemoryAllocated` (or the binding's helper) at allocation time.
In `.classes.ts`, `estimatedSize: true` generates both halves from one Rust method: the generated constructor and `${T}__create` call the generated `JS${T}::reportExtraMemoryAllocated(vm)` once the wrapper exists, and `visitChildren` re-reports `estimated_size()` as visited. Hand-written C++ that creates one of these wrappers itself (`JSBunRequest`, `JSBakeResponse`, the shell interpreter; `JSDOMFile` still needs it) calls the same member rather than reporting a size by hand. If the object may merely take a reference to memory another wrapper already reported (a `Blob` sharing its store with the blob it was sliced from), add `newlyAllocatedSize: true` and implement `newly_allocated_size()`: the member then reports that instead, while visits keep re-reporting `estimated_size()`. Otherwise every view reports the whole payload as a fresh allocation and JSC collects after every few views.

## `HeapAnalyzer` — heap snapshots and labelling

Expand Down
7 changes: 7 additions & 0 deletions bench/snippets/blob.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ bench("blob.slice()", function () {
return small.slice();
});

// slice() shares the source's bytes, so this only differs from the small case
// by what each new Blob tells the GC.
var large = new Blob([new Uint8Array(8 * 1024 * 1024)]);
bench("blob.slice() (8 MiB blob)", function () {
return large.slice();
});

if ((await small.text()) !== JSON.stringify("hello world ")) {
throw new Error("blob.text() failed");
}
Expand Down
12 changes: 12 additions & 0 deletions src/codegen/class-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,20 @@ export class ClassDefinition {
* ```
*
* Report `size_of::<Self>()` as well as any external allocations.
*
* Reported once per wrapper by the generated `JS${name}::reportExtraMemoryAllocated(vm)`
* and again from `visitChildren` on every GC.
Comment thread
robobun marked this conversation as resolved.
*/
estimatedSize?: boolean;
/**
* Requires `estimatedSize`. `JS${name}::reportExtraMemoryAllocated(vm)` reports this
* instead of `estimated_size()`, for objects that may only take a reference to memory
* another wrapper already reported (a Blob sharing its store). Called on the JS thread.
* ```rust
* pub fn newly_allocated_size(&self) -> usize;
* ```
*/
Comment thread
robobun marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.
newlyAllocatedSize?: boolean;
/**
* Used in heap snapshots.
*
Expand Down
89 changes: 46 additions & 43 deletions src/codegen/generate-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ function DOMJITName(fnName) {
return `${fnName}WithoutTypeChecks`;
}

// Emitted once `instance->m_ctx` is set at every generated wrapper creation site.
function reportExtraMemoryAllocated(obj: ClassDefinition) {
return obj.estimatedSize ? `instance->reportExtraMemoryAllocated(vm);` : "";
}

function argTypeName(arg) {
return {
["bool"]: "bool",
Expand Down Expand Up @@ -652,13 +657,7 @@ ${
${
obj.call
? ` RETURN_IF_EXCEPTION(scope, {});
${
obj.estimatedSize
? `
auto size = ${symbolName(typeName, "estimatedSize")}(ptr);
vm.heap.reportExtraMemoryAllocated(instance, size);`
: ""
}
${reportExtraMemoryAllocated(obj)}

RELEASE_AND_RETURN(scope, JSValue::encode(instance));`
: ""
Expand Down Expand Up @@ -708,13 +707,7 @@ JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ${name}::construct(JSC::JSGlobalObj
instance->m_ctx = ptr;
`) +
`
${
obj.estimatedSize
? `
auto size = ${symbolName(typeName, "estimatedSize")}(ptr);
vm.heap.reportExtraMemoryAllocated(instance, size);`
: ""
}
${reportExtraMemoryAllocated(obj)}

auto value = JSValue::encode(instance);
RELEASE_AND_RETURN(scope, value);
Expand Down Expand Up @@ -1294,6 +1287,13 @@ function generateClassHeader(typeName, obj: ClassDefinition) {

if (obj.estimatedSize) {
externs += `extern JSC_CALLCONV size_t ${symbolName(typeName, "estimatedSize")}(void* ptr);` + "\n";
if (obj.newlyAllocatedSize) {
externs += `extern JSC_CALLCONV size_t ${symbolName(typeName, "newlyAllocatedSize")}(void* ptr);` + "\n";
}
} else if (obj.newlyAllocatedSize) {
throw new Error(
`${typeName}: 'newlyAllocatedSize' only replaces the allocation-time half of 'estimatedSize'; set 'estimatedSize: true' as well.`,
);
}

for (const a of [...Object.values(klass), ...Object.values(proto)]) {
Expand Down Expand Up @@ -1415,7 +1415,17 @@ function generateClassHeader(typeName, obj: ClassDefinition) {
* Memory cost of the object from Zig, without necessarily having a JS wrapper alive.
*/
static size_t memoryCost(void* ptr);

${
obj.estimatedSize
? `
/**
* Call once per wrapper, after m_ctx is set. Reports estimatedSize, or
* newlyAllocatedSize when the class definition sets it.
*/
Comment thread
robobun marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.
void reportExtraMemoryAllocated(JSC::VM& vm);
`
: ""
}
void* m_ctx { nullptr };

${name}(JSC::VM& vm, JSC::Structure* structure, void* sinkPtr${obj.valuesArray ? ", WTF::FixedVector<JSC::WriteBarrier<JSC::Unknown>>&& jsvalueArray_" : ""})
Expand Down Expand Up @@ -1657,6 +1667,15 @@ size_t ${name}::memoryCost(void* ptr) {
`;
}

if (obj.estimatedSize) {
const sizeFn = symbolName(typeName, obj.newlyAllocatedSize ? "newlyAllocatedSize" : "estimatedSize");
output += `
void ${name}::reportExtraMemoryAllocated(JSC::VM& vm) {
vm.heap.reportExtraMemoryAllocated(this, ${sizeFn}(m_ctx));
}
`;
}

output += `

size_t ${name}::estimatedSize(JSC::JSCell* cell, JSC::VM& vm) {
Expand Down Expand Up @@ -1809,13 +1828,7 @@ extern JSC_CALLCONV JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ${typeName}__cr
auto &vm = globalObject->vm();
JSC::Structure* structure = globalObject->${className(typeName)}Structure();
${className(typeName)}* instance = ${className(typeName)}::create(vm, globalObject, structure, ptr);
${
obj.estimatedSize
? `
auto size = ${symbolName(typeName, "estimatedSize")}(ptr);
vm.heap.reportExtraMemoryAllocated(instance, size);`
: ""
}
${reportExtraMemoryAllocated(obj)}
return JSValue::encode(instance);
}

Expand All @@ -1830,13 +1843,7 @@ ${
jsvalueArray[i].setWithoutWriteBarrier(args->at(i));
}
${className(typeName)}* instance = ${className(typeName)}::create(vm, globalObject, structure, ptr, WTF::move(jsvalueArray));
${
obj.estimatedSize
? `
auto size = ${symbolName(typeName, "estimatedSize")}(ptr);
vm.heap.reportExtraMemoryAllocated(instance, size);`
: ""
}
${reportExtraMemoryAllocated(obj)}
return JSValue::encode(instance);
}`
: ""
Expand All @@ -1848,13 +1855,7 @@ ${
auto &vm = globalObject->vm();
JSC::Structure* structure = globalObject->${className(typeName)}Structure();
${className(typeName)}* instance = ${className(typeName)}::create(vm, globalObject, structure, ptr${obj.values.map(v => `, JSC::JSValue::decode(${v})`).join("")});
${
obj.estimatedSize
? `
auto size = ${symbolName(typeName, "estimatedSize")}(ptr);
vm.heap.reportExtraMemoryAllocated(instance, size);`
: ""
}
${reportExtraMemoryAllocated(obj)}
return JSValue::encode(instance);
}`
: ""
Expand All @@ -1871,13 +1872,7 @@ ${
jsvalueArray[i].setWithoutWriteBarrier(args->at(i));
}
${className(typeName)}* instance = ${className(typeName)}::create(vm, globalObject, structure, ptr, WTF::move(jsvalueArray)${obj.values.map(v => `, JSC::JSValue::decode(${v})`).join("")});
${
obj.estimatedSize
? `
auto size = ${symbolName(typeName, "estimatedSize")}(ptr);
vm.heap.reportExtraMemoryAllocated(instance, size);`
: ""
}
${reportExtraMemoryAllocated(obj)}
return JSValue::encode(instance);
}`
: ""
Expand Down Expand Up @@ -2184,6 +2179,7 @@ function generateRust(
noConstructor = false,
overridesToJS = false,
estimatedSize,
newlyAllocatedSize = false,
call = false,
memoryCost,
values = [],
Expand Down Expand Up @@ -2248,6 +2244,13 @@ function generateRust(
}
if (estimatedSize) {
thunk(symbolName(typeName, "estimatedSize"), `(this: &${T}) -> usize`, ` ${T}::estimated_size(this)`);
if (newlyAllocatedSize) {
thunk(
symbolName(typeName, "newlyAllocatedSize"),
`(this: &${T}) -> usize`,
` ${T}::newly_allocated_size(this)`,
);
}
}
if (!memoryCost && !estimatedSize) {
symbols.push(symbolName(typeName, "ZigStructSize"));
Expand Down
7 changes: 2 additions & 5 deletions src/jsc/bindings/JSBakeResponse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ extern "C" SYSV_ABI JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ResponseClass__
extern "C" SYSV_ABI JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ResponseClass__constructJSON(JSC::JSGlobalObject*, JSC::CallFrame*);
extern "C" SYSV_ABI JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES BakeResponseClass__constructRender(JSC::JSGlobalObject*, JSC::CallFrame*);
extern "C" SYSV_ABI JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES BakeResponseClass__constructRedirect(JSC::JSGlobalObject*, JSC::CallFrame*);
extern JSC_CALLCONV size_t Response__estimatedSize(void* ptr);

bool isJSXElement(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject)
{
Expand Down Expand Up @@ -224,8 +223,7 @@ class JSBakeResponseConstructor final : public JSC::InternalFunction {
instance->wrapInnerComponent(globalObject, vm, arg, responseOptions);
}

auto size = Response__estimatedSize(ptr);
vm.heap.reportExtraMemoryAllocated(instance, size);
instance->reportExtraMemoryAllocated(vm);

auto value = JSValue::encode(instance);
RELEASE_AND_RETURN(scope, value);
Expand All @@ -250,8 +248,7 @@ class JSBakeResponseConstructor final : public JSC::InternalFunction {

RETURN_IF_EXCEPTION(scope, {});

auto size = Response__estimatedSize(ptr);
vm.heap.reportExtraMemoryAllocated(instance, size);
instance->reportExtraMemoryAllocated(vm);

RELEASE_AND_RETURN(scope, JSValue::encode(instance));
}
Expand Down
6 changes: 1 addition & 5 deletions src/jsc/bindings/JSBunRequest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,10 @@ JSBunRequest::JSBunRequest(JSC::VM& vm, JSC::Structure* structure, void* sinkPtr
, m_cookies(nullptr, JSC::WriteBarrierEarlyInit)
{
}
extern SYSV_ABI "C" size_t Request__estimatedSize(void* requestPtr);
extern "C" void Bun__JSRequest__calculateEstimatedByteSize(void* requestPtr);
void JSBunRequest::finishCreation(JSC::VM& vm)
{
Base::finishCreation(vm);

auto size = Request__estimatedSize(this->wrapped());
vm.heap.reportExtraMemoryAllocated(this, size);
reportExtraMemoryAllocated(vm);
}

template<typename Visitor>
Expand Down
6 changes: 1 addition & 5 deletions src/jsc/bindings/ShellBindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

#include "ZigGeneratedClasses.h"

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

namespace Bun {

using namespace JSC;
Expand All @@ -23,9 +21,7 @@ extern "C" SYSV_ABI EncodedJSValue Bun__createShellInterpreter(Zig::GlobalObject
ASSERT(structure);

auto* result = WebCore::JSShellInterpreter::create(vm, globalObject, structure, ptr, WTF::move(args), resolveFn, rejectFn);

size_t size = ShellInterpreter__estimatedSize(ptr);
vm.heap.reportExtraMemoryAllocated(result, size);
result->reportExtraMemoryAllocated(vm);
return JSValue::encode(result);
}

Expand Down
59 changes: 35 additions & 24 deletions src/runtime/webcore/Blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ pub trait BlobExt {
Self: Sized;
fn calculate_estimated_byte_size(&self);
fn estimated_size(&self) -> usize;
fn newly_allocated_size(&self) -> usize;
fn to_js(&self, global_object: &JSGlobalObject) -> JSValue;
fn find_or_create_file_from_path(
path_or_fd: &mut PathOrFileDescriptor,
Expand Down Expand Up @@ -3521,36 +3522,20 @@ impl BlobExt for Blob {
// is_detached: defined once above; duplicate removed to fix E0034.

fn calculate_estimated_byte_size(&self) {
// in-memory size. not the size on disk.
let mut size: usize = core::mem::size_of::<Blob>();

if let Some(store) = self.store.get() {
size += core::mem::size_of::<Store>();
match &store.data {
store::Data::Bytes(bytes) => {
size += bytes.stored_name.len();
size += if self.size.get() != MAX_SIZE {
self.size.get() as usize
} else {
bytes.len() as usize
};
}
store::Data::File(file) => size += file.pathlike.estimated_size(),
store::Data::S3(s3) => size += s3.estimated_size(),
}
}

let ct = self.content_type.get();
self.reported_estimated_size.set(
size + (ct.as_slice().len() * (ct.is_owned() as usize))
+ self.name.get().byte_slice().len(),
);
self.reported_estimated_size
.set(estimate_in_memory_size(self, true));
}

fn estimated_size(&self) -> usize {
self.reported_estimated_size.get()
}

/// A store shared with other Blobs (`slice()`, `dupe()`) is already accounted
/// for through them; re-reporting it per view made JSC collect every few views.
Comment thread
robobun marked this conversation as resolved.
fn newly_allocated_size(&self) -> usize {
estimate_in_memory_size(self, self.store().is_none_or(|store| store.has_one_ref()))
}

fn to_js(&self, global_object: &JSGlobalObject) -> JSValue {
// if cfg!(debug_assertions) { debug_assert!(self.is_heap_allocated()); }
self.calculate_estimated_byte_size();
Expand Down Expand Up @@ -3700,6 +3685,32 @@ impl BlobExt for Blob {
}
}

/// In-memory size, not the size on disk.
fn estimate_in_memory_size(blob: &Blob, include_store: bool) -> usize {
let mut size: usize = core::mem::size_of::<Blob>();

if include_store {
if let Some(store) = blob.store() {
size += core::mem::size_of::<Store>();
match &store.data {
store::Data::Bytes(bytes) => {
size += bytes.stored_name.len();
size += if blob.size.get() != MAX_SIZE {
blob.size.get() as usize
} else {
bytes.len() as usize
};
}
store::Data::File(file) => size += file.pathlike.estimated_size(),
store::Data::S3(s3) => size += s3.estimated_size(),
}
}
}

let ct = blob.content_type.get();
size + (ct.as_slice().len() * (ct.is_owned() as usize)) + blob.name.get().byte_slice().len()
}

// ──────────────────────────────────────────────────────────────────────────
// Basic accessors
// ──────────────────────────────────────────────────────────────────────────
Expand Down
2 changes: 2 additions & 0 deletions src/runtime/webcore/response.classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ export default [
storable: true,
},
estimatedSize: true,
// slice() / dupe() share the source's store; see Blob::newly_allocated_size.
newlyAllocatedSize: true,
values: ["stream"],
overridesToJS: true,
proto: {
Expand Down
Loading