fix(dev-server): recycle the server once hot rebuilds pile up
The dev server got slower the longer it ran. Measured on the example app: 30 .wrn edits grew RSS from 117 MB to 137 MB and never gave it back, while 30 CSS edits cost nothing -- so the leak is exactly one retained module identity per rebuild, not caches or file handles. That is inherent to reloading a module in-process. Bun caches modules by path, so a rebuild has to be given a new identity to be picked up at all, and Bun has no API to unload the old one. At roughly 0.66 MB a rebuild, a long editing session is several hundred megabytes of garbage that cannot be collected. The process now recycles itself past a rebuild threshold, exiting with the RESTART_EXIT_CODE the CLI supervisor already respawns on; browsers reconnect because the HMR client already retries. It waits for a quiet period first so a live request is never cut off, and the threshold (300 rebuilds, about 200 MB) sits well above a normal session. Set WRNEXUS_DEV_RECYCLE_AFTER to tune it, or 0 to switch it off. Also bounds browserArtifactPaths and islandArtifactPaths, which are keyed by content hash and so gained an entry per rebuild that was never read again. Small next to the module leak, but unbounded is unbounded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -58,9 +58,28 @@ export function runMiddleware(
|
||||
*/
|
||||
const moduleCache = new Map<string, Promise<Record<string, unknown>>>();
|
||||
const moduleVersions = new Map<string, number>();
|
||||
/*
|
||||
* Artifact URLs carry a content hash, so a rebuild registers a new key and the
|
||||
* previous one is never requested again -- left alone these grow for the life
|
||||
* of the dev server. Bounded rather than cleared on rebuild because a page
|
||||
* already mid-load may still ask for the URL it was served.
|
||||
*/
|
||||
const ARTIFACT_PATH_LIMIT = 512;
|
||||
const browserArtifactPaths = new Map<string, string>();
|
||||
const islandArtifactPaths = new Map<string, string>();
|
||||
|
||||
function rememberArtifact(paths: Map<string, string>, pathname: string, artifact: string): void {
|
||||
// Re-insert so a key still in use is treated as recent.
|
||||
paths.delete(pathname);
|
||||
paths.set(pathname, artifact);
|
||||
|
||||
while (paths.size > ARTIFACT_PATH_LIMIT) {
|
||||
const oldest = paths.keys().next();
|
||||
if (oldest.done) break;
|
||||
paths.delete(oldest.value);
|
||||
}
|
||||
}
|
||||
|
||||
type ImportMode = "legacy" | "compatible" | "explicit";
|
||||
interface CompileImportOptions {
|
||||
mode: ImportMode;
|
||||
@@ -622,7 +641,7 @@ export function compileWrnArtifactsAsync(file: string, version = 0): Promise<Wrn
|
||||
);
|
||||
writeFileSync(artifacts.contract, JSON.stringify(targets.contract, null, 2) + "\n", "utf8");
|
||||
writeFileSync(artifacts.rpc, JSON.stringify(targets.rpc, null, 2) + "\n", "utf8");
|
||||
browserArtifactPaths.set(browserPath, artifacts.browser);
|
||||
rememberArtifact(browserArtifactPaths, browserPath, artifacts.browser);
|
||||
compileMetrics.compilations++;
|
||||
return artifacts;
|
||||
})().finally(() => asyncCompileInProgress.delete(key));
|
||||
@@ -673,7 +692,7 @@ export function compileWrnArtifacts(file: string, version = 0): WrnCompileArtifa
|
||||
.map(([, path]) => path);
|
||||
if (requiredArtifacts.every((path) => statSync(path).isFile())) {
|
||||
compileMetrics.hits++;
|
||||
browserArtifactPaths.set(`/__wrnexus/client/${stem}.mjs`, artifacts.browser);
|
||||
rememberArtifact(browserArtifactPaths, `/__wrnexus/client/${stem}.mjs`, artifacts.browser);
|
||||
// A cached .wrn still needs its island bundles: the .tsx may have changed
|
||||
// since, and after a restart with a warm cache nothing else would build them.
|
||||
let cachedIslands: Array<{ name: string; sourcePath: string }> = [];
|
||||
@@ -715,7 +734,7 @@ export function compileWrnArtifacts(file: string, version = 0): WrnCompileArtifa
|
||||
rewriteArtifactImports(targets.browser, result.ast, file, "browser"),
|
||||
"utf8",
|
||||
);
|
||||
browserArtifactPaths.set(browserPath, artifacts.browser);
|
||||
rememberArtifact(browserArtifactPaths, browserPath, artifacts.browser);
|
||||
writeFileSync(
|
||||
artifacts.server,
|
||||
rewriteArtifactImports(targets.server, result.ast, file, "server"),
|
||||
@@ -775,7 +794,7 @@ export function serveWrnBrowserArtifact(pathname: string): Response | null {
|
||||
|
||||
/** Registers a built island asset for serving under `/__wrnexus/island/`. */
|
||||
export function registerIslandArtifact(pathname: string, artifact: string): void {
|
||||
islandArtifactPaths.set(pathname, artifact);
|
||||
rememberArtifact(islandArtifactPaths, pathname, artifact);
|
||||
}
|
||||
|
||||
/** Serves a built island bundle, chunk, or the island mount runtime. */
|
||||
|
||||
Reference in New Issue
Block a user