RunService.Heartbeat switching to variable frequency


This is an advance announcement - Heartbeat will change behavior on Thursday (March 24th).

Currently Heartbeat is running at approximately 30 Hz (with the exception of slowly running games where we can't maintain 30 Hz heartbeat).
On Thursday Heartbeat will start running at whatever framerate the game runs at. This means that it will usually run somewhere between 30 Hz and 60 Hz (higher for VR).

Since the only high-frequency signal in ROBLOX at this time is RenderStepped, some developers of games and scripts are using it as a signal to run the game logic, custom animations, custom replication etc. at. Due to the way ROBLOX frame is scheduled, this limits the performance of games in certain cases. In the worst case, running code in RenderStepped as opposed to Heartbeat can slow down the game by ~2x.

After this change developers will be advised to migrate most of code like that to Heartbeat (with a notable exception of scripts that work with camera and/or character). Essentially if you want to run your code at a high-frequency, you should use Heartbeat; and you should only run code as part of RenderStepped if running it in Heartbeat introduces artifacts you can not tolerate (e.g. visual separation between camera and other objects due to different update times).

There are two practical consequences of this change:

  1. If you were relying on the fact that Heartbeat is 30 Hz, your code may change behavior. As a reminder, you're expected to use the "step" argument in Heartbeat signal to perform time-based updates. If you use Heartbeat, you should audit your code NOW to prepare for the change.

  2. If you had previously used Heartbeat signal to run lots of Lua code without regard to performance, average framerate in your game may drop. This is not really a problem in practice - if you had a lot of Lua code running as part of Heartbeat, the game would typically alternate between fast frames (e.g. 15 ms) and slow frames (e.g. 35 ms - assuming Heartbeat took 20 ms). While you may think that saving time every other frame is good for performance, in reality the odd-even frame pattern leads to stuttering that's much more objectionable than smooth but lower framerate. If you do have a lot of game logic the correct solution is so try to spread this logic out over multiple frames.

I will update this thread when the change goes live. Feel free to ask questions or post comments here before or after the change.

Why does this produce different results?
Best way to move a truck without physics?

So to get this straight:

-Heartbeat fires on each frame
-RenderStepped firest on each frame
-Code run through RenderStepped must finish running before the next frame can begin rendering
-Code run through Heartbeat has no effect on the next frame's rendering

You want us to run things through Heartbeat so we don't potentially slow down framerate?


Where does Stepped fall into all this? Is it similar to the (soon-to-be) old behavior of Heartbeat?


Stepped should fire everytime "a wait() cycle starts" I thought.
It's getting very complex and difficult to know right now.


Stepped is variable frequency right now and it executes before physics simulation. Heartbeat would execute after. I think at this point the only way to run logic at fixed frequency is to do a wait() loop.


Here's an example of how after this change two consecutive frames for a 60 FPS game may execute. Note that replication (processing incoming packets) and wait() resumption are currently 30 Hz (well, technically wait() resumption depends on the minimum wait time, which is 1/30 right now).

This is obviously not to-scale; the actual distribution of time between these items depends on the game and hardware.


Where has that sort of diagram been all my life?
I made it pretty for anyone who wants to write a wiki article about all this.

To clarify, Stepped would get a frequency boost too?

Edit: Made it less eye-melty, added user input


Yeah, Stepped is actually variable-frequency right now. I'm not sure when that happened - I'd guess about a year ago.


This change is now live!


Going to use it for custom animations and projectile raycasting on owner client and all other clients. Let's see if it can handle this.


I'm quite confused about this.

When am I supposed to be using RenderStepped, and when am I supposed to be using Heartbeat?

Currently, I do camera stuff, physics, movement, and pretty much everything else on RenderStepped. Is this bad?


Stick the camera stuff in RenderStepped, stick the rest you mention in Heartbeat.


I'm curious as to why, though.


If you look at the picture above, you can see that RenderStepped blocks the execution of any further steps until it yields. It runs before all the Rendering / Network Replicate / wait() resume / Humanoid / Stepped / Physics / Heartbeat. So if you stick all of your code in there, it means that all that code has to execute before the rest can begin. In that same image, everything after RenderStepped does run in parallel. So there are two pipelines there, and the top one (Rendering) will run in parallel with the bottom pipeline (all the other stuff). The bottom pipeline doesn't block the rendering of the next frame.

Now imagine that in the situation where you run all your code in RenderStepped, the RenderStepped cycle takes 10ms, the top pipeline takes 10ms, and the bottom pipeline takes 1ms. Since the top pipeline runs in parallel with the bottom one, the two combined will run for max(10ms, 1ms) = 10ms. RenderStepped needs to be executed before that, so the whole cycle takes 10ms + 10ms = 20ms.

In an alternative case, suppose that you move your non-camera code to Heartbeat instead of RenderStepped. The RenderStepped cycle now takes 1ms, the top pipeline takes 10ms still, and the bottom pipeline now takes 10ms. Again, the two pipelines together run in parallel so that's 10 ms. However, since your RenderStepped call only takes 1ms now, the total cycle time will be 1ms + 10ms = 11ms.

This means you get a performance increase when sticking non-rendering related code in Heartbeat due to parallelism and the fact that Heartbeat doesn't interfere with the rendering of the current frame.

Why does this produce different results?

But just stick with a while wait() do STUFF() end loop.
As may be visible on the image, it'll only run certain frames, at a rate of 30FPS (if possible).
Rendering and Physics might be running at 60FPS.
From my experience in SBs: Never use Heartbeat.
If your code in Heartbeat takes a tiny too much time, your game will slow down A LOT.
(When this happens in a SB, 1/wait() returns numbers below 1 (FPS), while the code behind it is simple)

EDIT: Don't trust what I said above (yet), maybe read the replies below to understand why this might be wrong.


Quite the opposite of what the person who knows how these pipelines work internally says though, and I'm more inclined to follow that advice:


Might change, but I still don't trust Heartbeat or Stepped.


@buildthomas's explanation and @zeuxcg's diagram do a good job of illustrating where and why Heartbeat should be used over RenderStepped. Consider rereading them.

Posting strong, unsubstantiated advice like this

Is ill-advised, as these replies are public and someone might be inclined to actually heed that advice.


I just converted my procedural animations to update based on Heartbeat and there seems to be little to no difference in performance(for me at least). If there's a chance that using heartbeat will improve the performance on lower-end machines then I'll definitely continue using it.

Using the step returned from heartbeat(similar to renderstepped), I can do a bit of math to make up for the time between frames. I've setup my lerping and increments to update based on step, which prevents the animations from "slowing down" at lower framerates. I do the same thing in my kart tech demo for acceleration and turning, thus making everyone turn and accelerate at an equal rate to one another regardless of framerate.


Performance gains are not guaranteed - if rendering time is short enough then running in RenderStepped and in Heartbeat is equivalent. However, you can easily hit cases where rendering takes more time - e.g. on low-end desktop machines you may become rendering bound even if you aren't on high-end; on mobile you're likely to be rendering-bound; etc. Mobile specifically is an important case where running code in Heartbeat may be "free" more often than not.