bun icon indicating copy to clipboard operation
bun copied to clipboard

Memory leak running Next.js standalone bundle

Open lithdew opened this issue 10 months ago • 8 comments

What version of Bun is running?

1.1.1+ca1dbb4eb

What platform is your computer?

Darwin 23.4.0 arm64 arm

What steps can reproduce the bug?

  1. Create a new Next.js app.
$ bun create next-app --ts --tailwind --app --use-bun --eslint --no-src-dir --import-alias "@/*" test`
  1. Make modifications to the following files.

next.config.mjs

/** @type {import('next').NextConfig} */
const nextConfig = {output: "standalone"};

export default nextConfig;

app/layout.tsx

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export const dynamic = "force-dynamic";

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  );
}
  1. Build Next.js standalone bundle.
$ bun run build
  1. Run Next.js standalone bundle.
$ bun run .next/standalone/server.js
  1. Spam your Next.js app with web requests.
$ go install -mod=mod github.com/codesenberg/bombardier
$ bombardier -c 200 -d 1000h http://localhost:3000
  1. Notice memory usage go up indefinitely.

What is the expected behavior?

No memory leak. Next.js on Node generally consumes roughly 400MB of memory. Even with assumptions on increased memory usage, I would assume Next.js on Bun should consume no more than 1GB of memory.

What do you see instead?

image

Additional information

Happy to help with debugging/fixing this memory leak - would like to know what tools maintainers are using for debugging these memory leaks.

lithdew avatar Apr 05 '24 10:04 lithdew

Getting the same with latest bun version. Was not facing in bun 1.0.35

mmarifat avatar Apr 05 '24 11:04 mmarifat

Getting the same with latest bun version. Was not facing in bun 1.0.35

Tested it out with bun 1.0.35+940448d6b and got the same issue.

lithdew avatar Apr 05 '24 12:04 lithdew

I unfortunately have some deadlines to attend to so I can't look further into this for a bit, but it looks like the main issue has to do with a blowup of objects/Function's/AbortSignal's being kept in memory and not being garbage collected.

Here are the initial object counts dumped via. require("bun:jsc").heapStats():

{
  "objects": {
    "AbortController": 530,
    "AbortSignal": 7335,
    "Arguments": 15045,
    "Array Iterator": 2,
    "Array": 234404,
    "ArrayBuffer": 7709,
    "ArrayIterator": 624,
    "AsyncContextFrame": 2192,
    "AsyncFromSyncIterator": 2,
    "AsyncFunction": 38181,
    "AsyncGenerator": 202,
    "AsyncGeneratorFunction": 4,
    "AsyncIterator": 2,
    "BigInt": 5,
    "BigInt64ArrayPrototype": 1,
    "BigUint64ArrayPrototype": 1,
    "Blob": 1,
    "Boolean": 42,
    "BroadcastChannel": 2,
    "Buffer": 1,
    "BufferList": 14268,
    "Bun": 1,
    "ByteLengthQueuingStrategy": 2,
    "CallSite": 186,
    "Callee": 4,
    "CountQueuingStrategy": 2,
    "Crypto": 2,
    "CryptoHasher": 1,
    "CryptoKey": 2,
    "CustomGetterSetter": 14371,
    "DOMAttributeGetterSetter": 151,
    "Dirent": 1,
    "Error": 17218,
    "EvalExecutable": 8,
    "Event": 2,
    "EventEmitter": 14270,
    "EventTarget": 2,
    "Exception": 376,
    "File": 1,
    "FinalizationRegistry": 3,
    "Float32ArrayPrototype": 1,
    "Float64ArrayPrototype": 1,
    "FormData": 2,
    "Function": 1403538,
    "FunctionCodeBlock": 2636,
    "FunctionExecutable": 13632,
    "FunctionRareData": 66861,
    "Generator": 7243,
    "GeneratorFunction": 37,
    "GetterSetter": 31332,
    "GlobalObject": 2,
    "HTTPServer": 2,
    "HashMapBucket": 59188,
    "Headers": 7263,
    "Immutable Butterfly": 2628,
    "ImportMeta": 2,
    "Int16ArrayPrototype": 1,
    "Int32ArrayPrototype": 1,
    "Int8ArrayPrototype": 1,
    "InternalFieldTuple": 2,
    "InternalModuleRegistry": 1,
    "InternalPromise": 17,
    "InternalPromisePrototype": 2,
    "Intl": 2,
    "Intl.DurationFormat": 2,
    "Intl.ListFormat": 2,
    "Iterator": 2,
    "JSAsyncGeneratorFunction": 5,
    "JSGlobalLexicalEnvironment": 2,
    "JSGlobalProxy": 3,
    "JSLexicalEnvironment": 438858,
    "JSModuleEnvironment": 9,
    "JSON": 1,
    "JSPropertyNameEnumerator": 81,
    "JSSourceCode": 8,
    "Map Iterator": 2,
    "Map": 22069,
    "Math": 1,
    "MessageChannel": 2,
    "MessagePort": 2,
    "Module": 302,
    "ModuleLoader": 2,
    "ModuleNamespaceObject": 7,
    "ModuleProgramExecutable": 4,
    "ModulePrototype": 1,
    "ModuleRecord": 8,
    "NativeExecutable": 883,
    "NextTickQueue": 1,
    "NodeJSFS": 2,
    "Number": 2,
    "Object": 665960,
    "Performance": 3,
    "PerformanceEntry": 2,
    "PerformanceMark": 2,
    "PerformanceMeasure": 2,
    "PerformanceObserver": 2,
    "PerformanceObserverEntryList": 2,
    "Process": 1,
    "ProcessBindingConstants": 1,
    "ProgramExecutable": 243,
    "Promise": 193255,
    "PropertyTable": 1092,
    "Prototype": 1,
    "Proxy": 1,
    "ProxyObject": 15,
    "ReadableByteStreamController": 1041,
    "ReadableHTTPResponseSinkController": 6934,
    "ReadableState": 14268,
    "ReadableStream": 10568,
    "ReadableStreamBYOBReader": 2,
    "ReadableStreamBYOBRequest": 2,
    "ReadableStreamDefaultController": 9329,
    "ReadableStreamDefaultReader": 9539,
    "Reflect": 1,
    "RegExp String Iterator": 2,
    "RegExp": 1859,
    "Request": 7134,
    "ResolveMessage": 1,
    "Response": 6934,
    "ScopedArgumentsTable": 12,
    "Script": 2,
    "Set Iterator": 42,
    "Set": 16363,
    "ShadowRealm": 2,
    "SparseArrayValueMap": 77,
    "Stats": 1,
    "String Iterator": 2,
    "String": 2,
    "StringDecoder": 2,
    "Structure": 16853,
    "StructureChain": 591,
    "StructureRareData": 2398,
    "SubtleCrypto": 3,
    "Symbol": 2,
    "SymbolTable": 3003,
    "TextDecoder": 1017,
    "TextEncoder": 631,
    "Timeout": 656,
    "TransformStream": 8777,
    "TransformStreamDefaultController": 8777,
    "URL": 2,
    "URLSearchParams": 3,
    "Uint16ArrayPrototype": 1,
    "Uint32ArrayPrototype": 1,
    "Uint8Array": 28546,
    "Uint8ArrayPrototype": 1,
    "Uint8ClampedArrayPrototype": 1,
    "UnlinkedEvalCodeBlock": 8,
    "UnlinkedFunctionCodeBlock": 2363,
    "UnlinkedFunctionExecutable": 14175,
    "UnlinkedModuleProgramCodeBlock": 4,
    "UnlinkedProgramCodeBlock": 263,
    "WeakMap": 414,
    "WeakRef": 16,
    "WeakSet": 23,
    "WebAssembly": 2,
    "WebAssembly.Instance": 2,
    "WebAssembly.Memory": 2,
    "WebAssembly.Module": 2,
    "WebAssembly.Table": 2,
    "WebAssemblyFunction": 42,
    "WebAssemblyModuleRecord": 1,
    "WebAssemblyWrapperFunction": 7,
    "WebSocket": 2,
    "Worker": 2,
    "WritableStream": 8839,
    "WritableStreamDefaultController": 21688,
    "WritableStreamDefaultWriter": 8732,
    "console": 1,
    "require": 1,
    "resolve": 1,
    "string": 251547,
    "symbol": 354
  },
  "protected": {
    "AsyncContextFrame": 487,
    "Function": 7030,
    "GlobalObject": 1,
    "HTTPServer": 1,
    "RegExp": 32,
    "UnlinkedFunctionExecutable": 5,
    "UnlinkedModuleProgramCodeBlock": 4,
    "UnlinkedProgramCodeBlock": 263
  }
}

After 3 minutes of spamming requests via. 200 concurrent connections using bombardier:

 {
  "objects": {
    "AbortController": 139,
    "AbortSignal": 47840,
    "Arguments": 95941,
    "Array Iterator": 2,
    "Array": 879894,
    "ArrayBuffer": 47781,
    "ArrayIterator": 61,
    "AsyncContextFrame": 260,
    "AsyncFromSyncIterator": 2,
    "AsyncFunction": 239558,
    "AsyncGenerator": 63,
    "AsyncGeneratorFunction": 4,
    "AsyncIterator": 2,
    "BigInt": 5,
    "BigInt64ArrayPrototype": 1,
    "BigUint64ArrayPrototype": 1,
    "Blob": 1,
    "Boolean": 2,
    "BroadcastChannel": 2,
    "Buffer": 1,
    "BufferList": 95602,
    "Bun": 1,
    "ByteLengthQueuingStrategy": 2,
    "CallSite": 186,
    "Callee": 4,
    "CountQueuingStrategy": 2,
    "Crypto": 2,
    "CryptoHasher": 1,
    "CryptoKey": 2,
    "CustomGetterSetter": 95705,
    "DOMAttributeGetterSetter": 151,
    "Dirent": 1,
    "Error": 97984,
    "EvalExecutable": 8,
    "Event": 2,
    "EventEmitter": 95604,
    "EventTarget": 2,
    "Exception": 61,
    "File": 1,
    "FinalizationRegistry": 3,
    "Float32ArrayPrototype": 1,
    "Float64ArrayPrototype": 1,
    "FormData": 2,
    "Function": 6346591,
    "FunctionCodeBlock": 2560,
    "FunctionExecutable": 13632,
    "FunctionRareData": 290854,
    "Generator": 1450,
    "GeneratorFunction": 37,
    "GetterSetter": 193872,
    "GlobalObject": 2,
    "HTTPServer": 2,
    "HashMapBucket": 198171,
    "Headers": 47863,
    "Immutable Butterfly": 451,
    "ImportMeta": 2,
    "Int16ArrayPrototype": 1,
    "Int32ArrayPrototype": 1,
    "Int8ArrayPrototype": 1,
    "InternalFieldTuple": 2,
    "InternalModuleRegistry": 1,
    "InternalPromise": 17,
    "InternalPromisePrototype": 2,
    "Intl": 2,
    "Intl.DurationFormat": 2,
    "Intl.ListFormat": 2,
    "Iterator": 2,
    "JSAsyncGeneratorFunction": 5,
    "JSGlobalLexicalEnvironment": 2,
    "JSGlobalProxy": 3,
    "JSLexicalEnvironment": 2036409,
    "JSModuleEnvironment": 9,
    "JSON": 1,
    "JSPropertyNameEnumerator": 74,
    "JSSourceCode": 8,
    "Map Iterator": 2,
    "Map": 97652,
    "Math": 1,
    "MessageChannel": 2,
    "MessagePort": 2,
    "Module": 302,
    "ModuleLoader": 2,
    "ModuleNamespaceObject": 7,
    "ModuleProgramExecutable": 4,
    "ModulePrototype": 1,
    "ModuleRecord": 8,
    "NativeExecutable": 885,
    "NextTickQueue": 1,
    "NodeJSFS": 2,
    "Number": 2,
    "Object": 2866775,
    "Performance": 3,
    "PerformanceEntry": 2,
    "PerformanceMark": 2,
    "PerformanceMeasure": 2,
    "PerformanceObserver": 2,
    "PerformanceObserverEntryList": 2,
    "Process": 1,
    "ProcessBindingConstants": 1,
    "ProgramExecutable": 243,
    "Promise": 783943,
    "PropertyTable": 999,
    "Prototype": 1,
    "Proxy": 1,
    "ProxyObject": 13,
    "ReadableByteStreamController": 277,
    "ReadableHTTPResponseSinkController": 47740,
    "ReadableState": 95602,
    "ReadableStream": 48534,
    "ReadableStreamBYOBReader": 2,
    "ReadableStreamBYOBRequest": 2,
    "ReadableStreamDefaultController": 48221,
    "ReadableStreamDefaultReader": 48297,
    "Reflect": 1,
    "RegExp String Iterator": 2,
    "RegExp": 606,
    "Request": 47801,
    "ResolveMessage": 1,
    "Response": 47740,
    "ScopedArgumentsTable": 12,
    "Script": 2,
    "Set Iterator": 2,
    "Set": 49816,
    "ShadowRealm": 2,
    "SparseArrayValueMap": 77,
    "Stats": 1,
    "String Iterator": 2,
    "String": 2,
    "StringDecoder": 2,
    "Structure": 16720,
    "StructureChain": 582,
    "StructureRareData": 2289,
    "SubtleCrypto": 3,
    "Symbol": 2,
    "SymbolTable": 2471,
    "TextDecoder": 237,
    "TextEncoder": 106,
    "Timeout": 62,
    "TransformStream": 48099,
    "TransformStreamDefaultController": 48099,
    "URL": 2,
    "URLSearchParams": 2,
    "Uint16ArrayPrototype": 1,
    "Uint32ArrayPrototype": 1,
    "Uint8Array": 49592,
    "Uint8ArrayPrototype": 1,
    "Uint8ClampedArrayPrototype": 1,
    "UnlinkedEvalCodeBlock": 8,
    "UnlinkedFunctionCodeBlock": 1300,
    "UnlinkedFunctionExecutable": 13775,
    "UnlinkedModuleProgramCodeBlock": 4,
    "UnlinkedProgramCodeBlock": 263,
    "WeakMap": 184,
    "WeakRef": 16,
    "WeakSet": 23,
    "WebAssembly": 2,
    "WebAssembly.Instance": 2,
    "WebAssembly.Memory": 2,
    "WebAssembly.Module": 2,
    "WebAssembly.Table": 2,
    "WebAssemblyFunction": 42,
    "WebAssemblyModuleRecord": 1,
    "WebAssemblyWrapperFunction": 7,
    "WebSocket": 2,
    "Worker": 2,
    "WritableStream": 48137,
    "WritableStreamDefaultController": 96276,
    "WritableStreamDefaultWriter": 48099,
    "console": 1,
    "require": 1,
    "resolve": 1,
    "string": 1122478,
    "symbol": 354
  },
  "protected": {
    "AsyncContextFrame": 61,
    "Function": 47764,
    "GlobalObject": 1,
    "HTTPServer": 1,
    "RegExp": 32,
    "UnlinkedFunctionExecutable": 5,
    "UnlinkedModuleProgramCodeBlock": 4,
    "UnlinkedProgramCodeBlock": 263
  }
}

Here is the custom Next.js server.js:

const signalPrototype = Object.getOwnPropertyDescriptor(
  AbortController.prototype,
  "signal"
);

Object.defineProperty(AbortController.prototype, "signal", {
  ...signalPrototype,
  get() {
    console.trace("AbortController.signal getter called");
    return signalPrototype.get.call(this);
  },
});

const { createServer } = require("http");
const { parse } = require("url");
const next = require("next");

const dev = process.env.NODE_ENV !== "production";
const hostname = "localhost";
const port = 3000;

const app = next({ dev, hostname, port });
const handle = app.getRequestHandler();

import { heapStats } from "bun:jsc";

export function dumpStats() {
  const stats = heapStats();
  const { objectTypeCounts, protectedObjectTypeCounts } = stats;
  Bun.write(
    `${new Date().toISOString()}`,
    JSON.stringify(
      {
        objects: Object.fromEntries(Object.entries(objectTypeCounts).sort()),
        protected: Object.fromEntries(
          Object.entries(protectedObjectTypeCounts).sort()
        ),
      },
      null,
      2
    )
  );
}

setInterval(() => {
  dumpStats();
}, 10_000);

let count = 0;

app.prepare().then(() => {
  createServer(async (req, res) => {
    try {
      // Be sure to pass `true` as the second argument to `url.parse`.
      // This tells it to parse the query portion of the URL.
      const parsedUrl = parse(req.url, true);
      const { pathname, query } = parsedUrl;

      await handle(req, res, parsedUrl);
    } catch (err) {
      console.error("Error occurred handling", req.url, err);
      res.statusCode = 500;
      res.end("internal server error");
    } finally {
      if (count++ % 100 === 0) {
        Bun.gc(true);
      }
    }
  })
    .once("error", (err) => {
      console.error(err);
      process.exit(1);
    })
    .listen(port, () => {
      console.log(`> Ready on http://${hostname}:${port}`);
    });
});

