RunService.Heartbeat switching to variable frequency

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?

13 Likes

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.

3 Likes

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.

27 Likes

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

66 Likes

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!

13 Likes

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.

1 Like

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.

13 Likes

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:

3 Likes

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.

4 Likes

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. [quote=“einsteinK, post:15, topic:23509”]
If your code in Heartbeat takes a tiny too much time, your game will slow down A LOT.
[/quote]

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.

2 Likes

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.

2 Likes

It used to be the case where if Heartbeat processing is above a certain threshold, the game starts falling into what’s commonly called a spiral of death.

This is a common problem of all fixed-framerate update systems: imagine you’re trying to run a 30 Hz update step that takes 40 ms. After every run of an update step like this you are “behind” real-time by an extra 40-33=7 ms. So after running 5 frames with 1 step/frame you’re now 35 ms behind so you’re forced to run 2 steps/frame, with every frame now processing 66 ms of real time in 80 ms - becoming 14 ms late. In 3 frames you have to add one more update step/frame, etc. - this cycle never ends.

We had some code that artificially stops the spiral by clamping the # of steps we run per frame. However, with variable-framerate update this is no longer necessary - game framerate depends on the time you take in Heartbeat/RenderStepped but there are no magical thresholds that, when being crossed, result in sudden unexpected performance drops (physics being the major exception that still runs at fixed framerate…).

wait() loops are not subject to spiral of death since they do not properly account for time - wait(0.1) does not really result in 10 Hz update, it will wake the thread after at least 100 ms have passed since the call to wait() but it does not account for time spent in the script or for extra time the script waited.

3 Likes