Skip to content

Commit aada6f9

Browse files
Fix heap snapshots memory usage stats. Introduce estimateDirectMemoryUsageOf function in "bun:jsc" (#15790)
1 parent 3906d02 commit aada6f9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1093
-99
lines changed

docs/api/utils.md

+25
Original file line numberDiff line numberDiff line change
@@ -771,3 +771,28 @@ console.log(obj); // => { foo: "bar" }
771771
```
772772

773773
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.
774+
775+
## `estimateDirectMemoryUsageOf` in `bun:jsc`
776+
777+
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`.
778+
779+
```js
780+
import { estimateDirectMemoryUsageOf } from "bun:jsc";
781+
782+
const obj = { foo: "bar" };
783+
const usage = estimateDirectMemoryUsageOf(obj);
784+
console.log(usage); // => 16
785+
786+
const buffer = Buffer.alloc(1024 * 1024);
787+
estimateDirectMemoryUsageOf(buffer);
788+
// => 1048624
789+
790+
const req = new Request("https://bun.sh");
791+
estimateDirectMemoryUsageOf(req);
792+
// => 167
793+
794+
const array = Array(1024).fill({ a: 1 });
795+
// Arrays are usually not stored contiguously in memory, so this will not return a useful value (which isn't a bug).
796+
estimateDirectMemoryUsageOf(array);
797+
// => 16
798+
```

packages/bun-types/bun.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2151,6 +2151,8 @@ declare module "bun" {
21512151
* });
21522152
*/
21532153
data: T;
2154+
2155+
getBufferedAmount(): number;
21542156
}
21552157

21562158
/**

packages/bun-types/jsc.d.ts

+12
Original file line numberDiff line numberDiff line change
@@ -214,4 +214,16 @@ declare module "bun:jsc" {
214214
* Run JavaScriptCore's sampling profiler
215215
*/
216216
function startSamplingProfiler(optionalDirectory?: string): void;
217+
218+
/**
219+
* Non-recursively estimate the memory usage of an object, excluding the memory usage of
220+
* properties or other objects it references. For more accurate per-object
221+
* memory usage, use {@link Bun.generateHeapSnapshot}.
222+
*
223+
* This is a best-effort estimate. It may not be 100% accurate. When it's
224+
* wrong, it may mean the memory is non-contiguous (such as a large array).
225+
*
226+
* Passing a primitive type that isn't heap allocated returns 0.
227+
*/
228+
function estimateDirectMemoryUsageOf(value: object | CallableFunction | bigint | symbol | string): number;
217229
}

packages/bun-uws/src/WebSocket.h

+4
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ struct WebSocket : AsyncSocket<SSL> {
7373
DROPPED
7474
};
7575

