Download

Memory not freed using FRunnable and TArray

Hi,

I am not able to clear the RAM used by a background task (FRunnable) and a TArray of my own structs. If I don’t use the background task it clears the memory.

See the code below:

MyStructs.h



    #pragma once

    struct FMyDataStruct
    {
        TArray<float> MyArray;
    
        FMyDataStruct() {}
    
        ~FMyDataStruct()
        {
            MyArray.Empty();
        }
    };
    
    struct FMyTaskResult
    {
        TArray<FMyDataStruct> MyItems;
    
        FMyTaskResult()    {}
    
        ~FMyTaskResult()
        {
            MyItems.Empty();
        }
    };


MyTask.h (A task class that loops the NumberOfItems and fills the main actors MyDataArray)



    #pragma once

    #include "Async/AsyncWork.h"
    #include "Runtime/Core/Public/Async/Async.h"
    
    #include "MyStructs.h"
    #include "MyMainActor.h"
    
    class FMyTask : public FRunnable
    {
    public:
        FMyTask(int32 NumberOfItems, TArray<FMyDataStruct>& MyDataArrayRef, FMyTaskCallback& CompleteCallback)
            : Thread(nullptr)
        {
            bKillThread = false;
    
            TotalItems = NumberOfItems;
            TaskCompleteCallback = CompleteCallback;
    
            DataArrayRef = &MyDataArrayRef;
        }
    
        ~FMyTask()
        {
            if (Thread)
            {
                delete Thread;
                Thread = nullptr;
            }
        };
    
        bool Init()
        {
            DataArrayRef->Empty();
    
            return true;
        }
    
        uint32 Run()
        {
            for (int32 Index = 0; Index < TotalItems; Index++)
            {
                if (bKillThread) return 0;
    
                FMyDataStruct MyDataStruct;
                MyDataStruct.MyArray.Add(0);
    
                DataArrayRef->Add(MyDataStruct);
            }
    
            return 0;
        }
    
        void Exit() override
        {
            AsyncTask(ENamedThreads::GameThread, [this]()
                {
                    if (this != nullptr)
                    {
                        if (TaskCompleteCallback.IsBound())
                        {
                            TaskCompleteCallback.Execute(&Result);
                        }
                    }
                });
        }
    
        void Stop() override
        {
            bKillThread = true;
        }
    
        //Start the thread
        bool Startup()
        {
            if (Thread != nullptr)
            {
                return false;
            }
    
            if (FPlatformProcess::SupportsMultithreading())
            {
                //Create the new thread
                Thread = FRunnableThread::Create(this, TEXT("FMyTask"), 0, TPri_AboveNormal);
            }
    
            return (Thread != nullptr);
        }

        //Shuts down the thread
        void Shutdown()
        {
            Stop();
    
            if (Thread)
            {
                Thread->WaitForCompletion();
            }
        }
    
    private:
        FRunnableThread* Thread; //Thread to run the worker on
        FThreadSafeBool bKillThread;
    
        int32 TotalItems;
        TArray<FMyDataStruct>* DataArrayRef;
    
        FMyTaskResult Result;
        FMyTaskCallback TaskCompleteCallback;
    };


MyMainActor.h



    #pragma once

    #include "CoreMinimal.h"
    #include "GameFramework/Actor.h"
    #include "TimerManager.h"
    #include "MyStructs.h"
    
    DECLARE_DELEGATE_OneParam(FMyTaskCallback, FMyTaskResult*);
    
    #include "MyTask.h"
    #include "MyMainActor.generated.h"
    
    UCLASS()
    class TESTPROJECT1_API AMyMainActor : public AActor
    {
        GENERATED_BODY()
    
    public:
        AMyMainActor();
        ~AMyMainActor();
    
    protected:
        virtual void BeginPlay() override;
        virtual void Destroyed() override;
    
    public:
        bool IsEditorOnly() const override;
        bool ShouldTickIfViewportsOnly() const override;
        void OnConstruction(const FTransform& Transform) override;
        virtual void Tick(float DeltaTime) override;
    
        void MyFunc();
        void OnTimerTriggered();
        void MyTaskCallback(FMyTaskResult* Result);
    
        TArray<FMyDataStruct> MyMainArray;
    
    private:
        FTimerHandle MyTimer;
    
        bool bBuild = false;
    
        FMyTask* MyTaskWorker;
        FMyTaskCallback MainTaskCallback;
    };