A monkey patch on AbortController.signal is included to dump stack traces where it is used in Next.js' runtime source code in case issues to do with garbage collection of AbortSignal's might be the issue.

lithdew avatar Apr 07 '24 23:04 lithdew

To further expand on findings from the comment above, the following places were checked:

  • Buffer pooling in HTTPServerWritable is not the problem. The capacity and length of buffers are valid, and buffers are properly freed/disposed of.
  • A suspicion was on a potential memory leak in timers (setInterval/setTimeout), or of an overflow in timer ID's in src.bun.js.api.BunObject.Timer. I noticed an unused boolean warned: boolean = false - but apart from that, timers were properly set up/cleaned up.
  • Whether or not timers or AbortSIgnal's are garbage collected properly has not yet been checked yet.

Debugging was done with the Instruments leaks profiler, the CPU profiler, Bun's WebKit debugger, and lldb.

lithdew avatar Apr 07 '24 23:04 lithdew

I got some time today to continue investigating this. I extracted the core parts of the Next.js server runtime that only deals with WritableStream and AbortController and reproduced the memory leak.

As a sanity check, the following code does not contain a memory leak:

import { createServer } from "http";
import { Readable } from "stream";

const hostname = "localhost";
const port = 3000;

createServer(async (req, res) => {
  try {
    const { errored, destroyed } = res;
    if (errored || destroyed) return;

    res.statusCode = 200;
    res.statusMessage = "OK";

    // send-payload.js
    if (req.method === "HEAD") {
      res.end(null);
      return;
    }

    const stream = new ReadableStream<Uint8Array>({
      start(controller) {
        controller.enqueue(Buffer.from("Hello world"));
        controller.close();
      },
    });

    Readable.fromWeb(stream).pipe(res);
    res.end();
  } catch (err) {
    console.error("Error occurred handling", req.url, err);
    res.statusCode = 500;
    res.end("internal server error");
  }
})
  .once("error", (err) => {
    console.error(err);
    process.exit(1);
  })
  .listen(3000, () => {
    console.log(`> Ready on http://${hostname}:${port}`);
  });

