Skip to content

Commit 32e9322

Browse files
committed
Add GC on idle for render server
1 parent 5727459 commit 32e9322

File tree

1 file changed

+70
-36
lines changed

1 file changed

+70
-36
lines changed

src/svelte-scripts/vite-render-server.js

Lines changed: 70 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,12 @@ function collectCssFromManifest(entryKey, collected = new Set()) {
5858

5959
// Add CSS from this entry
6060
if (entry.css) {
61-
entry.css.forEach(css => collected.add(css));
61+
entry.css.forEach((css) => collected.add(css));
6262
}
6363

6464
// Recursively collect from imports
6565
if (entry.imports) {
66-
entry.imports.forEach(importKey => {
66+
entry.imports.forEach((importKey) => {
6767
if (!collected.has(`visited:${importKey}`)) {
6868
collected.add(`visited:${importKey}`);
6969
collectCssFromManifest(importKey, collected);
@@ -72,7 +72,7 @@ function collectCssFromManifest(entryKey, collected = new Set()) {
7272
}
7373

7474
// Filter out visited markers and return just CSS paths
75-
return Array.from(collected).filter(item => !item.startsWith("visited:"));
75+
return Array.from(collected).filter((item) => !item.startsWith("visited:"));
7676
}
7777

7878
// Shared Vite plugin configuration
@@ -167,13 +167,13 @@ const layoutFiles = loadLayoutFiles(LAYOUT_DIR);
167167

168168
// CSS collection with caching for performance
169169
// Caches are invalidated when Vite detects file changes via HMR
170-
const cssCache = new Map(); // filePath -> css string
171-
const importsCache = new Map(); // filePath -> array of import paths
170+
const cssCache = new Map(); // filePath -> css string
171+
const importsCache = new Map(); // filePath -> array of import paths
172172

173173
// Clear caches when files change (Vite HMR)
174174
if (dev && vite) {
175-
vite.watcher.on('change', (filePath) => {
176-
if (filePath.endsWith('.svelte')) {
175+
vite.watcher.on("change", (filePath) => {
176+
if (filePath.endsWith(".svelte")) {
177177
cssCache.delete(filePath);
178178
importsCache.delete(filePath);
179179
}
@@ -193,7 +193,7 @@ async function collectComponentCss(componentPath) {
193193
try {
194194
const result = await vite.transformRequest(cssUrl);
195195
if (result?.code) {
196-
let css = '';
196+
let css = "";
197197
const viteMatch = result.code.match(/const __vite__css\s*=\s*"((?:[^"\\]|\\.)*)"/s);
198198
if (viteMatch) {
199199
css = viteMatch[1];
@@ -206,22 +206,24 @@ async function collectComponentCss(componentPath) {
206206
if (css) {
207207
// Unescape the CSS string
208208
css = css
209-
.replace(/\\r/g, '') // Remove carriage returns
210-
.replace(/\\n/g, '\n')
211-
.replace(/\\t/g, '\t')
209+
.replace(/\\r/g, "") // Remove carriage returns
210+
.replace(/\\n/g, "\n")
211+
.replace(/\\t/g, "\t")
212212
.replace(/\\"/g, '"')
213-
.replace(/\\\\/g, '\\');
213+
.replace(/\\\\/g, "\\");
214214

215215
// Validate this is actually CSS, not component source
216216
// Vite returns full component source for styleless components
217217
const trimmed = css.trim();
218-
if (trimmed.startsWith('<') ||
219-
trimmed.startsWith('<!--') ||
220-
trimmed.includes('<script>') ||
221-
trimmed.includes('<svg')) {
218+
if (
219+
trimmed.startsWith("<") ||
220+
trimmed.startsWith("<!--") ||
221+
trimmed.includes("<script>") ||
222+
trimmed.includes("<svg")
223+
) {
222224
// This is component markup, not CSS - skip it
223-
cssCache.set(svelteFilePath, '');
224-
return '';
225+
cssCache.set(svelteFilePath, "");
226+
return "";
225227
}
226228

227229
cssCache.set(svelteFilePath, css);
@@ -231,8 +233,8 @@ async function collectComponentCss(componentPath) {
231233
} catch (e) {
232234
// Component might not have styles
233235
}
234-
cssCache.set(svelteFilePath, '');
235-
return '';
236+
cssCache.set(svelteFilePath, "");
237+
return "";
236238
}
237239

238240
function getImportsFromSource(filePath) {
@@ -242,16 +244,16 @@ async function collectComponentCss(componentPath) {
242244
}
243245

244246
try {
245-
const source = readFileSync(filePath, 'utf-8');
247+
const source = readFileSync(filePath, "utf-8");
246248
const imports = [];
247249
const importRegex = /import\s+[\w\s{},*]+\s+from\s+["']([^"']+\.svelte)["']/g;
248250
let match;
249251
while ((match = importRegex.exec(source)) !== null) {
250252
const importPath = match[1];
251-
const fileDir = resolve(filePath, '..');
252-
if (importPath.startsWith('./') || importPath.startsWith('../')) {
253+
const fileDir = resolve(filePath, "..");
254+
if (importPath.startsWith("./") || importPath.startsWith("../")) {
253255
imports.push(resolve(fileDir, importPath));
254-
} else if (importPath.startsWith('/')) {
256+
} else if (importPath.startsWith("/")) {
255257
imports.push(join(COMPONENT_DIR, importPath));
256258
}
257259
}
@@ -265,11 +267,11 @@ async function collectComponentCss(componentPath) {
265267

266268
// Collect all component paths first (synchronous, uses cached imports)
267269
function collectAllPaths(filePath) {
268-
const normalizedPath = filePath.split('?')[0];
270+
const normalizedPath = filePath.split("?")[0];
269271
if (visited.has(normalizedPath)) return;
270272
visited.add(normalizedPath);
271273

272-
if (normalizedPath.endsWith('.svelte')) {
274+
if (normalizedPath.endsWith(".svelte")) {
273275
const imports = getImportsFromSource(normalizedPath);
274276
for (const importPath of imports) {
275277
collectAllPaths(importPath);
@@ -280,11 +282,9 @@ async function collectComponentCss(componentPath) {
280282
collectAllPaths(componentPath);
281283

282284
// Fetch all CSS in parallel
283-
const cssResults = await Promise.all(
284-
Array.from(visited).map(path => getCssForComponent(path))
285-
);
285+
const cssResults = await Promise.all(Array.from(visited).map((path) => getCssForComponent(path)));
286286

287-
return cssResults.filter(css => css).join('\n');
287+
return cssResults.filter((css) => css).join("\n");
288288
}
289289

290290
// Find the svelte runtime entry in manifest
@@ -398,10 +398,7 @@ async function handleRender(req) {
398398
} else {
399399
// Production: load pre-built SSR module
400400
const ssrPath = join(BUILD_DIR, "server", pathname.replace(/\.svelte$/, ".js"));
401-
const [componentModule, svelteModuleLoaded] = await Promise.all([
402-
import(ssrPath),
403-
import("svelte/server"),
404-
]);
401+
const [componentModule, svelteModuleLoaded] = await Promise.all([import(ssrPath), import("svelte/server")]);
405402
component = componentModule.default;
406403
svelteModule = svelteModuleLoaded;
407404
render = svelteModule.render;
@@ -438,7 +435,7 @@ async function handleRender(req) {
438435
injectHead += `<style>${css}</style>`;
439436
}
440437
} catch (e) {
441-
console.error('[vite-ssr] Failed to collect CSS:', e.message);
438+
console.error("[vite-ssr] Failed to collect CSS:", e.message);
442439
}
443440
} else if (manifest) {
444441
// Production: inject CSS links from manifest
@@ -461,6 +458,33 @@ async function handleRender(req) {
461458
});
462459
}
463460

461+
// Memory management: run GC during idle periods to prevent memory accumulation
462+
let idleGcTimer = null;
463+
const IDLE_GC_DELAY_MS = 5000; // Run GC after 5 seconds of no requests
464+
let requestsInFlight = 0;
465+
466+
function scheduleIdleGc() {
467+
// Clear any existing timer
468+
if (idleGcTimer) {
469+
clearTimeout(idleGcTimer);
470+
idleGcTimer = null;
471+
}
472+
473+
// Only schedule GC in production and when no requests are in flight
474+
if (requestsInFlight > 0) return;
475+
476+
idleGcTimer = setTimeout(() => {
477+
if (requestsInFlight === 0 && typeof Bun !== "undefined" && Bun.gc) {
478+
const before = process.memoryUsage();
479+
Bun.gc(true); // Synchronous full GC
480+
const after = process.memoryUsage();
481+
const freedMB = Math.round((before.heapUsed - after.heapUsed) / 1024 / 1024);
482+
console.log(`[vite-ssr] Idle GC freed ${freedMB}MB (heap: ${Math.round(after.heapUsed / 1024 / 1024)}MB)`);
483+
}
484+
idleGcTimer = null;
485+
}, IDLE_GC_DELAY_MS);
486+
}
487+
464488
// Start Bun HTTP server
465489
const server = Bun.serve({
466490
port: parseInt(NODE_PORT, 10),
@@ -473,17 +497,27 @@ const server = Bun.serve({
473497
return new Response("OK", { status: 200 });
474498
}
475499

500+
// Track requests in flight
501+
requestsInFlight++;
502+
476503
// Handle render requests
477504
const response = await handleRender(req);
478505

479506
const duration = (performance.now() - start).toFixed(2);
480507
console.log(`[vite-ssr] ${req.method} ${url.pathname} ${response.status} - ${duration}ms`);
481508

509+
// Decrement and schedule GC when idle
510+
requestsInFlight--;
511+
scheduleIdleGc();
512+
482513
return response;
483514
},
484515
});
485516

486517
console.log(`[vite-ssr] Svelte SSR renderer listening in ${NODE_ENV} mode on port ${server.port}`);
518+
if (isProductionBuild) {
519+
console.log(`[vite-ssr] Idle GC enabled: will run after ${IDLE_GC_DELAY_MS}ms of inactivity`);
520+
}
487521

488522
// Also start Vite dev server for client HMR in development
489523
if (dev) {
@@ -506,4 +540,4 @@ if (dev) {
506540
});
507541
await viteClientServer.listen();
508542
console.log(`[vite-ssr] Vite dev server running on ${devProtocol}://localhost:${VITE_PORT}`);
509-
}
543+
}

0 commit comments

Comments
 (0)