Skip to content

Fix renderer OOM crash loop during large collection installs - #23905

Open
DoomerDGR8 wants to merge 3 commits into
Nexus-Mods:masterfrom
DoomerDGR8:fix/collection-install-renderer-oom
Open

Fix renderer OOM crash loop during large collection installs#23905
DoomerDGR8 wants to merge 3 commits into
Nexus-Mods:masterfrom
DoomerDGR8:fix/collection-install-renderer-oom

Conversation

@DoomerDGR8

Copy link
Copy Markdown

Fixes #23904

Problem

Installing or updating a very large collection (reproduced with Cyberpunk 2077 p0qfwm rev 62 — 2852 required files — against a library of ~2900 existing archives) crashes the renderer with render-process-gone {"reason":"oom"} roughly 60–120s into every attempt. Vortex silently relaunches, reports the collection incomplete, and each resume recomputes everything and crashes identically — an endless loop (resume_count: 12, ~2.3h, net progress ≈ 0 in the issue's log).

Root cause

The dependency pipeline contains several O(refs × downloads/mods) hot paths that allocate fresh objects per probe. At 2852 references × ~3000 archives that is ~8.5 million probes per pass, and several passes run repeatedly:

  1. Per-probe allocation in download matchinglookupFromDownload() (util/dependencies.ts) built a fresh lookup object (plus two Sets and an identifier bundle in findDownloadByRef) for every download on every probe. Called per rule during resolution, per dependency in the post-phase ready-download scan (doInstallDependenciesPhase's finally, which begins right at the done installing dependencies log line — matching the observed crash ~30s later), and again in requeue/stall-rescue scans.
  2. Unbounded poll-tick scanpollAllPhasesComplete ticks every 500ms and called driveSelectedOptionalsselectedOptionalRules, which runs a findModByRef scan over all installed mods per candidate optional rule, on every tick, for the whole install. The cheap session-status filter ran after the expensive scan.
  3. Retained metadata — every dependency kept its full metadb lookup-result list alive for the entire install, though only lookupResults[0] is ever consumed.
  4. O(n²) rule updatesupdateRules dispatched removeModRule + addModRule per dependency; each dispatch clones the collection mod's full 2852-entry rules array and pushes a separate diff through the persist pipeline (the persist:diff ~1/s churn in the log).

The allocation rate outruns GC and the renderer heap dies. Small collections never notice because every term is small.

Changes

  • util/dependencies.ts
    • Memoize lookupFromDownload and the findDownloadByRef identifier bundle per download object via WeakMap (redux state objects are immutable, so object identity is a safe key).
    • Retain only lookupResults[0] per dependency (the only entry consumers read).
    • Resolve rules in bounded batches of 50, with a DEBG dependency resolution batch done {resolved, total} line so future user logs show resolution progress.
    • tagDuplicates skips the collateral scan for deps without a lookup result.
  • InstallManager.ts
    • updateRules collects all rule updates into one batchDispatch (DEBG batch updating dependency rules {count}); updateModRule shares the same logic.
    • driveSelectedOptionals filters by session status (pending) before the per-rule findModByRef scan.
  • Visible OOM handling (issue's "silently dying and restarting" point): on render-process-gone with reason === "oom", the main process writes a renderer-oom.json marker to userData (MainWindow.ts); on next startup the collections extension consumes it and shows an error notification instead of silently restarting into the same crash (collections/index.ts). The collection stays paused until explicitly resumed.

Verification

  • New stress test (util/dependencies.stress.test.ts): 3000 refs × 3000 downloads, the crash shape. Heap growth ~234 MB → ~10 MB with the memoization; suite runs ~6× faster.
  • Unit tests for the memoization identity/invalidation semantics; full mod_management + collections renderer suites pass (37 files / 506 tests).
  • Real-world repro: packaged this branch and resumed the exact failing collection (p0qfwm rev 62, resume Current Source - XML Installers getting stuck #13). Resolution completed all 2852 refs in ~74s at a steady ~50 refs/s with 0 errors, sailed past the point where all 12 previous attempts OOMed, and proceeded into downloads/installs normally (124 existing archives matched, fresh downloads + installs running in parallel).
  • Small-collection behavior unchanged: batching degenerates to a single batch below 50 rules; single-dep rule updates take the same code path as before.

Not addressed (possible follow-up)

Resolution still restarts from zero on every resume (missing: 2852 each time). With this fix a re-resolution is cheap enough not to OOM, but persisting resolution progress across resumes would still save time on very large collections.

🤖 Generated with Claude Code

DoomerDGR8 and others added 3 commits August 7, 2026 18:31
Installing a very large collection (e.g. 2852 mods) against a big archive
library crashed the renderer with reason "oom" shortly after dependency
resolution, looping forever on resume. The pipeline ran several
O(refs x downloads/mods) scans that allocated fresh lookup objects per
probe (~8.5M probes per pass), retained full metadb result lists per
dependency, re-scanned all installed mods on every 500ms poll tick, and
dispatched 2 rule-update actions per dependency (each cloning the
collection mod rules array and emitting a persist diff).

- memoize lookupFromDownload + findDownloadByRef identifiers per download
  object (WeakMap; redux state is immutable)
- retain only the first (only ever consumed) lookup result per dependency
- resolve dependency rules in bounded batches of 50 with DEBG progress logs
- batch updateRules into a single dispatch
- filter driveSelectedOptionals by session status before the per-rule
  findModByRef scan over all installed mods
- add a 3000x3000 stress test: heap growth ~234MB -> ~10MB

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A renderer OOM during a collection install previously relaunched Vortex
silently, inviting an immediate resume into the identical crash - an
infinite crash loop with no visible error. The main process now writes a
renderer-oom.json marker on render-process-gone with reason "oom"; on
startup the collections extension consumes the marker and shows an error
notification, leaving the collection paused until the user explicitly
resumes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mock was a local workaround for an environment without the native
toolchain; with the module built normally the tests pass without it,
matching how the rest of the suite treats native deps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@DoomerDGR8
DoomerDGR8 requested a review from a team as a code owner August 7, 2026 16:20
Copilot AI lite review requested due to automatic review settings August 7, 2026 16:20
@DoomerDGR8
DoomerDGR8 marked this pull request as draft August 7, 2026 16:22
@DoomerDGR8
DoomerDGR8 marked this pull request as ready for review August 7, 2026 16:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a renderer OOM crash loop during very large collection installs by reducing high-volume transient allocations in dependency resolution/matching, batching expensive operations, and surfacing a visible “renderer OOM” warning on next startup to avoid silent restart loops.

Changes:

  • Memoize per-download lookup/identifier derivations and reduce retained dependency lookup metadata to the first result only.
  • Batch dependency graph resolution and batch Redux rule updates to reduce in-flight promise chains and persist/diff churn.
  • Add an OOM marker written by the main process and consumed by the collections extension on next startup to notify the user instead of silently restarting into a crash loop.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/renderer/src/extensions/mod_management/util/dependencies.ts Adds per-download memoization, trims retained lookup results, batches dependency graph gathering, and optimizes duplicate tagging.
src/renderer/src/extensions/mod_management/util/dependencies.test.ts Adds unit coverage for memoization and reference-based download resolution behavior.
src/renderer/src/extensions/mod_management/util/dependencies.stress.test.ts Adds a large-scale stress test intended to guard against heap growth regressions in the matching hot path.
src/renderer/src/extensions/mod_management/InstallManager.ts Reduces per-tick optional scanning cost and batches dependency rule updates into a single dispatch.
src/renderer/src/extensions/collections/index.ts Reads/removes a renderer-OOM marker on startup and shows a user notification.
src/main/src/MainWindow.ts Writes the renderer-OOM marker file when Electron reports render-process-gone with reason oom.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1249 to +1257
const markerPath = path.join(getVortexPath("userData"), "renderer-oom.json");
let marker: { timestamp?: number } | undefined;
try {
marker = JSON.parse(readFileSync(markerPath, "utf8"));
unlinkSync(markerPath);
} catch {
// no marker (the usual case) or unreadable - nothing to report
return;
}
Comment on lines +64 to +83
const before = heapUsed();

let matched = 0;
for (const ref of references) {
if (findDownloadByRef(ref, downloads) !== undefined) {
++matched;
}
}

const after = heapUsed();
const growthMB = (after - before) / (1024 * 1024);

process.stdout.write(
`[stress] ${COUNT}x${COUNT} matching: heap growth ${growthMB.toFixed(1)} MB\n`,
);

expect(matched).toBe(COUNT);
// generous bound: without memoization this run allocates hundreds of MB of
// throw-away lookup objects; with it, growth stays in the low tens of MB
expect(growthMB).toBeLessThan(192);
@DoomerDGR8

DoomerDGR8 commented Aug 7, 2026

Copy link
Copy Markdown
Author

I was able to fully install the collection that was failing for me earlier today and the previous leftover huge files from an older version also help preventing too much re-downloading.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Renderer OOM crash loop when installing/updating large Cyberpunk 2077 collection (2852 files) — restarts endlessly reporting "collection incomplete"

2 participants