The following code however, with core parts of the Next.js server runtime, does contain a memory leak:

import { ServerResponse, createServer } from "http";
import { Writable } from "stream";

const hostname = "localhost";
const port = 3000;

// detached-promise.ts
export class DetachedPromise<T = any> {
  public readonly resolve: (value: T | PromiseLike<T>) => void;
  public readonly reject: (reason: any) => void;
  public readonly promise: Promise<T>;

  constructor() {
    let resolve: (value: T | PromiseLike<T>) => void;
    let reject: (reason: any) => void;

    // Create the promise and assign the resolvers to the object.
    this.promise = new Promise<T>((res, rej) => {
      resolve = res;
      reject = rej;
    });

    // We know that resolvers is defined because the Promise constructor runs
    // synchronously.
    this.resolve = resolve!;
    this.reject = reject!;
  }
}

// next-request.ts
export const ResponseAbortedName = "ResponseAborted";

export class ResponseAborted extends Error {
  public readonly name = ResponseAbortedName;
}

export function isAbortError(e: any): e is Error & { name: "AbortError" } {
  return e?.name === "AbortError" || e?.name === ResponseAbortedName;
}

export function createAbortController(response: Writable): AbortController {
  const controller = new AbortController();

  // If `finish` fires first, then `res.end()` has been called and the close is
  // just us finishing the stream on our side. If `close` fires first, then we
  // know the client disconnected before we finished.
  response.once("close", () => {
    if (response.writableFinished) return;

    controller.abort(new ResponseAborted());
  });

  return controller;
}

