Skip to content

MLP217: Frame-varying key with use_svg_cache=True grows the process-global SVG cache

qual reports MLP217 when an SVG-cached constructor (SVGMobject, Text, MarkupText, MathTex, SingleStringMathTex, Tex) runs inside a proven per-frame callback with a provably frame-varying key (an f-string) while use_svg_cache is provably enabled: every rendered frame then inserts a new entry — a deep copy of the whole constructed family — into the process-global SVG cache, which is never evicted. Memory grows O(F × family) for the life of the process.

  • Default severity: warning
  • Minimum confidence: high
  • Implementation phase: 3
  • Required capabilities: qualified-calls, cost-facts, local-fork-overlay
  • Fix: none — pass use_svg_cache=False for frame-varying keys or hoist the construction; both are suggested in the explanation

Profile gate (inert under upstream_0_20)

Per DESIGN 7.3 the rule is enabled only when the selected knowledge profile declares the process-global SVG cache semantics: KnowledgeProfile::svg_cache() must return a fact block with process_global: true and unbounded: true (the local_0_20_1_4d25c031 overlay curates SVG_HASH_TO_MOB_MAP from svg_mobject.py, including its key components and copy-on-hit behavior). The map happens to exist upstream too, but upstream_0_20 declares nothing — the accessor returns None and the rule is fully inert, so the memory-growth claim only ever rests on curated semantics.

Cache-flag resolution (conservative)

The flag is the real use_svg_cache parameter of the fork's SVGMobject.__init__ (verified in the fork source):

  • an explicit literal use_svg_cache=True / False wins for every class;
  • absent the keyword, the class default applies: the fork's Text defaults to False, every other cached class forwards to SVGMobject's default True;
  • a non-literal flag value or a **kwargs splat (which can hide the keyword) leaves the flag unknown — silence, never a guess.

Relationship to MLP226 / MLP201

The DESIGN 7.3 supersedes table orders neither rule above the other, because they claim different defects on the same span: MLP226 reports per-frame construction cost and distinct Text/TeX disk assets (K_resource ≈ F), MLP201 generic per-frame construction cost, while MLP217 reports unbounded process-global memory growth. A frame-varying Text/TeX key with the cache enabled therefore carries both MLP217 and MLP226; a frame-varying SVGMobject key carries MLP217 and MLP201. Suppressing one never silences the other defect.

Liveness and quantification

Like MLP226, the rule requires at least one play where the callback provably executes per frame (registration active, host in the scene family, not suspended by the play's own animations, dynamic wait). The retained-entry count is bound to real frame numbers summed over those proven plays only ("may retain ~480 cache entries" for one proven 8 s play at 60 fps); without literal durations the evidence stays "per-frame" — never a fabricated number.

Example

tracker = ValueTracker(0)
badge = always_redraw(
    lambda: SVGMobject(f"badge_{tracker.get_value():.0f}.svg")  # MLP217
)
label = always_redraw(
    lambda: Text(f"t = {tracker.get_value():.2f}", use_svg_cache=True)  # MLP217 + MLP226
)
self.add(badge, label)
self.play(tracker.animate.set_value(1), run_time=8)

Silent near misses: the fork's Text without the explicit flag (its default is False); use_svg_cache=False; a static key (one cache entry, reused every frame — that is the cache working); a non-literal flag or **kwargs splat; any profile that does not declare the process-global cache semantics.