Repository: ManimCommunity/manim

Issue:
Rendering DecimalNumber (SingleStringMathTex) with updater takes a long time to render, and gets slower over time
## Description of bug / unexpected behavior
When a `DecimalNumber` has an updater to become a `DecimalNumber` with the current `ValueTracker` value, it renders very slowly. It starts quickly, as expected, but gets slower and slower over time (as you can see in the video below). In the title I wrote `SingleStringMathTex` because I've found that this happens with classes that end up creating a `SingleStringMathTex` (`Text`, `Tex`, `MathTex`, `Integer`, `DecimalNumber`, ...). At first, I thought that the `become()` method was causing issues, leaving unreachable objects that the Garbage Collector couldn't delete, or something similar. But after running some tests with the Garbage Collector, the ammount of unreachable objs didn't seem to increase dramatically, while the render time did. (See code below to see this test). 

_If you are facing the same issue and can't afford having this long render times, checkout the partial solution in Edit 2, at the end of the Issue_

https://user-images.githubusercontent.com/68753500/130679100-253cf3c5-af79-4f2a-8e9b-417e0e5b5db7.mp4

Check _Additional Comments_ section for more odd behaviour I've noticed.

## Expected behavior
I expect the rendering to be faster, and remain constant.


## How to reproduce the issue

**Video 1 Original Code**

```py
from manim import *

config.tex_template = TexTemplate()
config.disable_caching = True

class Bug(Scene):

    def construct(self):

        angle_tracker = ValueTracker(0)

        label = DecimalNumber(0).scale(0.5)

        label.add_updater(
            lambda m: m.become(
                DecimalNumber(round(angle_tracker.get_value(), 2)).scale(0.5)
            )
        )

        self.add(label)
        self.wait(0.5)
        self.play(angle_tracker.animate.set_value(360), run_time=2)
        self.wait(0.5)
```

**Garbage Collector test Original Code**

```py
from manim import *
import gc

config.tex_template = TexTemplate()
config.disable_caching = True

class Bug(Scene):

    def construct(self):

        def get_decimal_num_and_print_unreachable():
            print("Unreachable:", gc.collect())
            return DecimalNumber(round(angle_tracker.get_value(), 2)).scale(0.5)

        angle_tracker = ValueTracker(0)

        label = DecimalNumber(0).scale(0.5)

        label.add_updater(
            lambda m: m.become(
                get_decimal_num_and_print_unreachable()
            )
        )

        self.add(label)
        self.wait(0.5)
        self.play(angle_tracker.animate.set_value(360), run_time=2)
        self.wait(0.5)
```


</details>


## Additional media files
<!-- Paste in the files manim produced on rendering the code above. -->

<details><summary>Images/GIFs</summary>