// pipe-readable.ts

function createWriterFromResponse(
  res: ServerResponse,
  waitUntilForEnd?: Promise<unknown>
): WritableStream<Uint8Array> {
  let started = false;

  // Create a promise that will resolve once the response has drained. See
  // https://nodejs.org/api/stream.html#stream_event_drain
  let drained = new DetachedPromise<void>();
  function onDrain() {
    drained.resolve();
  }
  res.on("drain", onDrain);

  // If the finish event fires, it means we shouldn't block and wait for the
  // drain event.
  res.once("close", () => {
    res.off("drain", onDrain);
    drained.resolve();
  });

  // Create a promise that will resolve once the response has finished. See
  // https://nodejs.org/api/http.html#event-finish_1
  const finished = new DetachedPromise<void>();
  res.once("finish", () => {
    finished.resolve();
  });

  // Create a writable stream that will write to the response.
  return new WritableStream<Uint8Array>({
    write: async (chunk) => {
      // You'd think we'd want to use `start` instead of placing this in `write`
      // but this ensures that we don't actually flush the headers until we've
      // started writing chunks.
      if (!started) {
        started = true;
        res.flushHeaders();
      }

      try {
        const ok = res.write(chunk);

        // Added by the `compression` middleware, this is a function that will
        // flush the partially-compressed response to the client.
        if ("flush" in res && typeof res.flush === "function") {
          res.flush();
        }

        // If the write returns false, it means there's some backpressure, so
        // wait until it's streamed before continuing.
        if (!ok) {
          await drained.promise;

          // Reset the drained promise so that we can wait for the next drain event.
          drained = new DetachedPromise<void>();
        }
      } catch (err) {
        res.end();
        throw new Error("failed to write chunk to response", { cause: err });
      }
    },
    abort: (err) => {
      if (res.writableFinished) return;

      res.destroy(err);
    },
    close: async () => {
      // if a waitUntil promise was passed, wait for it to resolve before
      // ending the response.
      if (waitUntilForEnd) {
        await waitUntilForEnd;
      }

      if (res.writableFinished) return;

      res.end();
      return finished.promise;
    },
  });
}

