diff --git a/src/bun.js/bindings/CommonJSModuleRecord.cpp b/src/bun.js/bindings/CommonJSModuleRecord.cpp index eb7bcb75eb8f5c..b1f60f940e4456 100644 --- a/src/bun.js/bindings/CommonJSModuleRecord.cpp +++ b/src/bun.js/bindings/CommonJSModuleRecord.cpp @@ -30,6 +30,8 @@ */ #include "headers.h" + +#include "JavaScriptCore/Synchronousness.h" #include "JavaScriptCore/JSCast.h" #include #include "root.h" @@ -72,6 +74,7 @@ #include "PathInlines.h" #include "wtf/NakedPtr.h" #include "wtf/URL.h" +#include "wtf/text/StringImpl.h" extern "C" bool Bun__isBunMain(JSC::JSGlobalObject* global, const BunString*); @@ -152,7 +155,6 @@ static bool evaluateCommonJSModuleOnce(JSC::VM& vm, Zig::GlobalObject* globalObj JSValue fnValue = JSC::evaluate(globalObject, code, jsUndefined(), exception); if (UNLIKELY(exception.get() || fnValue.isEmpty())) { - return false; } @@ -171,6 +173,13 @@ static bool evaluateCommonJSModuleOnce(JSC::VM& vm, Zig::GlobalObject* globalObj args.append(Zig::ImportMetaObject::create(globalObject, filename)); } + // Clear the source code as early as possible. + code = {}; + + // Call the CommonJS module wrapper function. + // + // fn(exports, require, module, __filename, __dirname) { /* code */ }(exports, require, module, __filename, __dirname) + // JSC::call(globalObject, fn, callData, moduleObject, args, exception); return exception.get() == nullptr; @@ -352,9 +361,15 @@ JSC_DEFINE_CUSTOM_GETTER(getterParent, (JSC::JSGlobalObject * globalObject, JSC: if (UNLIKELY(!thisObject)) { return JSValue::encode(jsUndefined()); } - auto v = thisObject->m_parent.get(); - if (v) - return JSValue::encode(thisObject->m_parent.get()); + + if (thisObject->m_overridenParent) { + return JSValue::encode(thisObject->m_overridenParent.get()); + } + + if (thisObject->m_parent) { + auto* parent = thisObject->m_parent.get(); + return JSValue::encode(parent); + } // initialize parent by checking if it is the main module. we do this lazily because most people // dont need `module.parent` and creating commonjs module records is done a ton. @@ -363,12 +378,11 @@ JSC_DEFINE_CUSTOM_GETTER(getterParent, (JSC::JSGlobalObject * globalObject, JSC: auto id = idValue->value(globalObject); auto idStr = Bun::toString(id); if (Bun__isBunMain(globalObject, &idStr)) { - thisObject->m_parent.set(globalObject->vm(), thisObject, jsNull()); + thisObject->m_overridenParent.set(globalObject->vm(), thisObject, jsNull()); return JSValue::encode(jsNull()); } } - thisObject->m_parent.set(globalObject->vm(), thisObject, jsUndefined()); return JSValue::encode(jsUndefined()); } @@ -454,7 +468,15 @@ JSC_DEFINE_CUSTOM_SETTER(setterParent, if (!thisObject) return false; - thisObject->m_parent.set(globalObject->vm(), thisObject, JSValue::decode(value)); + JSValue decodedValue = JSValue::decode(value); + + if (auto* parent = jsDynamicCast(decodedValue)) { + thisObject->m_parent = parent; + thisObject->m_overridenParent.clear(); + } else { + thisObject->m_parent = {}; + thisObject->m_overridenParent.set(globalObject->vm(), thisObject, JSValue::decode(value)); + } return true; } @@ -640,7 +662,7 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionCreateCommonJSModule, (JSGlobalObject * globa { RELEASE_ASSERT(callframe->argumentCount() == 4); - auto id = callframe->uncheckedArgument(0).toWTFString(globalObject); + auto id = callframe->uncheckedArgument(0).toString(globalObject); JSValue object = callframe->uncheckedArgument(1); JSValue hasEvaluated = callframe->uncheckedArgument(2); ASSERT(hasEvaluated.isBoolean()); @@ -651,15 +673,14 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionCreateCommonJSModule, (JSGlobalObject * globa JSCommonJSModule* JSCommonJSModule::create( Zig::GlobalObject* globalObject, - const WTF::String& key, + JSC::JSString* requireMapKey, JSValue exportsObject, bool hasEvaluated, JSValue parent) { auto& vm = globalObject->vm(); - JSString* requireMapKey = JSC::jsStringWithCache(vm, key); - - auto index = key.reverseFind(PLATFORM_SEP, key.length()); + auto key = requireMapKey->value(globalObject); + auto index = key->reverseFind(PLATFORM_SEP, key->length()); JSString* dirname; if (index != WTF::notFound) { @@ -679,11 +700,31 @@ JSCommonJSModule* JSCommonJSModule::create( exportsObject, 0); out->hasEvaluated = hasEvaluated; - out->m_parent.set(vm, out, parent); + if (parent && parent.isCell()) { + if (auto* parentModule = jsDynamicCast(parent)) { + out->m_parent = JSC::Weak(parentModule); + } else { + out->m_overridenParent.set(vm, out, parent); + } + } else if (parent) { + out->m_overridenParent.set(vm, out, parent); + } return out; } +JSCommonJSModule* JSCommonJSModule::create( + Zig::GlobalObject* globalObject, + const WTF::String& key, + JSValue exportsObject, + bool hasEvaluated, + JSValue parent) +{ + auto& vm = globalObject->vm(); + auto* requireMapKey = JSC::jsStringWithCache(vm, key); + return JSCommonJSModule::create(globalObject, requireMapKey, exportsObject, hasEvaluated, parent); +} + void JSCommonJSModule::destroy(JSC::JSCell* cell) { static_cast(cell)->JSCommonJSModule::~JSCommonJSModule(); @@ -908,6 +949,7 @@ void JSCommonJSModule::visitChildrenImpl(JSCell* cell, Visitor& visitor) visitor.append(thisObject->m_filename); visitor.append(thisObject->m_dirname); visitor.append(thisObject->m_paths); + visitor.append(thisObject->m_overridenParent); } DEFINE_VISIT_CHILDREN(JSCommonJSModule); @@ -1014,7 +1056,6 @@ bool JSCommonJSModule::evaluate( this->sourceCode = JSC::SourceCode(WTFMove(sourceProvider)); WTF::NakedPtr exception; - evaluateCommonJSModuleOnce(vm, globalObject, this, this->m_dirname.get(), this->m_filename.get(), exception); if (exception.get()) { diff --git a/src/bun.js/bindings/CommonJSModuleRecord.h b/src/bun.js/bindings/CommonJSModuleRecord.h index 8e8459836c8f77..33c9aac62463a8 100644 --- a/src/bun.js/bindings/CommonJSModuleRecord.h +++ b/src/bun.js/bindings/CommonJSModuleRecord.h @@ -1,5 +1,6 @@ #pragma once #include "JavaScriptCore/JSGlobalObject.h" +#include "JavaScriptCore/JSString.h" #include "root.h" #include "headers-handwritten.h" #include "wtf/NakedPtr.h" @@ -35,7 +36,19 @@ class JSCommonJSModule final : public JSC::JSDestructibleObject { mutable JSC::WriteBarrier m_filename; mutable JSC::WriteBarrier m_dirname; mutable JSC::WriteBarrier m_paths; - mutable JSC::WriteBarrier m_parent; + + // Visited by the GC. When the module is assigned a non-JSCommonJSModule + // parent, it is assigned to this field. + // + // module.parent = parent; + // + mutable JSC::WriteBarrier m_overridenParent; + + // Not visited by the GC. + // When the module is assigned a JSCommonJSModule parent, it is assigned to this field. + // This is the normal state. + JSC::Weak m_parent {}; + bool ignoreESModuleAnnotation { false }; JSC::SourceCode sourceCode = JSC::SourceCode(); @@ -70,6 +83,11 @@ class JSCommonJSModule final : public JSC::JSDestructibleObject { const WTF::String& key, JSValue exportsObject, bool hasEvaluated, JSValue parent); + static JSCommonJSModule* create( + Zig::GlobalObject* globalObject, + JSC::JSString* key, + JSValue exportsObject, bool hasEvaluated, JSValue parent); + static JSCommonJSModule* create( Zig::GlobalObject* globalObject, const WTF::String& key, diff --git a/src/bun.js/bindings/ModuleLoader.cpp b/src/bun.js/bindings/ModuleLoader.cpp index beb3e320ee5ee3..4c3b0a1ea2a0a6 100644 --- a/src/bun.js/bindings/ModuleLoader.cpp +++ b/src/bun.js/bindings/ModuleLoader.cpp @@ -42,6 +42,24 @@ using namespace JSC; using namespace Zig; using namespace WebCore; +class ResolvedSourceCodeHolder { +public: + ResolvedSourceCodeHolder(ErrorableResolvedSource* res_) + : res(res_) + { + } + + ~ResolvedSourceCodeHolder() + { + if (res->success && res->result.value.source_code.tag == BunStringTag::WTFStringImpl && res->result.value.needsDeref) { + res->result.value.needsDeref = false; + res->result.value.source_code.impl.wtf->deref(); + } + } + + ErrorableResolvedSource* res; +}; + extern "C" BunLoaderType Bun__getDefaultLoader(JSC::JSGlobalObject*, BunString* specifier); static JSC::JSInternalPromise* rejectedInternalPromise(JSC::JSGlobalObject* globalObject, JSC::JSValue value) @@ -289,13 +307,7 @@ static JSValue handleVirtualModuleResult( auto onLoadResult = handleOnLoadResult(globalObject, virtualModuleResult, specifier, wasModuleMock); JSC::VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - WTF::String sourceCodeStringForDeref; - const auto getSourceCodeStringForDeref = [&]() { - if (res->success && res->result.value.needsDeref && res->result.value.source_code.tag == BunStringTag::WTFStringImpl) { - res->result.value.needsDeref = false; - sourceCodeStringForDeref = String(res->result.value.source_code.impl.wtf); - } - }; + ResolvedSourceCodeHolder sourceCodeHolder(res); const auto reject = [&](JSC::JSValue exception) -> JSValue { if constexpr (allowPromise) { @@ -342,7 +354,6 @@ static JSValue handleVirtualModuleResult( if (!res->success) { return reject(JSValue::decode(reinterpret_cast(res->result.err.ptr))); } - getSourceCodeStringForDeref(); auto provider = Zig::SourceProvider::create(globalObject, res->result.value); return resolve(JSC::JSSourceCode::create(vm, JSC::SourceCode(provider))); @@ -396,13 +407,7 @@ extern "C" void Bun__onFulfillAsyncModule( BunString* specifier, BunString* referrer) { - WTF::String sourceCodeStringForDeref; - const auto getSourceCodeStringForDeref = [&]() { - if (res->result.value.needsDeref && res->result.value.source_code.tag == BunStringTag::WTFStringImpl) { - res->result.value.needsDeref = false; - sourceCodeStringForDeref = String(res->result.value.source_code.impl.wtf); - } - }; + ResolvedSourceCodeHolder sourceCodeHolder(res); auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); JSC::JSInternalPromise* promise = jsCast(JSC::JSValue::decode(encodedPromiseValue)); @@ -414,7 +419,6 @@ extern "C" void Bun__onFulfillAsyncModule( return promise->reject(globalObject, exception); } - getSourceCodeStringForDeref(); auto specifierValue = Bun::toJS(globalObject, *specifier); if (auto entry = globalObject->esmRegistryMap()->get(globalObject, specifierValue)) { @@ -467,13 +471,7 @@ JSValue fetchCommonJSModule( memset(&resValue, 0, sizeof(ErrorableResolvedSource)); ErrorableResolvedSource* res = &resValue; - WTF::String sourceCodeStringForDeref; - const auto getSourceCodeStringForDeref = [&]() { - if (res->success && res->result.value.needsDeref && res->result.value.source_code.tag == BunStringTag::WTFStringImpl) { - res->result.value.needsDeref = false; - sourceCodeStringForDeref = String(res->result.value.source_code.impl.wtf); - } - }; + ResolvedSourceCodeHolder sourceCodeHolder(res); auto& builtinNames = WebCore::clientData(vm)->builtinNames(); bool wasModuleMock = false; @@ -600,8 +598,6 @@ JSValue fetchCommonJSModule( } Bun__transpileFile(bunVM, globalObject, specifier, referrer, typeAttribute, res, false); - getSourceCodeStringForDeref(); - if (res->success && res->result.value.isCommonJSModule) { target->evaluate(globalObject, specifier->toWTFString(BunString::ZeroCopy), res->result.value); RETURN_IF_EXCEPTION(scope, {}); @@ -669,6 +665,7 @@ static JSValue fetchESMSourceCode( void* bunVM = globalObject->bunVM(); auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); + ResolvedSourceCodeHolder sourceCodeHolder(res); const auto reject = [&](JSC::JSValue exception) -> JSValue { if constexpr (allowPromise) { @@ -755,23 +752,13 @@ static JSValue fetchESMSourceCode( } } - WTF::String sourceCodeStringForDeref; - const auto getSourceCodeStringForDeref = [&]() { - if (res->success && res->result.value.needsDeref && res->result.value.source_code.tag == BunStringTag::WTFStringImpl) { - res->result.value.needsDeref = false; - sourceCodeStringForDeref = String(res->result.value.source_code.impl.wtf); - } - }; - if constexpr (allowPromise) { auto* pendingCtx = Bun__transpileFile(bunVM, globalObject, specifier, referrer, typeAttribute, res, true); - getSourceCodeStringForDeref(); if (pendingCtx) { return pendingCtx; } } else { Bun__transpileFile(bunVM, globalObject, specifier, referrer, typeAttribute, res, false); - getSourceCodeStringForDeref(); } if (res->success && res->result.value.isCommonJSModule) { diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index c783385a403206..2d0638c5bf14ee 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -978,9 +978,21 @@ pub const VirtualMachine = struct { }; } - pub fn loadExtraEnv(this: *VirtualMachine) void { + fn ensureSourceCodePrinter(this: *VirtualMachine) void { + if (source_code_printer == null) { + const allocator = if (bun.heap_breakdown.enabled) bun.heap_breakdown.namedAllocator("SourceCode") else this.allocator; + const writer = try js_printer.BufferWriter.init(allocator); + source_code_printer = allocator.create(js_printer.BufferPrinter) catch unreachable; + source_code_printer.?.* = js_printer.BufferPrinter.init(writer); + source_code_printer.?.ctx.append_null_byte = false; + } + } + + pub fn loadExtraEnvAndSourceCodePrinter(this: *VirtualMachine) void { var map = this.bundler.env.map; + ensureSourceCodePrinter(this); + if (map.get("BUN_SHOW_BUN_STACKFRAMES") != null) { this.hide_bun_stackframes = false; } @@ -1439,6 +1451,7 @@ pub const VirtualMachine = struct { this.macro_event_loop.global = this.global; this.macro_event_loop.virtual_machine = this; this.macro_event_loop.concurrent_tasks = .{}; + ensureSourceCodePrinter(this); } this.bundler.options.target = .bun_macro; @@ -1563,13 +1576,6 @@ pub const VirtualMachine = struct { vm.regular_event_loop.virtual_machine = vm; vm.jsc = vm.global.vm(); - if (source_code_printer == null) { - const writer = try js_printer.BufferWriter.init(allocator); - source_code_printer = allocator.create(js_printer.BufferPrinter) catch unreachable; - source_code_printer.?.* = js_printer.BufferPrinter.init(writer); - source_code_printer.?.ctx.append_null_byte = false; - } - vm.configureDebugger(opts.debugger); vm.body_value_hive_allocator = BodyValueHiveAllocator.init(bun.typedAllocator(JSC.WebCore.Body.Value)); @@ -1683,13 +1689,6 @@ pub const VirtualMachine = struct { if (opts.smol) is_smol_mode = opts.smol; - if (source_code_printer == null) { - const writer = try js_printer.BufferWriter.init(allocator); - source_code_printer = allocator.create(js_printer.BufferPrinter) catch unreachable; - source_code_printer.?.* = js_printer.BufferPrinter.init(writer); - source_code_printer.?.ctx.append_null_byte = false; - } - vm.configureDebugger(opts.debugger); vm.body_value_hive_allocator = BodyValueHiveAllocator.init(bun.typedAllocator(JSC.WebCore.Body.Value)); @@ -1828,12 +1827,6 @@ pub const VirtualMachine = struct { vm.regular_event_loop.virtual_machine = vm; vm.jsc = vm.global.vm(); vm.bundler.setAllocator(allocator); - if (source_code_printer == null) { - const writer = try js_printer.BufferWriter.init(allocator); - source_code_printer = allocator.create(js_printer.BufferPrinter) catch unreachable; - source_code_printer.?.* = js_printer.BufferPrinter.init(writer); - source_code_printer.?.ctx.append_null_byte = false; - } vm.body_value_hive_allocator = BodyValueHiveAllocator.init(bun.typedAllocator(JSC.WebCore.Body.Value)); return vm; @@ -2433,6 +2426,10 @@ pub const VirtualMachine = struct { // TODO: pub fn deinit(this: *VirtualMachine) void { + if (source_code_printer) |print| { + print.getMutableBuffer().deinit(); + print.ctx.written = &.{}; + } this.source_mappings.deinit(); if (this.rare_data) |rare_data| { rare_data.deinit(); diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index b7970046a7f63a..2c667f3c8d4aff 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -1905,10 +1905,10 @@ pub const ModuleLoader = struct { var printer = source_code_printer.*; printer.ctx.reset(); - + defer source_code_printer.* = printer; _ = brk: { var mapper = jsc_vm.sourceMapHandler(&printer); - defer source_code_printer.* = printer; + break :brk try jsc_vm.bundler.printWithSourceMap( parse_result, @TypeOf(&printer), @@ -1961,7 +1961,6 @@ pub const ModuleLoader = struct { if (written.len > 1024 * 1024 * 2 or jsc_vm.smol) { printer.ctx.buffer.deinit(); - source_code_printer.* = printer; } break :brk result; diff --git a/src/bun.js/web_worker.zig b/src/bun.js/web_worker.zig index f07dd0edbddc36..e54c2852272844 100644 --- a/src/bun.js/web_worker.zig +++ b/src/bun.js/web_worker.zig @@ -217,7 +217,7 @@ pub const WebWorker = struct { vm.bundler.env = loader; - vm.loadExtraEnv(); + vm.loadExtraEnvAndSourceCodePrinter(); vm.is_main_thread = false; JSC.VirtualMachine.is_main_thread_vm = false; vm.onUnhandledRejection = onUnhandledRejection; diff --git a/src/bun_js.zig b/src/bun_js.zig index 842a8a445fa7c1..8947e3721880fc 100644 --- a/src/bun_js.zig +++ b/src/bun_js.zig @@ -114,7 +114,7 @@ pub const Run = struct { AsyncHTTP.loadEnv(vm.allocator, vm.log, b.env); - vm.loadExtraEnv(); + vm.loadExtraEnvAndSourceCodePrinter(); vm.is_main_thread = true; JSC.VirtualMachine.is_main_thread_vm = true; @@ -257,7 +257,7 @@ pub const Run = struct { AsyncHTTP.loadEnv(vm.allocator, vm.log, b.env); - vm.loadExtraEnv(); + vm.loadExtraEnvAndSourceCodePrinter(); vm.is_main_thread = true; JSC.VirtualMachine.is_main_thread_vm = true; diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 39a3558f415a35..858b357d5d58b1 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -816,7 +816,7 @@ pub const TestCommand = struct { try vm.bundler.configureDefines(); - vm.loadExtraEnv(); + vm.loadExtraEnvAndSourceCodePrinter(); vm.is_main_thread = true; JSC.VirtualMachine.is_main_thread_vm = true; diff --git a/src/heap_breakdown.zig b/src/heap_breakdown.zig index d77b12917adefa..692018d1f1dd33 100644 --- a/src/heap_breakdown.zig +++ b/src/heap_breakdown.zig @@ -11,14 +11,14 @@ fn heapLabel(comptime T: type) [:0]const u8 { T.heap_label else bun.meta.typeBaseName(@typeName(T)); - return "Bun__" ++ base_name; + return base_name; } pub fn allocator(comptime T: type) std.mem.Allocator { return namedAllocator(comptime heapLabel(T)); } pub fn namedAllocator(comptime name: [:0]const u8) std.mem.Allocator { - return getZone(name).allocator(); + return getZone("Bun__" ++ name).allocator(); } pub fn getZoneT(comptime T: type) *Zone { diff --git a/src/js_printer.zig b/src/js_printer.zig index 76fb306a622b3f..47f431d5c860d8 100644 --- a/src/js_printer.zig +++ b/src/js_printer.zig @@ -5924,6 +5924,7 @@ pub const BufferWriter = struct { pub fn reset(ctx: *BufferWriter) void { ctx.buffer.reset(); ctx.approximate_newline_count = 0; + ctx.written = &.{}; } pub fn writtenWithoutTrailingZero(ctx: *const BufferWriter) []u8 { diff --git a/test/cli/run/require-cache.test.ts b/test/cli/run/require-cache.test.ts index 1d79577c7bd719..b897e4983217aa 100644 --- a/test/cli/run/require-cache.test.ts +++ b/test/cli/run/require-cache.test.ts @@ -1,5 +1,5 @@ import { test, expect, describe } from "bun:test"; -import { bunEnv, bunExe, isWindows } from "harness"; +import { bunEnv, bunExe, isWindows, tempDirWithFiles } from "harness"; import { join } from "path"; // This also tests __dirname and __filename @@ -26,6 +26,227 @@ test("require.cache does not include unevaluated modules", () => { expect(exitCode).toBe(0); }); +describe("files transpiled and loaded don't leak the output source code", () => { + test("via require() with a lot of long export names", () => { + let text = ""; + for (let i = 0; i < 10000; i++) { + text += `exports.superDuperExtraCrazyLongNameWowSuchNameLongYouveNeverSeenANameThisLongForACommonJSModuleExport${i} = 1;\n`; + } + + console.log("Text length:", text.length); + + const dir = tempDirWithFiles("require-cache-bug-leak-1", { + "index.js": text, + "require-cache-bug-leak-fixture.js": ` + const path = require.resolve("./index.js"); + const gc = global.gc || globalThis?.Bun?.gc || (() => {}); + function bust() { + const mod = require.cache[path]; + if (mod) { + mod.parent = null; + mod.children = []; + delete require.cache[path]; + } + } + + for (let i = 0; i < 50; i++) { + require(path); + bust(); + } + gc(true); + const baseline = process.memoryUsage.rss(); + for (let i = 0; i < 500; i++) { + require(path); + bust(path); + console.log("RSS", (process.memoryUsage.rss() / 1024 / 1024) | 0, "MB"); + } + gc(true); + const rss = process.memoryUsage.rss(); + const diff = rss - baseline; + console.log("RSS diff", (diff / 1024 / 1024) | 0, "MB"); + console.log("RSS", (diff / 1024 / 1024) | 0, "MB"); + if (diff > 100 * 1024 * 1024) { + // Bun v1.1.21 reported 844 MB here on macoS arm64. + throw new Error("Memory leak detected"); + } + + exports.abc = 123; + `, + }); + console.log({ dir }); + const { exitCode, resourceUsage } = Bun.spawnSync({ + cmd: [bunExe(), "run", "--smol", join(dir, "require-cache-bug-leak-fixture.js")], + env: bunEnv, + stdio: ["inherit", "inherit", "inherit"], + }); + + console.log(resourceUsage); + expect(exitCode).toBe(0); + }, 60000); + + test("via await import() with a lot of function calls", () => { + let text = "function i() { return 1; }\n"; + for (let i = 0; i < 20000; i++) { + text += `i();\n`; + } + text += "exports.forceCommonJS = true;\n"; + + console.log("Text length:", text.length); + + const dir = tempDirWithFiles("require-cache-bug-leak-3", { + "index.js": text, + "require-cache-bug-leak-fixture.js": ` + const path = require.resolve("./index.js"); + const gc = global.gc || globalThis?.Bun?.gc || (() => {}); + function bust() { + delete require.cache[path]; + } + + for (let i = 0; i < 100; i++) { + await import(path); + bust(); + } + gc(true); + const baseline = process.memoryUsage.rss(); + for (let i = 0; i < 400; i++) { + await import(path); + bust(path); + console.log("RSS", (process.memoryUsage.rss() / 1024 / 1024) | 0, "MB"); + } + gc(true); + const rss = process.memoryUsage.rss(); + const diff = rss - baseline; + console.log("RSS diff", (diff / 1024 / 1024) | 0, "MB"); + console.log("RSS", (diff / 1024 / 1024) | 0, "MB"); + if (diff > 64 * 1024 * 1024) { + // Bun v1.1.22 reported 1 MB here on macoS arm64. + // Bun v1.1.21 reported 257 MB here on macoS arm64. + throw new Error("Memory leak detected"); + } + + export default 123; + `, + }); + const { exitCode, resourceUsage } = Bun.spawnSync({ + cmd: [bunExe(), "run", "--smol", join(dir, "require-cache-bug-leak-fixture.js")], + env: bunEnv, + stdio: ["inherit", "inherit", "inherit"], + }); + + console.log(resourceUsage); + expect(exitCode).toBe(0); + }, 60000); // takes 4s on an M1 in release build + + test("via import() with a lot of long export names", () => { + let text = ""; + for (let i = 0; i < 10000; i++) { + text += `export const superDuperExtraCrazyLongNameWowSuchNameLongYouveNeverSeenANameThisLongForACommonJSModuleExport${i} = 1;\n`; + } + + const dir = tempDirWithFiles("require-cache-bug-leak-4", { + "index.js": text, + "require-cache-bug-leak-fixture.js": ` + const path = require.resolve("./index.js"); + const gc = global.gc || globalThis?.Bun?.gc || (() => {}); + function bust() { + delete require.cache[path]; + } + + for (let i = 0; i < 50; i++) { + await import(path); + bust(); + } + gc(true); + const baseline = process.memoryUsage.rss(); + for (let i = 0; i < 250; i++) { + await import(path); + bust(path); + console.log("RSS", (process.memoryUsage.rss() / 1024 / 1024) | 0, "MB"); + } + gc(true); + const rss = process.memoryUsage.rss(); + const diff = rss - baseline; + console.log("RSS diff", (diff / 1024 / 1024) | 0, "MB"); + console.log("RSS", (diff / 1024 / 1024) | 0, "MB"); + if (diff > 64 * 1024 * 1024) { + // Bun v1.1.21 reported 423 MB here on macoS arm64. + // Bun v1.1.22 reported 4 MB here on macoS arm64. + throw new Error("Memory leak detected"); + } + + export default 124; + `, + }); + console.log({ dir }); + const { exitCode, resourceUsage } = Bun.spawnSync({ + cmd: [bunExe(), "run", "--smol", join(dir, "require-cache-bug-leak-fixture.js")], + env: bunEnv, + stdio: ["inherit", "inherit", "inherit"], + }); + + console.log(resourceUsage); + expect(exitCode).toBe(0); + }, 60000); + + test("via require() with a lot of function calls", () => { + let text = "function i() { return 1; }\n"; + for (let i = 0; i < 20000; i++) { + text += `i();\n`; + } + text += "exports.forceCommonJS = true;\n"; + + console.log("Text length:", text.length); + + const dir = tempDirWithFiles("require-cache-bug-leak-2", { + "index.js": text, + "require-cache-bug-leak-fixture.js": ` + const path = require.resolve("./index.js"); + const gc = global.gc || globalThis?.Bun?.gc || (() => {}); + function bust() { + const mod = require.cache[path]; + if (mod) { + mod.parent = null; + mod.children = []; + delete require.cache[path]; + } + } + + for (let i = 0; i < 100; i++) { + require(path); + bust(); + } + gc(true); + const baseline = process.memoryUsage.rss(); + for (let i = 0; i < 400; i++) { + require(path); + bust(path); + console.log("RSS", (process.memoryUsage.rss() / 1024 / 1024) | 0, "MB"); + } + gc(true); + const rss = process.memoryUsage.rss(); + const diff = rss - baseline; + console.log("RSS diff", (diff / 1024 / 1024) | 0, "MB"); + console.log("RSS", (diff / 1024 / 1024) | 0, "MB"); + if (diff > 64 * 1024 * 1024) { + // Bun v1.1.22 reported 4 MB here on macoS arm64. + // Bun v1.1.21 reported 248 MB here on macoS arm64. + throw new Error("Memory leak detected"); + } + + exports.abc = 123; + `, + }); + const { exitCode, resourceUsage } = Bun.spawnSync({ + cmd: [bunExe(), "run", "--smol", join(dir, "require-cache-bug-leak-fixture.js")], + env: bunEnv, + stdio: ["inherit", "inherit", "inherit"], + }); + + console.log(resourceUsage); + expect(exitCode).toBe(0); + }, 60000); // takes 4s on an M1 in release build +}); + describe("files transpiled and loaded don't leak the AST", () => { test("via require()", () => { const { stdout, exitCode } = Bun.spawnSync({ diff --git a/test/harness.ts b/test/harness.ts index 6710381e756ead..4dd30f8483a848 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -48,6 +48,12 @@ for (let key in bunEnv) { delete bunEnv.NODE_ENV; +if (isDebug) { + // This makes debug build memory leak tests more reliable. + // The code for dumping out the debug build transpiled source code has leaks. + bunEnv.BUN_DEBUG_NO_DUMP = "1"; +} + export function bunExe() { if (isWindows) return process.execPath.replaceAll("\\", "/"); return process.execPath; diff --git a/test/regression/issue/03830.test.ts b/test/regression/issue/03830.test.ts index 8da5ffdcfd0c67..d89a64ce16b35f 100644 --- a/test/regression/issue/03830.test.ts +++ b/test/regression/issue/03830.test.ts @@ -24,6 +24,6 @@ it("macros should not lead to seg faults under any given input", async () => { stderr: "pipe", }); - expect(stderr.toString().trim().replaceAll(testDir, "[dir]")).toMatchSnapshot(); + expect(stderr.toString().trim().replaceAll(testDir, "[dir]").replaceAll("\\", "/")).toMatchSnapshot(); expect(exitCode).toBe(1); }); diff --git a/test/regression/issue/__snapshots__/03830.test.ts.snap b/test/regression/issue/__snapshots__/03830.test.ts.snap index 71a196a2faf754..ebaa50fa2efaf6 100644 --- a/test/regression/issue/__snapshots__/03830.test.ts.snap +++ b/test/regression/issue/__snapshots__/03830.test.ts.snap @@ -27,3 +27,17 @@ exports[`macros should not lead to seg faults under any given input 1`] = ` error: "Cannot convert argument type to JS" error in macro at [dir]/index.ts:2:1" `; + +exports[`macros should not lead to seg faults under any given input 1`] = ` +"2 | fn(\`©${''}\`); + ^ +error: "Cannot convert argument type to JS" error in macro + at [dir]/index.ts:2:1" +`; + +exports[`macros should not lead to seg faults under any given input 1`] = ` +"2 | fn(\`©${''}\`); + ^ +error: "Cannot convert argument type to JS" error in macro + at [dir]/index.ts:2:1" +`;