Repository: ManimCommunity/manim

Issue:
Slow Text creation in manim 0.17.3
## Description of bug / unexpected behavior
<!-- Add a clear and concise description of the problem you encountered. -->
I am working on a project involving moderately large groups of mobjects (100s), which has actually never been a problem. But upgrading to 17.3 has made this process incredibly slow.

My code is a simple VGroup of VGroups. These inner VGroups are composed of a Rectangle and a Text object. That is ~100s of these VGroups which add up to double the quantity in total VMobjects.

In 0.17.2 this took absolutely no time, in 0.17.3 it takes tenths of seconds.



## Setup
<!-- Add further context that you think might be relevant for this issue here. -->

Inspecting a bit, turns out is about the multiple text objects. Let me provide some examples. All these are running in `manim 0.17.3`



### Very slow
The following piece of code is a minimal working example of the setup I have. I actually have several classes and inheritance in my code, so this is what it'd look stripped down. We have a random 2D array and we iterate through it, creating a `VGroup` of a `Square` and some `Text`. Importantly, we are setting text properties such as the `font` attribute.

This piece of code takes a **very** long time to run. Unfortunately, I don't know how to measure manim run time. But hopefully the details help to those that can.

```py
class Test(MovingCameraScene):
    def construct(self):
        big_kernel = np.random.randint(0, 255, (10, 10))

        big_kernel_mob = (
            VGroup(
                *[
                    VGroup(
                        Square(),
                        Text(
                            f"{c}",
                            font="Arial",
                            weight=BOLD,
                        ),
                    ).arrange(ORIGIN)
                    for row in big_kernel
                    for c in row
                ]
            )
            .arrange_in_grid(rows=10, cols=10, buff=0)
            .scale(0.3)
        )

        self.add(big_kernel_mob)


```

### A bit faster
On the other hand, this very similar one runs normally fast. It's not immediate, but takes just under a second. The only difference is that the font is not being set, so it defaults to Times.

```py
class Test(MovingCameraScene):
    def construct(self):
        big_kernel = np.random.randint(0, 255, (10, 10))

        big_kernel_mob = (
            VGroup(
                *[
                    VGroup(
                        Square(),
                        Text(
                            f"{c}",
                            # font="Arial",
                            weight=BOLD,
                        ),
                    ).arrange(ORIGIN)
                    for row in big_kernel
                    for c in row
                ]
            )
            .arrange_in_grid(rows=10, cols=10, buff=0)
            .scale(0.3)
        )

        self.add(big_kernel_mob)


```


### The fastest
Even more so, if we ignore the matrix values and always use the same number, it runs even faster. One can assume that some text caching is going on with Pango, but I am not completely sure. Here, setting the font does not matter.

```py
class Test(MovingCameraScene):
    def construct(self):
        big_kernel = np.random.randint(0, 255, (10, 10))

        big_kernel_mob = (
            VGroup(
                *[
                    VGroup(
                        Square(),
                        Text(
                            f"{20}",
                            font="Arial",
                            weight=BOLD,
                        ),
                    ).arrange(ORIGIN)
                    for row in big_kernel
                    for c in row
                ]
            )
            .arrange_in_grid(rows=10, cols=10, buff=0)
            .scale(0.3)
        )

        self.add(big_kernel_mob)


```

### Changing to 0.17.2

However, if I reinstall manim at 17.2, the slowest piece of code now runs just as fast as usual. So, I guess some regression has happened in the new release.


## System specifications

- OS: MacOS 13.2.1
- RAM: 16GB


Hopefully I didn't miss anything. Please let me know if any other details are needed! Thanks so much beforehand.


Discussion:
Thanks for the report! This is a bit unfortunate given that we intended to improve performance of the creation of large texts.

@kjlubick, could you perhaps take a closer look?
My guess was the removal of the cache in https://github.com/ManimCommunity/manim/pull/3181/files

