Skip to content

MLP203: family-walk query of a confirmed-large mobject in a per-frame callback

qual reports MLP203 when a family-walk / geometry query (get_family, family_members_with_points, get_all_points, get_arc_length, proportion_from_point) runs inside a per-frame callback on a receiver whose family is confirmed large: every call recomputes its result from all family members, multiplying that walk by the frame count.

  • Default severity: info
  • Minimum confidence: high
  • Implementation phase: 3
  • Fix: none
  • Superseded by MLP224 for point_from_proportion (DESIGN §7.3 specificity: those entries are routed to the long-path rule and never reported here)

Emission gate (cited in evidence): N_family >= 32 (large-family-gate) — once the large family is confirmed, no repeated-query analysis is required (DESIGN §7.3). A single next_to / get_center on a small Dot — the catalog near-miss — never fires: those methods are not family-walk queries, and small or unproven families never confirm the gate. Receiver resolution uses the updater-lambda-host contract (see MLP202); 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 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):
        anchor = Square()
        group = VGroup(*[Square() for _ in range(40)])
        group.add_updater(lambda m: m.move_to(m.get_family()[0]))  # full walk per frame
        self.add(group, anchor)
        self.play(FadeIn(anchor), run_time=2)  # group stays live: its updater runs
class Demo(Scene):
    def construct(self):
        group = VGroup(*[Square() for _ in range(40)])
        members = group.get_family()  # walked once, outside the callback
        group.add_updater(lambda m: m.shift(RIGHT * 0.01))
        self.add(group)
        self.play(FadeIn(group), run_time=2)