export async function pipeToNodeResponse(
  readable: ReadableStream<Uint8Array>,
  res: ServerResponse,
  waitUntilForEnd?: Promise<unknown>
) {
  try {
    // If the response has already errored, then just return now.
    const { errored, destroyed } = res;
    if (errored || destroyed) return;

    // Create a new AbortController so that we can abort the readable if the
    // client disconnects.
    const controller = createAbortController(res);

    const writer = createWriterFromResponse(res, waitUntilForEnd);

    await readable.pipeTo(writer, { signal: controller.signal });
  } catch (err: any) {
    // If this isn't related to an abort error, re-throw it.
    if (isAbortError(err)) return;

    throw new Error("failed to pipe response", { cause: err });
  }
}

// OUR TEST

createServer(async (req, res) => {
  try {
    const { errored, destroyed } = res;
    if (errored || destroyed) return;

    res.statusCode = 200;
    res.statusMessage = "OK";

    // send-payload.js
    if (req.method === "HEAD") {
      res.end(null);
      return;
    }

    const stream = new ReadableStream<Uint8Array>({
      start(controller) {
        controller.enqueue(Buffer.from("Hello world"));
        controller.close();
      },
    });

    await pipeToNodeResponse(stream, res);
    res.end();
  } catch (err) {
    console.error("Error occurred handling", req.url, err);
    res.statusCode = 500;
    res.end("internal server error");
  }
})
  .once("error", (err) => {
    console.error(err);
    process.exit(1);
  })
  .listen(3000, () => {
    console.log(`> Ready on http://${hostname}:${port}`);
  });

