From aada6f930fbd41d355168f8590c32bcb7a5b833f Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 16 Dec 2024 20:16:23 -0800 Subject: [PATCH] Fix heap snapshots memory usage stats. Introduce `estimateDirectMemoryUsageOf` function in `"bun:jsc"` (#15790) --- docs/api/utils.md | 25 ++ packages/bun-types/bun.d.ts | 2 + packages/bun-types/jsc.d.ts | 12 + packages/bun-uws/src/WebSocket.h | 4 + src/bun.js/api/BunObject.classes.ts | 1 + src/bun.js/api/bun/process.zig | 4 + src/bun.js/api/bun/socket.zig | 4 + src/bun.js/api/bun/subprocess.zig | 43 +++ src/bun.js/api/server.classes.ts | 1 + src/bun.js/api/server.zig | 35 +++ src/bun.js/api/sockets.classes.ts | 1 + src/bun.js/api/streams.classes.ts | 1 + src/bun.js/bindings/CommonJSModuleRecord.cpp | 20 +- src/bun.js/bindings/CommonJSModuleRecord.h | 2 + src/bun.js/bindings/DOMFormData.cpp | 15 ++ src/bun.js/bindings/DOMFormData.h | 1 + src/bun.js/bindings/DOMURL.h | 5 + src/bun.js/bindings/URLSearchParams.cpp | 9 + src/bun.js/bindings/URLSearchParams.h | 1 + src/bun.js/bindings/blob.cpp | 5 + src/bun.js/bindings/blob.h | 2 + src/bun.js/bindings/headers.h | 7 +- src/bun.js/bindings/webcore/AbortSignal.cpp | 5 + src/bun.js/bindings/webcore/AbortSignal.h | 2 + src/bun.js/bindings/webcore/JSAbortSignal.cpp | 7 + src/bun.js/bindings/webcore/JSAbortSignal.h | 2 + src/bun.js/bindings/webcore/JSDOMFormData.cpp | 7 + src/bun.js/bindings/webcore/JSDOMFormData.h | 1 + src/bun.js/bindings/webcore/JSDOMURL.cpp | 7 + src/bun.js/bindings/webcore/JSDOMURL.h | 2 + .../bindings/webcore/JSFetchHeaders.cpp | 13 +- src/bun.js/bindings/webcore/JSFetchHeaders.h | 1 + .../bindings/webcore/JSURLSearchParams.cpp | 8 + .../bindings/webcore/JSURLSearchParams.h | 1 + src/bun.js/bindings/webcore/JSWebSocket.cpp | 6 + src/bun.js/bindings/webcore/JSWebSocket.h | 1 + src/bun.js/bindings/webcore/WebSocket.cpp | 25 ++ src/bun.js/bindings/webcore/WebSocket.h | 2 + src/bun.js/modules/BunJSCModule.h | 23 +- src/bun.js/webcore/blob.zig | 20 ++ src/bun.js/webcore/body.zig | 10 + src/bun.js/webcore/request.zig | 4 + src/bun.js/webcore/response.classes.ts | 1 + src/bun.js/webcore/streams.zig | 59 +++++ src/codegen/class-definitions.ts | 15 ++ src/codegen/generate-classes.ts | 66 ++++- src/codegen/generate-jssink.ts | 37 ++- src/deps/libuwsockets.cpp | 163 ++++++------ src/deps/uws.zig | 20 +- src/http/websocket_http_client.zig | 21 ++ src/io/PipeReader.zig | 8 + src/io/PipeWriter.zig | 20 ++ src/string.zig | 4 + test/js/bun/util/heap-snapshot.test.ts | 187 ++++++++++++++ test/js/bun/util/heap.ts | 244 ++++++++++++++++++ 55 files changed, 1093 insertions(+), 99 deletions(-) create mode 100644 test/js/bun/util/heap-snapshot.test.ts create mode 100644 test/js/bun/util/heap.ts diff --git a/docs/api/utils.md b/docs/api/utils.md index 3b87922106..c7743c2d43 100644 --- a/docs/api/utils.md +++ b/docs/api/utils.md @@ -771,3 +771,28 @@ console.log(obj); // => { foo: "bar" } ``` Internally, [`structuredClone`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) and [`postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) serialize and deserialize the same way. This exposes the underlying [HTML Structured Clone Algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) to JavaScript as an ArrayBuffer. + +## `estimateDirectMemoryUsageOf` in `bun:jsc` + +The `estimateDirectMemoryUsageOf` function returns a best-effort estimate of the memory usage of an object in bytes, excluding the memory usage of properties or other objects it references. For accurate per-object memory usage, use `Bun.generateHeapSnapshot`. + +```js +import { estimateDirectMemoryUsageOf } from "bun:jsc"; + +const obj = { foo: "bar" }; +const usage = estimateDirectMemoryUsageOf(obj); +console.log(usage); // => 16 + +const buffer = Buffer.alloc(1024 * 1024); +estimateDirectMemoryUsageOf(buffer); +// => 1048624 + +const req = new Request("https://bun.sh"); +estimateDirectMemoryUsageOf(req); +// => 167 + +const array = Array(1024).fill({ a: 1 }); +// Arrays are usually not stored contiguously in memory, so this will not return a useful value (which isn't a bug). +estimateDirectMemoryUsageOf(array); +// => 16 +``` diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index dbe0295d11..c077e3b6f0 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -2151,6 +2151,8 @@ declare module "bun" { * }); */ data: T; + + getBufferedAmount(): number; } /** diff --git a/packages/bun-types/jsc.d.ts b/packages/bun-types/jsc.d.ts index 01bf6b46ff..1f61a49d81 100644 --- a/packages/bun-types/jsc.d.ts +++ b/packages/bun-types/jsc.d.ts @@ -214,4 +214,16 @@ declare module "bun:jsc" { * Run JavaScriptCore's sampling profiler */ function startSamplingProfiler(optionalDirectory?: string): void; + + /** + * Non-recursively estimate the memory usage of an object, excluding the memory usage of + * properties or other objects it references. For more accurate per-object + * memory usage, use {@link Bun.generateHeapSnapshot}. + * + * This is a best-effort estimate. It may not be 100% accurate. When it's + * wrong, it may mean the memory is non-contiguous (such as a large array). + * + * Passing a primitive type that isn't heap allocated returns 0. + */ + function estimateDirectMemoryUsageOf(value: object | CallableFunction | bigint | symbol | string): number; } diff --git a/packages/bun-uws/src/WebSocket.h b/packages/bun-uws/src/WebSocket.h index ee17fb8225..3f48b91271 100644 --- a/packages/bun-uws/src/WebSocket.h +++ b/packages/bun-uws/src/WebSocket.h @@ -73,6 +73,10 @@ public: DROPPED }; + size_t memoryCost() { + return getBufferedAmount() + sizeof(WebSocket); + } + /* Sending fragmented messages puts a bit of effort on the user; you must not interleave regular sends * with fragmented sends and you must sendFirstFragment, [sendFragment], then finally sendLastFragment. */ SendStatus sendFirstFragment(std::string_view message, OpCode opCode = OpCode::BINARY, bool compress = false) { diff --git a/src/bun.js/api/BunObject.classes.ts b/src/bun.js/api/BunObject.classes.ts index 79cb755e8a..6d3e289f82 100644 --- a/src/bun.js/api/BunObject.classes.ts +++ b/src/bun.js/api/BunObject.classes.ts @@ -48,6 +48,7 @@ export default [ finalize: true, hasPendingActivity: true, configurable: false, + memoryCost: true, klass: {}, JSType: "0b11101110", proto: { diff --git a/src/bun.js/api/bun/process.zig b/src/bun.js/api/bun/process.zig index 036a12b800..0c353110da 100644 --- a/src/bun.js/api/bun/process.zig +++ b/src/bun.js/api/bun/process.zig @@ -153,6 +153,10 @@ pub const Process = struct { sync: bool = false, event_loop: JSC.EventLoopHandle, + pub fn memoryCost(_: *const Process) usize { + return @sizeOf(@This()); + } + pub usingnamespace bun.NewRefCounted(Process, deinit); pub fn setExitHandler(this: *Process, handler: anytype) void { diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 08007ae75b..9971f84d16 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -1394,6 +1394,10 @@ fn NewSocket(comptime ssl: bool) type { return this.has_pending_activity.load(.acquire); } + pub fn memoryCost(this: *This) usize { + return @sizeOf(This) + this.buffered_data_for_node_net.cap; + } + pub fn attachNativeCallback(this: *This, callback: NativeCallbacks) bool { if (this.native_callback != .none) return false; this.native_callback = callback; diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig index 9dfceca67a..3872f0436d 100644 --- a/src/bun.js/api/bun/subprocess.zig +++ b/src/bun.js/api/bun/subprocess.zig @@ -395,6 +395,14 @@ pub const Subprocess = struct { closed: void, buffer: []u8, + pub fn memoryCost(this: *const Readable) usize { + return switch (this.*) { + .pipe => @sizeOf(PipeReader) + this.pipe.memoryCost(), + .buffer => this.buffer.len, + else => 0, + }; + } + pub fn hasPendingActivity(this: *const Readable) bool { return switch (this.*) { .pipe => this.pipe.hasPendingActivity(), @@ -794,6 +802,16 @@ pub const Subprocess = struct { array_buffer: JSC.ArrayBuffer.Strong, detached: void, + pub fn memoryCost(this: *const Source) usize { + // Memory cost of Source and each of the particular fields is covered by @sizeOf(Subprocess). + return switch (this.*) { + .blob => this.blob.memoryCost(), + // ArrayBuffer is owned by GC. + .array_buffer => 0, + .detached => 0, + }; + } + pub fn slice(this: *const Source) []const u8 { return switch (this.*) { .blob => this.blob.slice(), @@ -921,6 +939,10 @@ pub const Subprocess = struct { this.destroy(); } + pub fn memoryCost(this: *const This) usize { + return @sizeOf(@This()) + this.source.memoryCost() + this.writer.memoryCost(); + } + pub fn loop(this: *This) *uws.Loop { return this.event_loop.loop(); } @@ -954,6 +976,10 @@ pub const Subprocess = struct { pub usingnamespace bun.NewRefCounted(PipeReader, PipeReader.deinit); + pub fn memoryCost(this: *const PipeReader) usize { + return this.reader.memoryCost(); + } + pub fn hasPendingActivity(this: *const PipeReader) bool { if (this.state == .pending) return true; @@ -1141,6 +1167,15 @@ pub const Subprocess = struct { inherit: void, ignore: void, + pub fn memoryCost(this: *const Writable) usize { + return switch (this.*) { + .pipe => |pipe| pipe.memoryCost(), + .buffer => |buffer| buffer.memoryCost(), + // TODO: memfd + else => 0, + }; + } + pub fn hasPendingActivity(this: *const Writable) bool { return switch (this.*) { .pipe => false, @@ -1415,6 +1450,14 @@ pub const Subprocess = struct { } }; + pub fn memoryCost(this: *const Subprocess) usize { + return @sizeOf(@This()) + + this.process.memoryCost() + + this.stdin.memoryCost() + + this.stdout.memoryCost() + + this.stderr.memoryCost(); + } + pub fn onProcessExit(this: *Subprocess, process: *Process, status: bun.spawn.Status, rusage: *const Rusage) void { log("onProcessExit()", .{}); const this_jsvalue = this.this_jsvalue; diff --git a/src/bun.js/api/server.classes.ts b/src/bun.js/api/server.classes.ts index 9182fa809e..6b9f205693 100644 --- a/src/bun.js/api/server.classes.ts +++ b/src/bun.js/api/server.classes.ts @@ -93,6 +93,7 @@ export default [ define({ name: "ServerWebSocket", JSType: "0b11101110", + memoryCost: true, proto: { send: { fn: "send", diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 5a37980083..8c968ec319 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -1686,6 +1686,29 @@ pub const AnyRequestContext = struct { pub fn init(request_ctx: anytype) AnyRequestContext { return .{ .tagged_pointer = Pointer.init(request_ctx) }; } + + pub fn memoryCost(self: AnyRequestContext) usize { + if (self.tagged_pointer.isNull()) { + return 0; + } + + switch (self.tagged_pointer.tag()) { + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => { + return self.tagged_pointer.as(HTTPServer.RequestContext).memoryCost(); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => { + return self.tagged_pointer.as(HTTPSServer.RequestContext).memoryCost(); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => { + return self.tagged_pointer.as(DebugHTTPServer.RequestContext).memoryCost(); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => { + return self.tagged_pointer.as(DebugHTTPSServer.RequestContext).memoryCost(); + }, + else => @panic("Unexpected AnyRequestContext tag"), + } + } + pub fn get(self: AnyRequestContext, comptime T: type) ?*T { return self.tagged_pointer.get(T); } @@ -1909,6 +1932,11 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp // TODO: support builtin compression const can_sendfile = !ssl_enabled and !Environment.isWindows; + pub fn memoryCost(this: *const RequestContext) usize { + // The Sink and ByteStream aren't owned by this. + return @sizeOf(RequestContext) + this.request_body_buf.capacity + this.response_buf_owned.capacity + this.blob.memoryCost(); + } + pub inline fn isAsync(this: *const RequestContext) bool { return this.defer_deinit_until_callback_completes == null; } @@ -4464,6 +4492,13 @@ pub const ServerWebSocket = struct { pub usingnamespace JSC.Codegen.JSServerWebSocket; pub usingnamespace bun.New(ServerWebSocket); + pub fn memoryCost(this: *const ServerWebSocket) usize { + if (this.flags.closed) { + return @sizeOf(ServerWebSocket); + } + return this.websocket().memoryCost() + @sizeOf(ServerWebSocket); + } + const log = Output.scoped(.WebSocketServer, false); pub fn onOpen(this: *ServerWebSocket, ws: uws.AnyWebSocket) void { diff --git a/src/bun.js/api/sockets.classes.ts b/src/bun.js/api/sockets.classes.ts index 75cdc6b733..7d30de0344 100644 --- a/src/bun.js/api/sockets.classes.ts +++ b/src/bun.js/api/sockets.classes.ts @@ -7,6 +7,7 @@ function generate(ssl) { hasPendingActivity: true, noConstructor: true, configurable: false, + memoryCost: true, proto: { getAuthorizationError: { fn: "getAuthorizationError", diff --git a/src/bun.js/api/streams.classes.ts b/src/bun.js/api/streams.classes.ts index f4419c5db8..140ac646e6 100644 --- a/src/bun.js/api/streams.classes.ts +++ b/src/bun.js/api/streams.classes.ts @@ -7,6 +7,7 @@ function source(name) { noConstructor: true, finalize: true, configurable: false, + memoryCost: true, proto: { drain: { fn: "drainFromJS", diff --git a/src/bun.js/bindings/CommonJSModuleRecord.cpp b/src/bun.js/bindings/CommonJSModuleRecord.cpp index ac0f28d6f3..e1d7e0573b 100644 --- a/src/bun.js/bindings/CommonJSModuleRecord.cpp +++ b/src/bun.js/bindings/CommonJSModuleRecord.cpp @@ -727,6 +727,19 @@ JSCommonJSModule* JSCommonJSModule::create( return JSCommonJSModule::create(globalObject, requireMapKey, exportsObject, hasEvaluated, parent); } +size_t JSCommonJSModule::estimatedSize(JSC::JSCell* cell, JSC::VM& vm) +{ + auto* thisObject = jsCast(cell); + size_t additionalSize = 0; + if (!thisObject->sourceCode.isNull() && !thisObject->sourceCode.view().isEmpty()) { + additionalSize += thisObject->sourceCode.view().length(); + if (!thisObject->sourceCode.view().is8Bit()) { + additionalSize *= 2; + } + } + return Base::estimatedSize(cell, vm) + additionalSize; +} + void JSCommonJSModule::destroy(JSC::JSCell* cell) { static_cast(cell)->JSCommonJSModule::~JSCommonJSModule(); @@ -999,9 +1012,14 @@ void JSCommonJSModule::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) if (auto* id = thisObject->m_id.get()) { if (!id->isRope()) { auto label = id->tryGetValue(false); - analyzer.setLabelForCell(cell, label); + analyzer.setLabelForCell(cell, makeString("CommonJS Module: "_s, StringView(label))); + } else { + analyzer.setLabelForCell(cell, "CommonJS Module"_s); } + } else { + analyzer.setLabelForCell(cell, "CommonJS Module"_s); } + Base::analyzeHeap(cell, analyzer); } diff --git a/src/bun.js/bindings/CommonJSModuleRecord.h b/src/bun.js/bindings/CommonJSModuleRecord.h index 70721f0842..e663dd7460 100644 --- a/src/bun.js/bindings/CommonJSModuleRecord.h +++ b/src/bun.js/bindings/CommonJSModuleRecord.h @@ -56,6 +56,8 @@ public: bool ignoreESModuleAnnotation { false }; JSC::SourceCode sourceCode = JSC::SourceCode(); + static size_t estimatedSize(JSC::JSCell* cell, JSC::VM& vm); + void setSourceCode(JSC::SourceCode&& sourceCode); static void destroy(JSC::JSCell*); diff --git a/src/bun.js/bindings/DOMFormData.cpp b/src/bun.js/bindings/DOMFormData.cpp index 456fe5da54..34cd431cb6 100644 --- a/src/bun.js/bindings/DOMFormData.cpp +++ b/src/bun.js/bindings/DOMFormData.cpp @@ -200,4 +200,19 @@ std::optional> DOMFormData return makeKeyValuePair(item.name, item.data); } +size_t DOMFormData::memoryCost() const +{ + size_t cost = m_items.sizeInBytes(); + for (auto& item : m_items) { + cost += item.name.sizeInBytes(); + if (auto value = std::get_if>(&item.data)) { + cost += value->get()->memoryCost(); + } else if (auto value = std::get_if(&item.data)) { + cost += value->sizeInBytes(); + } + } + + return cost; +} + } // namespace WebCore diff --git a/src/bun.js/bindings/DOMFormData.h b/src/bun.js/bindings/DOMFormData.h index 447be4e927..0f0c1518ce 100644 --- a/src/bun.js/bindings/DOMFormData.h +++ b/src/bun.js/bindings/DOMFormData.h @@ -74,6 +74,7 @@ public: Ref clone() const; size_t count() const { return m_items.size(); } + size_t memoryCost() const; String toURLEncodedString(); diff --git a/src/bun.js/bindings/DOMURL.h b/src/bun.js/bindings/DOMURL.h index e0aff84cbc..56db692140 100644 --- a/src/bun.js/bindings/DOMURL.h +++ b/src/bun.js/bindings/DOMURL.h @@ -61,6 +61,11 @@ public: static String createPublicURL(ScriptExecutionContext&, URLRegistrable&); + size_t memoryCost() const + { + return sizeof(DOMURL) + m_url.string().sizeInBytes(); + } + private: static ExceptionOr> create(const String& url, const URL& base); DOMURL(URL&& completeURL); diff --git a/src/bun.js/bindings/URLSearchParams.cpp b/src/bun.js/bindings/URLSearchParams.cpp index 21acb3662c..0b688b8be2 100644 --- a/src/bun.js/bindings/URLSearchParams.cpp +++ b/src/bun.js/bindings/URLSearchParams.cpp @@ -192,4 +192,13 @@ URLSearchParams::Iterator::Iterator(URLSearchParams& params) { } +size_t URLSearchParams::memoryCost() const +{ + size_t cost = sizeof(URLSearchParams); + for (const auto& pair : m_pairs) { + cost += pair.key.sizeInBytes(); + cost += pair.value.sizeInBytes(); + } + return cost; +} } diff --git a/src/bun.js/bindings/URLSearchParams.h b/src/bun.js/bindings/URLSearchParams.h index 65400d3672..a8744053e5 100644 --- a/src/bun.js/bindings/URLSearchParams.h +++ b/src/bun.js/bindings/URLSearchParams.h @@ -56,6 +56,7 @@ public: void updateFromAssociatedURL(); void sort(); size_t size() const { return m_pairs.size(); } + size_t memoryCost() const; class Iterator { public: diff --git a/src/bun.js/bindings/blob.cpp b/src/bun.js/bindings/blob.cpp index 85982f1b8b..2e650ae479 100644 --- a/src/bun.js/bindings/blob.cpp +++ b/src/bun.js/bindings/blob.cpp @@ -26,4 +26,9 @@ JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlo return JSC::JSValue::decode(encoded); } +size_t Blob::memoryCost() const +{ + return sizeof(Blob) + JSBlob::memoryCost(m_impl); +} + } diff --git a/src/bun.js/bindings/blob.h b/src/bun.js/bindings/blob.h index 1ddd851d1e..66eed55cb2 100644 --- a/src/bun.js/bindings/blob.h +++ b/src/bun.js/bindings/blob.h @@ -51,6 +51,8 @@ public: } void* m_impl; + size_t memoryCost() const; + private: Blob(void* impl, String fileName = String()) { diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index f3896c63ef..405c835e56 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -711,7 +711,7 @@ BUN_DECLARE_HOST_FUNCTION(FetchTaskletChunkedRequestSink__write); ZIG_DECL void Bun__WebSocketHTTPClient__cancel(WebSocketHTTPClient* arg0); ZIG_DECL WebSocketHTTPClient* Bun__WebSocketHTTPClient__connect(JSC__JSGlobalObject* arg0, void* arg1, CppWebSocket* arg2, const ZigString* arg3, uint16_t arg4, const ZigString* arg5, const ZigString* arg6, ZigString* arg7, ZigString* arg8, size_t arg9); ZIG_DECL void Bun__WebSocketHTTPClient__register(JSC__JSGlobalObject* arg0, void* arg1, void* arg2); - +ZIG_DECL size_t Bun__WebSocketHTTPClient__memoryCost(WebSocketHTTPClient* arg0); #endif #ifdef __cplusplus @@ -719,7 +719,7 @@ ZIG_DECL void Bun__WebSocketHTTPClient__register(JSC__JSGlobalObject* arg0, void ZIG_DECL void Bun__WebSocketHTTPSClient__cancel(WebSocketHTTPSClient* arg0); ZIG_DECL WebSocketHTTPSClient* Bun__WebSocketHTTPSClient__connect(JSC__JSGlobalObject* arg0, void* arg1, CppWebSocket* arg2, const ZigString* arg3, uint16_t arg4, const ZigString* arg5, const ZigString* arg6, ZigString* arg7, ZigString* arg8, size_t arg9); ZIG_DECL void Bun__WebSocketHTTPSClient__register(JSC__JSGlobalObject* arg0, void* arg1, void* arg2); - +ZIG_DECL size_t Bun__WebSocketHTTPSClient__memoryCost(WebSocketHTTPSClient* arg0); #endif #ifdef __cplusplus @@ -731,6 +731,7 @@ ZIG_DECL void* Bun__WebSocketClient__init(CppWebSocket* arg0, void* arg1, void* ZIG_DECL void Bun__WebSocketClient__register(JSC__JSGlobalObject* arg0, void* arg1, void* arg2); ZIG_DECL void Bun__WebSocketClient__writeBinaryData(WebSocketClient* arg0, const unsigned char* arg1, size_t arg2, unsigned char arg3); ZIG_DECL void Bun__WebSocketClient__writeString(WebSocketClient* arg0, const ZigString* arg1, unsigned char arg2); +ZIG_DECL size_t Bun__WebSocketClient__memoryCost(WebSocketClient* arg0); #endif @@ -743,7 +744,7 @@ ZIG_DECL void* Bun__WebSocketClientTLS__init(CppWebSocket* arg0, void* arg1, voi ZIG_DECL void Bun__WebSocketClientTLS__register(JSC__JSGlobalObject* arg0, void* arg1, void* arg2); ZIG_DECL void Bun__WebSocketClientTLS__writeBinaryData(WebSocketClientTLS* arg0, const unsigned char* arg1, size_t arg2, unsigned char arg3); ZIG_DECL void Bun__WebSocketClientTLS__writeString(WebSocketClientTLS* arg0, const ZigString* arg1, unsigned char arg2); - +ZIG_DECL size_t Bun__WebSocketClientTLS__memoryCost(WebSocketClientTLS* arg0); #endif #ifdef __cplusplus diff --git a/src/bun.js/bindings/webcore/AbortSignal.cpp b/src/bun.js/bindings/webcore/AbortSignal.cpp index b279d8d451..7d22ef7a9b 100644 --- a/src/bun.js/bindings/webcore/AbortSignal.cpp +++ b/src/bun.js/bindings/webcore/AbortSignal.cpp @@ -266,4 +266,9 @@ WebCoreOpaqueRoot root(AbortSignal* signal) return WebCoreOpaqueRoot { signal }; } +size_t AbortSignal::memoryCost() const +{ + return sizeof(AbortSignal) + m_native_callbacks.sizeInBytes() + m_algorithms.sizeInBytes() + m_sourceSignals.capacity() + m_dependentSignals.capacity(); +} + } // namespace WebCore diff --git a/src/bun.js/bindings/webcore/AbortSignal.h b/src/bun.js/bindings/webcore/AbortSignal.h index b603145ce3..92cd30f4fd 100644 --- a/src/bun.js/bindings/webcore/AbortSignal.h +++ b/src/bun.js/bindings/webcore/AbortSignal.h @@ -108,6 +108,8 @@ public: bool hasPendingActivity() const { return pendingActivityCount > 0; } bool isDependent() const { return m_isDependent; } + size_t memoryCost() const; + private: enum class Aborted : bool { No, diff --git a/src/bun.js/bindings/webcore/JSAbortSignal.cpp b/src/bun.js/bindings/webcore/JSAbortSignal.cpp index bc3f2e3426..a8f0031096 100644 --- a/src/bun.js/bindings/webcore/JSAbortSignal.cpp +++ b/src/bun.js/bindings/webcore/JSAbortSignal.cpp @@ -339,6 +339,13 @@ JSC_DEFINE_HOST_FUNCTION(jsAbortSignalPrototypeFunction_throwIfAborted, (JSGloba return IDLOperation::call(*lexicalGlobalObject, *callFrame, "throwIfAborted"); } +size_t JSAbortSignal::estimatedSize(JSC::JSCell* cell, JSC::VM& vm) +{ + auto* thisObject = jsCast(cell); + auto& wrapped = thisObject->wrapped(); + return Base::estimatedSize(cell, vm) + wrapped.memoryCost(); +} + JSC::GCClient::IsoSubspace* JSAbortSignal::subspaceForImpl(JSC::VM& vm) { return WebCore::subspaceForImpl( diff --git a/src/bun.js/bindings/webcore/JSAbortSignal.h b/src/bun.js/bindings/webcore/JSAbortSignal.h index d52af70bb0..5b0600a22f 100644 --- a/src/bun.js/bindings/webcore/JSAbortSignal.h +++ b/src/bun.js/bindings/webcore/JSAbortSignal.h @@ -43,6 +43,8 @@ public: static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); static AbortSignal* toWrapped(JSC::VM&, JSC::JSValue); + static size_t estimatedSize(JSC::JSCell* cell, JSC::VM& vm); + DECLARE_INFO; static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) diff --git a/src/bun.js/bindings/webcore/JSDOMFormData.cpp b/src/bun.js/bindings/webcore/JSDOMFormData.cpp index 3a1ecab66f..92bb50c8cf 100644 --- a/src/bun.js/bindings/webcore/JSDOMFormData.cpp +++ b/src/bun.js/bindings/webcore/JSDOMFormData.cpp @@ -760,4 +760,11 @@ DOMFormData* JSDOMFormData::toWrapped(JSC::VM&, JSC::JSValue value) return &wrapper->wrapped(); return nullptr; } + +size_t JSDOMFormData::estimatedSize(JSCell* cell, JSC::VM& vm) +{ + auto& wrapped = jsCast(cell)->wrapped(); + return Base::estimatedSize(cell, vm) + wrapped.memoryCost(); +} + } diff --git a/src/bun.js/bindings/webcore/JSDOMFormData.h b/src/bun.js/bindings/webcore/JSDOMFormData.h index 190757f68b..286e820b1e 100644 --- a/src/bun.js/bindings/webcore/JSDOMFormData.h +++ b/src/bun.js/bindings/webcore/JSDOMFormData.h @@ -57,6 +57,7 @@ public: } static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); + static size_t estimatedSize(JSCell* cell, JSC::VM& vm); protected: JSDOMFormData(JSC::Structure*, JSDOMGlobalObject&, Ref&&); diff --git a/src/bun.js/bindings/webcore/JSDOMURL.cpp b/src/bun.js/bindings/webcore/JSDOMURL.cpp index f82fcfd972..c190da7c09 100755 --- a/src/bun.js/bindings/webcore/JSDOMURL.cpp +++ b/src/bun.js/bindings/webcore/JSDOMURL.cpp @@ -141,6 +141,13 @@ static const HashTableValue JSDOMURLConstructorTableValues[] = { { "revokeObjectURL"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, Bun__revokeObjectURL, 1 } }, }; +size_t JSDOMURL::estimatedSize(JSC::JSCell* cell, JSC::VM& vm) +{ + auto* thisObject = jsCast(cell); + auto& wrapped = thisObject->wrapped(); + return Base::estimatedSize(cell, vm) + wrapped.memoryCost(); +} + template<> EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSDOMURLDOMConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) { auto& vm = lexicalGlobalObject->vm(); diff --git a/src/bun.js/bindings/webcore/JSDOMURL.h b/src/bun.js/bindings/webcore/JSDOMURL.h index 57424e6bdd..54e38cdc4b 100644 --- a/src/bun.js/bindings/webcore/JSDOMURL.h +++ b/src/bun.js/bindings/webcore/JSDOMURL.h @@ -59,6 +59,8 @@ public: static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); DECLARE_VISIT_CHILDREN; + static size_t estimatedSize(JSC::JSCell* cell, JSC::VM& vm); + static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); protected: diff --git a/src/bun.js/bindings/webcore/JSFetchHeaders.cpp b/src/bun.js/bindings/webcore/JSFetchHeaders.cpp index 814377532c..8a57ca1908 100644 --- a/src/bun.js/bindings/webcore/JSFetchHeaders.cpp +++ b/src/bun.js/bindings/webcore/JSFetchHeaders.cpp @@ -116,6 +116,13 @@ STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSFetchHeadersPrototype, JSFetchHeadersProto using JSFetchHeadersDOMConstructor = JSDOMConstructor; +size_t JSFetchHeaders::estimatedSize(JSC::JSCell* cell, JSC::VM& vm) +{ + auto* thisObject = jsCast(cell); + auto& wrapped = thisObject->wrapped(); + return Base::estimatedSize(cell, vm) + wrapped.memoryCost(); +} + template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSFetchHeadersDOMConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) { VM& vm = lexicalGlobalObject->vm(); @@ -335,8 +342,12 @@ void JSFetchHeaders::finishCreation(VM& vm) void JSFetchHeaders::computeMemoryCost() { + size_t previousCost = m_memoryCost; m_memoryCost = wrapped().memoryCost(); - globalObject()->vm().heap.reportExtraMemoryAllocated(this, m_memoryCost); + int64_t diff = static_cast(m_memoryCost) - static_cast(previousCost); + if (diff > 0) { + globalObject()->vm().heap.reportExtraMemoryAllocated(this, static_cast(diff)); + } } JSObject* JSFetchHeaders::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) diff --git a/src/bun.js/bindings/webcore/JSFetchHeaders.h b/src/bun.js/bindings/webcore/JSFetchHeaders.h index 8db47a66d1..d555b12914 100644 --- a/src/bun.js/bindings/webcore/JSFetchHeaders.h +++ b/src/bun.js/bindings/webcore/JSFetchHeaders.h @@ -43,6 +43,7 @@ public: DECLARE_INFO; DECLARE_VISIT_CHILDREN; + static size_t estimatedSize(JSC::JSCell* cell, JSC::VM& vm); static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) { diff --git a/src/bun.js/bindings/webcore/JSURLSearchParams.cpp b/src/bun.js/bindings/webcore/JSURLSearchParams.cpp index 66e1d13ec7..a938234a4a 100644 --- a/src/bun.js/bindings/webcore/JSURLSearchParams.cpp +++ b/src/bun.js/bindings/webcore/JSURLSearchParams.cpp @@ -656,4 +656,12 @@ URLSearchParams* JSURLSearchParams::toWrapped(JSC::VM& vm, JSC::JSValue value) return &wrapper->wrapped(); return nullptr; } + +size_t JSURLSearchParams::estimatedSize(JSC::JSCell* cell, JSC::VM& vm) +{ + auto* thisObject = jsCast(cell); + auto& wrapped = thisObject->wrapped(); + return Base::estimatedSize(cell, vm) + wrapped.memoryCost(); +} + } diff --git a/src/bun.js/bindings/webcore/JSURLSearchParams.h b/src/bun.js/bindings/webcore/JSURLSearchParams.h index d3cf240c2d..4c4ba34145 100644 --- a/src/bun.js/bindings/webcore/JSURLSearchParams.h +++ b/src/bun.js/bindings/webcore/JSURLSearchParams.h @@ -57,6 +57,7 @@ public: } static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); + static size_t estimatedSize(JSC::JSCell* cell, JSC::VM& vm); protected: JSURLSearchParams(JSC::Structure*, JSDOMGlobalObject&, Ref&&); diff --git a/src/bun.js/bindings/webcore/JSWebSocket.cpp b/src/bun.js/bindings/webcore/JSWebSocket.cpp index c8a7fd5174..eb28767edc 100644 --- a/src/bun.js/bindings/webcore/JSWebSocket.cpp +++ b/src/bun.js/bindings/webcore/JSWebSocket.cpp @@ -896,6 +896,12 @@ JSC::GCClient::IsoSubspace* JSWebSocket::subspaceForImpl(JSC::VM& vm) [](auto& spaces, auto&& space) { spaces.m_subspaceForWebSocket = std::forward(space); }); } +size_t JSWebSocket::estimatedSize(JSCell* cell, JSC::VM& vm) +{ + auto* thisObject = jsCast(cell); + return Base::estimatedSize(cell, vm) + thisObject->wrapped().memoryCost(); +} + void JSWebSocket::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) { auto* thisObject = jsCast(cell); diff --git a/src/bun.js/bindings/webcore/JSWebSocket.h b/src/bun.js/bindings/webcore/JSWebSocket.h index 3a4d13f6b5..29f82e4eec 100644 --- a/src/bun.js/bindings/webcore/JSWebSocket.h +++ b/src/bun.js/bindings/webcore/JSWebSocket.h @@ -58,6 +58,7 @@ public: } static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); + static size_t estimatedSize(JSCell*, JSC::VM&); WebSocket& wrapped() const { return static_cast(Base::wrapped()); diff --git a/src/bun.js/bindings/webcore/WebSocket.cpp b/src/bun.js/bindings/webcore/WebSocket.cpp index e44742c079..dc1a24c81c 100644 --- a/src/bun.js/bindings/webcore/WebSocket.cpp +++ b/src/bun.js/bindings/webcore/WebSocket.cpp @@ -285,6 +285,31 @@ ExceptionOr WebSocket::connect(const String& url, const Vector& pr return connect(url, protocols, std::nullopt); } +size_t WebSocket::memoryCost() const + +{ + size_t cost = sizeof(WebSocket); + cost += m_url.string().sizeInBytes(); + cost += m_subprotocol.sizeInBytes(); + cost += m_extensions.sizeInBytes(); + + if (m_connectedWebSocketKind == ConnectedWebSocketKind::Client) { + cost += Bun__WebSocketClient__memoryCost(m_connectedWebSocket.client); + } else if (m_connectedWebSocketKind == ConnectedWebSocketKind::ClientSSL) { + cost += Bun__WebSocketClientTLS__memoryCost(m_connectedWebSocket.clientSSL); + } + + if (m_upgradeClient) { + if (m_isSecure) { + cost += Bun__WebSocketHTTPSClient__memoryCost(m_upgradeClient); + } else { + cost += Bun__WebSocketHTTPClient__memoryCost(m_upgradeClient); + } + } + + return cost; +} + ExceptionOr WebSocket::connect(const String& url, const Vector& protocols, std::optional&& headersInit) { // LOG(Network, "WebSocket %p connect() url='%s'", this, url.utf8().data()); diff --git a/src/bun.js/bindings/webcore/WebSocket.h b/src/bun.js/bindings/webcore/WebSocket.h index 5373a736fe..b9abed7a24 100644 --- a/src/bun.js/bindings/webcore/WebSocket.h +++ b/src/bun.js/bindings/webcore/WebSocket.h @@ -163,6 +163,8 @@ public: updateHasPendingActivity(); } + size_t memoryCost() const; + private: typedef union AnyWebSocket { WebSocketClient* client; diff --git a/src/bun.js/modules/BunJSCModule.h b/src/bun.js/modules/BunJSCModule.h index 353e09fac9..60c5d7d2c1 100644 --- a/src/bun.js/modules/BunJSCModule.h +++ b/src/bun.js/modules/BunJSCModule.h @@ -889,6 +889,19 @@ JSC_DEFINE_HOST_FUNCTION(functionCodeCoverageForFile, basicBlocks.size(), functionStartOffset, ignoreSourceMap); } +JSC_DEFINE_HOST_FUNCTION(functionEstimateDirectMemoryUsageOf, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + JSValue value = callFrame->argument(0); + if (value.isCell()) { + auto& vm = globalObject->vm(); + EnsureStillAliveScope alive = value; + return JSValue::encode(jsDoubleNumber(alive.value().asCell()->estimatedSizeInBytes(vm))); + } + + return JSValue::encode(jsNumber(0)); +} + // clang-format off /* Source for BunJSCModuleTable.lut.h @begin BunJSCModuleTable @@ -918,17 +931,18 @@ JSC_DEFINE_HOST_FUNCTION(functionCodeCoverageForFile, totalCompileTime functionTotalCompileTime Function 0 getProtectedObjects functionGetProtectedObjects Function 0 generateHeapSnapshotForDebugging functionGenerateHeapSnapshotForDebugging Function 0 - profile functionRunProfiler Function 0 + profile functionRunProfiler Function 0 setTimeZone functionSetTimeZone Function 0 serialize functionSerialize Function 0 - deserialize functionDeserialize Function 0 + deserialize functionDeserialize Function 0 + estimateDirectMemoryUsageOf functionEstimateDirectMemoryUsageOf Function 1 @end */ namespace Zig { DEFINE_NATIVE_MODULE(BunJSC) { - INIT_NATIVE_MODULE(34); + INIT_NATIVE_MODULE(35); putNativeFn(Identifier::fromString(vm, "callerSourceOrigin"_s), functionCallerSourceOrigin); putNativeFn(Identifier::fromString(vm, "jscDescribe"_s), functionDescribe); @@ -957,10 +971,11 @@ DEFINE_NATIVE_MODULE(BunJSC) putNativeFn(Identifier::fromString(vm, "getProtectedObjects"_s), functionGetProtectedObjects); putNativeFn(Identifier::fromString(vm, "generateHeapSnapshotForDebugging"_s), functionGenerateHeapSnapshotForDebugging); putNativeFn(Identifier::fromString(vm, "profile"_s), functionRunProfiler); - putNativeFn(Identifier::fromString(vm, "codeCoverageForFile"_s), functionCodeCoverageForFile); + putNativeFn(Identifier::fromString(vm, "codeCoverageForFile"_s), functionCodeCoverageForFile); putNativeFn(Identifier::fromString(vm, "setTimeZone"_s), functionSetTimeZone); putNativeFn(Identifier::fromString(vm, "serialize"_s), functionSerialize); putNativeFn(Identifier::fromString(vm, "deserialize"_s), functionDeserialize); + putNativeFn(Identifier::fromString(vm, "estimateDirectMemoryUsageOf"_s), functionEstimateDirectMemoryUsageOf); // Deprecated putNativeFn(Identifier::fromString(vm, "describe"_s), functionDescribe); diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 9c1467bf5d..beb6b1f8f6 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -1655,6 +1655,13 @@ pub const Blob = struct { pub usingnamespace bun.New(@This()); + pub fn memoryCost(this: *const Store) usize { + return if (this.hasOneRef()) @sizeOf(@This()) + switch (this.data) { + .bytes => this.data.bytes.len, + .file => 0, + } else 0; + } + pub fn size(this: *const Store) SizeType { return switch (this.data) { .bytes => this.data.bytes.len, @@ -4770,6 +4777,15 @@ pub const AnyBlob = union(enum) { InternalBlob: InternalBlob, WTFStringImpl: bun.WTF.StringImpl, + /// Assumed that AnyBlob itself is covered by the caller. + pub fn memoryCost(this: *const AnyBlob) usize { + return switch (this.*) { + .Blob => |*blob| if (blob.store) |blob_store| blob_store.memoryCost() else 0, + .WTFStringImpl => |str| if (str.refCount() == 1) str.memoryCost() else 0, + .InternalBlob => |*internal_blob| internal_blob.memoryCost(), + }; + } + pub fn hasOneRef(this: *const AnyBlob) bool { if (this.store()) |s| { return s.hasOneRef(); @@ -5122,6 +5138,10 @@ pub const InternalBlob = struct { bytes: std.ArrayList(u8), was_string: bool = false, + pub fn memoryCost(this: *const @This()) usize { + return this.bytes.capacity; + } + pub fn toStringOwned(this: *@This(), globalThis: *JSC.JSGlobalObject) JSValue { const bytes_without_bom = strings.withoutUTF8BOM(this.bytes.items); if (strings.toUTF16Alloc(globalThis.allocator(), bytes_without_bom, false, false) catch &[_]u16{}) |out| { diff --git a/src/bun.js/webcore/body.zig b/src/bun.js/webcore/body.zig index 8505ee8789..2290def2c9 100644 --- a/src/bun.js/webcore/body.zig +++ b/src/bun.js/webcore/body.zig @@ -414,6 +414,16 @@ pub const Body = struct { }; } + pub fn memoryCost(this: *const Value) usize { + return switch (this.*) { + .InternalBlob => this.InternalBlob.bytes.items.len, + .WTFStringImpl => this.WTFStringImpl.memoryCost(), + .Locked => this.Locked.sizeHint(), + // .InlineBlob => this.InlineBlob.sliceConst().len, + else => 0, + }; + } + pub fn estimatedSize(this: *const Value) usize { return switch (this.*) { .InternalBlob => this.InternalBlob.sliceConst().len, diff --git a/src/bun.js/webcore/request.zig b/src/bun.js/webcore/request.zig index 6d745017ec..45a9f3fec3 100644 --- a/src/bun.js/webcore/request.zig +++ b/src/bun.js/webcore/request.zig @@ -77,6 +77,10 @@ pub const Request = struct { pub const getBlobWithoutCallFrame = RequestMixin.getBlobWithoutCallFrame; pub const WeakRef = bun.WeakPtr(Request, .weak_ptr_data); + pub fn memoryCost(this: *const Request) usize { + return @sizeOf(Request) + this.request_context.memoryCost() + this.url.byteSlice().len + this.body.value.memoryCost(); + } + pub export fn Request__getUWSRequest( this: *Request, ) ?*uws.Request { diff --git a/src/bun.js/webcore/response.classes.ts b/src/bun.js/webcore/response.classes.ts index 157f0abc38..567c029cb4 100644 --- a/src/bun.js/webcore/response.classes.ts +++ b/src/bun.js/webcore/response.classes.ts @@ -10,6 +10,7 @@ export default [ estimatedSize: true, configurable: false, overridesToJS: true, + memoryCost: true, proto: { text: { fn: "getText" }, json: { fn: "getJSON" }, diff --git a/src/bun.js/webcore/streams.zig b/src/bun.js/webcore/streams.zig index 346296cb34..165190e6be 100644 --- a/src/bun.js/webcore/streams.zig +++ b/src/bun.js/webcore/streams.zig @@ -1544,6 +1544,11 @@ pub const ArrayBufferSink = struct { return Sink.init(this); } + pub fn memoryCost(this: *const ArrayBufferSink) usize { + // Since this is a JSSink, the NewJSSink function does @sizeOf(JSSink) which includes @sizeOf(ArrayBufferSink). + return this.bytes.cap; + } + pub const JSSink = NewJSSink(@This(), "ArrayBufferSink"); }; @@ -1637,6 +1642,10 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { pub fn start(_: *@This()) void {} }; + pub fn memoryCost(this: *ThisSink) callconv(.C) usize { + return @sizeOf(ThisSink) + SinkType.memoryCost(&this.sink); + } + pub fn onClose(ptr: JSValue, reason: JSValue) callconv(.C) void { JSC.markBinding(@src()); @@ -1951,6 +1960,7 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { @export(jsConstruct, .{ .name = shim.symbolName("construct") }); @export(endWithSink, .{ .name = shim.symbolName("endWithSink") }); @export(updateRef, .{ .name = shim.symbolName("updateRef") }); + @export(memoryCost, .{ .name = shim.symbolName("memoryCost") }); shim.assertJSFunction(.{ write, @@ -2029,6 +2039,14 @@ pub fn HTTPServerWritable(comptime ssl: bool) type { this.signal = signal; } + // Don't include @sizeOf(This) because it's already included in the memoryCost of the sink + pub fn memoryCost(this: *@This()) usize { + // TODO: include Socket send buffer size. We can't here because we + // don't track if it's still accessible. + // Since this is a JSSink, the NewJSSink function does @sizeOf(JSSink) which includes @sizeOf(ArrayBufferSink). + return this.buffer.cap; + } + fn handleWrote(this: *@This(), amount1: usize) void { defer log("handleWrote: {d} offset: {d}, {d}", .{ amount1, this.offset, this.buffer.len }); const amount = @as(Blob.SizeType, @truncate(amount1)); @@ -2869,6 +2887,12 @@ pub const FetchTaskletChunkedRequestSink = struct { _ = this.end(null); return .{ .result = JSC.JSValue.jsNumber(0) }; } + + pub fn memoryCost(this: *const @This()) usize { + // Since this is a JSSink, the NewJSSink function does @sizeOf(JSSink) which includes @sizeOf(ArrayBufferSink). + return this.buffer.memoryCost(); + } + const name = "FetchTaskletChunkedRequestSink"; pub const JSSink = NewJSSink(@This(), name); }; @@ -2889,6 +2913,7 @@ pub fn ReadableStreamSource( comptime deinit_fn: fn (this: *Context) void, comptime setRefUnrefFn: ?fn (this: *Context, enable: bool) void, comptime drainInternalBuffer: ?fn (this: *Context) bun.ByteList, + comptime memoryCostFn: ?fn (this: *const Context) usize, comptime toBufferedValue: ?fn (this: *Context, globalThis: *JSC.JSGlobalObject, action: BufferedReadableStreamAction) bun.JSError!JSC.JSValue, ) type { return struct { @@ -3053,6 +3078,14 @@ pub fn ReadableStreamSource( pub const arrayBufferFromJS = JSReadableStreamSource.arrayBuffer; pub const blobFromJS = JSReadableStreamSource.blob; pub const bytesFromJS = JSReadableStreamSource.bytes; + + pub fn memoryCost(this: *const ReadableStreamSourceType) usize { + if (memoryCostFn) |function| { + return function(&this.context) + @sizeOf(@This()); + } + return @sizeOf(@This()); + } + pub const JSReadableStreamSource = struct { pub fn pull(this: *ReadableStreamSourceType, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSC.JSValue { JSC.markBinding(@src()); @@ -3310,6 +3343,11 @@ pub const FileSink = struct { pub const IOWriter = bun.io.StreamingWriter(@This(), onWrite, onError, onReady, onClose); pub const Poll = IOWriter; + pub fn memoryCost(this: *const FileSink) usize { + // Since this is a JSSink, the NewJSSink function does @sizeOf(JSSink) which includes @sizeOf(FileSink). + return this.writer.memoryCost(); + } + fn Bun__ForceFileSinkToBeSynchronousOnWindows(globalObject: *JSC.JSGlobalObject, jsvalue: JSC.JSValue) callconv(.C) void { comptime bun.assert(Environment.isWindows); @@ -4420,6 +4458,11 @@ pub const FileReader = struct { return this.reader.setRawMode(flag); } + pub fn memoryCost(this: *const FileReader) usize { + // ReadableStreamSource covers @sizeOf(FileReader) + return this.reader.memoryCost(); + } + pub const Source = ReadableStreamSource( @This(), "File", @@ -4429,6 +4472,7 @@ pub const FileReader = struct { deinit, setRefOrUnref, drain, + memoryCost, null, ); }; @@ -4570,6 +4614,14 @@ pub const ByteBlobLoader = struct { return .zero; } + pub fn memoryCost(this: *const ByteBlobLoader) usize { + // ReadableStreamSource covers @sizeOf(FileReader) + if (this.store) |store| { + return store.memoryCost(); + } + return 0; + } + pub const Source = ReadableStreamSource( @This(), "Blob", @@ -4579,6 +4631,7 @@ pub const ByteBlobLoader = struct { deinit, null, drain, + memoryCost, toBufferedValue, ); }; @@ -4985,6 +5038,11 @@ pub const ByteStream = struct { } } + pub fn memoryCost(this: *const @This()) usize { + // ReadableStreamSource covers @sizeOf(ByteStream) + return this.buffer.capacity; + } + pub fn deinit(this: *@This()) void { JSC.markBinding(@src()); if (this.buffer.capacity > 0) this.buffer.clearAndFree(); @@ -5080,6 +5138,7 @@ pub const ByteStream = struct { deinit, null, drain, + memoryCost, toBufferedValue, ); }; diff --git a/src/codegen/class-definitions.ts b/src/codegen/class-definitions.ts index fe5944fce4..ec2f9e8a43 100644 --- a/src/codegen/class-definitions.ts +++ b/src/codegen/class-definitions.ts @@ -59,7 +59,22 @@ export interface ClassDefinition { JSType?: string; noConstructor?: boolean; wantsThis?: boolean; + /** + * Called from any thread. + * + * Used for GC. + */ estimatedSize?: boolean; + /** + * Used in heap snapshots. + * + * If true, the class will have a `memoryCost` method that returns the size of the object in bytes. + * + * Unlike estimatedSize, this is always called on the main thread and not used for GC. + * + * If none is provided, we use the struct size. + */ + memoryCost?: boolean; hasPendingActivity?: boolean; isEventEmitter?: boolean; supportsObjectCreate?: boolean; diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index bc83c72f45..6f30301b08 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -1206,7 +1206,7 @@ function generateClassHeader(typeName, obj: ClassDefinition) { [...Object.values(klass), ...Object.values(proto)].find(a => !!a.cache) ? "DECLARE_VISIT_CHILDREN;\ntemplate void visitAdditionalChildren(Visitor&);\nDECLARE_VISIT_OUTPUT_CONSTRAINTS;\n" : ""; - const sizeEstimator = obj.estimatedSize ? "static size_t estimatedSize(JSCell* cell, VM& vm);" : ""; + const sizeEstimator = "static size_t estimatedSize(JSCell* cell, VM& vm);"; var weakOwner = ""; var weakInit = ``; @@ -1291,6 +1291,16 @@ function generateClassHeader(typeName, obj: ClassDefinition) { static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); static ptrdiff_t offsetOfWrapped() { return OBJECT_OFFSETOF(${name}, m_ctx); } + /** + * Estimated size of the object from Zig including the JS wrapper. + */ + static size_t estimatedSize(JSC::JSCell* cell, JSC::VM& vm); + + /** + * Memory cost of the object from Zig, without necessarily having a JS wrapper alive. + */ + static size_t memoryCost(void* ptr); + void* m_ctx { nullptr }; @@ -1397,6 +1407,8 @@ visitor.reportExtraMemoryVisited(size); DEFINE_VISIT_CHILDREN(${name}); + + template void ${name}::visitAdditionalChildren(Visitor& visitor) { @@ -1473,9 +1485,39 @@ ${name}::~${name}() `; } + if (!obj.estimatedSize && !obj.memoryCost) { + externs += `extern "C" const size_t ${symbolName(typeName, "ZigStructSize")};`; + } else if (obj.memoryCost) { + externs += `extern JSC_CALLCONV size_t ${symbolName(typeName, "memoryCost")}(void* ptr);`; + } + + if (obj.memoryCost) { + output += ` +size_t ${name}::memoryCost(void* ptr) { + return ptr ? ${symbolName(typeName, "memoryCost")}(ptr) : 0; +} +`; + } else if (obj.estimatedSize) { + output += ` +size_t ${name}::memoryCost(void* ptr) { + return ptr ? ${symbolName(typeName, "estimatedSize")}(ptr) : 0; +} + `; + } else { + output += ` +size_t ${name}::memoryCost(void* ptr) { + return ptr ? ${symbolName(typeName, "ZigStructSize")} : 0; +} + `; + } + output += ` - +size_t ${name}::estimatedSize(JSC::JSCell* cell, JSC::VM& vm) { + auto* thisObject = jsCast<${name}*>(cell); + auto* wrapped = thisObject->wrapped(); + return Base::estimatedSize(cell, vm) + ${name}::memoryCost(wrapped); +} void ${name}::destroy(JSCell* cell) { @@ -1539,16 +1581,15 @@ extern JSC_CALLCONV bool JSC_HOST_CALL_ATTRIBUTES ${typeName}__dangerouslySetPtr return true; } - extern "C" const size_t ${typeName}__ptrOffset = ${className(typeName)}::offsetOfWrapped(); void ${name}::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) { auto* thisObject = jsCast<${name}*>(cell); if (void* wrapped = thisObject->wrapped()) { - // if (thisObject->scriptExecutionContext()) - // analyzer.setLabelForCell(cell, makeString("url ", thisObject->scriptExecutionContext()->url().string())); + analyzer.setWrappedObjectForCell(cell, wrapped); } + Base::analyzeHeap(cell, analyzer); } @@ -1624,6 +1665,7 @@ function generateZig( overridesToJS = false, estimatedSize, call = false, + memoryCost, values = [], hasPendingActivity = false, structuredClone = false, @@ -1695,6 +1737,15 @@ const JavaScriptCoreBindings = struct { `; + if (memoryCost) { + exports.set("memoryCost", symbolName(typeName, "memoryCost")); + output += ` + pub fn ${symbolName(typeName, "memoryCost")}(thisValue: *${typeName}) callconv(JSC.conv) usize { + return @call(.always_inline, ${typeName}.memoryCost, .{thisValue}); + } + `; + } + if (estimatedSize) { exports.set("estimatedSize", symbolName(typeName, "estimatedSize")); output += ` @@ -1702,6 +1753,10 @@ const JavaScriptCoreBindings = struct { return @call(.always_inline, ${typeName}.estimatedSize, .{thisValue}); } `; + } else if (!memoryCost && !estimatedSize) { + output += ` + export const ${symbolName(typeName, "ZigStructSize")}: usize = @sizeOf(${typeName}); + `; } if (hasPendingActivity) { @@ -2075,6 +2130,7 @@ const GENERATED_CLASSES_IMPL_HEADER_PRE = ` #include "ZigGeneratedClasses.h" #include "ErrorCode+List.h" #include "ErrorCode.h" +#include #if !OS(WINDOWS) #define JSC_CALLCONV "C" diff --git a/src/codegen/generate-jssink.ts b/src/codegen/generate-jssink.ts index 527ef8caf0..4271cd1212 100644 --- a/src/codegen/generate-jssink.ts +++ b/src/codegen/generate-jssink.ts @@ -105,6 +105,8 @@ function header() { } static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); + static size_t estimatedSize(JSCell* cell, JSC::VM& vm); + static size_t memoryCost(void* sinkPtr); void ref(); void unref(); @@ -162,6 +164,8 @@ function header() { DECLARE_VISIT_CHILDREN; static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); + static size_t estimatedSize(JSCell* cell, JSC::VM& vm); + static size_t memoryCost(void* sinkPtr); void* m_sinkPtr; mutable WriteBarrier m_onPull; @@ -187,7 +191,7 @@ JSC_DECLARE_CUSTOM_GETTER(function${name}__getter); const outer = ` // AUTO-GENERATED FILE. DO NOT EDIT. -// Generated by 'make generate-sink' +// Generated by generate-jssink.ts // #pragma once @@ -223,10 +227,7 @@ Structure* createJSSinkControllerStructure(JSC::VM& vm, JSC::JSGlobalObject* glo async function implementation() { const head = ` // AUTO-GENERATED FILE. DO NOT EDIT. -// Generated by 'make generate-sink' -// To regenerate this file, run: -// -// make generate-sink +// Generated by 'generate-jssink.ts' // #include "root.h" #include "headers.h" @@ -284,9 +285,7 @@ extern "C" void Bun__onSinkDestroyed(uintptr_t destructor, void* sinkPtr); namespace WebCore { using namespace JSC; - - - +${classes.map(name => `extern "C" size_t ${name}__memoryCost(void* sinkPtr);`).join("\n")} JSC_DEFINE_HOST_FUNCTION(functionStartDirectStream, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame *callFrame)) { @@ -412,6 +411,28 @@ JSC_DEFINE_CUSTOM_GETTER(function${name}__getter, (JSC::JSGlobalObject * lexical return JSC::JSValue::encode(globalObject->${name}()); } +size_t ${className}::estimatedSize(JSCell* cell, JSC::VM& vm) { + return Base::estimatedSize(cell, vm) + ${className}::memoryCost(jsCast<${className}*>(cell)->wrapped()); +} + +size_t ${className}::memoryCost(void* sinkPtr) { + if (!sinkPtr) + return 0; + + return ${name}__memoryCost(sinkPtr); +} + +size_t ${controller}::memoryCost(void* sinkPtr) { + if (!sinkPtr) + return 0; + + return ${name}__memoryCost(sinkPtr); +} + +size_t ${controller}::estimatedSize(JSCell* cell, JSC::VM& vm) { + return Base::estimatedSize(cell, vm) + ${controller}::memoryCost(jsCast<${controller}*>(cell)->wrapped()); +} + JSC_DECLARE_HOST_FUNCTION(${controller}__close); JSC_DEFINE_HOST_FUNCTION(${controller}__close, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame *callFrame)) { diff --git a/src/deps/libuwsockets.cpp b/src/deps/libuwsockets.cpp index b683362431..d3aafb25af 100644 --- a/src/deps/libuwsockets.cpp +++ b/src/deps/libuwsockets.cpp @@ -17,6 +17,9 @@ static inline std::string_view stringViewFromC(const char* message, size_t lengt return std::string_view(); } +using TLSWebSocket = uWS::WebSocket; +using TCPWebSocket = uWS::WebSocket; + extern "C" { @@ -722,12 +725,12 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return *uws->getUserData(); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return *uws->getUserData(); } @@ -735,14 +738,14 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; uws->close(); } else { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; uws->close(); } } @@ -752,13 +755,13 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return (uws_sendstatus_t)uws->send(stringViewFromC(message, length), (uWS::OpCode)(unsigned char)opcode); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return (uws_sendstatus_t)uws->send(stringViewFromC(message, length), (uWS::OpCode)(unsigned char)opcode); } @@ -770,8 +773,8 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return (uws_sendstatus_t)uws->send(stringViewFromC(message, length), (uWS::OpCode)(unsigned char)opcode, compress, fin); @@ -779,8 +782,8 @@ extern "C" else { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return (uws_sendstatus_t)uws->send(stringViewFromC(message, length), (uWS::OpCode)(unsigned char)opcode, compress, fin); @@ -793,13 +796,13 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return (uws_sendstatus_t)uws->sendFragment( stringViewFromC(message, length), compress); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return (uws_sendstatus_t)uws->sendFragment(stringViewFromC(message, length), compress); } @@ -809,13 +812,13 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return (uws_sendstatus_t)uws->sendFirstFragment( stringViewFromC(message, length), uWS::OpCode::BINARY, compress); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return (uws_sendstatus_t)uws->sendFirstFragment( stringViewFromC(message, length), uWS::OpCode::BINARY, compress); } @@ -826,14 +829,14 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return (uws_sendstatus_t)uws->sendFirstFragment( stringViewFromC(message, length), (uWS::OpCode)(unsigned char)opcode, compress); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return (uws_sendstatus_t)uws->sendFirstFragment( stringViewFromC(message, length), (uWS::OpCode)(unsigned char)opcode, compress); @@ -844,13 +847,13 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return (uws_sendstatus_t)uws->sendLastFragment( stringViewFromC(message, length), compress); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return (uws_sendstatus_t)uws->sendLastFragment( stringViewFromC(message, length), compress); } @@ -860,14 +863,14 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; uws->end(code, stringViewFromC(message, length)); } else { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; uws->end(code, stringViewFromC(message, length)); } } @@ -877,15 +880,15 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; uws->cork([handler, user_data]() { handler(user_data); }); } else { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; uws->cork([handler, user_data]() { handler(user_data); }); @@ -896,12 +899,12 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return uws->subscribe(stringViewFromC(topic, length)); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return uws->subscribe(stringViewFromC(topic, length)); } bool uws_ws_unsubscribe(int ssl, uws_websocket_t *ws, const char *topic, @@ -909,12 +912,12 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return uws->unsubscribe(stringViewFromC(topic, length)); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return uws->unsubscribe(stringViewFromC(topic, length)); } @@ -923,12 +926,12 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return uws->isSubscribed(stringViewFromC(topic, length)); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return uws->isSubscribed(stringViewFromC(topic, length)); } void uws_ws_iterate_topics(int ssl, uws_websocket_t *ws, @@ -938,15 +941,15 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; uws->iterateTopics([callback, user_data](auto topic) { callback(topic.data(), topic.length(), user_data); }); } else { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; uws->iterateTopics([callback, user_data](auto topic) { callback(topic.data(), topic.length(), user_data); }); @@ -959,13 +962,13 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return uws->publish(stringViewFromC(topic, topic_length), stringViewFromC(message, message_length)); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return uws->publish(stringViewFromC(topic, topic_length), stringViewFromC(message, message_length)); } @@ -977,14 +980,14 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return uws->publish(stringViewFromC(topic, topic_length), stringViewFromC(message, message_length), (uWS::OpCode)(unsigned char)opcode, compress); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return uws->publish(stringViewFromC(topic, topic_length), stringViewFromC(message, message_length), (uWS::OpCode)(unsigned char)opcode, compress); @@ -994,12 +997,12 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return uws->getBufferedAmount(); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return uws->getBufferedAmount(); } @@ -1008,14 +1011,14 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; std::string_view value = uws->getRemoteAddress(); *dest = value.data(); return value.length(); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; std::string_view value = uws->getRemoteAddress(); *dest = value.data(); @@ -1027,15 +1030,15 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; std::string_view value = uws->getRemoteAddressAsText(); *dest = value.data(); return value.length(); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; std::string_view value = uws->getRemoteAddressAsText(); *dest = value.data(); @@ -1714,6 +1717,14 @@ __attribute__((callback (corker, ctx))) } } + size_t uws_ws_memory_cost(int ssl, uws_websocket_t *ws) { + if (ssl) { + return ((TLSWebSocket*)ws)->memoryCost(); + } else { + return ((TCPWebSocket*)ws)->memoryCost(); + } + } + void us_socket_sendfile_needs_more(us_socket_r s) { s->context->loop->data.last_write_failed = 1; us_poll_change(&s->p, s->context->loop, LIBUS_SOCKET_READABLE | LIBUS_SOCKET_WRITABLE); diff --git a/src/deps/uws.zig b/src/deps/uws.zig index 183bbacfb1..044149b731 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -2884,6 +2884,13 @@ pub const AnyWebSocket = union(enum) { }; } + pub fn memoryCost(this: AnyWebSocket) usize { + return switch (this) { + .ssl => this.ssl.memoryCost(), + .tcp => this.tcp.memoryCost(), + }; + } + pub fn close(this: AnyWebSocket) void { const ssl_flag = @intFromBool(this == .ssl); return uws_ws_close(ssl_flag, this.raw()); @@ -2974,7 +2981,13 @@ pub const AnyWebSocket = union(enum) { } }; -pub const RawWebSocket = opaque {}; +pub const RawWebSocket = opaque { + pub fn memoryCost(this: *RawWebSocket, ssl_flag: i32) usize { + return uws_ws_memory_cost(ssl_flag, this); + } + + extern fn uws_ws_memory_cost(ssl: i32, ws: *RawWebSocket) usize; +}; pub const uws_websocket_handler = ?*const fn (*RawWebSocket) callconv(.C) void; pub const uws_websocket_message_handler = ?*const fn (*RawWebSocket, [*c]const u8, usize, Opcode) callconv(.C) void; @@ -3960,6 +3973,11 @@ pub fn NewApp(comptime ssl: bool) type { pub fn sendWithOptions(this: *WebSocket, message: []const u8, opcode: Opcode, compress: bool, fin: bool) SendStatus { return uws_ws_send_with_options(ssl_flag, this.raw(), message.ptr, message.len, opcode, compress, fin); } + + pub fn memoryCost(this: *WebSocket) usize { + return this.raw().memoryCost(ssl_flag); + } + // pub fn sendFragment(this: *WebSocket, message: []const u8) SendStatus { // return uws_ws_send_fragment(ssl_flag, this.raw(), message: [*c]const u8, length: usize, compress: bool); // } diff --git a/src/http/websocket_http_client.zig b/src/http/websocket_http_client.zig index 806b3ba117..36fbf8d445 100644 --- a/src/http/websocket_http_client.zig +++ b/src/http/websocket_http_client.zig @@ -626,6 +626,13 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { } } + pub fn memoryCost(this: *HTTPClient) callconv(.C) usize { + var cost: usize = @sizeOf(HTTPClient); + cost += this.body.capacity; + cost += this.to_send.len; + return cost; + } + pub fn handleWritable( this: *HTTPClient, socket: Socket, @@ -669,6 +676,7 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { .connect = connect, .cancel = cancel, .register = register, + .memoryCost = memoryCost, }); comptime { @@ -682,6 +690,9 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { @export(register, .{ .name = Export[2].symbol_name, }); + @export(memoryCost, .{ + .name = Export[3].symbol_name, + }); } } }; @@ -1928,6 +1939,14 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { } } + pub fn memoryCost(this: *WebSocket) callconv(.C) usize { + var cost: usize = @sizeOf(WebSocket); + cost += this.send_buffer.buf.len; + cost += this.receive_buffer.buf.len; + // This is under-estimated a little, as we don't include usockets context. + return cost; + } + pub const Export = shim.exportFunctions(.{ .writeBinaryData = writeBinaryData, .writeString = writeString, @@ -1936,6 +1955,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { .register = register, .init = init, .finalize = finalize, + .memoryCost = memoryCost, }); comptime { @@ -1947,6 +1967,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { @export(register, .{ .name = Export[4].symbol_name }); @export(init, .{ .name = Export[5].symbol_name }); @export(finalize, .{ .name = Export[6].symbol_name }); + @export(memoryCost, .{ .name = Export[7].symbol_name }); } } }; diff --git a/src/io/PipeReader.zig b/src/io/PipeReader.zig index 38394793a5..668f7b55fe 100644 --- a/src/io/PipeReader.zig +++ b/src/io/PipeReader.zig @@ -704,6 +704,10 @@ const PosixBufferedReader = struct { return this.flags.is_done or this.flags.received_eof or this.flags.closed_without_reporting; } + pub fn memoryCost(this: *const PosixBufferedReader) usize { + return @sizeOf(@This()) + this._buffer.capacity; + } + pub fn from(to: *@This(), other: *PosixBufferedReader, parent_: *anyopaque) void { to.* = .{ .handle = other.handle, @@ -972,6 +976,10 @@ pub const WindowsBufferedReader = struct { const WindowsOutputReader = @This(); + pub fn memoryCost(this: *const WindowsOutputReader) usize { + return @sizeOf(@This()) + this._buffer.capacity; + } + const Flags = packed struct { is_done: bool = false, pollable: bool = false, diff --git a/src/io/PipeWriter.zig b/src/io/PipeWriter.zig index fd41ae637c..622fda1527 100644 --- a/src/io/PipeWriter.zig +++ b/src/io/PipeWriter.zig @@ -199,6 +199,10 @@ pub fn PosixBufferedWriter( closed_without_reporting: bool = false, close_fd: bool = true, + pub fn memoryCost(_: *const @This()) usize { + return @sizeOf(@This()); + } + const PosixWriter = @This(); pub const auto_poll = if (@hasDecl(Parent, "auto_poll")) Parent.auto_poll else true; @@ -393,6 +397,10 @@ pub fn PosixStreamingWriter( // TODO: chunk_size: usize = 0, + pub fn memoryCost(this: *const @This()) usize { + return @sizeOf(@This()) + this.buffer.capacity; + } + pub fn getPoll(this: *@This()) ?*Async.FilePoll { return this.handle.getPoll(); } @@ -909,6 +917,10 @@ pub fn WindowsBufferedWriter( } } + pub fn memoryCost(this: *const WindowsWriter) usize { + return @sizeOf(@This()) + this.write_buffer.len; + } + pub fn startWithCurrentPipe(this: *WindowsWriter) bun.JSC.Maybe(void) { bun.assert(this.source != null); this.is_done = false; @@ -1025,6 +1037,10 @@ pub const StreamBuffer = struct { this.list.clearRetainingCapacity(); } + pub fn memoryCost(this: *const StreamBuffer) usize { + return this.list.capacity; + } + pub fn size(this: *const StreamBuffer) usize { return this.list.items.len - this.cursor; } @@ -1153,6 +1169,10 @@ pub fn WindowsStreamingWriter( pub usingnamespace BaseWindowsPipeWriter(WindowsWriter, Parent); + pub fn memoryCost(this: *const WindowsWriter) usize { + return @sizeOf(@This()) + this.current_payload.memoryCost() + this.outgoing.memoryCost(); + } + fn onCloseSource(this: *WindowsWriter) void { this.source = null; if (this.closed_without_reporting) { diff --git a/src/string.zig b/src/string.zig index 054fe37e51..e4497381d1 100644 --- a/src/string.zig +++ b/src/string.zig @@ -47,6 +47,10 @@ pub const WTFStringImplStruct = extern struct { return this.m_refCount / s_refCountIncrement; } + pub fn memoryCost(this: WTFStringImpl) usize { + return this.byteLength(); + } + pub fn isStatic(this: WTFStringImpl) bool { return this.m_refCount & s_refCountIncrement != 0; } diff --git a/test/js/bun/util/heap-snapshot.test.ts b/test/js/bun/util/heap-snapshot.test.ts new file mode 100644 index 0000000000..c37b7fab01 --- /dev/null +++ b/test/js/bun/util/heap-snapshot.test.ts @@ -0,0 +1,187 @@ +import { describe, it, expect } from "bun:test"; +import { parseHeapSnapshot, summarizeByType } from "./heap"; +import { estimateDirectMemoryUsageOf } from "bun:jsc"; + +describe("Native types report their size correctly", () => { + it("FormData", () => { + var formData = new FormData(); + globalThis.formData = formData; + let original = estimateDirectMemoryUsageOf(formData); + formData.append("a", Buffer.alloc(1024 * 1024 * 8, "abc").toString()); + const afterBuffer = estimateDirectMemoryUsageOf(formData); + expect(afterBuffer).toBeGreaterThan(original + 1024 * 1024 * 8); + formData.append("a", new Blob([Buffer.alloc(1024 * 1024 * 2, "yooa")])); + const afterBlob = estimateDirectMemoryUsageOf(formData); + expect(afterBlob).toBeGreaterThan(afterBuffer + 1024 * 1024 * 2); + formData.append("a", new Blob([Buffer.alloc(1024 * 1024 * 2, "yooa")])); + const afterBlob2 = estimateDirectMemoryUsageOf(formData); + expect(afterBlob2).toBeGreaterThan(afterBlob + 1024 * 1024 * 2); + + const snapshot = Bun.generateHeapSnapshot(); + const parsed = parseHeapSnapshot(snapshot); + const summariesList = Array.from(summarizeByType(parsed)); + const summariesMap = new Map(summariesList.map(summary => [summary.name, summary])); + + expect(summariesMap.get("FormData")?.size).toBeGreaterThan( + // Test that FormData includes the size of the strings and the blobs + 1024 * 1024 * 8 + 1024 * 1024 * 2 + 1024 * 1024 * 2, + ); + + delete globalThis.formData; + }); + + it("Request", () => { + var request = new Request("https://example.com", { + body: Buffer.alloc(1024 * 1024 * 2, "yoo"), + }); + globalThis.request = request; + + const snapshot = Bun.generateHeapSnapshot(); + const parsed = parseHeapSnapshot(snapshot); + const summariesList = Array.from(summarizeByType(parsed)); + const summariesMap = new Map(summariesList.map(summary => [summary.name, summary])); + + expect(summariesMap.get("Request")?.size).toBeGreaterThan(1024 * 1024 * 2); + expect(summariesMap.get("Request")?.size).toBeLessThan(1024 * 1024 * 4); + + delete globalThis.request; + }); + + it("Response", () => { + var response = new Response(Buffer.alloc(1024 * 1024 * 4, "yoo"), { + headers: { + "Content-Type": "text/plain", + }, + }); + globalThis.response = response; + + const snapshot = Bun.generateHeapSnapshot(); + const parsed = parseHeapSnapshot(snapshot); + const summariesList = Array.from(summarizeByType(parsed)); + const summariesMap = new Map(summariesList.map(summary => [summary.name, summary])); + + expect(summariesMap.get("Response")?.size).toBeGreaterThan(1024 * 1024 * 4); + + delete globalThis.response; + }); + + it("URL", () => { + const searchParams = new URLSearchParams(); + for (let i = 0; i < 1000; i++) { + searchParams.set(`a${i}`, `b${i}`); + } + + var url = new URL("https://example.com"); + globalThis.url = url; + url.search = searchParams.toString(); + + const snapshot = Bun.generateHeapSnapshot(); + const parsed = parseHeapSnapshot(snapshot); + const summariesList = Array.from(summarizeByType(parsed)); + const summariesMap = new Map(summariesList.map(summary => [summary.name, summary])); + + expect(summariesMap.get("URL")?.size).toBeGreaterThan(searchParams.toString().length); + + delete globalThis.url; + }); + + it("URLSearchParams", () => { + const searchParams = new URLSearchParams(); + globalThis.searchParams = searchParams; + const original = estimateDirectMemoryUsageOf(searchParams); + for (let i = 0; i < 1000; i++) { + searchParams.set(`a${i}`, `b${i}`); + } + const after = estimateDirectMemoryUsageOf(searchParams); + expect(after).toBeGreaterThan(original + 1000 * 2); + + const snapshot = Bun.generateHeapSnapshot(); + const parsed = parseHeapSnapshot(snapshot); + const summariesList = Array.from(summarizeByType(parsed)); + const summariesMap = new Map(summariesList.map(summary => [summary.name, summary])); + + expect(summariesMap.get("URLSearchParams")?.size).toBeGreaterThan( + // toString() is greater because of the "?" and "&" + [...searchParams.keys(), ...searchParams.values()].join("").length, + ); + + delete globalThis.searchParams; + }); + + it("Headers", () => { + const headers = new Headers(); + const original = estimateDirectMemoryUsageOf(headers); + for (let i = 0; i < 1000; i++) { + headers.set(`a${i}`, `b${i}`); + } + const after = estimateDirectMemoryUsageOf(headers); + expect(after).toBeGreaterThan(original + 1000 * 2); + + globalThis.headers = headers; + + const snapshot = Bun.generateHeapSnapshot(); + const parsed = parseHeapSnapshot(snapshot); + const summariesList = Array.from(summarizeByType(parsed)); + const summariesMap = new Map(summariesList.map(summary => [summary.name, summary])); + + // Test that Headers includes the size of the strings + expect(summariesMap.get("Headers")?.size).toBeGreaterThan([...headers.keys(), ...headers.values()].join("").length); + + delete globalThis.headers; + }); + + it("WebSocket + ServerWebSocket + Request", async () => { + using server = Bun.serve({ + port: 0, + websocket: { + open(ws) {}, + drain(ws) {}, + message(ws, message) { + const before = estimateDirectMemoryUsageOf(ws); + ws.send(message); + const after = estimateDirectMemoryUsageOf(ws); + const bufferedAmount = ws.getBufferedAmount(); + if (bufferedAmount > 0) { + expect(after).toBeGreaterThan(before + bufferedAmount); + } + }, + }, + + fetch(req, server) { + const before = estimateDirectMemoryUsageOf(req); + server.upgrade(req); + const after = estimateDirectMemoryUsageOf(req); + + // We detach the request context from the request object on upgrade. + expect(after).toBeLessThan(before); + + return new Response("hello"); + }, + }); + const ws = new WebSocket(server.url); + const original = estimateDirectMemoryUsageOf(ws); + globalThis.ws = ws; + + const { promise, resolve } = Promise.withResolvers(); + ws.onopen = () => { + // Send more than we can possibly send in a single message + ws.send(Buffer.alloc(1024 * 128, "hello")); + }; + ws.onmessage = event => { + resolve(event.data); + }; + await promise; + + const after = estimateDirectMemoryUsageOf(ws); + expect(after).toBeGreaterThan(original + 1024 * 128); + + const snapshot = Bun.generateHeapSnapshot(); + const parsed = parseHeapSnapshot(snapshot); + const summariesList = Array.from(summarizeByType(parsed)); + const summariesMap = new Map(summariesList.map(summary => [summary.name, summary])); + + expect(summariesMap.get("WebSocket")?.size).toBeGreaterThan(1024 * 128); + + delete globalThis.ws; + }); +}); diff --git a/test/js/bun/util/heap.ts b/test/js/bun/util/heap.ts new file mode 100644 index 0000000000..dd696dc6bb --- /dev/null +++ b/test/js/bun/util/heap.ts @@ -0,0 +1,244 @@ +//! This is a decently effecient heap profiler reader. + +export interface HeapSnapshotData { + nodes: Float64Array; + edges: Float64Array; + nodeClassNames: string[]; + edgeNames: string[]; + edgeTypes: string[]; + type: "Inspector" | "GCDebugging"; +} + +const enum NodeLayout { + ID = 0, + SIZE = 1, + CLASS_NAME_IDX = 2, + FLAGS = 3, + LABEL_IDX = 4, + CELL_ADDR = 5, + WRAPPED_ADDR = 6, + STRIDE_GCDEBUGGING = 7, + STRIDE_INSPECTOR = 4, +} + +const enum EdgeLayout { + FROM_NODE = 0, + TO_NODE = 1, + TYPE = 2, + NAME_OR_INDEX = 3, + STRIDE = 4, +} + +const enum TypeStatsLayout { + NAME = 0, + SIZE = 1, + COUNT = 2, + RETAINED_SIZE = 3, + STRIDE = 4, +} + +export class TypeStats { + constructor(private stats: Array) {} + + [Symbol.iterator]() { + const stats = this.stats; + let i = 0; + var iterator: IterableIterator<{ + name: string; + size: number; + count: number; + retainedSize: number; + }> = { + [Symbol.iterator]() { + return iterator; + }, + next() { + if (i >= stats.length) { + return { done: true, value: undefined }; + } + const name = stats[i++] as string; + const size = stats[i++] as number; + const count = stats[i++] as number; + const retainedSize = stats[i++] as number; + return { + done: false, + value: { name, size, count, retainedSize }, + }; + }, + }; + return iterator; + } +} + +export function parseHeapSnapshot(data: { + nodes: number[]; + edges: number[]; + nodeClassNames: string[]; + edgeNames: string[]; + edgeTypes: string[]; + type: "Inspector" | "GCDebugging"; +}): HeapSnapshotData { + return { + nodes: new Float64Array(data.nodes), + edges: new Float64Array(data.edges), + nodeClassNames: data.nodeClassNames, + edgeNames: data.edgeNames, + edgeTypes: data.edgeTypes, + type: data.type, + }; +} + +function getNodeStride(data: HeapSnapshotData): number { + return data.type === "GCDebugging" ? NodeLayout.STRIDE_GCDEBUGGING : NodeLayout.STRIDE_INSPECTOR; +} + +export function summarizeByType(data: HeapSnapshotData): TypeStats { + const nodeStride = getNodeStride(data); + const statsArray = new Array(data.nodeClassNames.length * TypeStatsLayout.STRIDE); + + // Initialize the stats array + for (let i = 0, nameIdx = 0; nameIdx < data.nodeClassNames.length; nameIdx++) { + statsArray[i++] = data.nodeClassNames[nameIdx]; + statsArray[i++] = 0; // size + statsArray[i++] = 0; // count + statsArray[i++] = 0; // retained size + } + + // Calculate retained sizes + const retainedSizes = computeRetainedSizes(data); + + // Accumulate stats + for (let i = 0, nodeIndex = 0, nodes = data.nodes; i < nodes.length; i += nodeStride, nodeIndex++) { + const classNameIdx = nodes[i + NodeLayout.CLASS_NAME_IDX]; + const size = nodes[i + NodeLayout.SIZE]; + + const statsOffset = classNameIdx * TypeStatsLayout.STRIDE; + statsArray[statsOffset + 1] += size; // Add to size + statsArray[statsOffset + 2] += 1; // Increment count + statsArray[statsOffset + 3] += retainedSizes[nodeIndex]; // Add retained size + } + + return new TypeStats(statsArray); +} + +// TODO: this is wrong. +function computeRetainedSizes(data: HeapSnapshotData): Float64Array { + const nodeStride = getNodeStride(data); + const nodeCount = Math.floor(data.nodes.length / nodeStride); + + // Initialize arrays + const retainedSizes = new Float64Array(nodeCount); + const processedNodes = new Uint8Array(nodeCount); + const incomingEdgeCount = new Uint32Array(nodeCount); + const isRoot = new Uint8Array(nodeCount); + + // Initialize with shallow sizes + for (let i = 0; i < nodeCount; i++) { + const offset = i * nodeStride; + retainedSizes[i] = data.nodes[offset + NodeLayout.SIZE] || 0; + } + + // Mark node 0 as root + isRoot[0] = 1; + + // Build outgoing edges list and count incoming edges + const outgoingEdges = new Array(nodeCount); + for (let i = 0; i < nodeCount; i++) { + outgoingEdges[i] = []; + } + + // First pass - count incoming edges + for (let i = 0; i < data.edges.length; i += EdgeLayout.STRIDE) { + const fromNode = data.edges[i + EdgeLayout.FROM_NODE]; + const toNode = data.edges[i + EdgeLayout.TO_NODE]; + + if (fromNode >= 0 && fromNode < nodeCount && toNode >= 0 && toNode < nodeCount && fromNode !== toNode) { + incomingEdgeCount[toNode]++; + outgoingEdges[fromNode].push(toNode); + } + } + + // Find roots - nodes with no incoming edges + for (let i = 1; i < nodeCount; i++) { + if (incomingEdgeCount[i] === 0) { + isRoot[i] = 1; + } + } + + function computeRetainedSize(nodeIndex: number): number { + if (processedNodes[nodeIndex]) return retainedSizes[nodeIndex]; + processedNodes[nodeIndex] = 1; + + let size = retainedSizes[nodeIndex]; + + // If we're a root, include everything we retain + if (isRoot[nodeIndex]) { + const outgoing = outgoingEdges[nodeIndex]; + for (let i = 0; i < outgoing.length; i++) { + const childIndex = outgoing[i]; + if (childIndex !== nodeIndex) { + size += computeRetainedSize(childIndex); + } + } + } else { + // For non-roots, only include uniquely retained children + const outgoing = outgoingEdges[nodeIndex]; + for (let i = 0; i < outgoing.length; i++) { + const childIndex = outgoing[i]; + if (childIndex !== nodeIndex && incomingEdgeCount[childIndex] === 1) { + size += computeRetainedSize(childIndex); + } + } + } + + retainedSizes[nodeIndex] = size; + return size; + } + + // Process roots first + for (let i = 0; i < nodeCount; i++) { + if (isRoot[i]) { + computeRetainedSize(i); + } + } + + // Process remaining nodes + for (let i = 0; i < nodeCount; i++) { + if (!processedNodes[i]) { + computeRetainedSize(i); + } + } + + return retainedSizes; +} + +if (import.meta.main) { + let json = JSON.parse(require("fs").readFileSync(process.argv[2], "utf-8")); + if (json?.snapshot) { + json = json.snapshot; + } + + const snapshot = parseHeapSnapshot(json); + + const classNames = summarizeByType(snapshot); + const numberFormatter = new Intl.NumberFormat(); + const formatBytes = (bytes: number) => { + if (bytes < 1024) { + return `${bytes} bytes`; + } + if (bytes < 1024 * 1024) { + return `${(bytes / 1024).toFixed(2)} KB`; + } + + return `${(bytes / 1024 / 1024).toFixed(2)} MB`; + }; + + let results = Array.from(classNames).sort((a, b) => b.retainedSize - a.retainedSize); + for (const { name, size, count, retainedSize } of results) { + console.log( + `${name}: ${numberFormatter.format(count)} instances, ${formatBytes( + size, + )} size, ${formatBytes(retainedSize)} retained`, + ); + } +}