# Failure modes: runtime allocation and caching Ten ways a running game allocates more each minute, keeps what it should drop, or dereferences memory that moved. The shared property splits this list in two, and the split is worth stating because it changes how you look for each half. **The dangling-pointer half is data-dependent.** It needs a second key inserted while a first entry is still live, or a new statistic to appear mid-session. Neither happens in a short test, both happen in a match, and the crash lands far from the cause. A null check never catches it, because the pointer is not null. **The growth half is time-dependent.** Nothing is wrong at any instant; the defect is a slope. A single memory sample cannot show it and a single frame cannot contain it, which is why every recipe below asks for two measurements rather than one. Recipes use `rg` from a project source root, and were executed against the audited project while this file was written. --- ## Pointers into moving storage ### RC-01 - A raw pointer into a map value, held across frames **Mechanism.** A reference to a map value is taken, its address is stored in a long-lived entry, and the map later gains a key. The map reallocates; the stored address points into freed memory. **Why it is silent.** Until a second key arrives, the address stays valid and everything works. The window is narrow and specific — a live entry plus an insert — so ordinary play produces it and ordinary testing does not. **Why the obvious check misses it.** The code often *has* a check, and the check passes: the pointer is not null, it is non-null garbage. Reading the storing line shows a reference to a pool, which is exactly what the entry needs. The defect is a property of the container's reallocation behaviour, which is nowhere in view. **Symptom.** A crash inside the pooled object's own method, under load, with a stack that looks like the pool is corrupt. In the audited project the window is a one-second component lifespan plus a second visual style — trivially produced by mixed normal and critical damage in the same second. **Detect.** Find addresses taken from containers and stored: ```bash rg -n -B2 -A4 "FindOrAdd\(|\.Find\(" --glob "*.cpp" . | rg "&\w+|Emplace\(|= &" rg -n "\w+\*\s+\w+ = nullptr;" --glob "*.h" . | rg -i "pool|cache|list|entry" ``` The second search finds the *declaration* side — a bare pointer member whose name suggests it points at a collection — which is often easier to spot than the assignment. **Guardrail.** Store a key or an index and resolve at use. If a stable address is genuinely required, use a container that guarantees stable element addresses and say so at the declaration. When you suspect one, run under the allocator's stomp mode: it converts a probabilistic corruption into a deterministic crash on the right line, and costs one run. --- ### RC-02 - A cache pointer handed out and stored by a widget **Mechanism.** A subsystem returns a pointer into its own map so a widget can read samples cheaply. The widget stores it. The map grows a key when a new data source appears — a network connection, an optional module — and reallocates. **Why it is silent.** The optimization is real and the pointer is valid at hand- out. Growth is lazy and event-driven: the map gains keys minutes into a session, when a connection is established or a feature is enabled, long after the widget cached its pointer. **Why the obvious check misses it.** Both sides are idiomatic. Returning a pointer to avoid copying a sample buffer is good practice; caching it to avoid a lookup per frame is also good practice. Neither side knows the map is still growing, and nothing in either signature says the pointer has a lifetime. **Symptom.** A crash in the widget's paint path, appearing only after the session has been running long enough to acquire a new statistic — and therefore never in a short repro. Compounded when the subsystem's teardown resets the tracker while the widget still holds the pointer. **Detect.** Find getters returning interior pointers, then find who stores them: ```bash rg -n "const \w+\* \w+::Get\w*Data\(" --glob "*.cpp" . -A4 | rg "\.Find\(" rg -n "const \w+\* \w+ = nullptr;" --glob "*.h" . | rg -i "widget|graph|view" ``` A getter returning `Find` on a member map, plus a member pointer of that type in a widget, is the pair. **Guardrail.** Return a copy of the small value, or a handle the subsystem can invalidate, or require the caller to re-fetch each frame. If a pointer must escape, give the subsystem an invalidation broadcast and make subscribing mandatory. --- ## Unbounded growth ### RC-03 - Read, append, write back — with no clear **Mechanism.** Each event fetches an entire collection from another system, appends one element, and writes the whole thing back. Nothing ever clears it. **Why it is silent.** Every individual call is correct and fast. The array is a legitimate data channel to the effects system, and its contents are consumed visually, so nobody looks for a removal path. **Why the obvious check misses it.** The two lines read as an idiomatic accessor pair. The cost — two full copies per event — is invisible at small sizes, and the growth is invisible without a second measurement. Reviewing the function shows three lines of correct code. **Symptom.** Cost per event rising through a match, total work quadratic in event count, and memory that never returns. In a damage-number path this means the hundredth hit costs measurably more than the first. **Detect.** Find get-append-set triples on the same key: ```bash rg -n -A3 "= \w*::Get\w*Array\w*\(" --glob "*.cpp" . | rg "Add\(|SetNiagara|Set\w*Array" ``` Then, for each hit, search the file for any clear: ```bash rg -n "Empty\(\)|Reset\(\)|SetNum\(0\)|Clear" ``` An append path with no clear anywhere in the file is the finding. **Guardrail.** Give the collection an owner and an eviction rule — a cap, a time-out, or a clear on the event that logically ends its contents. If the consuming system owns the lifetime, ask it for a removal API rather than assuming one. --- ### RC-04 - Re-append every previous element on every trigger **Mechanism.** A component that spawns audio and effect components copies all previously active ones into temporary arrays, appends the new ones, and writes the union back — with no removal of finished components. **Why it is silent.** Effects play and stop correctly, because stopping is the component's own business. What accumulates is the *references*, and those are invisible until something counts them. **Why the obvious check misses it.** The function reads as careful state management: gather, combine, store. The absent operation is a filter for finished components, and absence in a function that is otherwise thorough is the hardest kind to see. **Symptom.** Thousands of retained effect components after a match. Because the arrays are strong references, none of them can be collected, so this is simultaneously a memory leak and a quadratic allocation profile in the hottest cosmetic path — footsteps, at a few per second. **Detect.** Find union-rebuild patterns and check for a prune: ```bash rg -n -B4 -A6 "\.Append\(" --glob "*.cpp" . | rg "Empty\(\)|Reset\(\)" rg -n "UPROPERTY\(Transient\)" -A2 --glob "*.h" . | rg "TArray