PostSimulation and Heartbeat

https://create.roblox.com/docs/performance-optimization/microprofiler/task-scheduler
https://create.roblox.com/docs/reference/engine/classes/RunService#Heartbeat

According to documentation, PostSimulation is before Heartbeat and also most of the scripts run in Heartbeat.

I tried this script:

--!strict


local RunService = game:GetService("RunService")


RunService.PreRender:Connect(function()
	print("PreRender")
	task.defer(function()
		print("Deferred PreRender")
	end)
end)


RunService.PreAnimation:Connect(function()
	print("PreAnimation")
	task.defer(function()
		print("Deferred PreAnimation")
	end)
end)


RunService.PostSimulation:Connect(function()
	print("PostSimulation")
	task.defer(function()
		print("Deffered PostSimulation")
	end)
end)


RunService.Heartbeat:Connect(function()
	print("Heartbeat")
	task.defer(function()
		print("Deferred Heartbeat")
	end)
end)


while true do
	task.wait()
	print("Loop!")
end

Here is a result:

21:43:33.581  PreRender  -  Client - LocalScript:8
  21:43:33.582  Deferred PreRender  -  Client - LocalScript:10
  21:43:33.915  PreAnimation  -  Client - LocalScript:16
  21:43:33.915  Deferred PreAnimation  -  Client - LocalScript:18
  21:43:33.915  Loop!  -  Client - LocalScript:41
  21:43:33.945  PostSimulation  -  Client - LocalScript:24
  21:43:33.946  Heartbeat  -  Client - LocalScript:32
  21:43:33.946  Deffered PostSimulation  -  Client - LocalScript:26
  21:43:33.946  Deferred Heartbeat  -  Client - LocalScript:34
  1. Why “Loop!” is before PostSimulation? We were told in documentation that scripts run on Heartbeat which goes after PostSimulation.

  2. Why it seems like that PostSimulation and Heartbeat have intersection? As you can see, with all other sections, we have “Section X” and on the end of section X we have “Deferred Section X”. Why then we don’t see in output this:

Post Simulation → Deferred Post Simulation → Heartbeat → Deferred Heartbeat?

My guess would be:

  1. Loop is before PostSim: The documentation is outdated. Threads scheduled for resumption from task lib seem to be resumed right before PostSimulation.
  2. PostSim/Heartbeat: PostSimulation is essentially the “new” Heartbeat, thus they occur on the same step, and thus their deferred resumptions follow after both. This is the same for RenderStepped/PreRender and Stepped/PreSimulation. This probably also explains #1 a bit.

In general, you can use PostSimulation instead of Heartbeat, PreRender instead of RenderStepped, and PreSimulation instead of Stepped. They can be seen as equivalent in terms of scheduling. I don’t know why the prior ones are not deprecated.

To me, the weird part is if you stick a task.defer after your task.wait. It will be execute after PostSimulation but before Heartbeat.

1 Like

Hello, sleitnick! Thank you for the answer. It seems like you are right. But I kinda hate the fact that docs are outdated :frowning:

I don’t know why deferring after task.wait have this behaviour. The strangest thing that in docs we have that

I believe “Queue Delayed Scripts” can be a box where this task.defer() is executed. But as you stated before, Heartbeat and PostSimulation are the same thing, so now it all again makes no sense. So I don’t know what happens in fact.

I can make a bug report for us just to get proper answer from staff.

Yeah I believe this specific image is outdated unfortunately

It’s not about difference. It’s about understanding what’s going on.

What do you mean by “in AI”? I am not using ChatGPT or anything like that.

I’m sorry, but I still don’t exactly understand what you are saying. I opened documentation and gathered information from it. Or do you mean that documentations are made with AI so you can’t fully trust them?

1 Like

As far as I’m aware, the documentations aren’t made with AI, the engine just changes over time and people probably sometimes forget to change the documents. I might be wrong, but I think you can create a bug report on this.

What’s interesting is that in immediate signal mode, task.wait actually does resume after PostSimulation and before Heartbeat.

PreRender
Deferred PreRender
PreAnimation
Deferred PreAnimation
Stepped
PostSimulation
Loop!
Heartbeat
Deffered PostSimulation
Deferred Heartbeat

Replacing task.wait with Heartbeat:Wait() also results with the same order in deferred mode.

My guess is that despite what the docs say, task.wait() isn’t exactly equivalent to Heartbeat:Wait(), either because it always resumes ‘immediately’ or resumes at a different stage in the frame.

1 Like

task.wait() waits about 1/60th of a second each time. Heartbeat and Stepped will always wait until the next step, making task.wait time-based, while Heartbeat and Stepped are frame-bound. That is why this stuff is a bit hard to nail down.. It’s not always the same.

I'll save you a few reports here.. and just delete them :roll_eyes:

This is not true. task.wait with 0 or no arguments resumes every frame regardless of the time passed.

You can test this out by printing the delta time returned in a loop and changing the in-game framerate settings.

Kind of, this is true with () and (0), but it’s still roughly the same time anyway.
“task.wait() waits about 1/60th of a second”