@jesi-rgb can you try adding `use_svg_cache=True` to your Text() calls?
(btw, https://jiffyclub.github.io/snakeviz/ is a neat way to visualize profiles that you can generate from Python or the command line)
@kjlubick So, like this?

```py
Text("a", use_svg_cache=True, font="Arial")
```
I get this error:

```
TypeError: manim.mobject.svg.svg_mobject.SVGMobject.__init__() got multiple values for keyword argument 'use_svg_cache'
```
Apparently, this is already set in SVGMobject, if I'm not mistaken
I guess that means I forgot to pop a kwarg.
I think it's because of the PR which checks whether the font is installed or not, https://github.com/ManimCommunity/manim/pull/3046. Calling `manimpango.list_fonts()` multiple times without caching would makes things slow.
Good insight Naveen. Maybe try adding `warn_missing_font=False` to the Text calls?
Looks like it's the main cause, it's from the first slow example provided
![image](https://user-images.githubusercontent.com/49693820/233827187-1ec017fc-06ae-48f8-947c-06187efe2273.png)

Measuring manim is as easy as executing a profiler on the python script, there are multiple options with multiple viewers, i personally prefer `vprof` for ease of use. 

Then you would just add 
```
if __name__ == "__main__":
    with tempconfig({"renderer": "cairo"}):
        Test().render()
```

to your python script and run `vprof test.py`

![grafik](https://github.com/ManimCommunity/manim/assets/17803932/80a0c274-e820-4db9-87ee-bc7ccd95b275)

https://github.com/nvdv/vprof

Other:
- https://github.com/plasma-umass/scalene
- https://jiffyclub.github.io/snakeviz/ + CProfile builtin

I can confirm @naveen521kk 's observation for the first example.
![grafik](https://github.com/ManimCommunity/manim/assets/17803932/670c9d24-0055-4f7c-8e45-97099245d4d1)

It improves with caching

![grafik](https://github.com/ManimCommunity/manim/assets/17803932/faaaa762-bcd1-4018-8f62-3f34fc023843)


Now the slowest part is parsing the svg.
Oh and in general yes, different fonts take different time, because they have a different amount of complexity. For example some Roboto fonts take million of years to render just because every letter consists of 100 curves which makes our svgmobject suffer through the for loop for days.
Honestly i am not quite sure why it takes longer.

Slow version:
![grafik](https://github.com/ManimCommunity/manim/assets/17803932/755be0b8-582c-4c08-9960-e68a245aaf61)

Fast version:
![grafik](https://github.com/ManimCommunity/manim/assets/17803932/58f36800-0b53-4434-903c-2cc9c4c0dee6)

Theres nothing that stands out to me but it still takes a lot more time, that doesn't make sense to me maybe it's literally the letter of the number 20 which are just efficent

Resolving patch:
diff --git a/manim/mobject/text/text_mobject.py b/manim/mobject/text/text_mobject.py
--- a/manim/mobject/text/text_mobject.py
+++ b/manim/mobject/text/text_mobject.py
@@ -49,6 +49,8 @@ def construct(self):
 
 from __future__ import annotations
 
+import functools
+
 __all__ = ["Text", "Paragraph", "MarkupText", "register_font"]
 
 
@@ -407,6 +409,11 @@ def construct(self):
 
     """
 
+    @staticmethod
+    @functools.lru_cache(maxsize=None)
+    def font_list() -> list[str]:
+        return manimpango.list_fonts()
+
     def __init__(
         self,
         text: str,
@@ -431,13 +438,12 @@ def __init__(
         width: float = None,
         should_center: bool = True,
         disable_ligatures: bool = False,
+        use_svg_cache: bool = False,
         **kwargs,
     ) -> None:
         self.line_spacing = line_spacing
-        if font and warn_missing_font:
-            fonts_list = manimpango.list_fonts()
-            if font not in fonts_list:
-                logger.warning(f"Font {font} not in {fonts_list}.")
+        if font and warn_missing_font and font not in Text.font_list():
+            logger.warning(f"Font {font} not in {Text.font_list()}.")
         self.font = font
         self._font_size = float(font_size)
         # needs to be a float or else size is inflated when font_size = 24
@@ -491,7 +497,7 @@ def __init__(
             height=height,
             width=width,
             should_center=should_center,
-            use_svg_cache=False,
+            use_svg_cache=use_svg_cache,
             **kwargs,
         )
         self.text = text
@@ -1133,6 +1139,11 @@ def construct(self):
 
     """
 
+    @staticmethod
+    @functools.lru_cache(maxsize=None)
+    def font_list() -> list[str]:
+        return manimpango.list_fonts()
+
     def __init__(
         self,
         text: str,
@@ -1156,10 +1167,8 @@ def __init__(
     ) -> None:
         self.text = text
         self.line_spacing = line_spacing
-        if font and warn_missing_font:
-            fonts_list = manimpango.list_fonts()
-            if font not in fonts_list:
-                logger.warning(f"Font {font} not in {fonts_list}.")
+        if font and warn_missing_font and font not in Text.font_list():
+            logger.warning(f"Font {font} not in {Text.font_list()}.")
         self.font = font
         self._font_size = float(font_size)
         self.slant = slant


Test patch: