Memory leak with Placement objects

Here's the place for discussion related to coding in FreeCAD, C++ or Python. Design, interfaces and structures.
Post Reply
Fred3
Posts: 7
Joined: Tue Jun 23, 2020 8:31 am

Memory leak with Placement objects

Post by Fred3 »

Hi,

I'm working on a workbench doing some animations and I found a memory leak while modifying Placement objects. I managed to reproduce the issue with a macro.

I'm moving a part from the part workbench back and forth along the x axis for 5 minutes with a lot of modifications of the Placement object of the part. I use way to much translations to make the problem more obvious. Here is the macro:

Code: Select all

import time


def SimulateTranslationAxis(elementName, direction, positionFrom, positionTo, duration, returnInPosition):
    element = FreeCAD.ActiveDocument.getObject(elementName)
    FPS = 30
    length = abs(positionTo - positionFrom)
    stepCount = int(duration * FPS)
    stepLength = length / stepCount

    directionVector = FreeCAD.Vector(direction[0], direction[1], direction[2]).normalize()
    target = element.Placement.copy()
    target.move(directionVector * (positionTo - positionFrom))

    if positionFrom > positionTo:
        directionVector *= -1

    stepMove = directionVector * stepLength / 100

    for _ in range(stepCount):
        start = time.perf_counter()
        for _ in range(100):
            base = element.Placement.Base
            element.Placement.Base.x = base.x + stepMove.x
            element.Placement.Base.y = base.y + stepMove.y
            element.Placement.Base.z = base.z + stepMove.z
        Gui.updateGui()
        sl = 1.0 / FPS - (time.perf_counter() - start)
        time.sleep(sl if sl > 0.0 else 0.0)

    element.Placement = target


startTime = time.perf_counter()
while time.perf_counter() - startTime < 300:
    SimulateTranslationAxis("Box", (1, 0, 0), 0, 150, 1, False)
    time.sleep(1.1)
    SimulateTranslationAxis("Box", (1, 0, 0), 150, 0, 1, False)
    time.sleep(1.1)
    SimulateTranslationAxis("Box", (-1, 0, 0), 0, 150, 1, False)
    time.sleep(1.1)
    SimulateTranslationAxis("Box", (-1, 0, 0), 150, 0, 1, False)
    time.sleep(1.1)
To use it simply create a box with the park workbench and execute the macro. On my computer this will use around 300MB of ram and never release it.
Except for the useless "for _ in range(100)" loop in this macro is there anything wrong that can produce this result? I tried different ways of moving the part (Placement.move(), Placement.multiply(), creating a new Placement and assigning it,...) but nothing I tried works.

I tried with version 0.18, 0.19 and 0.20 of FreeCAD:

Code: Select all

OS: Windows 10 Version 1903
Word size of FreeCAD: 64-bit
Version: 0.20.29177 (Git)
Build type: Release
Branch: releases/FreeCAD-0-20
Hash: 68e337670e227889217652ddac593c93b5e8dc94
Python 3.8.10, Qt 5.15.2, Coin 4.0.1, Vtk 8.2.0, OCC 7.6.2
Locale: French/Switzerland (fr_CH)
Installed mods: 
  * Assembly4 0.11.10
  * MeshRemodel 1.8918.0
  * Render 2022.1.0
wmayer
Founder
Posts: 18807
Joined: Thu Feb 19, 2009 10:32 am

Re: Memory leak with Placement objects

Post by wmayer »

Except for the useless "for _ in range(100)" loop in this macro is there anything wrong that can produce this result?
How big is stepCount? In case it's a huge number then the call of "for _ in range(stepCount):" may allocate a huge amount of memory.

At the end of the script you should explicitly run the garbage collector in order to see if the allocated memory will be freed or not.
Fred3
Posts: 7
Joined: Tue Jun 23, 2020 8:31 am

Re: Memory leak with Placement objects

Post by Fred3 »

wmayer wrote: Wed Jun 29, 2022 9:56 am How big is stepCount? In case it's a huge number then the call of "for _ in range(stepCount):" may allocate a huge amount of memory.
In this example stepCount is equal to 30.
The speed in which the memory is allocated with my macro is not realistic at all but I don't need the for loop to have the problem. It's here just to speed up the process and have a big enough memory leak in minutes instead of hours.
wmayer wrote: Wed Jun 29, 2022 9:56 am At the end of the script you should explicitly run the garbage collector in order to see if the allocated memory will be freed or not.
With an explicit call to gc.collect() the behaviour is the same. The memory is not freed.
Fred3
Posts: 7
Joined: Tue Jun 23, 2020 8:31 am

Re: Memory leak with Placement objects

Post by Fred3 »

I have narrowed down the suspect and something is definitely fishy with the placement accessor. With this following macro:

Code: Select all

import gc
import tracemalloc


element = FreeCAD.ActiveDocument.getObject("Box")
tracemalloc.start()
snapshot1 = tracemalloc.take_snapshot()

for _ in range(100000):
    element.Placement

gc.collect()
snapshot2 = tracemalloc.take_snapshot()
tracemalloc.stop()
for stat in snapshot2.compare_to(snapshot1, 'lineno')[:10]:
    print(stat)
We take a snapshot of the allocated memory, do 100000 access to element.Placement, explicitly call the garbage collector and then take a second snapshot to compare it with the first one.

To execute the macro simply create a cube with the part workbench and run the macro.
On my computer the output is the following:

Code: Select all

memory4.FCMacro:10: size=7032 KiB (+7032 KiB), count=100005 (+100005), average=72 B
Line 10 is:

Code: Select all

element.Placement
We can see that doing 100000 access to element.Placement will allocate around 7MB of memory and never free them.
Fred3
Posts: 7
Joined: Tue Jun 23, 2020 8:31 am

Re: Memory leak with Placement objects

Post by Fred3 »

I have kind of a workaround for this issue. It's definitely the getter of the Placement object that causes a memory leak. It's possible to reduce its impact by going from:

Code: Select all

element = FreeCAD.ActiveDocument.getObject("Box")

for _ in range(1000):
    vector = FreeCAD.Vector(1, 1, 1)
    element.Placement.move(vector)
    Gui.updateGui()
To:

Code: Select all

element = FreeCAD.ActiveDocument.getObject("Box")
placement = element.Placement.copy()

for _ in range(1000):
    vector = FreeCAD.Vector(1, 1, 1)
    placement.move(vector)
    element.Placement = placement
    Gui.updateGui()
The less element.Placement is called the better. The first way leaks around 70KB whereas the second only 1KB.
Post Reply