about and roughly mean this isn’t always the case.
“That is why this stuff is a bit hard to nail down.. It’s not always the same.”

Let’s ask AI .. Yes, that is 100% accurate.
[end of line]

I would say I was pretty close to finishing the logic of all of that, but I have come to the strange wall that makes me think that unfortunately there is a bug / bugs here. But I don’t know, maybe someone can fully finish my logic.

My assumption #1: No, PostSimulation and Heartbeat are different things and I will explain why you can think that they are in fact the same thing, but I want to say that graph from docs is correct but there are still some missing parts.

My assumption #2: Yes, you are right. Heartbeat and task.wait() have some differencies.

Here is again the code that we will use:
Also we will again use the graph from the site:

--!strict

local RunService = game:GetService("RunService")


RunService.PreRender:Connect(function()
	print("PreRender")
	task.defer(function()
		print("Deferred PreRender")
	end)
end)


RunService.PreAnimation:Connect(function()
	print("PreAnimation")
	task.defer(function()
		print("Deferred PreAnimation")
	end)
end)


RunService.PreSimulation:Connect(function()
	print("PreSimulation")
	task.defer(function()
		print("Deferred PreSimulation")
	end)
end)


RunService.PostSimulation:Connect(function()
	print("PostSimulation")
	task.defer(function()
		print("Deferred PostSimulation")
	end)
end)


RunService.Heartbeat:Connect(function()
	print("Heartbeat")
	task.defer(function()
		print("Deferred Heartbeat")
	end)
end)




while true do
	task.wait()
	print("Loop!")
	task.defer(function()
		print("Deferred loop!")
	end)
end

Let’s test in Immediate mode.

 14:48:28.990  PreRender  -  Client - LocalScript:7
  14:48:28.992  Deferred PreRender  -  Client - LocalScript:9
  14:48:29.072  PreAnimation  -  Client - LocalScript:15
  14:48:29.072  Deferred PreAnimation  -  Client - LocalScript:17
  14:48:29.072  PreSimulation  -  Client - LocalScript:23
  14:48:29.072  Deferred PreSimulation  -  Client - LocalScript:25
  14:48:29.073  PostSimulation  -  Client - LocalScript:31
  14:48:29.073  Loop!  -  Client - LocalScript:50
  14:48:29.090  Heartbeat  -  Client - LocalScript:39
  14:48:29.090  Deferred PostSimulation  -  Client - LocalScript:33
  14:48:29.090  Deferred loop!  -  Client - LocalScript:52
  14:48:29.090  Deferred Heartbeat  -  Client - LocalScript:41

Let’s now don’t focus on Deferred loop!, Deferred PostSimulation, Deferred Heartbeat.

As we can see, as documentation said, we have PostSimulation → Loop (“Queue Delayed Scripts”) → Heartbeat. Everything seems to work fine.

Let’s switch to Deferred mode:

14:52:02.357  PreRender  -  Client - LocalScript:7
  14:52:02.357  Deferred PreRender  -  Client - LocalScript:9
  14:52:02.717  PreAnimation  -  Client - LocalScript:15
  14:52:02.717  Deferred PreAnimation  -  Client - LocalScript:17
  14:52:02.717  PreSimulation  -  Client - LocalScript:23
  14:52:02.718  Deferred PreSimulation  -  Client - LocalScript:25
  14:52:02.718  Loop!  -  Client - LocalScript:50
  14:52:02.752  PostSimulation  -  Client - LocalScript:31
  14:52:02.752  Deferred loop!  -  Client - LocalScript:52
  14:52:02.753  Heartbeat  -  Client - LocalScript:39
  14:52:02.753  Deferred PostSimulation  -  Client - LocalScript:33
  14:52:02.753  Deferred Heartbeat  -  Client - LocalScript:41

Now you can think: hm, PostSimulation and Heartbeat are the same things.

But what happens in fact? Let’s not forget that handlers in Deferred mode are executed on invocation points. Here is a list of this points that Roblox have provided here: [Beta] Deferred Lua Event Handling.

RenderStepped
Waiting script resumption (e.g. wait, spawn, delay)
Stepped
Heartbeat
BindToClose

Here where this realisation comes: yes, PostSimulation happens before “Queue Delayed Scripts”, as we saw in Immediate mode. And even now it happened before. But PostSimulation is not invocation point. That’s why connected function to PostSimulation was called only after “Loop!” was printed. It’s because while true loop was resumed through task.wait() so it is Waiting script resumption invocation point, so our queue of callbacks had inside of it function that should print PostSimulation. After that we, as graph suggested, go to Heartbeat.

But the problem is that I am not able to understand the order of task.defer() unfortunately since there is no description of term “end of resume point” which is used in docs in description of task.defer(). It is reasonable to say that resume points are “Input Events” are fired, callbacks are executed; “PreRender Event” is fired, callbacks are executed and so on but unfortunately this logic doesn’t work when it comes to “Queue Delayed Scripts”, Heartbeat and PostSimulation so I don’t know.