Skip to content

Added Scene caching #166

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 59 commits into from
Aug 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
384b84a
added caching functionnality
huguesdevimeux Jun 25, 2020
deeade5
deleted comments
huguesdevimeux Jun 25, 2020
04d8c45
fixed wait bug
huguesdevimeux Jun 26, 2020
555b304
added docstrings
huguesdevimeux Jun 26, 2020
1e6e7aa
fixed minor typo in doc
huguesdevimeux Jun 27, 2020
b5ac0e2
added cache cleaner
huguesdevimeux Jun 27, 2020
ac8a598
Merge branch 'master' into scene-caching [skip ci]
huguesdevimeux Jul 6, 2020
39b39d8
added disable_caching option
huguesdevimeux Jul 6, 2020
ae3d3d4
supported camera_config, and hashing functions
huguesdevimeux Jul 10, 2020
6488252
use now digital naming when disable_caching
huguesdevimeux Jul 22, 2020
d02309b
added an option to flush the cache.
huguesdevimeux Jul 22, 2020
f1da2ff
fixed shameful typo
huguesdevimeux Jul 22, 2020
83b03eb
added max_cached_files
huguesdevimeux Jul 22, 2020
79553dd
Merge branch 'master' into scene-caching
huguesdevimeux Jul 22, 2020
c5a0b1f
fixed merge issues
huguesdevimeux Jul 22, 2020
3fb8fe2
added digital naming when disable_caching
huguesdevimeux Jul 22, 2020
9ff6d41
foxed skip_animations dlag that #98 broke
huguesdevimeux Jul 22, 2020
64d82a3
removed cairo context from hash
huguesdevimeux Jul 22, 2020
ebfcbc1
removed deprecated code
huguesdevimeux Jul 22, 2020
7f98b1c
fixed tests by setting write_to_movie to False
huguesdevimeux Jul 22, 2020
d10ee72
removed useless code
huguesdevimeux Jul 23, 2020
9c5dbcd
Merge branch 'scene-caching' into scene-caching
Aathish04 Jul 28, 2020
d497e1c
Merge branch 'scene-caching' of https://github.com/aathish04/manim in…
Jul 28, 2020
b27a416
Revert "Use file_writer_config instead of config to get text_dir for …
Aathish04 Jul 28, 2020
d52d9f3
Apply suggestions from code review
huguesdevimeux Jul 28, 2020
8c3276c
Merge branch 'master' into scene-caching
huguesdevimeux Jul 28, 2020
cdfbfa9
fixed max_files_cached typo
huguesdevimeux Jul 28, 2020
8499535
Use file_writer_config instead of config to get text_dir for Text() (…
Aathish04 Jul 26, 2020
0ace9a6
removed useless code
huguesdevimeux Jul 23, 2020
b1a378e
used self.camera instead of __dict__
huguesdevimeux Jul 28, 2020
2c95a7e
Apply suggestions from code review
Aathish04 Jul 28, 2020
4844847
more pythonic
huguesdevimeux Jul 28, 2020
1d71a05
Merge branch 'scene-caching' of https://github.com/huguesdevimeux/man…
Aathish04 Jul 28, 2020
9b8cb61
Merge branch 'scene-caching' of https://github.com/huguesdevimeux/man…
huguesdevimeux Jul 28, 2020
5427491
fixed bug related to dict keys
huguesdevimeux Jul 29, 2020
f2e924b
fixed minor bufg of the last commit
huguesdevimeux Jul 29, 2020
901934d
Now clean_cache can remove multiple files.
huguesdevimeux Jul 29, 2020
a03da90
fixed bug when rendering multiple scenes.
huguesdevimeux Jul 30, 2020
ec0bdaf
fixed bug related to nested dict with wrong keys
huguesdevimeux Jul 30, 2020
6fd8c4b
added infinity support for max_file_cached
huguesdevimeux Jul 31, 2020
3100156
changed default value of max_files_cached to 100
huguesdevimeux Aug 1, 2020
66631e8
deleted comments
huguesdevimeux Aug 1, 2020
90b69c5
Merge branch 'master' into scene-caching
huguesdevimeux Aug 1, 2020
18ccc27
fixed docstrings
huguesdevimeux Aug 1, 2020
7bca134
Merge branch 'master' into scene-caching
huguesdevimeux Aug 2, 2020
4f303e4
Merge branch 'scene-caching' of https://github.com/huguesdevimeux/man…
huguesdevimeux Aug 2, 2020
ccad56b
fixed format (maybe)
huguesdevimeux Aug 2, 2020
a36dfbf
Merge branch 'master' into scene-caching
huguesdevimeux Aug 9, 2020
64403c0
fixed format
huguesdevimeux Aug 9, 2020
3d0cd01
fixed tests
huguesdevimeux Aug 10, 2020
1198108
Merge branch 'master' into scene-caching
huguesdevimeux Aug 10, 2020
8bcc05a
Update expected.txt
Aathish04 Aug 11, 2020
e0fc6e5
fixed merge conflict
huguesdevimeux Aug 11, 2020
2d1d58d
fixed logging test
huguesdevimeux Aug 11, 2020
e5e1597
fixed docstrings
huguesdevimeux Aug 11, 2020
f9cc9ed
minor doc improvement
huguesdevimeux Aug 11, 2020
e3ab30a
Apply suggestions from code review
huguesdevimeux Aug 11, 2020
947e146
Apply suggestions from code review
huguesdevimeux Aug 12, 2020
7a3e1dc
fixed typo of the last commit -_-
huguesdevimeux Aug 12, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions manim/default.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ frame_rate = 60
pixel_height = 1440
pixel_width = 2560

# Use -1 to set max_files_cached to infinity.
max_files_cached = 100
#Flush cache will delete all the cached partial-movie-files.
flush_cache = False
disable_caching = False

# These override the previous by using -t, --transparent
[transparent]
png_mode = RGBA
Expand Down
100 changes: 88 additions & 12 deletions manim/scene/scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import random
import warnings
import platform
import copy

from tqdm import tqdm as ProgressDisplay
import numpy as np
Expand All @@ -16,6 +17,7 @@
from ..mobject.mobject import Mobject
from ..scene.scene_file_writer import SceneFileWriter
from ..utils.iterables import list_update
from ..utils.hashing import get_hash_from_play_call, get_hash_from_wait_call


class Scene(Container):
Expand Down Expand Up @@ -55,7 +57,7 @@ def __init__(self, **kwargs):
Container.__init__(self, **kwargs)
self.camera = self.camera_class(**camera_config)
self.file_writer = SceneFileWriter(self, **file_writer_config,)

self.play_hashes_list = []
self.mobjects = []
# TODO, remove need for foreground mobjects
self.foreground_mobjects = []
Expand All @@ -72,6 +74,9 @@ def __init__(self, **kwargs):
except EndSceneEarlyException:
pass
self.tear_down()
# We have to reset these settings in case of multiple renders.
file_writer_config["skip_animations"] = False
self.original_skipping_status = file_writer_config["skip_animations"]
self.file_writer.finish()
self.print_end_message()

Expand Down Expand Up @@ -373,6 +378,17 @@ def add_mobjects_among(self, values):
self.add(*filter(lambda m: isinstance(m, Mobject), values))
return self

def add_mobjects_from_animations(self, animations):

curr_mobjects = self.get_mobject_family_members()
for animation in animations:
# Anything animated that's not already in the
# scene gets added to the scene
mob = animation.mobject
if mob not in curr_mobjects:
self.add(mob)
curr_mobjects += mob.get_family()

def remove(self, *mobjects):
"""
Removes mobjects in the passed list of mobjects
Expand Down Expand Up @@ -832,6 +848,71 @@ def update_skipping_status(self):
file_writer_config["skip_animations"] = True
raise EndSceneEarlyException()

def handle_caching_play(func):
"""
Decorator that returns a wrapped version of func that will compute the hash of the play invocation.

The returned function will act according to the computed hash: either skip the animation because it's already cached, or let the invoked function play normally.

Parameters
----------
func : Callable[[...], None]
The play like function that has to be written to the video file stream. Take the same parameters as `scene.play`.
"""

def wrapper(self, *args, **kwargs):
self.revert_to_original_skipping_status()
animations = self.compile_play_args_to_animation_list(*args, **kwargs)
self.add_mobjects_from_animations(animations)
if not file_writer_config["disable_caching"]:
mobjects_on_scene = self.get_mobjects()
hash_play = get_hash_from_play_call(
self.camera, animations, mobjects_on_scene
)
self.play_hashes_list.append(hash_play)
if self.file_writer.is_already_cached(hash_play):
logger.info(
f"Animation {self.num_plays} : Using cached data (hash : {hash_play})"
)
file_writer_config["skip_animations"] = True
else:
hash_play = "uncached_{:05}".format(self.num_plays)
self.play_hashes_list.append(hash_play)
func(self, *args, **kwargs)

return wrapper

def handle_caching_wait(func):
"""
Decorator that returns a wrapped version of func that will compute the hash of the wait invocation.

The returned function will act according to the computed hash: either skip the animation because it's already cached, or let the invoked function play normally.

Parameters
----------
func : Callable[[...], None]
The wait like function that has to be written to the video file stream. Take the same parameters as `scene.wait`.
"""

def wrapper(self, duration=DEFAULT_WAIT_TIME, stop_condition=None):
self.revert_to_original_skipping_status()
if not file_writer_config["disable_caching"]:
hash_wait = get_hash_from_wait_call(
self.camera, duration, stop_condition, self.get_mobjects()
)
self.play_hashes_list.append(hash_wait)
if self.file_writer.is_already_cached(hash_wait):
logger.info(
f"Wait {self.num_plays} : Using cached data (hash : {hash_wait})"
)
file_writer_config["skip_animations"] = True
else:
hash_wait = "uncached_{:05}".format(self.num_plays)
self.play_hashes_list.append(hash_wait)
func(self, duration, stop_condition)

return wrapper

def handle_play_like_call(func):
"""
This method is used internally to wrap the
Expand Down Expand Up @@ -875,16 +956,9 @@ def begin_animations(self, animations):
List of involved animations.

"""
curr_mobjects = self.get_mobject_family_members()
for animation in animations:
# Begin animation
animation.begin()
# Anything animated that's not already in the
# scene gets added to the scene
mob = animation.mobject
if mob not in curr_mobjects:
self.add(mob)
curr_mobjects += mob.get_family()

def progress_through_animations(self, animations):
"""
Expand Down Expand Up @@ -933,6 +1007,7 @@ def finish_animations(self, animations):
else:
self.update_mobjects(0)

@handle_caching_play
@handle_play_like_call
def play(self, *args, **kwargs):
"""
Expand Down Expand Up @@ -1032,6 +1107,7 @@ def get_wait_time_progression(self, duration, stop_condition):
time_progression.set_description("Waiting {}".format(self.num_plays))
return time_progression

@handle_caching_wait
@handle_play_like_call
def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None):
"""
Expand Down Expand Up @@ -1105,8 +1181,8 @@ def force_skipping(self):
Scene
The Scene, with skipping turned on.
"""
self.original_skipping_status = self.SKIP_ANIMATIONS
self.SKIP_ANIMATIONS = True
self.original_skipping_status = file_writer_config["skip_animations"]
file_writer_config["skip_animations"] = True
return self

def revert_to_original_skipping_status(self):
Expand All @@ -1121,7 +1197,7 @@ def revert_to_original_skipping_status(self):
The Scene, with the original skipping status.
"""
if hasattr(self, "original_skipping_status"):
self.SKIP_ANIMATIONS = self.original_skipping_status
file_writer_config["skip_animations"] = self.original_skipping_status
return self

def add_frames(self, *frames):
Expand Down Expand Up @@ -1156,7 +1232,7 @@ def add_sound(self, sound_file, time_offset=0, gain=None, **kwargs):
gain :

"""
if self.SKIP_ANIMATIONS:
if file_writer_config["skip_animations"]:
return
time = self.get_time() + time_offset
self.file_writer.add_sound(sound_file, time, gain, **kwargs)
Expand Down
98 changes: 79 additions & 19 deletions manim/scene/scene_file_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from ..utils.config_ops import digest_config
from ..utils.file_ops import guarantee_existence
from ..utils.file_ops import add_extension_if_not_present
from ..utils.file_ops import get_sorted_integer_files
from ..utils.file_ops import modify_atime
from ..utils.sounds import get_full_sound_file_path


Expand Down Expand Up @@ -170,8 +170,9 @@ def get_next_partial_movie_path(self):
"""
result = os.path.join(
self.partial_movie_directory,
"{:05}{}".format(
self.scene.num_plays, file_writer_config["movie_file_extension"],
"{}{}".format(
self.scene.play_hashes_list[self.scene.num_plays],
file_writer_config["movie_file_extension"],
),
)
return result
Expand Down Expand Up @@ -351,6 +352,10 @@ def finish(self):
if hasattr(self, "writing_process"):
self.writing_process.terminate()
self.combine_movie_files()
if file_writer_config["flush_cache"]:
self.flush_cache_directory()
else:
self.clean_cache()
if file_writer_config["save_last_frame"]:
self.scene.update_frame(ignore_skipping=True)
self.save_final_image(self.scene.get_image())
Expand Down Expand Up @@ -421,6 +426,28 @@ def close_movie_pipe(self):
shutil.move(
self.temp_partial_movie_file_path, self.partial_movie_file_path,
)
logger.debug(
f"Animation {self.scene.num_plays} : Partial movie file written in {self.partial_movie_file_path}"
)

def is_already_cached(self, hash_invocation):
"""Will check if a file named with `hash_invocation` exists.

Parameters
----------
hash_invocation : :class:`str`
The hash corresponding to an invocation to either `scene.play` or `scene.wait`.

Returns
-------
:class:`bool`
Whether the file exists.
"""
path = os.path.join(
self.partial_movie_directory,
"{}{}".format(hash_invocation, self.movie_file_extension),
)
return os.path.exists(path)

def combine_movie_files(self):
"""
Expand All @@ -435,34 +462,27 @@ def combine_movie_files(self):
# cuts at all the places you might want. But for viewing
# the scene as a whole, one of course wants to see it as a
# single piece.
kwargs = {
"remove_non_integer_files": True,
"extension": file_writer_config["movie_file_extension"],
}
if file_writer_config["from_animation_number"] is not None:
kwargs["min_index"] = file_writer_config["from_animation_number"]
if file_writer_config["upto_animation_number"] not in [None, np.inf]:
kwargs["max_index"] = file_writer_config["upto_animation_number"]
else:
kwargs["remove_indices_greater_than"] = self.scene.num_plays - 1
Comment on lines -442 to -447
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: where did these go?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been removed.
This is now depreceted as the system with integreerds index names for partial movie files (ie 0001.mp4, 0002.mp4) does not exist in a post 166 world.
As kwargs["min_index"] and all this stuff was used to handle the files named with that these names, it's now useless.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait so the names of the files in partial_movie_files are changing too?

partial_movie_files = get_sorted_integer_files(
self.partial_movie_directory, **kwargs
)
partial_movie_files = [
os.path.join(
self.partial_movie_directory,
"{}{}".format(hash_play, file_writer_config["movie_file_extension"]),
)
for hash_play in self.scene.play_hashes_list
]
if len(partial_movie_files) == 0:
logger.error("No animations in this scene")
return

# Write a file partial_file_list.txt containing all
# partial movie files
# partial movie files. This is used by FFMPEG.
file_list = os.path.join(
self.partial_movie_directory, "partial_movie_file_list.txt"
)
with open(file_list, "w") as fp:
fp.write("# This file is used internally by FFMPEG.\n")
for pf_path in partial_movie_files:
if os.name == "nt":
pf_path = pf_path.replace("\\", "/")
fp.write("file 'file:{}'\n".format(pf_path))

movie_file_path = self.get_movie_file_path()
commands = [
FFMPEG_BIN,
Expand Down Expand Up @@ -527,6 +547,46 @@ def combine_movie_files(self):
os.remove(sound_file_path)

self.print_file_ready_message(movie_file_path)
if file_writer_config["write_to_movie"]:
for file_path in partial_movie_files:
# We have to modify the accessed time so if we have to clean the cache we remove the one used the longest.
modify_atime(file_path)

def clean_cache(self):
"""Will clean the cache by removing the partial_movie_files used by manim the longest ago."""
cached_partial_movies = [
os.path.join(self.partial_movie_directory, file_name)
for file_name in os.listdir(self.partial_movie_directory)
if file_name != "partial_movie_file_list.txt"
]
if len(cached_partial_movies) > file_writer_config["max_files_cached"]:
number_files_to_delete = (
len(cached_partial_movies) - file_writer_config["max_files_cached"]
)
oldest_files_to_delete = sorted(
[partial_movie_file for partial_movie_file in cached_partial_movies],
key=os.path.getatime,
)[:number_files_to_delete]
# oldest_file_path = min(cached_partial_movies, key=os.path.getatime)
for file_to_delete in oldest_files_to_delete:
os.remove(file_to_delete)
logger.info(
f"The partial movie directory is full (> {file_writer_config['max_files_cached']} files). Therefore, manim has removed {number_files_to_delete} file(s) used by it the longest ago."
+ "You can change this behaviour by changing max_files_cached in config."
)

def flush_cache_directory(self):
"""Delete all the cached partial movie files"""
cached_partial_movies = [
os.path.join(self.partial_movie_directory, file_name)
for file_name in os.listdir(self.partial_movie_directory)
if file_name != "partial_movie_file_list.txt"
]
for f in cached_partial_movies:
os.remove(f)
logger.info(
f"Cache flushed. {len(cached_partial_movies)} file(s) deleted in {self.partial_movie_directory}."
)

def print_file_ready_message(self, file_path):
"""
Expand Down
Loading