MyMainActor.cpp



#pragma once

#include "MyMainActor.h"

AMyMainActor::AMyMainActor()
{
    PrimaryActorTick.bCanEverTick = true;
    PrimaryActorTick.TickInterval = 0.1f;

#if WITH_EDITOR
    bRunConstructionScriptOnDrag = false;
#endif
}

AMyMainActor::~AMyMainActor()
{
    //Class Destructor
}

void AMyMainActor::Destroyed()
{
    MyMainArray.Empty();
}

void AMyMainActor::BeginPlay()
{
    Super::BeginPlay();
}

void AMyMainActor::OnConstruction(const FTransform& Transform)
{
    if (!HasAnyFlags(RF_ClassDefaultObject))
    {
        bBuild = true;
    }
}

bool AMyMainActor::IsEditorOnly() const
{
    return true;
}

bool AMyMainActor::ShouldTickIfViewportsOnly() const
{
    return true;
}

void AMyMainActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    if (bBuild)
    {
        bBuild = false;
        MyFunc();
    }
}

void AMyMainActor::MyFunc()
{
    FTimerManager& TimerManager = GetWorldTimerManager();

    if (TimerManager.IsTimerActive(MyTimer))
        TimerManager.ClearTimer(MyTimer);

    TimerManager.SetTimer(MyTimer, this, &AMyMainActor::OnTimerTriggered, 5.f, true);
}

void AMyMainActor::OnTimerTriggered()
{
    FTimerManager& TimerManager = GetWorldTimerManager();
    TimerManager.ClearTimer(MyTimer);

    //CODE 1: This part DOES empty the RAM when run
    /*for (int32 Index = 0; Index < 100000000; Index++)
    {
        FMyDataStruct MyDataStruct;
        MyDataStruct.MyArray.Add(0);
        MyMainArray.Add(MyDataStruct);
    }*/

    //CODE 2: This does NOT empty the RAM when run
    if (!MainTaskCallback.IsBound())
    {
        MainTaskCallback = FMyTaskCallback::CreateUObject(this, &AMyMainActor::MyTaskCallback);
    }

    MyTaskWorker = new FMyTask(100000000, MyMainArray, MainTaskCallback);
    MyTaskWorker->Startup();
}

void AMyMainActor::MyTaskCallback(FMyTaskResult* Result)
{
    MyMainArray = TArray<FMyDataStruct>(Result->MyItems);
    Result->MyItems.Empty();

    if (MyTaskWorker)
    {
        MyTaskWorker->Shutdown();
        delete MyTaskWorker;
        MyTaskWorker = nullptr;
    }
}


I have included two code sections in the OnTimerTriggered(). They demonstrate that the code 1 does work and code 2 does not work.

When you delete the actor inside the editor window/level editor code 1 is emptying the RAM correctly but blocking the editor when its fills (intended) and code 2 does not empty the ram but running in the background.

What I want it to be able to run a background task that produces an array/works on an array then that array be completely emptied in RAM when that actor is deleted from the editor window.

This is an editor only actor. Not for running in a game.

Note: I know the Result is not returning anything useful but I did test entirely working on a TArray inside the task only with its own version then posting the “result” back to the main thread using the AsyncTask run on main thread code but that didn’t remove the RAM when the TArray was emptied either.

Thank you.

I’m still struggling with this one. Does anyone know why the TArrays memory does not get cleared upon actor deletion in the editor?

In the callback function that I was using that the thread calls when it is done was setting the MyMainArray TArray to be a copy of the Results items, which that results code was not even in use inside the thread.

After that was removed the array now does not suddenly change to zero items when the task runs its callback function.

So the TArray is emptied but still says that the ram is sitting at 1300mb when it should be 600mb when the actor is deleted.

I tracked down this extra 700mb to be the one float I was adding to the test struct array I was adding inside the loop that fills the array in the first place inside the thread.

I was under the impression that running the Empty() function on a TArray would invoke the destructor of the struct for each element in the main TArray which would in turn empty its own float array as the code shows.

That does not seem to be the case as the 700mb from that one float value multiplied up by the number of structs is still in memory and never released.

As of yet I don’t know why… :slight_smile:

You are relying on destructures, assuming an actor is immediately destroyed, when in fact subtype of Objects are just marked “pending destroy” and depend on garbage collector to be cleaned up which runs by default every 60 seconds.

