[Bugfix] mcrfpy.get_metrics() leaks 8 allocator blocks per call (PyDict_SetItemString does not steal) #413
Labels
No labels
Alpha Release Requirement
Bugfix
Demo Target
Documentation
Major Feature
Minor Feature
priority:tier1-active
priority:tier2-foundation
priority:tier3-future
priority:tier4-deferred
Refactoring & Cleanup
system:animation
system:documentation
system:grid
system:input
system:performance
system:procgen
system:python-binding
system:rendering
system:ui-hierarchy
Tiny Feature
workflow:blocked
workflow:needs-benchmark
workflow:needs-documentation
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
john/McRogueFace#413
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found while calibrating the allocator gate for the #395 soak test, and verified independently.
Measured
build/mcrogueface --headless, 2000-call warmup,gc.collect()before and after, 20000 calls:Exactly 8
sys.getallocatedblocks()per call, perfectly linear, with clean controls either side. Never reclaimed.The defect
McRFPy_API::_getMetrics(src/McRFPy_API.cpp):PyDict_SetItemStringdoes not steal a reference — it increfs the value. EveryPyFloat_FromDouble/PyLong_FromLonghere is a new reference that is never released, so each one leaks. It is 8 rather than 17 because the small-int cache serves the values in [-5, 256] from singletons, so leaking those costs a refcount but no allocator block; the floats and the out-of-cache ints are real allocations. (That also means the rate varies with the values: a long-running process withcurrent_frame > 256leaks more per call than a fresh one.)Two return values are also unchecked for
NULL, andPyDict_SetItemString's own return is discarded throughout.Why it matters more than it looks
get_metrics()is the natural thing to call once per frame from a monitor, a HUD, or a test harness. At 60 fps that is ~1.7 million leaked objects per hour. Several tests already call it in a loop —tests/integration/observe_under_frame_lock_test.pydoes — and it had to be kept out of the measured region oftests/integration/frame_lock_soak_test.pyprecisely because it would have swamped that file's allocator gate.Fix
Either hold each value in a temporary and
Py_DECREFafter the set, or build the dict withPy_BuildValue("{s:d, s:d, s:i, ...}")which handles ownership. Check the returns while there. A regression gate belongs with it:sys.getallocatedblocks()across N calls, gated well under 1 block per call, in the style oftests/integration/frame_lock_soak_test.pysection 4 — RSS is far too coarse to see this.Sibling: #414 (
entity.cell_posassignment leaks 1 block). Related: #395.