-
Notifications
You must be signed in to change notification settings - Fork 739
Gcpressure #2604
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Gcpressure #2604
Conversation
- Add reusable BigUint64Array buffer to avoid per-tick allocations - Replace map() + new BigUint64Array() with direct buffer writes - Buffer grows dynamically as needed, eliminating intermediate arrays
- Cache OutlineFilter instances (red/green/white) to avoid per-render allocations - Reuse empty filter array instead of creating new ones - Eliminates frequent filter object creation in renderGhost and modifyVisibility
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
WalkthroughThis PR optimizes memory allocation across two core components by caching and reusing objects instead of creating new instances repeatedly. StructureIconsLayer caches outline filters as private fields; GameRunner reuses a tile-update buffer with dynamic growth. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (2)
src/core/GameRunner.ts (1)
183-193: Good buffer growth + direct write; consider capping growth to avoid “spike” memory retention.
IfnumUpdatesspikes once,tileUpdateBuffermay stay huge for the whole match. Optional: cap to a max, or shrink on map change / new game.src/client/graphics/layers/StructureIconsLayer.ts (1)
271-277: You still allocate 1-element filter arrays in hot paths; cache those too.
container.filters = [this.redOutlineFilter]allocates each time. You already solved the “clear” case withemptyFilters; you can do the same for set cases.class StructureIconsLayer implements Layer { // Cached filter instances to reduce GC pressure private readonly redOutlineFilter = new OutlineFilter({ thickness: 2, color: "rgba(255, 0, 0, 1)" }); private readonly greenOutlineFilter = new OutlineFilter({ thickness: 2, color: "rgba(0, 255, 0, 1)" }); private readonly whiteOutlineFilter = new OutlineFilter({ thickness: 2, color: "rgb(255, 255, 255)" }); private readonly emptyFilters: PIXI.Filter[] = []; + private readonly redFilters: PIXI.Filter[] = [this.redOutlineFilter]; + private readonly greenFilters: PIXI.Filter[] = [this.greenOutlineFilter]; + private readonly whiteFilters: PIXI.Filter[] = [this.whiteOutlineFilter]; }Then replace
[this.redOutlineFilter]→this.redFilters, etc.Also applies to: 289-314, 510-515
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/client/graphics/layers/StructureIconsLayer.ts(5 hunks)src/core/GameRunner.ts(2 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-11-26T20:49:29.140Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2519
File: src/core/game/GameView.ts:516-525
Timestamp: 2025-11-26T20:49:29.140Z
Learning: In GameView.ts, when usesSharedTileState is true (SAB mode), packedTileUpdates contains unpacked tile references as BigInt(tileRef) only, because all tile state lives in the shared Uint16Array. In non-SAB mode, packedTileUpdates contains packed TileUpdate bigints in the format (tileRef << 16n | state), which must be decoded via updateTile(tu). Therefore, Number(tu) is correct in SAB mode and shifting right by 16 bits would be wrong.
Applied to files:
src/core/GameRunner.ts
📚 Learning: 2025-11-26T22:27:31.844Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2519
File: src/core/worker/SharedTileRing.ts:55-63
Timestamp: 2025-11-26T22:27:31.844Z
Learning: In SharedTileRing.ts, the ring buffer is sized to width * height (the map dimensions). Combined with the dirty flag deduplication mechanism (each tile can only be enqueued once until the consumer clears its dirty flag), the number of pending entries is naturally bounded by the map size and drained every render frame. Therefore, ring buffer overflow should be extremely rare or effectively impossible, and any theoretical race condition between producer and consumer modifying the read index would only manifest as redundant tile refs being rendered, not memory corruption, which is acceptable.
<!-- </add_learning>
Applied to files:
src/core/GameRunner.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.
Applied to files:
src/core/GameRunner.ts
🔇 Additional comments (2)
src/core/GameRunner.ts (1)
99-100: The concern aboutsubarray()causing data corruption does not apply here.The callback receives
packedTileUpdatesas a view and passes it toGameView.update(), which immediately iterates it synchronously (line 520:forEach) and decodes the data intoupdatedTiles(line 521:this.updateTile(tu)). The view itself is never stored or accessed again. Each new tick generates a fresh view into the reused buffer, and stale views are automatically discarded—there is no deferred or async processing that would cause the view to outlive its validity.The buffer reuse is a safe optimization here because consumption is synchronous and immediate within the same call stack.
Likely an incorrect or invalid review comment.
src/client/graphics/layers/StructureIconsLayer.ts (1)
79-94: The cached filter pattern is safe and a solid GC win. OutlineFilter'scoloroption accepts CSS strings like"rgba(255, 0, 0, 1)", and sharing one immutable filter instance across multiple containers is safe as long as the filter's properties (thickness, color, etc.) are never changed after creation. Your code does exactly this—each filter is created once with fixed config and only assigned to container filter arrays, never mutated. Theprivate readonlydeclarations reinforce this pattern.No action needed; this is idiomatic and efficient.
Very valid |
Examples of reducing GC churn
Game performance suffers from excessive garbage collection pressure.
Implement targeted optimizations to reduce per-frame allocations in hot code paths.
These are straightforward examples showing how small changes can significantly reduce GC churn.
Changes
1. GameRunner Tick Packing Buffer Reuse
File:
src/core/GameRunner.tsmap()and newBigUint64ArrayinstancestileUpdateBufferthat grows dynamically and writes data directlyBefore (lines ~179-184):
After (lines 179-199):
2. StructureIconsLayer Filter Caching
File:
src/client/graphics/layers/StructureIconsLayer.tsOutlineFilterinstances created repeatedly (~50ms intervals during ghost structure rendering)Before (line ~275):
Usage example (line ~276):
*After (lines 80-93):
Result:
new OutlineFilter(...)→this.redOutlineFilter,this.emptyFilters