[Performance] GridView shim's get_grid re-allocates a Grid wrapper + weakref on every call (100% cache miss) #348
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#348
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?
Summary
Discovered via Callgrind (the #345 profiling rig), not from reading structure — this is an emergent cost of the #252 GridView shim.
mcrfpy.Gridis a GridView shim overGridData. Internal code that needs the underlyingUIGridcallsUIGridView::get_grid(src/UIGridView.cpp). That function:shared_ptr<UIGrid>fromgrid_data.PythonObjectCacheby serial number.tp_allocs a temporaryPyUIGridObjectwrapper, allocates a weakref to it viaPyWeakref_NewRef, andregisterObjects that weakref in the cache.The caller uses the wrapper and drops it → the wrapper is freed → the cached weakref immediately dies → the next
get_gridcall misses and re-allocates the entire wrapper + weakref + hashtable entry. The cache can never hit because it only ever holds a weakref to a short-lived temporary.Evidence (Callgrind, full-loop workload)
get_gridcalled 3,600× in the workload; callee counts inside it:PythonObjectCache::lookupPyWeakref_NewRefPythonObjectCache::registerObjectLookups == registrations == weakref allocations == call count → 0% cache hit rate. Inclusive cost ~3.55M Ir (~987 Ir/call), all of it wrapper alloc + weakref alloc + hashtable insert/erase churn, thrown away each call.
Impact
Hits any code that derives the grid repeatedly through the shim:
cell_posaccessInvisible from project structure — none of #342/#343/#344 would surface it. Same class of fix as #331: stop re-doing per-call ceremony on a hot path.
Proposed fix (direction)
Avoid reconstructing a throwaway wrapper per call. Options:
UIGridwrapper (own it for the shim's lifetime) instead of a weakref-to-temporary, orPyObject*wrapper onGridData/UIGridand return it (INCREF) without re-allocating, orWatch the ownership cycle (Grid ↔ wrapper) — a strong ref needs a break path (the existing tp_dealloc
use_count()guard pattern, #251).Benchmark
Reproduce with a per-cell loop (
grid.at()/ layerset()over a large grid) undermake callgrind; compareget_gridinclusive Ir andregisterObject/PyWeakref_NewRefcall counts before/after. Cache-hit path should drop weakref allocations to ~1.Notes
make profile+ Callgrind workflow (#345).automation.screenshot()spends ~96% of instructions in PNG encoding (libsfmlstbi_zlib_compress) — profile update/animation withstep()-only to avoid that.Related: #331, #345, #252 (GridView shim), #251 (dealloc ownership guard).