Skip to content

MLP215: provably no-op updater still costs per-frame work

qual reports MLP215 when a registered updater's body provably does nothing — it never mutates its parameter, never writes captured state, and calls nothing unresolved (mutates_target == No, calls_unknown == No, and the pure-effect verdict holds) — yet still costs per-frame work through one of two separate evidence channels (DESIGN 7.3):

  • (a) dynamicized waits — a dt-parameter mobject updater or a Scene updater makes waits dynamic (DESIGN 3.3). The rule attributes a wait only when the updater is provably the sole dynamicizing source: no stop condition, always_update_mobjects off, no other scene updater, no other possibly-time-based family updater, and no unknown or intrinsically per-frame object in the family.
  • (b) moving scope — any family updater makes its host a moving member of a play's Cairo scope (scene.py get_moving_mobjects; suspension does not matter there). Quantified with the display-order position only when the host is the exact first moving member of a Known order; qualitative otherwise.

  • Default severity: warning

  • Minimum confidence: high
  • Implementation phase: 3
  • Fix: none (removal is the user's call; the body may be a stub)

Binding catalog near miss: a one-argument no-op updater does not dynamicize a plain wait — with no play in sight the rule stays silent.

Wrong

class Demo(Scene):
    def construct(self):
        square = Square()
        self.add(square)
        square.add_updater(lambda m, dt: None)   # no-op, but dt-based
        self.wait(3)                              # renders every frame
class Demo(Scene):
    def construct(self):
        square = Square()
        self.add(square)
        self.wait(3)                              # frozen wait