diff options
author | 2023-08-24 19:39:00 -0700 | |
---|---|---|
committer | 2023-08-24 19:39:00 -0700 | |
commit | 8a48e8bb0b7de985a96b3a4cae389e3294a2c0e3 (patch) | |
tree | 20d8dc382512313061c20296d1e33e8d18fbbde3 | |
parent | 097ae4e982a9cbcae6b4886c4efb82d452629b99 (diff) | |
download | bun-8a48e8bb0b7de985a96b3a4cae389e3294a2c0e3.tar.gz bun-8a48e8bb0b7de985a96b3a4cae389e3294a2c0e3.tar.zst bun-8a48e8bb0b7de985a96b3a4cae389e3294a2c0e3.zip |
Report extra memory more (#4289)
* Report memory allocated in fetch
* Memory size reporting to `Headers`
* Fixup memory reporting allocator
* Make these tests do more
* cleanup some of this
---------
Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
-rw-r--r-- | src/bun.js/base.zig | 83 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGeneratedClasses.cpp | 9 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGlobalObject.cpp | 1 | ||||
-rw-r--r-- | src/bun.js/bindings/bindings.cpp | 21 | ||||
-rw-r--r-- | src/bun.js/bindings/bindings.zig | 5 | ||||
-rw-r--r-- | src/bun.js/bindings/generated_classes.zig | 5 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/FetchHeaders.cpp | 5 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/FetchHeaders.h | 2 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/HTTPHeaderMap.cpp | 19 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/HTTPHeaderMap.h | 2 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/JSFetchHeaders.cpp | 17 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/JSFetchHeaders.h | 4 | ||||
-rw-r--r-- | src/bun.js/javascript.zig | 4 | ||||
-rw-r--r-- | src/bun.js/webcore/blob.zig | 28 | ||||
-rw-r--r-- | src/bun.js/webcore/response.classes.ts | 1 | ||||
-rw-r--r-- | src/bun.js/webcore/response.zig | 190 | ||||
-rw-r--r-- | src/http_client_async.zig | 10 | ||||
-rw-r--r-- | test/js/bun/http/serve.test.ts | 18 |
18 files changed, 320 insertions, 104 deletions
diff --git a/src/bun.js/base.zig b/src/bun.js/base.zig index e72e196a3..0ed780a5d 100644 --- a/src/bun.js/base.zig +++ b/src/bun.js/base.zig @@ -2484,3 +2484,86 @@ pub const AsyncTaskTracker = struct { bun.JSC.Debugger.didDispatchAsyncCall(globalObject, bun.JSC.Debugger.AsyncCallType.EventListener, this.id); } }; + +pub const MemoryReportingAllocator = struct { + child_allocator: std.mem.Allocator, + memory_cost: std.atomic.Atomic(usize) = std.atomic.Atomic(usize).init(0), + const log = Output.scoped(.MEM, false); + + fn alloc(this: *MemoryReportingAllocator, n: usize, log2_ptr_align: u8, return_address: usize) ?[*]u8 { + var result = this.child_allocator.rawAlloc(n, log2_ptr_align, return_address) orelse return null; + _ = this.memory_cost.fetchAdd(n, .Monotonic); + if (comptime Environment.allow_assert) + log("malloc({d}) = {d}", .{ n, this.memory_cost.loadUnchecked() }); + return result; + } + + pub fn discard(this: *MemoryReportingAllocator, buf: []const u8) void { + _ = this.memory_cost.fetchSub(buf.len, .Monotonic); + if (comptime Environment.allow_assert) + log("discard({d}) = {d}", .{ buf.len, this.memory_cost.loadUnchecked() }); + } + + fn resize(this: *MemoryReportingAllocator, buf: []u8, buf_align: u8, new_len: usize, ret_addr: usize) bool { + if (this.child_allocator.rawResize(buf, buf_align, new_len, ret_addr)) { + _ = this.memory_cost.fetchAdd(new_len -| buf.len, .Monotonic); + if (comptime Environment.allow_assert) + log("resize() = {d}", .{this.memory_cost.loadUnchecked()}); + return true; + } else { + return false; + } + } + + fn free(this: *MemoryReportingAllocator, buf: []u8, buf_align: u8, ret_addr: usize) void { + this.child_allocator.rawFree(buf, buf_align, ret_addr); + + const prev = this.memory_cost.fetchSub(buf.len, .Monotonic); + if (comptime Environment.allow_assert) { + // check for overflow, racily + std.debug.assert(prev > this.memory_cost.load(.Monotonic)); + log("free({d}) = {d}", .{ buf.len, this.memory_cost.loadUnchecked() }); + } + } + + pub fn wrap(this: *MemoryReportingAllocator, allocator_: std.mem.Allocator) std.mem.Allocator { + this.* = .{ + .child_allocator = allocator_, + }; + + return this.allocator(); + } + + pub fn allocator(this: *MemoryReportingAllocator) std.mem.Allocator { + return std.mem.Allocator{ + .ptr = this, + .vtable = &MemoryReportingAllocator.VTable, + }; + } + + pub fn report(this: *MemoryReportingAllocator, vm: *JSC.VM) void { + const mem = this.memory_cost.load(.Monotonic); + if (mem > 0) { + vm.reportExtraMemory(mem); + if (comptime Environment.allow_assert) + log("report({d})", .{mem}); + } + } + + pub inline fn assert(this: *const MemoryReportingAllocator) void { + if (comptime !Environment.allow_assert) { + return; + } + + const memory_cost = this.memory_cost.load(.Monotonic); + if (memory_cost > 0) { + Output.panic("MemoryReportingAllocator still has {d} bytes allocated", .{memory_cost}); + } + } + + pub const VTable = std.mem.Allocator.VTable{ + .alloc = @ptrCast(&MemoryReportingAllocator.alloc), + .resize = @ptrCast(&MemoryReportingAllocator.resize), + .free = @ptrCast(&MemoryReportingAllocator.free), + }; +}; diff --git a/src/bun.js/bindings/ZigGeneratedClasses.cpp b/src/bun.js/bindings/ZigGeneratedClasses.cpp index 3f8a92e3d..0ab7a1b5d 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.cpp +++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp @@ -1728,6 +1728,8 @@ void JSBlobPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObj JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); } +extern "C" size_t Blob__estimatedSize(void* ptr); + void JSBlobConstructor::finishCreation(VM& vm, JSC::JSGlobalObject* globalObject, JSBlobPrototype* prototype) { Base::finishCreation(vm, 0, "Blob"_s, PropertyAdditionMode::WithoutStructureTransition); @@ -1775,6 +1777,7 @@ JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSBlobConstructor::construct(JSC::J } JSBlob* instance = JSBlob::create(vm, globalObject, structure, ptr); + vm.heap.reportExtraMemoryAllocated(Blob__estimatedSize(instance->wrapped())); return JSValue::encode(instance); } @@ -1868,7 +1871,7 @@ extern "C" EncodedJSValue Blob__create(Zig::GlobalObject* globalObject, void* pt auto& vm = globalObject->vm(); JSC::Structure* structure = globalObject->JSBlobStructure(); JSBlob* instance = JSBlob::create(vm, globalObject, structure, ptr); - + vm.heap.reportExtraMemoryAllocated(Blob__estimatedSize(ptr)); return JSValue::encode(instance); } @@ -1878,7 +1881,9 @@ void JSBlob::visitChildrenImpl(JSCell* cell, Visitor& visitor) JSBlob* thisObject = jsCast<JSBlob*>(cell); ASSERT_GC_OBJECT_INHERITS(thisObject, info()); Base::visitChildren(thisObject, visitor); - + if (auto* ptr = thisObject->wrapped()) { + visitor.reportExtraMemoryVisited(Blob__estimatedSize(ptr)); + } thisObject->visitAdditionalChildren<Visitor>(visitor); } diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 5aee64451..467667953 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -289,7 +289,6 @@ extern "C" void JSCInitialize(const char* envp[], size_t envc, void (*onCrash)(c } } } - JSC::Options::assertOptionsAreCoherent(); } } diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index badfd3437..674957218 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -1087,8 +1087,16 @@ WebCore__FetchHeaders* WebCore__FetchHeaders__createFromJS(JSC__JSGlobalObject* JSC__JSValue WebCore__FetchHeaders__toJS(WebCore__FetchHeaders* headers, JSC__JSGlobalObject* lexicalGlobalObject) { Zig::GlobalObject* globalObject = reinterpret_cast<Zig::GlobalObject*>(lexicalGlobalObject); + bool needsMemoryCost = headers->hasOneRef(); - return JSC::JSValue::encode(WebCore::toJS(lexicalGlobalObject, globalObject, headers)); + JSValue value = WebCore::toJS(lexicalGlobalObject, globalObject, headers); + + if (needsMemoryCost) { + JSFetchHeaders* jsHeaders = jsCast<JSFetchHeaders*>(value); + jsHeaders->computeMemoryCost(); + } + + return JSC::JSValue::encode(value); } JSC__JSValue WebCore__FetchHeaders__clone(WebCore__FetchHeaders* headers, JSC__JSGlobalObject* arg1) { @@ -1263,7 +1271,11 @@ JSC__JSValue WebCore__FetchHeaders__createValue(JSC__JSGlobalObject* arg0, Strin WebCore::propagateException(*arg0, throwScope, headers->fill(WebCore::FetchHeaders::Init(WTFMove(pairs)))); pairs.releaseBuffer(); - return JSC::JSValue::encode(WebCore::toJSNewlyCreated(arg0, reinterpret_cast<Zig::GlobalObject*>(arg0), WTFMove(headers))); + JSValue value = WebCore::toJSNewlyCreated(arg0, reinterpret_cast<Zig::GlobalObject*>(arg0), WTFMove(headers)); + + JSFetchHeaders* fetchHeaders = jsCast<JSFetchHeaders*>(value); + fetchHeaders->computeMemoryCost(); + return JSC::JSValue::encode(value); } void WebCore__FetchHeaders__get_(WebCore__FetchHeaders* headers, const ZigString* arg1, ZigString* arg2, JSC__JSGlobalObject* global) { @@ -3968,6 +3980,11 @@ void JSC__VM__deleteAllCode(JSC__VM* arg1, JSC__JSGlobalObject* globalObject) arg1->heap.reportAbandonedObjectGraph(); } +void JSC__VM__reportExtraMemory(JSC__VM* arg0, size_t arg1) +{ + arg0->heap.deprecatedReportExtraMemory(arg1); +} + void JSC__VM__deinit(JSC__VM* arg1, JSC__JSGlobalObject* globalObject) {} void JSC__VM__drainMicrotasks(JSC__VM* arg0) { arg0->drainMicrotasks(); } diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 5e4c6f5b9..c8c87648a 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -5007,6 +5007,11 @@ pub const VM = extern struct { pub fn deferGC(this: *VM, ctx: ?*anyopaque, callback: *const fn (ctx: ?*anyopaque) callconv(.C) void) void { cppFn("deferGC", .{ this, ctx, callback }); } + extern fn JSC__VM__reportExtraMemory(*VM, usize) void; + pub fn reportExtraMemory(this: *VM, size: usize) void { + JSC.markBinding(@src()); + JSC__VM__reportExtraMemory(this, size); + } pub fn deleteAllCode( vm: *VM, diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig index 50e1a620b..e98b8f973 100644 --- a/src/bun.js/bindings/generated_classes.zig +++ b/src/bun.js/bindings/generated_classes.zig @@ -467,6 +467,10 @@ pub const JSBlob = struct { extern fn Blob__dangerouslySetPtr(JSC.JSValue, ?*Blob) bool; comptime { + if (@TypeOf(Blob.estimatedSize) != (fn (*Blob) callconv(.C) usize)) { + @compileLog("Blob.estimatedSize is not a size function"); + } + if (@TypeOf(Blob.onStructuredCloneSerialize) != (fn (*Blob, globalThis: *JSC.JSGlobalObject, ctx: *anyopaque, writeBytes: *const fn (*anyopaque, ptr: [*]const u8, len: u32) callconv(.C) void) callconv(.C) void)) { @compileLog("Blob.onStructuredCloneSerialize is not a structured clone serialize function"); } @@ -513,6 +517,7 @@ pub const JSBlob = struct { @compileLog("Expected Blob.getWriter to be a callback but received " ++ @typeName(@TypeOf(Blob.getWriter))); if (!JSC.is_bindgen) { @export(Blob.constructor, .{ .name = "BlobClass__construct" }); + @export(Blob.estimatedSize, .{ .name = "Blob__estimatedSize" }); @export(Blob.finalize, .{ .name = "BlobClass__finalize" }); @export(Blob.getArrayBuffer, .{ .name = "BlobPrototype__getArrayBuffer" }); @export(Blob.getExists, .{ .name = "BlobPrototype__getExists" }); diff --git a/src/bun.js/bindings/webcore/FetchHeaders.cpp b/src/bun.js/bindings/webcore/FetchHeaders.cpp index f54214733..02ddbac45 100644 --- a/src/bun.js/bindings/webcore/FetchHeaders.cpp +++ b/src/bun.js/bindings/webcore/FetchHeaders.cpp @@ -214,6 +214,11 @@ ExceptionOr<void> FetchHeaders::remove(const String& name) return {}; } +size_t FetchHeaders::memoryCost() const +{ + return m_headers.memoryCost() + sizeof(*this); +} + ExceptionOr<String> FetchHeaders::get(const String& name) const { if (!isValidHTTPToken(name)) diff --git a/src/bun.js/bindings/webcore/FetchHeaders.h b/src/bun.js/bindings/webcore/FetchHeaders.h index 81f8e89c2..515f01227 100644 --- a/src/bun.js/bindings/webcore/FetchHeaders.h +++ b/src/bun.js/bindings/webcore/FetchHeaders.h @@ -62,6 +62,8 @@ public: ExceptionOr<void> fill(const FetchHeaders&); void filterAndFill(const HTTPHeaderMap&, Guard); + size_t memoryCost() const; + inline uint32_t size() { return m_headers.size(); diff --git a/src/bun.js/bindings/webcore/HTTPHeaderMap.cpp b/src/bun.js/bindings/webcore/HTTPHeaderMap.cpp index 99fc9cf13..5021c699d 100644 --- a/src/bun.js/bindings/webcore/HTTPHeaderMap.cpp +++ b/src/bun.js/bindings/webcore/HTTPHeaderMap.cpp @@ -77,6 +77,25 @@ String HTTPHeaderMap::get(const String& name) const return getUncommonHeader(name); } +size_t HTTPHeaderMap::memoryCost() const +{ + size_t cost = m_commonHeaders.size() * sizeof(CommonHeader); + cost += m_uncommonHeaders.size() * sizeof(UncommonHeader); + cost += m_setCookieHeaders.size() * sizeof(String); + for (auto& header : m_commonHeaders) + cost += header.value.sizeInBytes(); + + for (auto& header : m_uncommonHeaders) { + cost += header.key.sizeInBytes(); + cost += header.value.sizeInBytes(); + } + + for (auto& header : m_setCookieHeaders) + cost += header.sizeInBytes(); + + return cost; +} + String HTTPHeaderMap::getUncommonHeader(const String& name) const { auto index = m_uncommonHeaders.findIf([&](auto& header) { diff --git a/src/bun.js/bindings/webcore/HTTPHeaderMap.h b/src/bun.js/bindings/webcore/HTTPHeaderMap.h index 42fdbf41a..8522efdce 100644 --- a/src/bun.js/bindings/webcore/HTTPHeaderMap.h +++ b/src/bun.js/bindings/webcore/HTTPHeaderMap.h @@ -191,6 +191,8 @@ public: WEBCORE_EXPORT bool contains(HTTPHeaderName) const; WEBCORE_EXPORT bool remove(HTTPHeaderName); + size_t memoryCost() const; + // Instead of passing a string literal to any of these functions, just use a HTTPHeaderName instead. template<size_t length> String get(const char (&)[length]) const = delete; template<size_t length> void set(const char (&)[length], const String &) = delete; diff --git a/src/bun.js/bindings/webcore/JSFetchHeaders.cpp b/src/bun.js/bindings/webcore/JSFetchHeaders.cpp index 11e9157c1..eec128373 100644 --- a/src/bun.js/bindings/webcore/JSFetchHeaders.cpp +++ b/src/bun.js/bindings/webcore/JSFetchHeaders.cpp @@ -317,6 +317,12 @@ void JSFetchHeaders::finishCreation(VM& vm) // static_assert(!std::is_base_of<ActiveDOMObject, FetchHeaders>::value, "Interface is not marked as [ActiveDOMObject] even though implementation class subclasses ActiveDOMObject."); } +void JSFetchHeaders::computeMemoryCost() +{ + m_memoryCost = wrapped().memoryCost(); + globalObject()->vm().heap.reportExtraMemoryAllocated(m_memoryCost); +} + JSObject* JSFetchHeaders::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) { return JSFetchHeadersPrototype::create(vm, &globalObject, JSFetchHeadersPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype())); @@ -645,6 +651,17 @@ bool JSFetchHeadersOwner::isReachableFromOpaqueRoots(JSC::Handle<JSC::Unknown> h return false; } +template<typename Visitor> +void JSFetchHeaders::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = jsCast<JSFetchHeaders*>(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.reportExtraMemoryVisited(thisObject->m_memoryCost); +} + +DEFINE_VISIT_CHILDREN(JSFetchHeaders); + void JSFetchHeadersOwner::finalize(JSC::Handle<JSC::Unknown> handle, void* context) { auto* jsFetchHeaders = static_cast<JSFetchHeaders*>(handle.slot()->asCell()); diff --git a/src/bun.js/bindings/webcore/JSFetchHeaders.h b/src/bun.js/bindings/webcore/JSFetchHeaders.h index 1611772ca..4efcc5f5f 100644 --- a/src/bun.js/bindings/webcore/JSFetchHeaders.h +++ b/src/bun.js/bindings/webcore/JSFetchHeaders.h @@ -42,6 +42,7 @@ public: static void destroy(JSC::JSCell*); DECLARE_INFO; + DECLARE_VISIT_CHILDREN; static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) { @@ -58,6 +59,9 @@ public: static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); + size_t m_memoryCost { 0 }; + void computeMemoryCost(); + protected: JSFetchHeaders(JSC::Structure*, JSDOMGlobalObject&, Ref<FetchHeaders>&&); diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 03cdab205..e5d7626d5 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -387,6 +387,7 @@ pub const VirtualMachine = struct { standalone_module_graph: ?*bun.StandaloneModuleGraph = null, hot_reload: bun.CLI.Command.HotReload = .none, + jsc: *JSC.VM = undefined, /// hide bun:wrap from stack traces /// bun:wrap is very noisy @@ -1020,6 +1021,7 @@ pub const VirtualMachine = struct { ); vm.regular_event_loop.global = vm.global; vm.regular_event_loop.virtual_machine = vm; + vm.jsc = vm.global.vm(); if (source_code_printer == null) { var writer = try js_printer.BufferWriter.init(allocator); @@ -1122,6 +1124,7 @@ pub const VirtualMachine = struct { ); vm.regular_event_loop.global = vm.global; vm.regular_event_loop.virtual_machine = vm; + vm.jsc = vm.global.vm(); if (source_code_printer == null) { var writer = try js_printer.BufferWriter.init(allocator); @@ -1237,6 +1240,7 @@ pub const VirtualMachine = struct { ); vm.regular_event_loop.global = vm.global; vm.regular_event_loop.virtual_machine = vm; + vm.jsc = vm.global.vm(); if (source_code_printer == null) { var writer = try js_printer.BufferWriter.init(allocator); diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index c5e893a5a..604726c1e 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -1233,6 +1233,34 @@ pub const Blob = struct { return blob_; } + fn estimatedByteSize(this: *Blob) usize { + // in-memory size. not the size on disk. + if (this.size != Blob.max_size) { + return this.size; + } + + var store = this.store orelse return 0; + if (store.data == .bytes) { + return store.data.bytes.len; + } + + return 0; + } + + pub fn estimatedSize(this: *Blob) callconv(.C) usize { + var size = this.estimatedByteSize() + @sizeOf(Blob); + + if (this.store) |store| { + size += @sizeOf(Blob.Store); + size += switch (store.data) { + .bytes => store.data.bytes.stored_name.len, + .file => store.data.file.pathlike.path.slice().len, + }; + } + + return size + (this.content_type.len * @as(usize, @intFromBool(this.content_type_allocated))); + } + comptime { if (!JSC.is_bindgen) { _ = JSDOMFile__hasInstance; diff --git a/src/bun.js/webcore/response.classes.ts b/src/bun.js/webcore/response.classes.ts index abd29c654..6f3c05aa4 100644 --- a/src/bun.js/webcore/response.classes.ts +++ b/src/bun.js/webcore/response.classes.ts @@ -126,6 +126,7 @@ export default [ klass: {}, configurable: false, structuredClone: { transferable: false, tag: 254 }, + estimatedSize: true, proto: { text: { fn: "getText" }, json: { fn: "getJSON" }, diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index d947a7d4e..0838a4580 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -631,6 +631,7 @@ pub const Fetch = struct { promise: JSC.JSPromise.Strong, concurrent_task: JSC.ConcurrentTask = .{}, poll_ref: JSC.PollRef = .{}, + memory_reporter: *JSC.MemoryReportingAllocator, /// For Http Client requests /// when Content-Length is provided this represents the whole size of the request /// If chunked encoded this will represent the total received size (ignoring the chunk headers) @@ -691,18 +692,19 @@ pub const Fetch = struct { } fn clearData(this: *FetchTasklet) void { + const allocator = this.memory_reporter.allocator(); if (this.url_proxy_buffer.len > 0) { - bun.default_allocator.free(this.url_proxy_buffer); + allocator.free(this.url_proxy_buffer); this.url_proxy_buffer.len = 0; } if (this.hostname) |hostname| { - bun.default_allocator.free(hostname); + allocator.free(hostname); this.hostname = null; } - this.request_headers.entries.deinit(bun.default_allocator); - this.request_headers.buf.deinit(bun.default_allocator); + this.request_headers.entries.deinit(allocator); + this.request_headers.buf.deinit(allocator); this.request_headers = Headers{ .allocator = undefined }; if (this.http != null) { @@ -710,7 +712,7 @@ pub const Fetch = struct { } if (this.metadata != null) { - this.metadata.?.deinit(); + this.metadata.?.deinit(allocator); this.metadata = null; } @@ -730,8 +732,11 @@ pub const Fetch = struct { } pub fn deinit(this: *FetchTasklet) void { - if (this.http) |http| this.javascript_vm.allocator.destroy(http); - this.javascript_vm.allocator.destroy(this); + var reporter = this.memory_reporter; + if (this.http) |http| reporter.allocator().destroy(http); + reporter.allocator().destroy(this); + reporter.assert(); + bun.default_allocator.destroy(reporter); } pub fn onBodyReceived(this: *FetchTasklet) void { @@ -816,6 +821,7 @@ pub const Fetch = struct { // we will reach here when not streaming if (!this.result.has_more) { var scheduled_response_buffer = this.scheduled_response_buffer.list; + this.memory_reporter.discard(scheduled_response_buffer.allocatedSlice()); // done resolve body var old = body.value; @@ -1006,6 +1012,7 @@ pub const Fetch = struct { } var scheduled_response_buffer = this.scheduled_response_buffer.list; + this.memory_reporter.discard(scheduled_response_buffer.allocatedSlice()); const response = Body.Value{ .InternalBlob = .{ .bytes = scheduled_response_buffer.toManaged(bun.default_allocator), @@ -1024,26 +1031,22 @@ pub const Fetch = struct { fn toResponse(this: *FetchTasklet, allocator: std.mem.Allocator) Response { // at this point we always should have metadata - std.debug.assert(this.metadata != null); - if (this.metadata) |metadata| { - const http_response = metadata.response; - this.is_waiting_body = this.result.has_more; - return Response{ - .allocator = allocator, - .url = bun.String.createAtomIfPossible(metadata.href), - .status_text = bun.String.createAtomIfPossible(http_response.status), - .redirected = this.result.redirected, - .body = .{ - .init = .{ - .headers = FetchHeaders.createFromPicoHeaders(http_response.headers), - .status_code = @as(u16, @truncate(http_response.status_code)), - }, - .value = this.toBodyValue(), + var metadata = this.metadata.?; + const http_response = metadata.response; + this.is_waiting_body = this.result.has_more; + return Response{ + .allocator = allocator, + .url = bun.String.createAtomIfPossible(metadata.href), + .status_text = bun.String.createAtomIfPossible(http_response.status), + .redirected = this.result.redirected, + .body = .{ + .init = .{ + .headers = FetchHeaders.createFromPicoHeaders(http_response.headers), + .status_code = @as(u16, @truncate(http_response.status_code)), }, - }; - } - - @panic("fetch metadata should be provided"); + .value = this.toBodyValue(), + }, + }; } pub fn onResolve(this: *FetchTasklet) JSValue { @@ -1063,25 +1066,25 @@ pub const Fetch = struct { fetch_options: FetchOptions, ) !*FetchTasklet { var jsc_vm = globalThis.bunVM(); - var fetch_tasklet = try jsc_vm.allocator.create(FetchTasklet); + var fetch_tasklet = try allocator.create(FetchTasklet); fetch_tasklet.* = .{ .mutex = Mutex.init(), .scheduled_response_buffer = .{ - .allocator = bun.default_allocator, + .allocator = fetch_options.memory_reporter.allocator(), .list = .{ .items = &.{}, .capacity = 0, }, }, .response_buffer = MutableString{ - .allocator = bun.default_allocator, + .allocator = fetch_options.memory_reporter.allocator(), .list = .{ .items = &.{}, .capacity = 0, }, }, - .http = try jsc_vm.allocator.create(HTTPClient.AsyncHTTP), + .http = try allocator.create(HTTPClient.AsyncHTTP), .javascript_vm = jsc_vm, .request_body = fetch_options.body, .global_this = globalThis, @@ -1091,6 +1094,7 @@ pub const Fetch = struct { .signal = fetch_options.signal, .hostname = fetch_options.hostname, .tracker = JSC.AsyncTaskTracker.init(jsc_vm), + .memory_reporter = fetch_options.memory_reporter, }; fetch_tasklet.signals = fetch_tasklet.signal_store.to(); @@ -1114,7 +1118,7 @@ pub const Fetch = struct { } fetch_tasklet.http.?.* = HTTPClient.AsyncHTTP.init( - allocator, + fetch_options.memory_reporter.allocator(), fetch_options.method, fetch_options.url, fetch_options.headers.entries, @@ -1186,6 +1190,8 @@ pub const Fetch = struct { globalThis: ?*JSGlobalObject, // Custom Hostname hostname: ?[]u8 = null, + + memory_reporter: *JSC.MemoryReportingAllocator, }; pub fn queue( @@ -1283,17 +1289,24 @@ pub const Fetch = struct { callframe: *JSC.CallFrame, ) callconv(.C) JSC.JSValue { JSC.markBinding(@src()); + const globalThis = ctx.ptr(); + const arguments = callframe.arguments(2); var exception_val = [_]JSC.C.JSValueRef{null}; var exception: JSC.C.ExceptionRef = &exception_val; + var memory_reporter = bun.default_allocator.create(JSC.MemoryReportingAllocator) catch @panic("out of memory"); + var free_memory_reporter = false; + var allocator = memory_reporter.wrap(bun.default_allocator); defer { if (exception.* != null) { + free_memory_reporter = true; ctx.throwValue(JSC.JSValue.c(exception.*)); } - } - const globalThis = ctx.ptr(); - const arguments = callframe.arguments(2); + memory_reporter.report(globalThis.vm()); + + if (free_memory_reporter) bun.default_allocator.destroy(memory_reporter); + } if (arguments.len == 0) { const err = JSC.toTypeError(.ERR_MISSING_ARGS, fetch_error_no_args, .{}, ctx); @@ -1340,13 +1353,14 @@ pub const Fetch = struct { const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_blank_url, .{}, ctx); // clean hostname if any if (hostname) |host| { - bun.default_allocator.free(host); + allocator.free(host); } + free_memory_reporter = true; return JSPromise.rejectedPromiseValue(globalThis, err); } if (request.url.hasPrefixComptime("data:")) { - var url_slice = request.url.toUTF8WithoutRef(bun.default_allocator); + var url_slice = request.url.toUTF8WithoutRef(allocator); defer url_slice.deinit(); var data_url = DataURL.parseWithoutCheck(url_slice.slice()) catch { @@ -1355,16 +1369,16 @@ pub const Fetch = struct { }; data_url.url = request.url; - return dataURLResponse(data_url, globalThis, bun.default_allocator); + return dataURLResponse(data_url, globalThis, allocator); } - url = ZigURL.fromString(bun.default_allocator, request.url) catch { + url = ZigURL.fromString(allocator, request.url) catch { const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "fetch() URL is invalid", .{}, ctx); // clean hostname if any if (hostname) |host| { - bun.default_allocator.free(host); + allocator.free(host); } - + free_memory_reporter = true; return JSPromise.rejectedPromiseValue( globalThis, err, @@ -1376,7 +1390,7 @@ pub const Fetch = struct { if (args.nextEat()) |options| { if (options.isObject() or options.jsType() == .DOMWrapper) { if (options.fastGet(ctx.ptr(), .method)) |method_| { - var slice_ = method_.toSlice(ctx.ptr(), getAllocator(ctx)); + var slice_ = method_.toSlice(ctx.ptr(), allocator); defer slice_.deinit(); method = Method.which(slice_.slice()) orelse .GET; } else { @@ -1392,7 +1406,7 @@ pub const Fetch = struct { } else { // clean hostname if any if (hostname) |host| { - bun.default_allocator.free(host); + allocator.free(host); } // an error was thrown return JSC.JSValue.jsUndefined(); @@ -1404,24 +1418,24 @@ pub const Fetch = struct { if (options.fastGet(ctx.ptr(), .headers)) |headers_| { if (headers_.as(FetchHeaders)) |headers__| { if (headers__.fastGet(JSC.FetchHeaders.HTTPHeaderName.Host)) |_hostname| { - hostname = _hostname.toOwnedSliceZ(bun.default_allocator) catch unreachable; + hostname = _hostname.toOwnedSliceZ(allocator) catch unreachable; } - headers = Headers.from(headers__, bun.default_allocator, .{ .body = &body }) catch unreachable; + headers = Headers.from(headers__, allocator, .{ .body = &body }) catch unreachable; // TODO: make this one pass } else if (FetchHeaders.createFromJS(ctx.ptr(), headers_)) |headers__| { if (headers__.fastGet(JSC.FetchHeaders.HTTPHeaderName.Host)) |_hostname| { - hostname = _hostname.toOwnedSliceZ(bun.default_allocator) catch unreachable; + hostname = _hostname.toOwnedSliceZ(allocator) catch unreachable; } - headers = Headers.from(headers__, bun.default_allocator, .{ .body = &body }) catch unreachable; + headers = Headers.from(headers__, allocator, .{ .body = &body }) catch unreachable; headers__.deref(); } else if (request.headers) |head| { if (head.fastGet(JSC.FetchHeaders.HTTPHeaderName.Host)) |_hostname| { - hostname = _hostname.toOwnedSliceZ(bun.default_allocator) catch unreachable; + hostname = _hostname.toOwnedSliceZ(allocator) catch unreachable; } - headers = Headers.from(head, bun.default_allocator, .{ .body = &body }) catch unreachable; + headers = Headers.from(head, allocator, .{ .body = &body }) catch unreachable; } } else if (request.headers) |head| { - headers = Headers.from(head, bun.default_allocator, .{ .body = &body }) catch unreachable; + headers = Headers.from(head, allocator, .{ .body = &body }) catch unreachable; } if (options.get(ctx, "timeout")) |timeout_value| { @@ -1462,14 +1476,14 @@ pub const Fetch = struct { const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "fetch() proxy URL is invalid", .{}, ctx); // clean hostname if any if (hostname) |host| { - bun.default_allocator.free(host); + allocator.free(host); } - bun.default_allocator.free(url_proxy_buffer); + allocator.free(url_proxy_buffer); return JSPromise.rejectedPromiseValue(globalThis, err); } defer href.deref(); - var buffer = std.fmt.allocPrint(bun.default_allocator, "{s}{}", .{ url_proxy_buffer, href }) catch { + var buffer = std.fmt.allocPrint(allocator, "{s}{}", .{ url_proxy_buffer, href }) catch { globalThis.throwOutOfMemory(); return .zero; }; @@ -1477,7 +1491,7 @@ pub const Fetch = struct { is_file_url = url.isFile(); proxy = ZigURL.parse(buffer[url.href.len..]); - bun.default_allocator.free(url_proxy_buffer); + allocator.free(url_proxy_buffer); url_proxy_buffer = buffer; } } @@ -1487,9 +1501,9 @@ pub const Fetch = struct { body = request.body.value.useAsAnyBlob(); if (request.headers) |head| { if (head.fastGet(JSC.FetchHeaders.HTTPHeaderName.Host)) |_hostname| { - hostname = _hostname.toOwnedSliceZ(bun.default_allocator) catch unreachable; + hostname = _hostname.toOwnedSliceZ(allocator) catch unreachable; } - headers = Headers.from(head, bun.default_allocator, .{ .body = &body }) catch unreachable; + headers = Headers.from(head, allocator, .{ .body = &body }) catch unreachable; } if (request.signal) |signal_| { _ = signal_.ref(); @@ -1502,13 +1516,13 @@ pub const Fetch = struct { const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_blank_url, .{}, ctx); // clean hostname if any if (hostname) |host| { - bun.default_allocator.free(host); + allocator.free(host); } return JSPromise.rejectedPromiseValue(globalThis, err); } if (str.hasPrefixComptime("data:")) { - var url_slice = str.toUTF8WithoutRef(bun.default_allocator); + var url_slice = str.toUTF8WithoutRef(allocator); defer url_slice.deinit(); var data_url = DataURL.parseWithoutCheck(url_slice.slice()) catch { @@ -1517,13 +1531,13 @@ pub const Fetch = struct { }; data_url.url = str; - return dataURLResponse(data_url, globalThis, bun.default_allocator); + return dataURLResponse(data_url, globalThis, allocator); } - url = ZigURL.fromString(bun.default_allocator, str) catch { + url = ZigURL.fromString(allocator, str) catch { // clean hostname if any if (hostname) |host| { - bun.default_allocator.free(host); + allocator.free(host); } const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "fetch() URL is invalid", .{}, ctx); return JSPromise.rejectedPromiseValue(globalThis, err); @@ -1535,7 +1549,7 @@ pub const Fetch = struct { if (args.nextEat()) |options| { if (options.isObject() or options.jsType() == .DOMWrapper) { if (options.fastGet(ctx.ptr(), .method)) |method_| { - var slice_ = method_.toSlice(ctx.ptr(), getAllocator(ctx)); + var slice_ = method_.toSlice(ctx.ptr(), allocator); defer slice_.deinit(); method = Method.which(slice_.slice()) orelse .GET; } @@ -1549,7 +1563,7 @@ pub const Fetch = struct { } else { // clean hostname if any if (hostname) |host| { - bun.default_allocator.free(host); + allocator.free(host); } // an error was thrown return JSC.JSValue.jsUndefined(); @@ -1559,16 +1573,16 @@ pub const Fetch = struct { if (options.fastGet(ctx.ptr(), .headers)) |headers_| { if (headers_.as(FetchHeaders)) |headers__| { if (headers__.fastGet(JSC.FetchHeaders.HTTPHeaderName.Host)) |_hostname| { - hostname = _hostname.toOwnedSliceZ(bun.default_allocator) catch unreachable; + hostname = _hostname.toOwnedSliceZ(allocator) catch unreachable; } - headers = Headers.from(headers__, bun.default_allocator, .{ .body = &body }) catch unreachable; + headers = Headers.from(headers__, allocator, .{ .body = &body }) catch unreachable; // TODO: make this one pass } else if (FetchHeaders.createFromJS(ctx.ptr(), headers_)) |headers__| { defer headers__.deref(); if (headers__.fastGet(JSC.FetchHeaders.HTTPHeaderName.Host)) |_hostname| { - hostname = _hostname.toOwnedSliceZ(bun.default_allocator) catch unreachable; + hostname = _hostname.toOwnedSliceZ(allocator) catch unreachable; } - headers = Headers.from(headers__, bun.default_allocator, .{ .body = &body }) catch unreachable; + headers = Headers.from(headers__, allocator, .{ .body = &body }) catch unreachable; } else { // Converting the headers failed; return null and // let the set exception get thrown @@ -1615,20 +1629,20 @@ pub const Fetch = struct { const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "fetch() proxy URL is invalid", .{}, ctx); // clean hostname if any if (hostname) |host| { - bun.default_allocator.free(host); + allocator.free(host); } - bun.default_allocator.free(url_proxy_buffer); - + allocator.free(url_proxy_buffer); + free_memory_reporter = true; return JSPromise.rejectedPromiseValue(globalThis, err); } defer href.deref(); - var buffer = std.fmt.allocPrint(bun.default_allocator, "{s}{}", .{ url_proxy_buffer, href }) catch { + var buffer = std.fmt.allocPrint(allocator, "{s}{}", .{ url_proxy_buffer, href }) catch { globalThis.throwOutOfMemory(); return .zero; }; url = ZigURL.parse(buffer[0..url.href.len]); proxy = ZigURL.parse(buffer[url.href.len..]); - bun.default_allocator.free(url_proxy_buffer); + allocator.free(url_proxy_buffer); url_proxy_buffer = buffer; } } @@ -1643,6 +1657,7 @@ pub const Fetch = struct { } if (url.isEmpty()) { + free_memory_reporter = true; const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_blank_url, .{}, ctx); return JSPromise.rejectedPromiseValue(globalThis, err); } @@ -1651,7 +1666,7 @@ pub const Fetch = struct { // We don't pass along headers, we ignore method, we ignore status code... // But it's better than status quo. if (is_file_url) { - defer bun.default_allocator.free(url_proxy_buffer); + defer allocator.free(url_proxy_buffer); var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; const PercentEncoding = @import("../../url.zig").PercentEncoding; var path_buf2: [bun.MAX_PATH_BYTES]u8 = undefined; @@ -1706,22 +1721,24 @@ pub const Fetch = struct { if (url.protocol.len > 0) { if (!(url.isHTTP() or url.isHTTPS())) { - defer bun.default_allocator.free(url_proxy_buffer); + defer allocator.free(url_proxy_buffer); const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "protocol must be http: or https:", .{}, ctx); + free_memory_reporter = true; return JSPromise.rejectedPromiseValue(globalThis, err); } } if (!method.hasRequestBody() and body.size() > 0) { - defer bun.default_allocator.free(url_proxy_buffer); + defer allocator.free(url_proxy_buffer); const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_unexpected_body, .{}, ctx); + free_memory_reporter = true; return JSPromise.rejectedPromiseValue(globalThis, err); } if (headers == null and body.size() > 0 and body.hasContentTypeFromUser()) { headers = Headers.from( null, - bun.default_allocator, + allocator, .{ .body = &body }, ) catch unreachable; } @@ -1739,15 +1756,15 @@ pub const Fetch = struct { const opened_fd = switch (opened_fd_res) { .err => |err| { - bun.default_allocator.free(url_proxy_buffer); + allocator.free(url_proxy_buffer); const rejected_value = JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)); body.detach(); if (headers) |*headers_| { - headers_.buf.deinit(bun.default_allocator); - headers_.entries.deinit(bun.default_allocator); + headers_.buf.deinit(allocator); + headers_.entries.deinit(allocator); } - + free_memory_reporter = true; return rejected_value; }, .result => |fd| fd, @@ -1817,20 +1834,20 @@ pub const Fetch = struct { switch (res) { .err => |err| { - bun.default_allocator.free(url_proxy_buffer); - + allocator.free(url_proxy_buffer); + free_memory_reporter = true; const rejected_value = JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)); body.detach(); if (headers) |*headers_| { - headers_.buf.deinit(bun.default_allocator); - headers_.entries.deinit(bun.default_allocator); + headers_.buf.deinit(allocator); + headers_.entries.deinit(allocator); } return rejected_value; }, .result => |result| { body.detach(); - body.from(std.ArrayList(u8).fromOwnedSlice(bun.default_allocator, @constCast(result.slice()))); + body.from(std.ArrayList(u8).fromOwnedSlice(allocator, @constCast(result.slice()))); http_body = .{ .AnyBlob = body }; }, } @@ -1845,13 +1862,13 @@ pub const Fetch = struct { // var resolve = FetchTasklet.FetchResolver.Class.make(ctx: js.JSContextRef, ptr: *ZigType) _ = FetchTasklet.queue( - default_allocator, + allocator, globalThis, .{ .method = method, .url = url, .headers = headers orelse Headers{ - .allocator = bun.default_allocator, + .allocator = allocator, }, .body = http_body, .timeout = std.time.ns_per_hour, @@ -1864,6 +1881,7 @@ pub const Fetch = struct { .signal = signal, .globalThis = globalThis, .hostname = hostname, + .memory_reporter = memory_reporter, }, // Pass the Strong value instead of creating a new one, or else we // will leak it diff --git a/src/http_client_async.zig b/src/http_client_async.zig index 26978db22..09319966e 100644 --- a/src/http_client_async.zig +++ b/src/http_client_async.zig @@ -2571,8 +2571,8 @@ fn cloneMetadata(this: *HTTPClient) void { var builder = &builder_; this.state.pending_response.count(builder); builder.count(this.url.href); - builder.allocate(bun.default_allocator) catch unreachable; - var headers_buf = bun.default_allocator.alloc(picohttp.Header, this.state.pending_response.headers.len) catch unreachable; + builder.allocate(this.allocator) catch unreachable; + var headers_buf = this.allocator.alloc(picohttp.Header, this.state.pending_response.headers.len) catch unreachable; const response = this.state.pending_response.clone(headers_buf, builder); this.state.pending_response = response; @@ -2665,9 +2665,9 @@ pub const HTTPClientResult = struct { href: []const u8 = "", headers_buf: []picohttp.Header = &.{}, - pub fn deinit(this: *ResultMetadata) void { - if (this.metadata_buf.len > 0) bun.default_allocator.free(this.metadata_buf); - if (this.headers_buf.len > 0) bun.default_allocator.free(this.headers_buf); + pub fn deinit(this: *ResultMetadata, allocator: std.mem.Allocator) void { + if (this.metadata_buf.len > 0) allocator.free(this.metadata_buf); + if (this.headers_buf.len > 0) allocator.free(this.headers_buf); this.headers_buf = &.{}; this.metadata_buf = &.{}; this.href = ""; diff --git a/test/js/bun/http/serve.test.ts b/test/js/bun/http/serve.test.ts index 4c55e779a..3066b4b37 100644 --- a/test/js/bun/http/serve.test.ts +++ b/test/js/bun/http/serve.test.ts @@ -449,11 +449,12 @@ describe("streaming", () => { const textToExpect = readFileSync(fixture, "utf-8"); await runTest( { - fetch(req) { + async fetch(req) { return new Response( new ReadableStream({ - start(controller) { + async start(controller) { controller.enqueue(textToExpect.substring(0, 100)); + await Bun.sleep(0); queueMicrotask(() => { controller.enqueue(textToExpect.substring(100)); controller.close(); @@ -502,8 +503,9 @@ describe("streaming", () => { fetch(req) { return new Response( new ReadableStream({ - pull(controller) { + async pull(controller) { controller.enqueue(textToExpect.substring(0, 100)); + await Bun.sleep(0); queueMicrotask(() => { controller.enqueue(textToExpect.substring(100)); controller.close(); @@ -540,9 +542,9 @@ describe("streaming", () => { async pull(controller) { for (let chunk of chunks) { controller.enqueue(Buffer.from(chunk)); - await 1; + await Bun.sleep(0); } - await 1; + await Bun.sleep(0); controller.close(); }, }), @@ -569,9 +571,9 @@ describe("streaming", () => { new ReadableStream({ async pull(controller) { controller.enqueue(textToExpect.substring(0, 100)); - await Promise.resolve(); + await Bun.sleep(0); controller.enqueue(textToExpect.substring(100)); - await Promise.resolve(); + await Bun.sleep(0); controller.close(); }, }), @@ -598,7 +600,7 @@ describe("streaming", () => { for (let i = 0; i < 10 && remain.length > 0; i++) { controller.enqueue(remain.substring(0, 100)); remain = remain.substring(100); - await new Promise(resolve => queueMicrotask(resolve)); + await Bun.sleep(0); } controller.enqueue(remain); |