MLP201: Expensive construction inside a per-frame callback¶
qual reports MLP201 when a Text, MarkupText, MathTex, Tex,
SVGMobject, ImageMobject, or Surface constructor runs inside a proven
per-frame context: an updater callback, an always_redraw factory, an
UpdateFromFunc update function, a stop_condition, a
TracedPath point function, or a project Animation.interpolate* override
(including project helpers those callbacks call).
Construction of these classes includes shaping, SVG generation and parsing, and family construction — and for the TeX classes a cache lookup whose miss launches the external TeX compiler. Inside a per-frame callback that cost is multiplied by the rendered frame count.
- Default severity:
warning - Minimum confidence:
high - Implementation phase:
3 - Fix: none — the common alternative (
DecimalNumber+set_value) changes typography, so it is suggested in the explanation only - Superseded by
MLP226when the cache key is a provably frame-varying f-string: the same span then carries only the more specific diagnostic
A construction executed once in construct never fires, and a callback
whose identity cannot be statically resolved contributes no hotness
(silence over a false positive). Cheap constructors (Dot(), Square())
are not diagnosed on hotness alone (DESIGN §4.4).
Evidence carries the invocation context, multiplicity factors, and frame bounds; an unknown duration stays "per-frame" and never becomes a number.
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):
tracker = ValueTracker(0)
label = always_redraw(lambda: MathTex(f"x={tracker.get_value():.2f}"))
self.add(label)
self.play(tracker.animate.set_value(8), run_time=8)