You should override AActor::BeginDestroy()

When an Actor is flagged as destroyed, it executes the function.

Hi,

I understand that is how it works but this actor seems to be doing something different/strange.

I have implemented both the Destroyed() and BeginDestroy() functions but Destroyed() is run before BeginDestroy() for some reason and it reports zero items in the array at the time it is run in the editor.

Please see these two images that show log outputs from the actor and the FRunnable thread it runs:


I just cannot get the RAM to empty fully back to what it was before the actor was added to the level editor window.

The only way I have managed to return the RAM back was to not add the float to the array inside the struct which is unexplainable for me at the moment.

Thanks for taking a look at it.

Hmm… maybe you should quit holding a pointer to your data and take a look at TFuture<>

Ok, so I tried the FAutoDeleteAsyncTask which just had the same memory left after the actor was deleted.

I also tried switching to use C++'s std::vector to create an array of FMyDataStruct (my own struct) and use that array in the same way as the TArray that Unreal provides.


struct FMyDataStruct
{
std::vector<float> MyArray;

FMyDataStruct()
{

}

~FMyDataStruct()
{
MyArray.clear();
MyArray.shrink_to_fit();
}
};


void DoWork()
{
DataArrayRef->reserve(TotalItems);

for (int32 Index = 0; Index < TotalItems; Index++)
{
FMyDataStruct MyDataStruct;
MyDataStruct.MyArray.push_back(0.f);

DataArrayRef->push_back(MyDataStruct);
}

UE_LOG(LogTemp, Warning, TEXT("Filled Items: %i"), DataArrayRef->size());
}


void AMyMainActor:Destroyed()
{
UE_LOG(LogTemp, Warning, TEXT("Destroyed Function Items: %i"), MyCPPArray.size());

for (int32 index = 0; index < MyCPPArray.size(); index++)
{
MyCPPArray[index].MyArray.clear();
MyCPPArray[index].MyArray.shrink_to_fit();
}

MyCPPArray.clear();
MyCPPArray.shrink_to_fit();

UE_LOG(LogTemp, Warning, TEXT("Destroyed Function Items After Empty: %i"), MyCPPArray.size());
}

This still left nearly 1400mb behind after the actor was deleted from the level editor window.

There seems to be a problem with the array (TArray and std::vector) data being disconnected from the array such that the array inside the Destroyed() function reports that the array is filled and can be emptied or cleared and shrunk but that disconnection leaves the structs data in place in memory.

@Bruno I have done what you said regarding making the threads pointer to my actors MyMainArray be set to nullptr but that did nothing to help things:


void Shutdown()
{
Stop();

if (Thread)
{
Thread->WaitForCompletion();
}

UE_LOG(LogTemp, Warning, TEXT("Shutdown Items before setting nullptr: %i"), DataArrayRef->Num());

DataArrayRef = nullptr;
}

An interesting observation is that if you empty (TArray) or clear/shrink to fit (std::vector) during the callback from the thread, which runs on the main thread it clears all of the memory successfully. This is pointless however because my data is deleted when I then need to use it.

As I said, if I use the Destroyed() function the array is emptied, which changes the reported number of items from 100,000,000 to 0 but yet the RAM from the 100,000,000 structs array of floats is left intact in memory which it should be totally wiped out.

Note: The code above is while use an FRunnable which allows for shutting down the thread while running where as the FAutoDeleteAsyncTask cannot be stopped and after reporting its result through the callback it just deletes itself as the name suggests.

I could really do with an Epic engine developers opinion of this matter and what the best course of action would be.

Thanks.

Here’s the smallest example I have made that demonstrates the problem.


struct FMyDataStruct
{
     TArray<float> ArrayOfFloats;
     FMyDataStruct(){}
};

//CODE 1: This part DOES empty the RAM when run (ie: Run on main thread)
/*for (int32 Index = 0; Index < 50000000; Index++)
{
    FMyDataStruct MyDataStruct;
    MyDataStruct.ArrayOfFloats.Add(FMath::Rand());
    MyDataStruct.ArrayOfFloats.Add(FMath::Rand());
    MyMainArray.Add(MyDataStruct);
}*/
 
