manim icon indicating copy to clipboard operation
manim copied to clipboard

Investigate large memory usage

Open Darylgolden opened this issue 2 years ago • 2 comments

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

Darylgolden avatar Apr 01 '22 02:04 Darylgolden

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

MrDiver avatar May 26 '22 08:05 MrDiver

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

MrDiver avatar May 26 '22 12:05 MrDiver