Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
Loading