Skip to content

Release GenStructures' cached column state after the postpass - #271

Merged
Zaldaryon merged 1 commit into
indevfrom
fix/genstructures-threadstatic-leak
Aug 20, 2026
Merged

Release GenStructures' cached column state after the postpass#271
Zaldaryon merged 1 commit into
indevfrom
fix/genstructures-threadstatic-leak

Conversation

@Zaldaryon

@Zaldaryon Zaldaryon commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

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/BlockSchematicPartial arrays reachable through WorldGenStructuresConfig.LoadedSchematicsCache via GenStructures, or through GenDungeons via 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 owning GenStructures instance 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 the GenStructures instance's own lifetime, not to a specific thread.

Fix

Release stratumStateOwner and stratumHeightmap at 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

  • Bug fix

Checklist

  • scripts/extract-patches.sh ran clean. One file touched: GenStructures.cs.patch.
  • dotnet build VintageStory.slnx -c Release is green (0 errors, 0 new warnings).
  • Every vanilla edit has a // Stratum marker. The new lines carry one explaining why they're there.
  • No vanilla source committed.
  • Tested on a real server start, not just compilation (make smoke: PASS, reached RunGame).

Related issues

Refs #270

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 tehtelev 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.

It looks like everything is fine now.

@Zaldaryon
Zaldaryon merged commit 4d45b63 into indev Aug 20, 2026
2 checks passed
@Zaldaryon
Zaldaryon deleted the fix/genstructures-threadstatic-leak branch August 20, 2026 09:39
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.

2 participants