![image](https://user-images.githubusercontent.com/68753500/130680233-a63a385b-b9a0-4635-83ee-61117e5b224a.png)

</details>


## Logs
<details><summary>Terminal output</summary>
<!-- Add "-v DEBUG" when calling manim to generate more detailed logs -->

```
PASTE HERE OR PROVIDE LINK TO https://pastebin.com/ OR SIMILAR
```

<!-- Insert screenshots here (only when absolutely necessary, we prefer copy/pasted output!) -->

</details>


## System specifications

<details><summary>System Details</summary>

- 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
```
</details>

<details><summary>LaTeX details</summary>

+ LaTeX distribution (e.g. TeX Live 2020):
+ Installed LaTeX packages:
<!-- output of `tlmgr list --only-installed` for TeX Live or a screenshot of the Packages page for MikTeX -->
</details>

<details><summary>FFMPEG</summary>

Output of `ffmpeg -version`:

```
PASTE HERE
```
</details>

## Additional comments

Some extra behaviour I've noticed:

- The 'slowness' seems to pile up. In this animation, there's a second `ValueTracker` animation, and you can clearly see that the second one doesn't start as fast as the first one. Instead it starts with the same speed the previous one ended. This doesn't only happen to `ValueTracker` animations, all other animations will be slowed-down in some way.

https://user-images.githubusercontent.com/68753500/130680309-a99fd787-050f-45ef-8bb1-edb2816b1294.mp4


Edit:

I've done some more investigating and it seems like the number of points of the `VMobject` increases throughout the rendering. When rendering starts, the object has ~120 points, and after 120 frames, it has 1600 points. Something in the `align_points()` method in `vmobject.py` is causing the number of points to increase. This explains the 'slowness piling up' thing mentioned above, since the object keeps the same numbers of points throughout rendering, so when the second animation starts, the mobject has 1600 points, and this number keeps increasing.
My knowledge ends at the bezier curves stuff, so I don't really know where to go from here. I will point out though that commenting these two lines (1271 & 1272 in ` vmobject.py` ) keeps the number of points constant and thus the animation renders fast, but it breaks things:

```
sp1 = self.insert_n_curves_to_point_list(diff1, sp1)
sp2 = self.insert_n_curves_to_point_list(diff2, sp2)
```

When I say it breaks things I say it because when using ` Transform` having those lines commnted, it spits out an error like this:
` ` ` ValueError: operands could not be broadcast together with shapes (80,3) (72,3)` ` ` 
Since those 2 commented lines make the number of points (?) or curves (?) _(I don't really know)_ equal, the program has some trouble when this number doesn't coincide between the objects.

Edit 2 **PARTIAL SOLUTION**: In case you are having the same issue and want to render faster, I have a hacky (and very far from perfect) solution that will increase render speeds dramatically. So far, I haven't had any issue with it, but it's very likely that it breaks something in very specific situations. Basically, I'm setting a max number of points a vmboject can have while aligning, so it doesn't keep increasing indefinitely. I have found that ~200 points is enough for most objects to render properly, and it allows render time to remain constant (and fast).

In the for-loop in `align_points()` I've added a condition and indented the two lines that call `self.insert_n_curves_to_point_list()` as such:

```py
for n in range(n_subpaths):
      sp1 = get_nth_subpath(subpaths1, n)
      sp2 = get_nth_subpath(subpaths2, n)
      diff1 = max(0, (len(sp2) - len(sp1)) // nppcc)
      diff2 = max(0, (len(sp1) - len(sp2)) // nppcc)
      if max(len(sp1), len(sp2)) < 215: # new condition
          sp1 = self.insert_n_curves_to_point_list(diff1, sp1) # indented lines
          sp2 = self.insert_n_curves_to_point_list(diff2, sp2)
      new_path1 = np.append(new_path1, sp1, axis=0)
      new_path2 = np.append(new_path2, sp2, axis=0)
```

This will increase rendering speed drastically. I say it's far from perfect because even though I haven't had any problems with it so far, I can imagine `VMobject`s with lots of points may be rendered incorrectly.
Rendering DecimalNumber (SingleStringMathTex) with updater takes a long time to render, and gets slower over time
## Description of bug / unexpected behavior
When a `DecimalNumber` has an updater to become a `DecimalNumber` with the current `ValueTracker` value, it renders very slowly. It starts quickly, as expected, but gets slower and slower over time (as you can see in the video below). In the title I wrote `SingleStringMathTex` because I've found that this happens with classes that end up creating a `SingleStringMathTex` (`Text`, `Tex`, `MathTex`, `Integer`, `DecimalNumber`, ...). At first, I thought that the `become()` method was causing issues, leaving unreachable objects that the Garbage Collector couldn't delete, or something similar. But after running some tests with the Garbage Collector, the ammount of unreachable objs didn't seem to increase dramatically, while the render time did. (See code below to see this test). 

_If you are facing the same issue and can't afford having this long render times, checkout the partial solution in Edit 2, at the end of the Issue_

https://user-images.githubusercontent.com/68753500/130679100-253cf3c5-af79-4f2a-8e9b-417e0e5b5db7.mp4

Check _Additional Comments_ section for more odd behaviour I've noticed.

## Expected behavior
I expect the rendering to be faster, and remain constant.


## How to reproduce the issue

**Video 1 Original Code**

```py
from manim import *

config.tex_template = TexTemplate()
config.disable_caching = True

class Bug(Scene):

    def construct(self):

        angle_tracker = ValueTracker(0)

        label = DecimalNumber(0).scale(0.5)

        label.add_updater(
            lambda m: m.become(
                DecimalNumber(round(angle_tracker.get_value(), 2)).scale(0.5)
            )
        )

        self.add(label)
        self.wait(0.5)
        self.play(angle_tracker.animate.set_value(360), run_time=2)
        self.wait(0.5)
```

**Garbage Collector test Original Code**

```py
from manim import *
import gc

config.tex_template = TexTemplate()
config.disable_caching = True

class Bug(Scene):

    def construct(self):

        def get_decimal_num_and_print_unreachable():
            print("Unreachable:", gc.collect())
            return DecimalNumber(round(angle_tracker.get_value(), 2)).scale(0.5)

        angle_tracker = ValueTracker(0)

        label = DecimalNumber(0).scale(0.5)

        label.add_updater(
            lambda m: m.become(
                get_decimal_num_and_print_unreachable()
            )
        )

        self.add(label)
        self.wait(0.5)
        self.play(angle_tracker.animate.set_value(360), run_time=2)
        self.wait(0.5)
```


</details>


## Additional media files
<!-- Paste in the files manim produced on rendering the code above. -->

<details><summary>Images/GIFs</summary>

![image](https://user-images.githubusercontent.com/68753500/130680233-a63a385b-b9a0-4635-83ee-61117e5b224a.png)

</details>


## Logs
<details><summary>Terminal output</summary>
<!-- Add "-v DEBUG" when calling manim to generate more detailed logs -->

```
PASTE HERE OR PROVIDE LINK TO https://pastebin.com/ OR SIMILAR
```

<!-- Insert screenshots here (only when absolutely necessary, we prefer copy/pasted output!) -->

</details>


## System specifications

<details><summary>System Details</summary>

- 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
```
</details>

<details><summary>LaTeX details</summary>

+ LaTeX distribution (e.g. TeX Live 2020):
+ Installed LaTeX packages:
<!-- output of `tlmgr list --only-installed` for TeX Live or a screenshot of the Packages page for MikTeX -->
</details>

<details><summary>FFMPEG</summary>

Output of `ffmpeg -version`:

```
PASTE HERE
```
</details>

## Additional comments

Some extra behaviour I've noticed:

- The 'slowness' seems to pile up. In this animation, there's a second `ValueTracker` animation, and you can clearly see that the second one doesn't start as fast as the first one. Instead it starts with the same speed the previous one ended. This doesn't only happen to `ValueTracker` animations, all other animations will be slowed-down in some way.

https://user-images.githubusercontent.com/68753500/130680309-a99fd787-050f-45ef-8bb1-edb2816b1294.mp4


Edit:

I've done some more investigating and it seems like the number of points of the `VMobject` increases throughout the rendering. When rendering starts, the object has ~120 points, and after 120 frames, it has 1600 points. Something in the `align_points()` method in `vmobject.py` is causing the number of points to increase. This explains the 'slowness piling up' thing mentioned above, since the object keeps the same numbers of points throughout rendering, so when the second animation starts, the mobject has 1600 points, and this number keeps increasing.
My knowledge ends at the bezier curves stuff, so I don't really know where to go from here. I will point out though that commenting these two lines (1271 & 1272 in ` vmobject.py` ) keeps the number of points constant and thus the animation renders fast, but it breaks things:

```
sp1 = self.insert_n_curves_to_point_list(diff1, sp1)
sp2 = self.insert_n_curves_to_point_list(diff2, sp2)
```

When I say it breaks things I say it because when using ` Transform` having those lines commnted, it spits out an error like this:
` ` ` ValueError: operands could not be broadcast together with shapes (80,3) (72,3)` ` ` 
Since those 2 commented lines make the number of points (?) or curves (?) _(I don't really know)_ equal, the program has some trouble when this number doesn't coincide between the objects.

Edit 2 **PARTIAL SOLUTION**: In case you are having the same issue and want to render faster, I have a hacky (and very far from perfect) solution that will increase render speeds dramatically. So far, I haven't had any issue with it, but it's very likely that it breaks something in very specific situations. Basically, I'm setting a max number of points a vmboject can have while aligning, so it doesn't keep increasing indefinitely. I have found that ~200 points is enough for most objects to render properly, and it allows render time to remain constant (and fast).

In the for-loop in `align_points()` I've added a condition and indented the two lines that call `self.insert_n_curves_to_point_list()` as such:

```py
for n in range(n_subpaths):
      sp1 = get_nth_subpath(subpaths1, n)
      sp2 = get_nth_subpath(subpaths2, n)
      diff1 = max(0, (len(sp2) - len(sp1)) // nppcc)
      diff2 = max(0, (len(sp1) - len(sp2)) // nppcc)
      if max(len(sp1), len(sp2)) < 215: # new condition
          sp1 = self.insert_n_curves_to_point_list(diff1, sp1) # indented lines
          sp2 = self.insert_n_curves_to_point_list(diff2, sp2)
      new_path1 = np.append(new_path1, sp1, axis=0)
      new_path2 = np.append(new_path2, sp2, axis=0)
```

This will increase rendering speed drastically. I say it's far from perfect because even though I haven't had any problems with it so far, I can imagine `VMobject`s with lots of points may be rendered incorrectly.


Discussion:
Can it happen because at the end of your animation you just have to render more digits?
I mean, to render `0.00` is much easier than `360.00`.
No, that's not just it. I tried changing it from `100.00` to `999.99` and saw similar slowdowns.
Nope, my tests showed that rendering still slows down while the number of digits remains constant.
I've reviewed this bug.

You are 100% correct, the issue is in align_points. It causes the amount of points in a vmobject to grow unboundedly.

Firstly, i note that you don't really need `become` here. I got a similar effect using `DecimalNumber.set_value` and it works very fast as it doesn't trigger this bug.

---

But in terms of diagnosis, here's my thoughts:

`align_points` is designed to add extra points to each object to ensure they have the same amount for each "subpath". But subpaths are a fiddly concept.

Suppose are initial object `O` has suppath lengths [10]. And we alternate aligning with objects `A`, `B` with subpath lengths [20], and [10, 10]. (A and B are created fresh each time, like in the xample)

My understanding is the following:

* Align 1:
  * starts at O=[10], aligning with A=[20], so add 10 points to O in suppath 1, to get [20]
* Align 2:
  * starts at O=[20], aligning with B=[10, 10], so add 10 dummy points to O for subpath 2 to get [20, 10]. Also update B to [20, 10]
* Align 3:
  * starts at O=[20, 10], aligning with A=[20]. Nothing needs doing to O, but A needs 10 dummy points added at suppath 2.
* Align 4:
  * starts at O=[30] (!), aligning with B=[10, 10]. So add 10 dummy points to O to get [30, 10]. Also add 20 points to B.

("dummy" points occur when createing a new path entirely. Each dummy is a curve with start/end/control points all at the same value)

You can see it's gone wrong at align 4. O has 30 points in suppath 1, despite never aligning with anything of that length! The problem is how subpaths are determined. At the end of align 3, we added dummy points to A. Then between align 3 and align 4, we copy all of A's points to O (this is what `become` does). But then when we ask O what subpaths it has, it can no longer tell that the dummy points where *intended* to be part of subpath 2, it just counts them as part of subpath 1.

This repeats ad infinitum. We'll keep adding points to pad subpath 2 when aligning with A. Then that is forgotten, with the poitns becoming part of suppath 1.

---

As for fixing it, it looks tricky to fiddle too much with how subpaths or dummy points work.

I propose stripping dummy points from the end of each subpath before doing alignment, stopping them growing indefinitely.
This is a very fine and precise analysis, kudos and thank you! Your suggestion seems like a good idea.
Thanks for the comment! Your explanation makes a lot of sense. ManimCE is still mostly Grant's old code, which isn't perfect.
If you take a look at the current 3b1b/manim repository, he's changed most of that stuff and the code seems much more solid now, and I doubt this issue exists.

A major re-design is in order, to make sure we don't have fiddly stuff. But well, that's hard. Maybe once we fully transition to OpenGL (if that ever happens), the whole tool will be much better.
> he's changed most of that stuff and the code seems much more solid now, and I doubt this issue exists.

While the code isn't identical, it looks similar to me.

https://github.com/3b1b/manim/blob/master/manimlib/mobject/types/vectorized_mobject.py#L680
Ýes you are correct. But take a look at the `insert_n_curves_to_point_list` function, which is the function that ultimately adds the points (I think). Grant seems to have implemented some extra logic in the middle. 
I'm not exactly sure what his code is doing since I'm not an expert on the subject, but the commit in which he modified the function is titled: _Rewrite insert_n_curves_to_point_list to be faster and more even._ https://github.com/3b1b/manim/commit/4b827adfee50f3d6a9b023eabae3e100d22b2e04

Maybe this fixes the issue with the dummy points?
Interesting. I think that doesn't fix things, but it should probably be ported anyway. I did mentally note that that method looked slighty off when i was reviewing this code.
Can it happen because at the end of your animation you just have to render more digits?
I mean, to render `0.00` is much easier than `360.00`.
No, that's not just it. I tried changing it from `100.00` to `999.99` and saw similar slowdowns.
Nope, my tests showed that rendering still slows down while the number of digits remains constant.
I've reviewed this bug.

You are 100% correct, the issue is in align_points. It causes the amount of points in a vmobject to grow unboundedly.

Firstly, i note that you don't really need `become` here. I got a similar effect using `DecimalNumber.set_value` and it works very fast as it doesn't trigger this bug.

---

But in terms of diagnosis, here's my thoughts:

`align_points` is designed to add extra points to each object to ensure they have the same amount for each "subpath". But subpaths are a fiddly concept.

Suppose are initial object `O` has suppath lengths [10]. And we alternate aligning with objects `A`, `B` with subpath lengths [20], and [10, 10]. (A and B are created fresh each time, like in the xample)

My understanding is the following:

* Align 1:
  * starts at O=[10], aligning with A=[20], so add 10 points to O in suppath 1, to get [20]
* Align 2:
  * starts at O=[20], aligning with B=[10, 10], so add 10 dummy points to O for subpath 2 to get [20, 10]. Also update B to [20, 10]
* Align 3:
  * starts at O=[20, 10], aligning with A=[20]. Nothing needs doing to O, but A needs 10 dummy points added at suppath 2.
* Align 4:
  * starts at O=[30] (!), aligning with B=[10, 10]. So add 10 dummy points to O to get [30, 10]. Also add 20 points to B.

("dummy" points occur when createing a new path entirely. Each dummy is a curve with start/end/control points all at the same value)

You can see it's gone wrong at align 4. O has 30 points in suppath 1, despite never aligning with anything of that length! The problem is how subpaths are determined. At the end of align 3, we added dummy points to A. Then between align 3 and align 4, we copy all of A's points to O (this is what `become` does). But then when we ask O what subpaths it has, it can no longer tell that the dummy points where *intended* to be part of subpath 2, it just counts them as part of subpath 1.

This repeats ad infinitum. We'll keep adding points to pad subpath 2 when aligning with A. Then that is forgotten, with the poitns becoming part of suppath 1.

---

As for fixing it, it looks tricky to fiddle too much with how subpaths or dummy points work.

I propose stripping dummy points from the end of each subpath before doing alignment, stopping them growing indefinitely.
This is a very fine and precise analysis, kudos and thank you! Your suggestion seems like a good idea.
Thanks for the comment! Your explanation makes a lot of sense. ManimCE is still mostly Grant's old code, which isn't perfect.
If you take a look at the current 3b1b/manim repository, he's changed most of that stuff and the code seems much more solid now, and I doubt this issue exists.

A major re-design is in order, to make sure we don't have fiddly stuff. But well, that's hard. Maybe once we fully transition to OpenGL (if that ever happens), the whole tool will be much better.
> he's changed most of that stuff and the code seems much more solid now, and I doubt this issue exists.

While the code isn't identical, it looks similar to me.

https://github.com/3b1b/manim/blob/master/manimlib/mobject/types/vectorized_mobject.py#L680
Ýes you are correct. But take a look at the `insert_n_curves_to_point_list` function, which is the function that ultimately adds the points (I think). Grant seems to have implemented some extra logic in the middle. 
I'm not exactly sure what his code is doing since I'm not an expert on the subject, but the commit in which he modified the function is titled: _Rewrite insert_n_curves_to_point_list to be faster and more even._ https://github.com/3b1b/manim/commit/4b827adfee50f3d6a9b023eabae3e100d22b2e04

Maybe this fixes the issue with the dummy points?
Interesting. I think that doesn't fix things, but it should probably be ported anyway. I did mentally note that that method looked slighty off when i was reviewing this code.

Resolving patch:
diff --git a/manim/animation/indication.py b/manim/animation/indication.py
--- a/manim/animation/indication.py
+++ b/manim/animation/indication.py
@@ -100,23 +100,19 @@ def __init__(
         self.color = color
         self.opacity = opacity
         remover = True
-        # Initialize with blank mobject, while create_target
-        # and create_starting_mobject handle the meat
-        super().__init__(VGroup(), run_time=run_time, remover=remover, **kwargs)
-
-    def create_target(self) -> "Dot":
-        little_dot = Dot(radius=0)
-        little_dot.set_fill(self.color, opacity=self.opacity)
-        little_dot.add_updater(lambda d: d.move_to(self.focus_point))
-        return little_dot
-
-    def create_starting_mobject(self) -> "Dot":
-        return Dot(
+        starting_dot = Dot(
             radius=config["frame_x_radius"] + config["frame_y_radius"],
             stroke_width=0,
             fill_color=self.color,
             fill_opacity=0,
         )
+        super().__init__(starting_dot, run_time=run_time, remover=remover, **kwargs)
+
+    def create_target(self) -> Dot:
+        little_dot = Dot(radius=0)
+        little_dot.set_fill(self.color, opacity=self.opacity)
+        little_dot.add_updater(lambda d: d.move_to(self.focus_point))
+        return little_dot
 
 
 class Indicate(Transform):
diff --git a/manim/mobject/types/opengl_vectorized_mobject.py b/manim/mobject/types/opengl_vectorized_mobject.py
--- a/manim/mobject/types/opengl_vectorized_mobject.py
+++ b/manim/mobject/types/opengl_vectorized_mobject.py
@@ -708,7 +708,8 @@ def get_subpaths_from_points(self, points):
     def get_subpaths(self):
         """Returns subpaths formed by the curves of the OpenGLVMobject.
 
-        We define a subpath between two curve if one of their extreminities are coincidents.
+        Subpaths are ranges of curves with each pair of consecutive
+        curves having their end/start points coincident.
 
         Returns
         -------
@@ -1150,6 +1151,7 @@ def refresh_unit_normal(self):
 
     # Alignment
     def align_points(self, vmobject):
+        # TODO: This shortcut can be a bit over eager. What if they have the same length, but different subpath lengths?
         if self.get_num_points() == len(vmobject.points):
             return
 
@@ -1177,7 +1179,16 @@ def get_nth_subpath(path_list, n):
             if n >= len(path_list):
                 # Create a null path at the very end
                 return [path_list[-1][-1]] * nppc
-            return path_list[n]
+            path = path_list[n]
+            # Check for useless points at the end of the path and remove them
+            # https://github.com/ManimCommunity/manim/issues/1959
+            while len(path) > nppc:
+                # If the last nppc points are all equal to the preceding point
+                if self.consider_points_equals(path[-nppc:], path[-nppc - 1]):
+                    path = path[:-nppc]
+                else:
+                    break
+            return path
 
         for n in range(n_subpaths):
             sp1 = get_nth_subpath(subpaths1, n)
diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py
--- a/manim/mobject/types/vectorized_mobject.py
+++ b/manim/mobject/types/vectorized_mobject.py
@@ -1018,7 +1018,7 @@ def gen_subpaths_from_points_2d(self, points):
     def get_subpaths(self) -> typing.Tuple:
         """Returns subpaths formed by the curves of the VMobject.
 
-        We define a subpath between two curve if one of their extreminities are coincidents.
+        Subpaths are ranges of curves with each pair of consecutive curves having their end/start points coincident.
 
         Returns
         -------
@@ -1363,10 +1363,25 @@ def get_arc_length(self, sample_points_per_curve: Optional[int] = None) -> float
         )
 
     # Alignment
-    def align_points(self, vmobject):
-        # This probably makes the current vmobject and the given one have the same number of points,
-        # by adding extra points to the last sub-path. This method is never used in the whole library.
+    def align_points(self, vmobject: "VMobject"):
+        """Adds points to self and vmobject so that they both have the same number of subpaths, with
+        corresponding subpaths each containing the same number of points.
+
+        Points are added either by subdividing curves evenly along the subpath, or by creating new subpaths consisting
+        of a single point repeated.
+
+        Parameters
+        ----------
+        vmobject
+            The object to align points with.
+
+        Returns
+        -------
+        :class:`VMobject`
+           ``self``
+        """
         self.align_rgbas(vmobject)
+        # TODO: This shortcut can be a bit over eager. What if they have the same length, but different subpath lengths?
         if self.get_num_points() == vmobject.get_num_points():
             return
 
@@ -1380,7 +1395,7 @@ def align_points(self, vmobject):
             if mob.has_new_path_started():
                 mob.add_line_to(mob.get_last_point())
 
-        # Figure out what the subpaths are, and align
+        # Figure out what the subpaths are
         subpaths1 = self.get_subpaths()
         subpaths2 = vmobject.get_subpaths()
         n_subpaths = max(len(subpaths1), len(subpaths2))
@@ -1394,9 +1409,19 @@ def get_nth_subpath(path_list, n):
             if n >= len(path_list):
                 # Create a null path at the very end
                 return [path_list[-1][-1]] * nppcc
-            return path_list[n]
+            path = path_list[n]
+            # Check for useless points at the end of the path and remove them
+            # https://github.com/ManimCommunity/manim/issues/1959
+            while len(path) > nppcc:
+                # If the last nppc points are all equal to the preceding point
+                if self.consider_points_equals(path[-nppcc:], path[-nppcc - 1]):
+                    path = path[:-nppcc]
+                else:
+                    break
+            return path
 
         for n in range(n_subpaths):
+            # For each pair of subpaths, add points until they are the same length
             sp1 = get_nth_subpath(subpaths1, n)
             sp2 = get_nth_subpath(subpaths2, n)
             diff1 = max(0, (len(sp2) - len(sp1)) // nppcc)


Test patch:
diff --git a/tests/test_vectorized_mobject.py b/tests/test_vectorized_mobject.py
--- a/tests/test_vectorized_mobject.py
+++ b/tests/test_vectorized_mobject.py
@@ -1,7 +1,10 @@
+from math import cos, sin
+
 import numpy as np
 import pytest
 
 from manim import Circle, Line, Mobject, Square, VDict, VGroup, VMobject
+from manim.constants import PI
 
 
 def test_vmobject_point_from_propotion():
@@ -204,3 +207,61 @@ def test_vgroup_item_assignment_only_allows_vmobjects():
     vgroup = VGroup(VMobject())
     with pytest.raises(TypeError, match="All submobjects must be of type VMobject"):
         vgroup[0] = "invalid object"
+
+
+def test_trim_dummy():
+    o = VMobject()
+    o.start_new_path(np.array([0, 0, 0]))
+    o.add_line_to(np.array([1, 0, 0]))
+    o.add_line_to(np.array([2, 0, 0]))
+    o.add_line_to(np.array([2, 0, 0]))  # Dummy point, will be stripped from points
+    o.start_new_path(np.array([0, 1, 0]))
+    o.add_line_to(np.array([1, 2, 0]))
+
+    o2 = VMobject()
+    o2.start_new_path(np.array([0, 0, 0]))
+    o2.add_line_to(np.array([0, 1, 0]))
+    o2.start_new_path(np.array([1, 0, 0]))
+    o2.add_line_to(np.array([1, 1, 0]))
+    o2.add_line_to(np.array([1, 2, 0]))
+
+    def path_length(p):
+        return len(p) // o.n_points_per_cubic_curve
+
+    assert tuple(map(path_length, o.get_subpaths())) == (3, 1)
+    assert tuple(map(path_length, o2.get_subpaths())) == (1, 2)
+
+    o.align_points(o2)
+
+    assert tuple(map(path_length, o.get_subpaths())) == (2, 2)
+    assert tuple(map(path_length, o2.get_subpaths())) == (2, 2)
+
+
+def test_bounded_become():
+    """Tests that align_points generates a bounded number of points.
+    https://github.com/ManimCommunity/manim/issues/1959
+    """
+    o = VMobject()
+
+    def draw_circle(m: VMobject, n_points, x=0, y=0, r=1):
+        center = np.array([x, y, 0])
+        m.start_new_path(center + [r, 0, 0])
+        for i in range(1, n_points + 1):
+            theta = 2 * PI * i / n_points
+            m.add_line_to(center + [cos(theta) * r, sin(theta) * r, 0])
+
+    # o must contain some points, or else become behaves differently
+    draw_circle(o, 2)
+
+    for _ in range(20):
+        # Alternate between calls to become with different subpath sizes
+        a = VMobject()
+        draw_circle(a, 20)
+        o.become(a)
+        b = VMobject()
+        draw_circle(b, 15)
+        draw_circle(b, 15, x=3)
+        o.become(b)
+
+    # The number of points should be similar to the size of a and b
+    assert len(o.points) <= (20 + 15 + 15) * 4