MLP202: copy/become/align of a confirmed-large mobject in a per-frame callback¶
qual reports MLP202 when a copy / generate_target / become /
restore / align_data / align_points / insert_n_curves call runs
inside a per-frame callback and the receiver's family or point count is
confirmed large: each invocation walks the receiver's entire family
(deep copy, in-place rewrite, or topology alignment), so the cost scales
with N_family / P × the frame count.
- Default severity:
warning - Minimum confidence:
high - Implementation phase:
3 - Fix: none
Emission gates (cited in evidence, DESIGN §7.3): N_family >= 32
(large-family-gate) or P >= 1024 (large-points-gate) — only a
proven lower bound confirms a gate; Unknown, symbolic, and
open-below interval sizes never fire the rule. The implicit per-frame
become inside always_redraw belongs to MLP201 / MLP216 and is
never double-reported here.
Scope: the receiver is resolved via the updater-lambda-host contract —
the callee must be exactly <param>.<method> directly inside a
Mobject.add_updater lambda, so the receiver is provably the updater
host. Named-function callbacks and other receivers stay silent.
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; quantities sum over the proven plays only, and only-maybe
evidence (a branch-dependent registration, an unknown animation that may
suspend the host) never reaches this rule's high confidence.
Wrong¶
class Demo(Scene):
def construct(self):
anchor = Square()
group = VGroup(*[Square() for _ in range(40)]) # large family
group.add_updater(lambda m: m.copy()) # whole-family deep copy per frame
self.add(group, anchor)
self.play(FadeIn(anchor), run_time=2) # group stays live: its updater runs
(The loop above is illustrative; the analyzer confirms the gate for straight-line construction, and widens loop-built families to open intervals that stay silent.)