Performance: Cleaning Up Spawned Tasks

Hey,

So in this post I'm going to talk about Tasks, and how to handle cleaning them up to prevent them building up and still running in the background. Tasks that are left running can affect the performance of your game the longer the server instance is running.

I recommend using the Performance Profiler to see if this, or other things may be affecting your game.

What is a Task?

Things like Scripts, Tick functions, required scripts are all coroutines. Because Lua is single threaded, it uses coroutines to run things like scripts concurrently.

Spawned Tasks in Core are pretty much coroutines, so they can be spawned on their own and are non-blocking. Meaning, they are asynchronous. So you could have some code in a spawned task, and the current task will not be blocked.

Something to note about spawned tasks, is the scheduler will run the task at the end of the current frame.

Spawning Tasks

Before I go into the potential problem that spawned tasks can cause, let's first understand how to use them.

Non-Repeating Tasks

A non-repeating task, is a task that is spawned and automatically cleaned up. These types of tasks don't usually need cleaning up, though that depends on what the task is being used for.

Task.Spawn(function()
    print("Hello World")
end)

Not the most useful way to use a task, but it shows you how simple it is to create them.

Task.Spawn supports a second argument where you can specify the delay of the task in seconds.

Task.Spawn(function()
    print("5 Seconds has passed")
end, 5)

Repeating Tasks

A repeating task is a task that will repeat constantly until either it reaches the specified repeat count, or when it is cancelled.

local my_ticking_task = Task.Spawn(function()
    print("A ticking task is ticking")
end)

my_ticking_task.repeatCount = -1
my_ticking_task.repeatInterval = 1

In the above example, a task is spawned that will repeat forever every second. If you don't set a value for repeatInterval, then the spawned task will behave like the Tick function (but without the delta time). This is useful when you need the behavior of the Tick function, but the Tick function can't be used (i.e. a library script that would get required).

Example Problems

So in this section, am going go over a few example problems that can catch people out, then later I will discuss how to fix the problem.

Problem Example 1 - Delayed Task Still Called

If you have a spawned task in a script that has a delay. This script or parent object is destroyed, but the spawned task still runs.

A use case could be that an NPC is spawned, after X seconds, the NPC deploys a turret that will shoot the player. The player may have killed the NPC before the turret is deployed, however, because the task has been created, after X seconds, the turret still gets deployed even though there is no NPC.

In the video, you will see that the NPC is killed, but the turret still gets deployed. For the example, this is the wrong behavior, I do not want to see a turret appear later after the NPC has been killed.

print("NPC spawned")

Task.Spawn(function()
    print("You can't get rid of me that easily...")
    
    Events.Broadcast("spawn_turret")
end, 4)

The above code is in a script that is a child of the NPC. Due to the task being delayed, it still gets called.

Problem Example 2 - Repeating Task Lives Forever

Similar to the first problem, the difference this time is that the NPC can control if the turret can shoot the player or not by turning it off and on every X seconds. This uses a repeating task to handle the on and off switching for the turret.

In the video you can see when the NPC has been killed, the turret still spawns (problem 1), and the new problem exists where the turret is turned on and off when the NPC has been killed.

local repeating_task = Task.Spawn(function()
    Events.Broadcast("toggle_turret")
end, 4)

repeating_task.repeatCount = -1
repeating_task.repeatInterval = 4

The code above is in addition to the first problem code. This task is repeated forever every 4 seconds.

Imagine if you are spawning in objects that players can destroy, and these objects all have their own repeating task. Over time, the performance of the game would be affected as these tasks build up.

Cancelling Tasks

If you have repeating tasks, or delayed tasks that should not be run at all, then a good solution is to cancel them.

In the case of the 2 problem examples, both tasks should be cancelled. The first task is not repeating, so it will get automatically cleaned up, but it introduces a game play bug.

Let's look at how we can fix the above 2 problems.

print("NPC spawned")

local spawn_turret_task = Task.Spawn(function()
    print("You can't get rid of me that easily...")
    
    Events.Broadcast("spawn_turret")
end, 4)

local repeating_task = Task.Spawn(function()
    Events.Broadcast("toggle_turret")
end, 4)

repeating_task.repeatCount = -1
repeating_task.repeatInterval = 4

-- Check for when the script is destroyed

script.destroyEvent:Connect(function()
    print("Script destroyed, cleaning up tasks.")

    spawn_turret_task:Cancel()
    repeating_task:Cancel()
end)

Each task created is stored in a variable, so that when the script is destroyed, they can be cancelled. By cancelling a task, it will return a status of 0, which indicates the task reference is invalid or the task has been destroyed.

With cancelling the tasks, they are immediately cancelled, and it solves the issues when the NPC is killed.

Self Cancelling Task

You could have a repeating task that does self cancelling.

local count = 1
local repeating_task = nil

repeating_task = Task.Spawn(function()
    if(count == 5) then
        repeating_task:Cancel()
    end

    print("Count", count)

    count = count + 1
end)

repeating_task.repeatCount = -1
repeating_task.repeatInterval = 1

In this case, the repeating_task variable is moved above the task so it has a reference to it in the function that is called on each task iteration. Once the count equals 5, the task is cancelled.

Summary

Understanding how to use tasks, and more importantly the potential issues that could occur with them, is a very useful tool to have in your toolbox.

The side effects from spawning tasks that should be cancelled is easily overlooked. So have a think next time you need spawn a task, and consider what is the goal of the task to see if it should be cancelled or not.

If you have any questions, feedback, or additional information that other creators would benefit from, please post below.

4 Likes