lithdew avatar Apr 08 '24 16:04 lithdew

bun-debug logs running the memory leak reproduction:

[RequestContext] create (src.bun.js.api.server.NewRequestContext(false,false,src.bun.js.api.server.NewServer(ZigGeneratedClasses.JSHTTPServer,false,false))@200005e0100)
[RequestContext] render
[RequestContext] doRender
[RequestContext] finalize (src.bun.js.api.server.NewRequestContext(false,false,src.bun.js.api.server.NewServer(ZigGeneratedClasses.JSHTTPServer,false,false))@200005e0100)
[RequestContext] finalizeWithoutDeinit (src.bun.js.api.server.NewRequestContext(false,false,src.bun.js.api.server.NewServer(ZigGeneratedClasses.JSHTTPServer,false,false))@200005e0100)
[RequestContext] finalizeWithoutDeinit: has_finalized false
[RequestContext] finalizeWithoutDeinit: response_jsvalue != .zero
[RequestContext] finalizeWithoutDeinit: request_body != null
[Server] deinitIfWeCan
[RequestContext] deferred deinit  (src.bun.js.api.server.NewRequestContext(false,false,src.bun.js.api.server.NewServer(ZigGeneratedClasses.JSHTTPServer,false,false))@200005e0100)
[RequestContext] deinit (src.bun.js.api.server.NewRequestContext(false,false,src.bun.js.api.server.NewServer(ZigGeneratedClasses.JSHTTPServer,false,false))@200005e0100)

lithdew avatar Apr 08 '24 16:04 lithdew

The memory leak has been isolated to the following line:

await readable.pipeTo(writer, { signal: controller.signal });

Specifically, passing in controller.signal here causes the memory leak to happen.

lithdew avatar Apr 08 '24 18:04 lithdew

Filed PR #10086. pipeTo no longer leaks memory. It seems like there may potentially be some far more minor memory leaks in the NextJS standalone build, though. Will look into it later after working on getting #10086 merged.

lithdew avatar Apr 08 '24 20:04 lithdew