//CODE 2: This does NOT empty the RAM when run (The two floats * 50,000,000) are left in system memory after the actor is deleted)
auto Result = Async(EAsyncExecution::Thread, &]()
{
    for (int32 Index = 0; Index < 50000000; Index++)
    {
        FMyDataStruct MyDataStruct;
        MyDataStruct.ArrayOfFloats.Add(FMath::Rand());
        MyDataStruct.ArrayOfFloats.Add(FMath::Rand());
        MyMainArray.Add(MyDataStruct);
    }
});

Main array is defined in the actors header file as:


TArray<FMyDataStruct> MyMainArray;

and I am clearing the array in the Destroyed() method of the main actor.

I’m sure its something to do with Pointers/References/TSharedPtr/TSharedRef and the array of structs that the array contains going out of scope.

I’ve looked at and tried FRunnable, FNonAbandonableTask and now TFuture/Async. All show the same RAM not clearing problem.

Any help would be appreciated.

Thanks.

You really should consider make further investigation with futures… but one quick dirty thing you can do is this:



struct FMyDataStruct
{
    ////TArray<float>ArrayOfFloats;
    TArray< float , TInlineAllocator<32> > ArrayOfFloats;

    FMyDataStruct(){}
};


You are basically having issues with TArray’s default allocator.
You could go further and create your own custom allocator as well.

(wait for Garbage Collector to kick in when you test it).

Hi Bruno,

Yes that seems to be working. So with that I’m going to have to define the number of elements that the float is going to contain before I use it.

I don’t know enough about allocators at the moment so will have to research it.

Thank you for your help.

No if you do that you’re going to stack overflow.
They have this old blog post explaining a bit about this:

So using TInlineAllocator is not the real answer to this problem.

Structs created and added to a TArray inside of an Async task of whatever kind (TFuture/FRunnable/FNonAbandonableTask) do not release their memory when the actor which holds them is deleted inside of the editor.

If I use the default allocator and initialize the TArray on the main thread, then let the async task compute whatever it needs to, then delete the actor after completion all of the memory is cleared. This is because the memory was allocated on the main thread and not the async thread.

Does anyone know why this would be?

Here’s the code I used to test it:


TWeakObjectPtr<AMyMainActor> Self = this;

//Initialize the array and its structs (plus float array inside the struct)
MyMainArray.Init(FMyDataStruct(2), 50000000);

//TFuture/Async task
auto Result = Async(EAsyncExecution::Thread, [Self]()
{
for (int32 Index = 0; Index < 50000000; Index++)
{
Self->MyMainArray[Index].ArrayOfFloats[0] = FMath::Rand();
Self->MyMainArray[Index].ArrayOfFloats[1] = FMath::Rand();
}

//Call the main threads task completed function
AsyncTask(ENamedThreads::GameThread, [Self]()
{
if (Self != nullptr)
{
Self->MyTaskComplete();
}
});
});

Can anyone answer this question for me?

With a struct like this:


struct FMyFloatArrayStruct
{
public:
    TArray<float> FloatArray;
};

A defined array in the actors header like this:


TArray<FMyFloatArrayStruct> TestStructArray;

An actor Destroyed() function like this:


void AMyActor:Destroyed()
{
    TestStructArray.Empty();
}

Running this code on the actors MAIN THREAD clears out all of the RAM when the actor is deleted in the level editor window:


TestStructArray.Empty();
TestStructArray.Init(FMyFloatArrayStruct(), 300000000);

for (int32 Index = 0; Index < 300000000; Index++)
{
    FMyFloatArrayStruct* Struct1 = &TestStructArray[Index];
    Struct1->FloatArray.Push(FMath::Rand());
    Struct1 = nullptr;
}

…but this code run using an ASYNC TASK, does NOT clear out all of its RAM when the actor is deleted in the level editor window:


AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, &]()
{
   TestStructArray.Empty();
   TestStructArray.Init(FMyFloatArrayStruct(), 300000000);

   for (int32 Index = 0; Index < 300000000; Index++)
   {
      FMyFloatArrayStruct* Struct1 = &TestStructArray[Index];
      Struct1->FloatArray.Push(FMath::Rand());
      Struct1 = nullptr;
   }
});

My RAM for both functions goes up to 9800Mb when fully finished.

The RAM for the MAIN THREAD version goes back down to 600Mb after the actor is deleted.
The RAM for the ASYNC TASK version goes down to 5200Mb after the actor is deleted.

Why is there a difference?