Skip to content

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 MLP226 when 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 with suspend_mobject_updating=False,
  • the play actually runs callbacks: a play(...), or a wait that is provably dynamic (scene.py should_update_mobjects). A one-argument updater leaves the default wait() 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)
class Demo(Scene):
    def construct(self):
        tracker = ValueTracker(0)
        label = DecimalNumber(0)
        label.add_updater(lambda m: m.set_value(tracker.get_value()))
        self.add(label)
        self.play(tracker.animate.set_value(8), run_time=8)