76+
size_t memoryCost() {
77+
return getBufferedAmount() + sizeof(WebSocket);
78+
}
79+
7680
/* Sending fragmented messages puts a bit of effort on the user; you must not interleave regular sends
7781
* with fragmented sends and you must sendFirstFragment, [sendFragment], then finally sendLastFragment. */
7882
SendStatus sendFirstFragment(std::string_view message, OpCode opCode = OpCode::BINARY, bool compress = false) {

src/bun.js/api/BunObject.classes.ts

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export default [
4848
finalize: true,
4949
hasPendingActivity: true,
5050
configurable: false,
51+
memoryCost: true,
5152
klass: {},
5253
JSType: "0b11101110",
5354
proto: {

src/bun.js/api/bun/process.zig

+4
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ pub const Process = struct {
153153
sync: bool = false,
154154
event_loop: JSC.EventLoopHandle,
155155

156+
pub fn memoryCost(_: *const Process) usize {
157+
return @sizeOf(@This());
158+
}
159+
156160
pub usingnamespace bun.NewRefCounted(Process, deinit);
157161

158162
pub fn setExitHandler(this: *Process, handler: anytype) void {

src/bun.js/api/bun/socket.zig

+4
Original file line numberDiff line numberDiff line change
@@ -1394,6 +1394,10 @@ fn NewSocket(comptime ssl: bool) type {
13941394
return this.has_pending_activity.load(.acquire);
13951395
}
13961396

1397+
pub fn memoryCost(this: *This) usize {
1398+
return @sizeOf(This) + this.buffered_data_for_node_net.cap;
1399+
}
1400+
13971401
pub fn attachNativeCallback(this: *This, callback: NativeCallbacks) bool {
13981402
if (this.native_callback != .none) return false;
13991403
this.native_callback = callback;

src/bun.js/api/bun/subprocess.zig

+43
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,14 @@ pub const Subprocess = struct {
395395
closed: void,
396396
buffer: []u8,
397397

398+
pub fn memoryCost(this: *const Readable) usize {
399+
return switch (this.*) {
400+
.pipe => @sizeOf(PipeReader) + this.pipe.memoryCost(),
401+
.buffer => this.buffer.len,
402+
else => 0,
403+
};
404+
}
405+
398406
pub fn hasPendingActivity(this: *const Readable) bool {
399407
return switch (this.*) {
400408
.pipe => this.pipe.hasPendingActivity(),
@@ -794,6 +802,16 @@ pub const Subprocess = struct {
794802
array_buffer: JSC.ArrayBuffer.Strong,
795803
detached: void,
796804

805+
pub fn memoryCost(this: *const Source) usize {
806+
// Memory cost of Source and each of the particular fields is covered by @sizeOf(Subprocess).
807+
return switch (this.*) {
808+
.blob => this.blob.memoryCost(),
809+
// ArrayBuffer is owned by GC.
810+
.array_buffer => 0,
811+
.detached => 0,
812+
};
813+
}
814+
797815
pub fn slice(this: *const Source) []const u8 {
798816
return switch (this.*) {
799817
.blob => this.blob.slice(),
@@ -921,6 +939,10 @@ pub const Subprocess = struct {
921939
this.destroy();
922940
}
923941

942+
pub fn memoryCost(this: *const This) usize {
943+
return @sizeOf(@This()) + this.source.memoryCost() + this.writer.memoryCost();
944+
}
945+
924946
pub fn loop(this: *This) *uws.Loop {
925947
return this.event_loop.loop();
926948
}
@@ -954,6 +976,10 @@ pub const Subprocess = struct {
954976

955977
pub usingnamespace bun.NewRefCounted(PipeReader, PipeReader.deinit);
956978

979+
pub fn memoryCost(this: *const PipeReader) usize {
980+
return this.reader.memoryCost();
981+
}
982+
957983
pub fn hasPendingActivity(this: *const PipeReader) bool {
958984
if (this.state == .pending)
959985
return true;
@@ -1141,6 +1167,15 @@ pub const Subprocess = struct {
11411167
inherit: void,
11421168
ignore: void,
11431169

1170+
pub fn memoryCost(this: *const Writable) usize {
1171+
return switch (this.*) {
1172+
.pipe => |pipe| pipe.memoryCost(),
1173+
.buffer => |buffer| buffer.memoryCost(),
1174+
// TODO: memfd
1175+
else => 0,
1176+
};
1177+
}
1178+
11441179
pub fn hasPendingActivity(this: *const Writable) bool {
11451180
return switch (this.*) {
11461181
.pipe => false,
@@ -1415,6 +1450,14 @@ pub const Subprocess = struct {
14151450
}
14161451
};
14171452

1453+
pub fn memoryCost(this: *const Subprocess) usize {
1454+
return @sizeOf(@This()) +
1455+
this.process.memoryCost() +
1456+
this.stdin.memoryCost() +
1457+
this.stdout.memoryCost() +
1458+
this.stderr.memoryCost();
1459+
}
1460+
14181461
pub fn onProcessExit(this: *Subprocess, process: *Process, status: bun.spawn.Status, rusage: *const Rusage) void {
14191462
log("onProcessExit()", .{});
14201463
const this_jsvalue = this.this_jsvalue;

src/bun.js/api/server.classes.ts

+1
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export default [
9393
define({
9494
name: "ServerWebSocket",
9595
JSType: "0b11101110",
96+
memoryCost: true,
9697
proto: {
9798
send: {
9899
fn: "send",

src/bun.js/api/server.zig

+35
Original file line numberDiff line numberDiff line change
@@ -1686,6 +1686,29 @@ pub const AnyRequestContext = struct {
16861686
pub fn init(request_ctx: anytype) AnyRequestContext {
16871687
return .{ .tagged_pointer = Pointer.init(request_ctx) };
16881688
}
1689+
1690+
pub fn memoryCost(self: AnyRequestContext) usize {
1691+
if (self.tagged_pointer.isNull()) {
1692+
return 0;
1693+
}
1694+
1695+
switch (self.tagged_pointer.tag()) {
1696+
@field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => {
1697+
return self.tagged_pointer.as(HTTPServer.RequestContext).memoryCost();
1698+
},
1699+
@field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => {
1700+
return self.tagged_pointer.as(HTTPSServer.RequestContext).memoryCost();
1701+
},
1702+
@field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => {
1703+
return self.tagged_pointer.as(DebugHTTPServer.RequestContext).memoryCost();
1704+
},
1705+
@field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => {
1706+
return self.tagged_pointer.as(DebugHTTPSServer.RequestContext).memoryCost();
1707+
},
1708+
else => @panic("Unexpected AnyRequestContext tag"),
1709+
}
1710+
}
1711+
16891712
pub fn get(self: AnyRequestContext, comptime T: type) ?*T {
16901713
return self.tagged_pointer.get(T);
16911714
}
@@ -1909,6 +1932,11 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
19091932
// TODO: support builtin compression
19101933
const can_sendfile = !ssl_enabled and !Environment.isWindows;
19111934

1935+
pub fn memoryCost(this: *const RequestContext) usize {
1936+
// The Sink and ByteStream aren't owned by this.
1937+
return @sizeOf(RequestContext) + this.request_body_buf.capacity + this.response_buf_owned.capacity + this.blob.memoryCost();
1938+
}
1939+
19121940
pub inline fn isAsync(this: *const RequestContext) bool {
19131941
return this.defer_deinit_until_callback_completes == null;
19141942
}
@@ -4464,6 +4492,13 @@ pub const ServerWebSocket = struct {
44644492
pub usingnamespace JSC.Codegen.JSServerWebSocket;
44654493
pub usingnamespace bun.New(ServerWebSocket);
44664494

4495+
pub fn memoryCost(this: *const ServerWebSocket) usize {
4496+
if (this.flags.closed) {
4497+
return @sizeOf(ServerWebSocket);
4498+
}
4499+
return this.websocket().memoryCost() + @sizeOf(ServerWebSocket);
4500+
}
4501+
44674502
const log = Output.scoped(.WebSocketServer, false);
44684503

44694504
pub fn onOpen(this: *ServerWebSocket, ws: uws.AnyWebSocket) void {

src/bun.js/api/sockets.classes.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ function generate(ssl) {
77
hasPendingActivity: true,
88
noConstructor: true,
99
configurable: false,
10+
memoryCost: true,
1011
proto: {
1112
getAuthorizationError: {
1213
fn: "getAuthorizationError",

src/bun.js/api/streams.classes.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ function source(name) {
77
noConstructor: true,
88
finalize: true,
99
configurable: false,
10+
memoryCost: true,
1011
proto: {
1112
drain: {
1213
fn: "drainFromJS",

src/bun.js/bindings/CommonJSModuleRecord.cpp

+19-1
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,19 @@ JSCommonJSModule* JSCommonJSModule::create(
727727
return JSCommonJSModule::create(globalObject, requireMapKey, exportsObject, hasEvaluated, parent);
728728
}
729729

730+
size_t JSCommonJSModule::estimatedSize(JSC::JSCell* cell, JSC::VM& vm)
731+
{
732+
auto* thisObject = jsCast<JSCommonJSModule*>(cell);
733+
size_t additionalSize = 0;
734+
if (!thisObject->sourceCode.isNull() && !thisObject->sourceCode.view().isEmpty()) {
735+
additionalSize += thisObject->sourceCode.view().length();
736+
if (!thisObject->sourceCode.view().is8Bit()) {
737+
additionalSize *= 2;
738+
}
739+
}
740+
return Base::estimatedSize(cell, vm) + additionalSize;
741+
}
742+
730743
void JSCommonJSModule::destroy(JSC::JSCell* cell)
731744
{
732745
static_cast<JSCommonJSModule*>(cell)->JSCommonJSModule::~JSCommonJSModule();
@@ -999,9 +1012,14 @@ void JSCommonJSModule::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer)
9991012
if (auto* id = thisObject->m_id.get()) {
10001013
if (!id->isRope()) {
10011014
auto label = id->tryGetValue(false);
1002-
analyzer.setLabelForCell(cell, label);
1015+
analyzer.setLabelForCell(cell, makeString("CommonJS Module: "_s, StringView(label)));
1016+
} else {
1017+
analyzer.setLabelForCell(cell, "CommonJS Module"_s);
10031018
}
1019+
} else {
1020+
analyzer.setLabelForCell(cell, "CommonJS Module"_s);
10041021
}
1022+
10051023
Base::analyzeHeap(cell, analyzer);
10061024
}
10071025

src/bun.js/bindings/CommonJSModuleRecord.h

+2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ class JSCommonJSModule final : public JSC::JSDestructibleObject {
5656
bool ignoreESModuleAnnotation { false };
5757
JSC::SourceCode sourceCode = JSC::SourceCode();
5858

59+
static size_t estimatedSize(JSC::JSCell* cell, JSC::VM& vm);
60+
5961
void setSourceCode(JSC::SourceCode&& sourceCode);
6062

6163
static void destroy(JSC::JSCell*);

src/bun.js/bindings/DOMFormData.cpp

+15
Original file line numberDiff line numberDiff line change
@@ -200,4 +200,19 @@ std::optional<KeyValuePair<String, DOMFormData::FormDataEntryValue>> DOMFormData
200200
return makeKeyValuePair(item.name, item.data);
201201
}
202202

203+
size_t DOMFormData::memoryCost() const
204+
{
205+
size_t cost = m_items.sizeInBytes();
206+
for (auto& item : m_items) {
207+
cost += item.name.sizeInBytes();
208+
if (auto value = std::get_if<RefPtr<Blob>>(&item.data)) {
209+
cost += value->get()->memoryCost();
210+
} else if (auto value = std::get_if<String>(&item.data)) {
211+
cost += value->sizeInBytes();
212+
}
213+
}
214+
215+
return cost;
216+
}
217+
203218
} // namespace WebCore

src/bun.js/bindings/DOMFormData.h

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ class DOMFormData : public RefCounted<DOMFormData>, public ContextDestructionObs
7474
Ref<DOMFormData> clone() const;
7575

7676
size_t count() const { return m_items.size(); }
77+
size_t memoryCost() const;
7778

7879
String toURLEncodedString();
7980

src/bun.js/bindings/DOMURL.h

+5
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ class DOMURL final : public RefCounted<DOMURL>, public CanMakeWeakPtr<DOMURL>, p
6161

6262
static String createPublicURL(ScriptExecutionContext&, URLRegistrable&);
6363

64+
size_t memoryCost() const
65+
{
66+
return sizeof(DOMURL) + m_url.string().sizeInBytes();
67+
}
68+
6469
private:
6570
static ExceptionOr<Ref<DOMURL>> create(const String& url, const URL& base);
6671
DOMURL(URL&& completeURL);

src/bun.js/bindings/URLSearchParams.cpp

+9
Original file line numberDiff line numberDiff line change
@@ -192,4 +192,13 @@ URLSearchParams::Iterator::Iterator(URLSearchParams& params)
192192
{
193193
}
194194

195+
size_t URLSearchParams::memoryCost() const
196+
{
197+
size_t cost = sizeof(URLSearchParams);
198+
for (const auto& pair : m_pairs) {
199+
cost += pair.key.sizeInBytes();
200+
cost += pair.value.sizeInBytes();
201+
}
202+
return cost;
203+
}
195204
}

src/bun.js/bindings/URLSearchParams.h

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class URLSearchParams : public RefCounted<URLSearchParams> {
5656
void updateFromAssociatedURL();
5757
void sort();
5858
size_t size() const { return m_pairs.size(); }
59+
size_t memoryCost() const;
5960

6061
class Iterator {
6162
public:

src/bun.js/bindings/blob.cpp

+5
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,9 @@ JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlo
2626
return JSC::JSValue::decode(encoded);
2727
}
2828

29+
size_t Blob::memoryCost() const
30+
{
31+
return sizeof(Blob) + JSBlob::memoryCost(m_impl);
32+
}
33+
2934
}

src/bun.js/bindings/blob.h

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ class Blob : public RefCounted<Blob> {
5151
}
5252
void* m_impl;
5353

54+
size_t memoryCost() const;
55+
5456
private:
5557
Blob(void* impl, String fileName = String())
5658
{

0 commit comments

Comments
 (0)