Skip to content

Commit

Permalink
fix require cache memory leak
Browse files Browse the repository at this point in the history
  • Loading branch information
slorber committed Oct 22, 2024
1 parent a272cd6 commit 387635e
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 7 deletions.
10 changes: 7 additions & 3 deletions packages/docusaurus/src/common.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ export type AppRenderResult = {
collectedData: PageCollectedData;
};

export type AppRenderer = (params: {
pathname: string;
}) => Promise<AppRenderResult>;
export type AppRenderer = {
render: (params: {pathname: string}) => Promise<AppRenderResult>;

// It's important to shut down the app renderer
// Otherwise Node.js require cache leaks memory
shutdown: () => Promise<void>;
};

export type PageCollectedData = {
// TODO Docusaurus v4 refactor: helmet state is non-serializable
Expand Down
20 changes: 16 additions & 4 deletions packages/docusaurus/src/ssg/ssg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
*/

import fs from 'fs-extra';
import {createRequire} from 'module';
import path from 'path';
import _ from 'lodash';
import evaluate from 'eval';
Expand All @@ -19,6 +18,7 @@ import {
type SSGTemplateCompiled,
} from './ssgTemplate';
import {SSGConcurrency, writeStaticFile} from './ssgUtils';
import {createSSGRequire} from './ssgNodeRequire';
import type {SSGParams} from './ssgParams';
import type {AppRenderer, AppRenderResult, SiteCollectedData} from '../common';
import type {HtmlMinifier} from '@docusaurus/bundler';
Expand Down Expand Up @@ -58,6 +58,8 @@ export async function loadAppRenderer({

const filename = path.basename(serverBundlePath);

const ssgRequire = createSSGRequire(serverBundlePath);

const globals = {
// When using "new URL('file.js', import.meta.url)", Webpack will emit
// __filename, and this plugin will throw. not sure the __filename value
Expand All @@ -67,7 +69,7 @@ export async function loadAppRenderer({

// This uses module.createRequire() instead of very old "require-like" lib
// See also: https://github.com/pierrec/node-eval/issues/33
require: createRequire(serverBundlePath),
require: ssgRequire.require,
};

const serverEntry = await PerfLogger.async(
Expand All @@ -86,7 +88,15 @@ export async function loadAppRenderer({
`Server bundle export from "${filename}" must be a function that renders the Docusaurus React app.`,
);
}
return serverEntry.default;

async function shutdown() {
ssgRequire.cleanup();
}

return {
render: serverEntry.default,
shutdown,
};
}

export function printSSGWarnings(
Expand Down Expand Up @@ -191,6 +201,8 @@ export async function generateStaticFiles({
{concurrency: SSGConcurrency},
);

await renderer.shutdown();

printSSGWarnings(results);

const [allSSGErrors, allSSGSuccesses] = _.partition(
Expand Down Expand Up @@ -235,7 +247,7 @@ async function generateStaticFile({
}): Promise<SSGSuccessResult & {warnings: string[]}> {
try {
// This only renders the app HTML
const result = await renderer({
const result = await renderer.render({
pathname,
});
// This renders the full page HTML, including head tags...
Expand Down
49 changes: 49 additions & 0 deletions packages/docusaurus/src/ssg/ssgNodeRequire.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import {createRequire} from 'module';

export type SSGNodeRequire = {
require: NodeJS.Require;
cleanup: () => void;
};

// The eval/vm.Script used for running the server bundle need a require() impl
// This impl has to be relative to the server bundler path
// This enables the server bundle to resolve relative paths such as:
// - require('./assets/js/some-chunk.123456.js')
//
// Unfortunately, Node.js vm.Script doesn't isolate memory / require.cache
// This means that if we build multiple Docusaurus localized sites in a row
// The Node.js require cache will keep growing and retain in memory the JS
// assets of the former SSG builds
// We have to clean up the node require cache manually to avoid leaking memory!
// See also https://x.com/sebastienlorber/status/1848399310116831702
export function createSSGRequire(serverBundlePath: string): SSGNodeRequire {
const realRequire = createRequire(serverBundlePath);

const allRequiredIds: string[] = [];

const ssgRequireFunction: NodeJS.Require = (id) => {
const module = realRequire(id);
allRequiredIds.push(id);
return module;
};

const cleanup = () => {
allRequiredIds.forEach((id) => {
delete realRequire.cache[realRequire.resolve(id)];
});
};

ssgRequireFunction.resolve = realRequire.resolve;
ssgRequireFunction.cache = realRequire.cache;
ssgRequireFunction.extensions = realRequire.extensions;
ssgRequireFunction.main = realRequire.main;

return {require: ssgRequireFunction, cleanup};
}

0 comments on commit 387635e

Please sign in to comment.