manim
manim copied to clipboard
Investigate large memory usage
Description of bug / unexpected behavior
The following scene:
from manim import *
class Frac(Scene):
def construct(self):
tri = RegularPolygon(n=3, fill_opacity=1, stroke_opacity=0, fill_color=GREEN)
tri.scale(2)
a, b, c = tri.get_vertices()
tri.scale(2)
self.play(FadeIn(tri))
for it in range(7):
A, B, C = tri, tri.copy(), tri.copy()
if it == 0:
B.set_color(BLUE)
C.set_color(RED)
self.play(
AnimationGroup(
C.animate.move_to(c).scale(0.5),
B.animate.move_to(b).scale(0.5),
A.animate.move_to(a).scale(0.5),
lag_ratio=0.5
)
)
tri = VGroup(C, B, A)
tiny_triangles = sorted(tri.family_members_with_points(), key=lambda mob: mob.fill_color.get_hex())
self.play(VGroup(*tiny_triangles).animate(lag_ratio=0.25).set_opacity(0), run_time=3)
self.wait(0.5)
uses a large amount of memory. It's worth investigating how exactly this memory is used, and how it can be reduced.
Expected behavior
How to reproduce the issue
Code for reproducing the problem
Paste your code here.
Additional media files
Images/GIFs
Logs
Terminal output
PASTE HERE OR PROVIDE LINK TO https://pastebin.com/ OR SIMILAR
System specifications
System Details
- OS (with version, e.g., Windows 10 v2004 or macOS 10.15 (Catalina)):
- RAM:
- Python version (
python/py/python3 --version
): - Installed modules (provide output from
pip list
):
PASTE HERE
LaTeX details
- LaTeX distribution (e.g. TeX Live 2020):
- Installed LaTeX packages:
FFMPEG
Output of ffmpeg -version
:
PASTE HERE
Additional comments
Filename: bench.py
Line # Mem usage Increment Occurrences Line Contents
=============================================================
6 96.6 MiB 96.6 MiB 1 @profile
7 def construct(self):
8 96.6 MiB 0.0 MiB 1 tri = RegularPolygon(n=3, fill_opacity=1, stroke_opacity=0, fill_color=GREEN)
9 96.6 MiB 0.0 MiB 1 tri.scale(2)
10 96.6 MiB 0.0 MiB 1 a, b, c = tri.get_vertices()
11 96.6 MiB 0.0 MiB 1 tri.scale(2)
12 98.3 MiB 1.8 MiB 1 self.play(FadeIn(tri))
13 348.0 MiB 0.0 MiB 7 for it in range(6):
14 175.2 MiB 42.0 MiB 6 A, B, C = tri, tri.copy(), tri.copy()
15 175.2 MiB 0.0 MiB 6 if it == 0:
16 98.3 MiB 0.0 MiB 1 B.set_color(BLUE)
17 98.3 MiB 0.0 MiB 1 C.set_color(RED)
18 348.0 MiB 149.9 MiB 12 self.play(
19 223.4 MiB 0.0 MiB 12 AnimationGroup(
20 191.5 MiB 19.6 MiB 6 C.animate.move_to(c).scale(0.5),
21 207.4 MiB 18.8 MiB 6 B.animate.move_to(b).scale(0.5),
22 223.4 MiB 19.3 MiB 6 A.animate.move_to(a).scale(0.5),
23 223.4 MiB 0.0 MiB 6 lag_ratio=0.5
24 )
25 )
26 348.0 MiB 0.0 MiB 6 tri = VGroup(C, B, A)
27
28 348.0 MiB 0.0 MiB 1459 tiny_triangles = sorted(tri.family_members_with_points(), key=lambda mob: mob.fill_color.get_hex())
29 303.2 MiB -44.8 MiB 1 self.play(VGroup(*tiny_triangles).animate(lag_ratio=0.25).set_opacity(0), run_time=3)
30 310.1 MiB 6.9 MiB 1 self.wait(0.5)
First memory profile output
Protocol
First checking with the memory_profiler
module gave the following result with the cairo renderer.
Filename: bench.py
Line # Mem usage Increment Occurrences Line Contents
=============================================================
6 96.8 MiB 96.8 MiB 1 @profile
7 def construct(self):
8 96.8 MiB 0.0 MiB 1 tri = RegularPolygon(n=3, fill_opacity=1, stroke_opacity=0, fill_color=GREEN)
9 96.8 MiB 0.0 MiB 1 tri.scale(2)
10 96.8 MiB 0.0 MiB 1 a, b, c = tri.get_vertices()
11 96.8 MiB 0.0 MiB 1 tri.scale(2)
12 106.1 MiB 9.3 MiB 1 self.play(FadeIn(tri))
13 356.1 MiB 0.0 MiB 7 for it in range(6):
14 175.7 MiB 32.0 MiB 6 A, B, C = tri, tri.copy(), tri.copy()
15 175.7 MiB 0.0 MiB 6 if it == 0:
16 106.1 MiB 0.0 MiB 1 B.set_color(BLUE)
17 106.1 MiB 0.0 MiB 1 C.set_color(RED)
18 356.1 MiB 163.6 MiB 12 self.play(
19 223.7 MiB 0.0 MiB 12 AnimationGroup(
20 192.0 MiB 18.6 MiB 6 C.animate.move_to(c).scale(0.5),
21 208.0 MiB 18.0 MiB 6 B.animate.move_to(b).scale(0.5),
22 223.7 MiB 17.8 MiB 6 A.animate.move_to(a).scale(0.5),
23 223.7 MiB 0.0 MiB 6 lag_ratio=0.5
24 )
25 )
26 356.1 MiB 0.0 MiB 6 tri = VGroup(C, B, A)
27
28 356.1 MiB 0.0 MiB 1459 tiny_triangles = sorted(tri.family_members_with_points(), key=lambda mob: mob.fill_color.get_hex())
29 312.5 MiB -43.6 MiB 1 self.play(VGroup(*tiny_triangles).animate(lag_ratio=0.25).set_opacity(0), run_time=3)
30 319.0 MiB 6.6 MiB 1 self.wait(0.5)
Following the memory usage to the play call gives the following result for the last call of the for loop
Filename: /home/user/Desktop/Python/manim.git/branches/memory_consumption/manim/scene/scene.py
Line # Mem usage Increment Occurrences Line Contents
=============================================================
983 223.9 MiB 223.9 MiB 1 def play(
984 self,
985 *args,
986 subcaption=None,
987 subcaption_duration=None,
988 subcaption_offset=0,
989 **kwargs,
990 ):
1012 start_time = self.renderer.time
1013 223.9 MiB 0.0 MiB 1 self.renderer.play(self, *args, **kwargs)
1014 356.1 MiB 132.1 MiB 1 run_time = self.renderer.time - start_time
1015 356.1 MiB 0.0 MiB 1 if subcaption:
1016 356.1 MiB 0.0 MiB 1 if subcaption_duration is None:
1017 subcaption_duration = run_time
1018 # The start of the subcaption needs to be offset by the
1019 # run_time of the animation because it is added after
1020 # the animation has already been played (and Scene.renderer.time
1021 # has already been updated).
1022 self.add_subcaption(
1023 content=subcaption,
1024 duration=subcaption_duration,
1025 offset=-run_time + subcaption_offset,
1026 )
Let's check if the outpout might be misleading and profile self.renderer.play
because a variable assignment shouldn't take 132 MiB of space
Filename: /home/user/Desktop/Python/manim.git/branches/memory_consumption/manim/renderer/cairo_renderer.py
Line # Mem usage Increment Occurrences Line Contents
=============================================================
52 223.4 MiB 223.4 MiB 1 @profile
53 def play(self, scene, *args, **kwargs):
54 # Reset skip_animations to the original state.
55 # Needed when rendering only some animations, and skipping others.
56 223.4 MiB 0.0 MiB 1 self.skip_animations = self._original_skipping_status
57 223.4 MiB 0.0 MiB 1 self.update_skipping_status()
58
59 223.4 MiB 0.0 MiB 1 scene.compile_animation_data(*args, **kwargs)
60
61 223.4 MiB 0.0 MiB 1 if self.skip_animations:
62 logger.debug(f"Skipping animation {self.num_plays}")
63 hash_current_animation = None
64 self.time += scene.duration
65 else:
66 223.4 MiB 0.0 MiB 1 if config["disable_caching"]:
67 223.4 MiB 0.0 MiB 1 logger.info("Caching disabled.")
68 223.4 MiB 0.0 MiB 1 hash_current_animation = f"uncached_{self.num_plays:05}"
69 else:
70 hash_current_animation = get_hash_from_play_call(
71 scene,
72 self.camera,
73 scene.animations,
74 scene.mobjects,
75 )
76 if self.file_writer.is_already_cached(hash_current_animation):
77 logger.info(
78 f"Animation {self.num_plays} : Using cached data (hash : %(hash_current_animation)s)",
79 {"hash_current_animation": hash_current_animation},
80 )
81 self.skip_animations = True
82 self.time += scene.duration
84 223.4 MiB 0.0 MiB 1 self.file_writer.add_partial_movie_file(hash_current_animation)
85 223.4 MiB 0.0 MiB 1 self.animations_hashes.append(hash_current_animation)
86 223.4 MiB 0.0 MiB 2 logger.debug(
87 223.4 MiB 0.0 MiB 1 "List of the first few animation hashes of the scene: %(h)s",
88 223.4 MiB 0.0 MiB 1 {"h": str(self.animations_hashes[:5])},
89 )
90
91 223.4 MiB 0.0 MiB 1 self.file_writer.begin_animation(not self.skip_animations)
92 348.7 MiB 125.3 MiB 1 scene.begin_animations()
93
95 348.7 MiB 0.0 MiB 1 self.static_image = self.save_static_frame_data(scene, scene.static_mobjects)
96
97 348.7 MiB 0.0 MiB 1 if scene.is_current_animation_frozen_frame():
98 self.update_frame(scene, mobjects=scene.moving_mobjects)
101 self.freeze_current_frame(scene.duration)
102 else:
103 355.7 MiB 7.0 MiB 1 scene.play_internal()
104 355.7 MiB 0.0 MiB 1 self.file_writer.end_animation(not self.skip_animations)
105
106 355.7 MiB 0.0 MiB 1 self.num_plays += 1
We seem to have two little leaks in scene.begin_animations()
and scene.play_internal()
, we will investigate what happens here later on.
Let's look at scene.begin_animations()
first
Filename: /home/user/Desktop/Python/manim.git/branches/memory_consumption/manim/scene/scene.py
Line # Mem usage Increment Occurrences Line Contents
=============================================================
1140 223.6 MiB 223.6 MiB 1 @profile()
1141 def begin_animations(self) -> None:
1142 """Start the animations of the scene."""
1143 348.9 MiB 0.0 MiB 2 for animation in self.animations:
1144 223.6 MiB 0.0 MiB 1 animation._setup_scene(self)
1145 348.9 MiB 125.3 MiB 1 animation.begin()
1147 348.9 MiB 0.0 MiB 1 if config.renderer != "opengl":
1150 349.2 MiB 0.0 MiB 1 (
1151 349.2 MiB 0.0 MiB 1 self.moving_mobjects,
1152 349.2 MiB 0.0 MiB 1 self.static_mobjects,
1153 349.2 MiB 0.3 MiB 1 ) = self.get_moving_and_static_mobjects(self.animations)
Oh dear it seems to be the animation.begin()
call.
Filename: /home/user/Desktop/Python/manim.git/branches/memory_consumption/manim/animation/animation.py
Line # Mem usage Increment Occurrences Line Contents
=============================================================
186 313.1 MiB 313.1 MiB 1 @profile()
187 def begin(self) -> None:
195 348.7 MiB 35.6 MiB 1 self.starting_mobject = self.create_starting_mobject()
196 348.7 MiB 0.0 MiB 1 if self.suspend_mobject_updating:
203 348.7 MiB 0.0 MiB 1 self.mobject.suspend_updating()
204 348.7 MiB 0.0 MiB 1 self.interpolate(0)
Well as probably expected with manim here we have our issue
Filename: /home/user/Desktop/Python/manim.git/branches/memory_consumption/manim/animation/animation.py
Line # Mem usage Increment Occurrences Line Contents
=============================================================
252 313.1 MiB 313.1 MiB 1 @profile()
253 def create_starting_mobject(self) -> Mobject:
255 348.7 MiB 35.6 MiB 1 return self.mobject.copy()
Let's try to see why the objects are not deleted after the play call.
In Scene
we can change the code as follows to get an idea why the objects cannot be deleted.
import objgraph
objgraph.show_growth()
new_ids = objgraph.get_new_ids(limit=3)
self.renderer.play(
self,
*args,
**kwargs
)
# del self.animations
objgraph.show_growth()
new_ids = objgraph.get_new_ids(limit=3)
new_polygons = objgraph.at_addrs(new_ids['RegularPolygon'])
# print(new_polygons)
objgraph.show_backrefs(new_polygons, max_depth=100, filename="chain.png")
After some playing around with deleting mobjects and copies i came to the conclusion that it is actually just the structure. Because the VGroups are all Mobjects themselves and make the whole structure in the end pretty big.
There might also be still some leaking in the in the animation system but can't figure out where it is coming from.
https://docs.python.org/3/library/gc.html https://mg.pov.lt/objgraph/objgraph.html#objgraph.show_backrefs https://github.com/pythonprofilers/memory_profiler
That might be helpful