From 0a724aed71248329ec44cbc5ac2cf6b90285f090 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Tue, 10 Mar 2026 14:19:49 +0100 Subject: [PATCH] Fix BSOD MEMORY_MANAGEMENT on low-RAM servers - Dynamically compute JVM -Xmx based on system RAM instead of hardcoded 32g - Reduce peak memory during session loading (inline JSON string, skip redundant clone) Co-Authored-By: Claude Opus 4.6 --- src/main/download-manager.ts | 5 ++++- src/main/extractor.ts | 12 ++++++++++-- src/main/storage.ts | 8 +++++--- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 58e26f3..3793603 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -1409,7 +1409,10 @@ export class DownloadManager extends EventEmitter { this.appSessionStartedAt = startedAt; this.runtimePersistedTotalMs = Math.max(0, Number(settings.totalRuntimeAllTimeMs || 0)); this.runtimePersistedAt = startedAt; - this.session = cloneSession(session); + // loadSession already returns a fresh, standalone object graph — no need to + // deep-clone again. This avoids duplicating the entire session in memory at + // startup which can spike peak heap on low-RAM servers. + this.session = session; this.itemCount = Object.keys(this.session.items).length; this.storagePaths = storagePaths; this.debridService = new DebridService(settings, { diff --git a/src/main/extractor.ts b/src/main/extractor.ts index 8658d15..75b781a 100644 --- a/src/main/extractor.ts +++ b/src/main/extractor.ts @@ -89,6 +89,14 @@ let resolveExtractorCommandInFlight: Promise | null = null; const EXTRACTOR_RETRY_AFTER_MS = 30_000; const DEFAULT_ZIP_ENTRY_MEMORY_LIMIT_MB = 256; + +/** Compute a safe JVM -Xmx value based on available physical RAM. + * Reserves 4 GB for Windows + Electron + other processes, caps at 16 GB. */ +function jvmMaxHeapArg(): string { + const totalGb = os.totalmem() / (1024 ** 3); + const heapGb = Math.max(1, Math.min(Math.floor(totalGb - 4), 16)); + return `-Xmx${heapGb}g`; +} const EXTRACTOR_PROBE_TIMEOUT_MS = 8_000; const DEFAULT_EXTRACT_CPU_BUDGET_PERCENT = 80; let currentExtractCpuPriority: string | undefined; @@ -1392,7 +1400,7 @@ function startDaemon(layout: JvmExtractorLayout): boolean { "-Dfile.encoding=UTF-8", `-Djava.io.tmpdir=${jvmTmpDir}`, "-Xms1g", - "-Xmx32g", + jvmMaxHeapArg(), "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=50", "-cp", @@ -1640,7 +1648,7 @@ async function runJvmExtractCommand( "-Dfile.encoding=UTF-8", `-Djava.io.tmpdir=${jvmTmpDir}`, "-Xms1g", - "-Xmx32g", + jvmMaxHeapArg(), "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=50", "-cp", diff --git a/src/main/storage.ts b/src/main/storage.ts index a4e8c1b..3699465 100644 --- a/src/main/storage.ts +++ b/src/main/storage.ts @@ -721,12 +721,14 @@ export function normalizeLoadedSessionTransientFields(session: SessionState): Se function readSessionFile(filePath: string): SessionState | null { try { - const raw = fs.readFileSync(filePath, "utf8"); - const parsed = JSON.parse(raw) as unknown; + // Inline readFileSync into JSON.parse so the raw string is not bound to a + // variable and can be GC'd immediately — avoids holding the full JSON text + // and the parsed object graph in memory simultaneously. + const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown; const session = normalizeLoadedSessionTransientFields(normalizeLoadedSession(parsed)); const pkgCount = Object.keys(session.packages).length; const itemCount = Object.keys(session.items).length; - logger.info(`Session geladen: ${filePath} (${pkgCount} Pakete, ${itemCount} Items, ${raw.length} Bytes)`); + logger.info(`Session geladen: ${filePath} (${pkgCount} Pakete, ${itemCount} Items)`); return session; } catch (error) { logger.error(`Session-Datei nicht lesbar: ${filePath}: ${String(error)}`);