Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
b41366e011 Add arraybuffer output option to Bun.generateHeapSnapshot("v8")
Add support for `Bun.generateHeapSnapshot("v8", "arraybuffer")` which
returns the V8 heap snapshot as an ArrayBuffer instead of a string.
This avoids potential integer overflow crashes in WTF::String when heap
snapshots approach max uint32 length, and eliminates the overhead of
creating a JavaScript string for large snapshots.

The ArrayBuffer contains UTF-8 encoded JSON and can be written directly
to a file or decoded with TextDecoder.

Depends on oven-sh/WebKit#158

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-10 06:07:10 +00:00
3 changed files with 87 additions and 1 deletions

View File

@@ -4554,6 +4554,20 @@ declare module "bun" {
*/
function generateHeapSnapshot(format: "v8"): string;
/**
* Show precise statistics about memory usage of your application
*
* Generate a V8 Heap Snapshot as an ArrayBuffer.
*
* This avoids the overhead of creating a JavaScript string for large heap snapshots.
* The ArrayBuffer contains the UTF-8 encoded JSON.
* ```ts
* const snapshot = Bun.generateHeapSnapshot("v8", "arraybuffer");
* await Bun.write("heap.heapsnapshot", snapshot);
* ```
*/
function generateHeapSnapshot(format: "v8", encoding: "arraybuffer"): ArrayBuffer;
/**
* The next time JavaScriptCore is idle, clear unused memory and attempt to reduce the heap size.
*

View File

@@ -797,6 +797,7 @@ JSC_DEFINE_HOST_FUNCTION(functionGenerateHeapSnapshot, (JSC::JSGlobalObject * gl
JSValue arg0 = callFrame->argument(0);
auto throwScope = DECLARE_THROW_SCOPE(vm);
bool useV8 = false;
bool useArrayBuffer = false;
if (!arg0.isUndefined()) {
if (arg0.isString()) {
auto str = arg0.toWTFString(globalObject);
@@ -813,6 +814,31 @@ JSC_DEFINE_HOST_FUNCTION(functionGenerateHeapSnapshot, (JSC::JSGlobalObject * gl
}
if (useV8) {
JSValue arg1 = callFrame->argument(1);
if (!arg1.isUndefined()) {
if (arg1.isString()) {
auto str = arg1.toWTFString(globalObject);
RETURN_IF_EXCEPTION(throwScope, {});
if (str == "arraybuffer"_s) {
useArrayBuffer = true;
} else {
throwTypeError(globalObject, throwScope, "Expected 'arraybuffer' or undefined as second argument"_s);
return {};
}
}
}
if (useArrayBuffer) {
JSC::BunV8HeapSnapshotBuilder builder(heapProfiler);
auto bytes = builder.jsonBytes();
auto released = bytes.releaseBuffer();
auto span = released.leakSpan();
auto buffer = ArrayBuffer::createFromBytes(std::span<const uint8_t> { span.data(), span.size() }, createSharedTask<void(void*)>([](void* p) {
fastFree(p);
}));
return JSC::JSValue::encode(JSC::JSArrayBuffer::create(vm, globalObject->arrayBufferStructure(), WTF::move(buffer)));
}
JSC::BunV8HeapSnapshotBuilder builder(heapProfiler);
return JSC::JSValue::encode(jsString(vm, builder.json()));
}
@@ -947,7 +973,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
file BunObject_callback_file DontDelete|Function 1
fileURLToPath functionFileURLToPath DontDelete|Function 1
gc Generated::BunObject::jsGc DontDelete|Function 1
generateHeapSnapshot functionGenerateHeapSnapshot DontDelete|Function 1
generateHeapSnapshot functionGenerateHeapSnapshot DontDelete|Function 2
gunzipSync BunObject_callback_gunzipSync DontDelete|Function 1
gzipSync BunObject_callback_gzipSync DontDelete|Function 1
hash BunObject_lazyPropCb_wrap_hash DontDelete|PropertyCallback

View File

@@ -44,6 +44,52 @@ test("v8.writeHeapSnapshot()", async () => {
expect(await v8HeapSnapshot.parseSnapshot(snapshot)).toBeDefined();
});
test("v8 heap snapshot arraybuffer", async () => {
const snapshot = Bun.generateHeapSnapshot("v8", "arraybuffer");
expect(snapshot).toBeInstanceOf(ArrayBuffer);
expect(snapshot.byteLength).toBeGreaterThan(0);
// Decode the ArrayBuffer as UTF-8 and parse it as JSON
const text = new TextDecoder().decode(snapshot);
const parsed = JSON.parse(text);
// Validate structure
expect(parsed.snapshot).toBeDefined();
expect(parsed.snapshot.meta).toBeDefined();
expect(parsed.nodes).toBeInstanceOf(Array);
expect(parsed.edges).toBeInstanceOf(Array);
expect(parsed.strings).toBeInstanceOf(Array);
expect(parsed.nodes.length).toBeGreaterThan(0);
expect(parsed.edges.length).toBeGreaterThan(0);
expect(parsed.strings.length).toBeGreaterThan(0);
// Also validate via v8-heapsnapshot library
const parsedSnapshot = await v8HeapSnapshot.parseSnapshot(parsed);
expect(parsedSnapshot.nodes.length).toBeGreaterThan(0);
expect(parsedSnapshot.edges.length).toBeGreaterThan(0);
});
test("v8 heap snapshot arraybuffer matches string output", async () => {
// The arraybuffer output should produce valid JSON identical in structure to the string output
const snapshotBuffer = Bun.generateHeapSnapshot("v8", "arraybuffer");
const text = new TextDecoder().decode(snapshotBuffer);
const parsed = JSON.parse(text);
// Verify it has the same meta structure
expect(parsed.snapshot.meta.node_fields).toEqual([
"type",
"name",
"id",
"self_size",
"edge_count",
"trace_node_id",
"detachedness",
]);
expect(parsed.snapshot.meta.edge_fields).toEqual(["type", "name_or_index", "to_node"]);
expect(parsed.snapshot.node_count).toBeGreaterThan(0);
expect(parsed.snapshot.edge_count).toBeGreaterThan(0);
});
test("v8.writeHeapSnapshot() with path", async () => {
const dir = tempDirWithFiles("v8-heap-snapshot", {
"test.heapsnapshot": "",