Repository: vercel/next.js
Pull request: Turbopack: add test case that checks memory leak

### What?

add test case for memory leak


Closes PACK-5498

Changed files:
filename: test/development/basic/next-rs-api.test.ts
patch: @@ -12,7 +12,8 @@ import type {
   UpdateInfo,
 } from 'next/dist/build/swc/types'
 import loadConfig from 'next/dist/server/config'
-import path from 'path'
+import path, { join } from 'path'
+import { spawnSync } from 'child_process'
 
 function normalizePath(path: string) {
   return path
@@ -147,6 +148,163 @@ function appPageCode(text) {
 export default () => <div>${text}<Client /></div>;`
 }
 
+describe('next.rs api writeToDisk multiple times', () => {
+  it('should allow to write to disk multiple times', async () => {
+    let next: NextInstance
+
+    next = await createNext({
+      skipStart: true,
+      files: {
+        'pages/index.js': pagesIndexCode('hello world'),
+        'lib/props.js': 'export default {}',
+        'pages/page-nodejs.js': 'export default () => <div>hello world</div>',
+        'pages/page-edge.js':
+          'export default () => <div>hello world</div>\nexport const config = { runtime: "experimental-edge" }',
+        'pages/api/nodejs.js':
+          'export default () => Response.json({ hello: "world" })',
+        'pages/api/edge.js':
+          'export default () => Response.json({ hello: "world" })\nexport const config = { runtime: "edge" }',
+        'app/layout.tsx':
+          'export default function RootLayout({ children }: { children: any }) { return (<html><body>{children}</body></html>)}',
+        'app/loading.tsx':
+          'export default function Loading() { return <>Loading</> }',
+        'app/app/page.tsx': appPageCode('hello world'),
+        'app/app/client.tsx':
+          '"use client";\nexport default () => <div>hello world</div>',
+        'app/app-edge/page.tsx':
+          'export default () => <div>hello world</div>\nexport const runtime = "edge"',
+        'app/app-nodejs/page.tsx':
+          'export default () => <div>hello world</div>',
+        'app/route-nodejs/route.ts':
+          'export function GET() { return Response.json({ hello: "world" }) }',
+        'app/route-edge/route.ts':
+          'export function GET() { return Response.json({ hello: "world" }) }\nexport const runtime = "edge"',
+        'server.js': `
+process.title = 'next.rs api run test';
+const path = require('path');
+const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');
+const loadConfig = require('next/dist/server/config');
+const { loadBindings } = require('next/dist/build/swc');
+const { createDefineEnv } = require('next/dist/build/swc');
+async function main() {
+  const nextConfig = await loadConfig.default(
+    PHASE_DEVELOPMENT_SERVER,
+    __dirname
+  );
+  const bindings = await loadBindings();
+  const rootPath = __dirname;
+  const distDir = '.next';
+  const project = await bindings.turbo.createProject({
+    env: {},
+    nextConfig: nextConfig,
+    rootPath,
+    projectPath: '.',
+    distDir,
+    watch: {
+      enable: true,
+    },
+    dev: true,
+    defineEnv: createDefineEnv({
+      projectPath: __dirname,
+      isTurbopack: true,
+      clientRouterFilters: undefined,
+      config: nextConfig,
+      dev: true,
+      distDir: path.join(rootPath, distDir),
+      fetchCacheKeyPrefix: undefined,
+      hasRewrites: false,
+      middlewareMatchers: undefined,
+      rewrites: {
+        beforeFiles: [],
+        afterFiles: [],
+        fallback: [],
+      },
+    }),
+    buildId: 'development',
+    encryptionKey: '12345',
+    previewProps: {
+      previewModeId: 'development',
+      previewModeEncryptionKey: '12345',
+      previewModeSigningKey: '12345',
+    },
+    browserslistQuery: 'last 2 versions',
+    noMangling: false,
+    currentNodeJsVersion: '18.0.0',
+  });
+
+  const entrypointsSubscription = project.entrypointsSubscribe();
+  const entrypoints = (await entrypointsSubscription.next()).value;
+
+  const RUNS = 10000;
+  async function compileRoute(route) {
+    const endpoint = route.endpoint ?? route.htmlEndpoint ?? route.pages[0].htmlEndpoint;
+    if (!endpoint) {
+      console.log('unexpected route', route);
+      process.exit(1);
+    }
+    for (let i = 0; i < RUNS; i++) {
+      await endpoint.writeToDisk();
+    }
+  }
+
+  for (const [name, route] of entrypoints.routes) {
+    console.log(name, route.type);
+
+    console.log('first runs');
+    await compileRoute(route);
+    gc(true);
+    let old = process.memoryUsage();
+    const initial = old;
+    let stabilized = false;
+    for (let j = 0; j < 10; j++) {
+      await compileRoute(route);
+      gc(true);
+      const newMemory = process.memoryUsage();
+      const addedMemory = newMemory.rss - old.rss;
+      console.log(\`#\${j} \${RUNS} runs add \${addedMemory} bytes of memory, memory since first runs \${newMemory.rss - initial.rss} bytes\`);
+      old = newMemory;
+      if (addedMemory === 0) {
+        console.log('memory usage stabilized');
+        stabilized = true;
+        break;
+      }
+    }
+    if (!stabilized) {
+      console.log('memory usage did not stabilize, failing');
+      process.exit(1);
+    }
+  }
+}
+
+main()
+  .then(() => {
+    process.exit(0);
+  })
+  .catch((err) => {
+    console.error(err);
+    process.exit(1);
+  });
+
+        `,
+      },
+    })
+
+    const result = spawnSync(
+      'node',
+      ['--expose-gc', join(next.testDir, 'server.js')],
+      {
+        stdio: 'inherit',
+        env: {
+          ...process.env,
+        },
+      }
+    )
+    expect(result.status).toBe(0)
+
+    await next.destroy()
+  })
+})
+
 describe('next.rs api', () => {
   let next: NextInstance
   beforeAll(async () => {