MLP211: large per-frame allocation inside a per-frame callback¶
qual reports MLP211 when a call inside a per-frame callback
provably allocates a large buffer on every invocation, multiplying
allocator and memory-bandwidth cost by the frame count.
- Default severity:
info - Minimum confidence:
medium - Implementation phase:
3 - Fix: none
- Superseded by
MLP220on the same span (declared byMLP220, DESIGN §7.3)
Provable arms (DESIGN §7.3, gates cited in evidence):
- a recognized ndarray construction (
numpy.zeros/ones/empty/full) whose literal shape proves at least 64 KiB per invocation (per-frame-allocation-gate). Literal integer lengths and literal tuple / list shapes (np.zeros((400, 400))) are analyzable; the element size comes from the literaldtypeor NumPy'sfloat64default. Non-literal shapes and unknown dtypes stayUnknownand silent — never guessed. Small coordinate vectors are excluded by the byte gate itself. - a hot call whose resolved target object has a confirmed point
count of at least 1024 (
large-points-gate); calls already owned by the receiver-sized rules (MLP202/MLP203/MLP224) are excluded so one call yields one diagnostic.
The catalog's third arm — any allocation inside a loop over the family within a hot callback — is out of scope: hot-context provenance records entry points and helper calls, not enclosing loops inside callback bodies, so "family loop" containment is not provable from the current fact layers.
Proven execution (liveness)¶
A hot context only proves the call is reachable from a per-frame entry point. The diagnostic additionally requires at least one play where the callback provably executes per frame (DESIGN 3.2/3.3):
- the registration is active at the play (registered on every path and not removed before it),
- the host is in the scene family there,
- the host is not suspended:
Animation.begin()suspends the animated mobject's updaters — recursively over its family — for the whole play (animation.py), unless the animation was constructed withsuspend_mobject_updating=False, - the play actually runs callbacks: a
play(...), or a wait that is provably dynamic (scene.pyshould_update_mobjects). A one-argument updater leaves the defaultwait()a static frame.
self.add(label); self.play(FadeIn(label), run_time=2); self.wait(3)
therefore stays silent: the only play suspends the updater and the wait
is static, so the callback runs about once — never once per frame.
Evidence lists the counted plays (execution) so the claim is
auditable. A callback that provably never runs per frame is silent; when
execution is proven the message states the per-frame claim; when it is
only possible (maybe plays, or an entry the lifecycle facts cannot
resolve) the rule still fires at its medium catalog confidence with
qualitative wording — the allocation size itself is statically proven
either way.
Wrong¶
class Demo(Scene):
def construct(self):
anchor = Circle()
sq = Square()
sq.add_updater(lambda m: m.move_to(np.zeros((400, 400))[0])) # 1.2 MiB per frame
self.add(sq, anchor)
self.play(FadeIn(anchor), run_time=2) # sq stays live: its updater runs