Release GenStructures' cached column state after the postpass - #271
Merged
Conversation
Investigated tehtelev's memory-growth report by pulling and reading the attached profiler screenshots directly. They show BlockSchematicStructure and BlockSchematicPartial arrays reachable through WorldGenStructuresConfig.LoadedSchematicsCache via GenStructures, and through GenDungeons via a similar chain, in every case terminating at per-thread storage rather than a normal object reference. The likely source: Stratum's own concurrency patch to GenStructures.cs adds [ThreadStatic] fields (stratumStateOwner, stratumHeightmap, and siblings) so the postpass can reuse a chunk's already-computed heightmap/climate/forest state instead of recomputing it. That state is only ever overwritten when the same worker thread happens to process another chunk's main pass. Once worldgen quiets down, whatever a thread last touched, including the owning GenStructures instance and its loaded schematic cache, stays reachable from that thread for as long as the thread itself lives. Vanilla's own unpatched code does not have this property, since it keeps the equivalent state in plain instance fields tied to the GenStructures instance's own lifetime. Fixed by releasing stratumStateOwner and stratumHeightmap at the end of the postpass, the point at which a column's structure generation is actually done. Build green, make smoke passed. This addresses a real, demonstrable deviation from vanilla's GC behavior, not necessarily the full extent of what the profiler screenshots show: a single snapshot can't distinguish "leaked" from "a worldgen thread that's mid-column right now," so confirming this closes the gap fully needs a longer session on the reporter's own end.
tehtelev
approved these changes
Aug 20, 2026
tehtelev
left a comment
Contributor
There was a problem hiding this comment.
It looks like everything is fine now.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Investigates #270 (BlockSchematicStructure objects never collected) by pulling and reading the actual profiler screenshots attached to the issue, then fixes the mechanism they point at.
Every retention chain in the screenshots ends the same way:
BlockSchematicStructure/BlockSchematicPartialarrays reachable throughWorldGenStructuresConfig.LoadedSchematicsCacheviaGenStructures, or throughGenDungeonsvia a similar path, terminating at what looks like per-thread storage rather than a plain object reference. One screenshot names the exact field:ServerEventManager.stratumCachedWorldSavedInvocations.That pointed at Stratum's own concurrency patch to
GenStructures.cs(added to let TerrainFeatures generate multiple columns at once). It adds[ThreadStatic]fields (stratumStateOwner,stratumHeightmap, and a few siblings) so the postpass can reuse a chunk's already-computed heightmap and climate/forest values instead of recomputing them. The problem: that cached state is only ever overwritten when the same worker thread happens to process another chunk's main pass. Once worldgen quiets down, whatever a thread last touched (the owningGenStructuresinstance and its loaded schematic cache included) stays reachable from that thread for as long as the thread itself lives. Vanilla's own unpatched code doesn't have this property, since the equivalent state sits in plain instance fields tied to theGenStructuresinstance's own lifetime, not to a specific thread.Fix
Release
stratumStateOwnerandstratumHeightmapat the end of the postpass, the point at which a column's structure generation is actually finished. Both fields get set fresh by the main pass regardless of which branch runs, so clearing them right after the postpass reads them is always safe: the next chunk on that thread recomputes from scratch rather than reusing anything.Caveat
This is a real, demonstrable gap from vanilla's GC behavior, but a single heap snapshot can't distinguish "leaked forever" from "a worldgen thread that's mid-column right now." I can't reproduce a long-running server session with active generation followed by an idle period to directly confirm the counts stop growing. @tehtelev , since you filed this with the profiler already set up: if you get a chance to rerun the same capture after a while on this branch, that would close the loop for real.
Type
Checklist
scripts/extract-patches.shran clean. One file touched:GenStructures.cs.patch.dotnet build VintageStory.slnx -c Releaseis green (0 errors, 0 new warnings).// Stratummarker. The new lines carry one explaining why they're there.make smoke: PASS, reachedRunGame).Related issues
Refs #270