[Bugfix] entity.cell_pos = (x, y) leaks exactly 1 allocator block per assignment #414

Open
opened 2026-07-27 18:24:29 +00:00 by john · 0 comments
Owner

Found while calibrating the allocator gate for the #395 soak test, and verified independently.

Measured

build/mcrogueface --headless, 2000-iteration warmup, gc.collect() before and after, 20000 assignments:

entity.cell_pos = (x, y)           N= 20000  blocks 23373 -> 43375  growth=  20002  per-call=1.0001
frame.x = float (control)          N= 20000  blocks 219394 -> 219396  growth=      2  per-call=0.0001
entity.cell_pos read (control)     N= 20000  blocks 219395 -> 219397  growth=      2  per-call=0.0001

Exactly one sys.getallocatedblocks() per assignment, perfectly linear. Reading cell_pos is clean, and an equivalent float-property assignment (Frame.x) is clean, so it is specific to the cell_pos setterUIEntity::set_cell_pos, src/UIEntity.cpp. Values were varied across the run, so it is not a small-int / interned-tuple artefact.

Why it matters

cell_pos is the entity-movement API. A single entity moving once per frame at 60 fps leaks ~216 000 objects per hour; a scenario with fifty entities moving on a turn loop leaks proportionally faster. This is the shape that presents as "the process grows until it is killed" after a long unattended run, with nothing pointing at the cause.

RSS will not show it early: one leaked object per call is roughly 300 KB over 5000 calls, which the allocator can satisfy from arenas it already holds. A previous reviewer on this repo demonstrated exactly that — a real ~320 KB leak sitting invisible under a 4096 KB RSS threshold. sys.getallocatedblocks() sees it immediately.

Not diagnosed further here

I measured it rather than root-caused it; the setter's conversion path (PyPosition/sfVector2i handling, and whatever it builds for the position-change notification) is where to look. Filed rather than fixed because it needs its own refcount audit of that path and belongs with a regression gate, not folded into a frame-barrier task.

A gate in the style of tests/integration/frame_lock_soak_test.py section 4 is the right shape: getallocatedblocks() across N assignments, gated well under 1 block per call, with a clean control property alongside so the gate cannot pass by measuring nothing.

Sibling: #413 (mcrfpy.get_metrics() leaks 8 blocks per call). Related: #395.

Found while calibrating the allocator gate for the #395 soak test, and verified independently. ## Measured `build/mcrogueface --headless`, 2000-iteration warmup, `gc.collect()` before and after, 20000 assignments: ``` entity.cell_pos = (x, y) N= 20000 blocks 23373 -> 43375 growth= 20002 per-call=1.0001 frame.x = float (control) N= 20000 blocks 219394 -> 219396 growth= 2 per-call=0.0001 entity.cell_pos read (control) N= 20000 blocks 219395 -> 219397 growth= 2 per-call=0.0001 ``` Exactly one `sys.getallocatedblocks()` per assignment, perfectly linear. **Reading** `cell_pos` is clean, and an equivalent float-property assignment (`Frame.x`) is clean, so it is specific to the `cell_pos` **setter** — `UIEntity::set_cell_pos`, `src/UIEntity.cpp`. Values were varied across the run, so it is not a small-int / interned-tuple artefact. ## Why it matters `cell_pos` is *the* entity-movement API. A single entity moving once per frame at 60 fps leaks ~216 000 objects per hour; a scenario with fifty entities moving on a turn loop leaks proportionally faster. This is the shape that presents as "the process grows until it is killed" after a long unattended run, with nothing pointing at the cause. RSS will not show it early: one leaked object per call is roughly 300 KB over 5000 calls, which the allocator can satisfy from arenas it already holds. A previous reviewer on this repo demonstrated exactly that — a real ~320 KB leak sitting invisible under a 4096 KB RSS threshold. `sys.getallocatedblocks()` sees it immediately. ## Not diagnosed further here I measured it rather than root-caused it; the setter's conversion path (`PyPosition`/`sfVector2i` handling, and whatever it builds for the position-change notification) is where to look. Filed rather than fixed because it needs its own refcount audit of that path and belongs with a regression gate, not folded into a frame-barrier task. A gate in the style of `tests/integration/frame_lock_soak_test.py` section 4 is the right shape: `getallocatedblocks()` across N assignments, gated well under 1 block per call, with a clean control property alongside so the gate cannot pass by measuring nothing. Sibling: #413 (`mcrfpy.get_metrics()` leaks 8 blocks per call). Related: #395.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
john/McRogueFace#414
No description provided.