Skip to content

MLP226: Frame-varying Text/TeX cache key in a per-frame callback

qual reports MLP226 when a Text / MarkupText / MathTex / Tex constructor inside a proven per-frame context takes an f-string key — so the cache key provably varies per frame and K_resource ≈ F.

Each invocation performs object construction and a cache-key lookup; a frame-varying key defeats the cache, so each frame pays a cache miss, and for the TeX classes each distinct key also launches the external TeX compiler and dvisvgm, leaving roughly one disk asset per rendered frame.

  • Default severity: warning
  • Minimum confidence: high
  • Implementation phase: 3
  • Fix: none — DecimalNumber + set_value is suggested in the explanation (it changes typography)
  • Supersedes MLP201 on the same span: the same construction carries only this more specific diagnostic

The distinct-key count is bound to real frame numbers summed over the plays where the callback provably executes (e.g. "may create ~480 distinct keys" for one proven 8 s play at 60 fps); maybe plays widen only the upper bound, and without literal durations the evidence stays "per-frame" — never a fabricated number. A static key in a hot context is MLP201 territory (the cache absorbs the compile, construction remains), and a key whose variation cannot be proven (concatenation, .format) falls back to MLP201 as well.

Proven execution (liveness)

A hot context only proves the construction 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, the host is in the scene family, the host is not suspended (Animation.begin() suspends the animated mobject's updaters for the whole play unless suspend_mobject_updating=False was passed), and the play actually runs callbacks (a play(...), or a provably dynamic wait — a one-argument always_redraw updater leaves the default wait() static).

The canonical near miss stays silent:

label = always_redraw(lambda: MathTex(f"x = {tracker.get_value():.2f}"))
self.add(label)
self.play(FadeIn(label), run_time=2)  # begin() suspends label's updaters
self.wait(3)                          # static: no dt updater, no scene updater

The callback here runs about once, not ~120 times — no per-frame key claim is made (the scene's real defect, a frozen tracker-driven updater, is MLC112 territory). Evidence lists the counted plays (execution) so the claim is auditable; only-maybe evidence 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)