RunService.Heartbeat switching to variable frequency

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

This reply is spot-on btw, thanks!

The reason why specifically camera-related scripts are running in RenderStepped is that we want to change the camera based on user input as soon as possible to minimize the latency between mouse movement and camera turns. User input is processed right before render stepped for the same reason (I forgot to draw this in my diagram).

4 Likes

@zeuxcg
Would you advise using Heartbeat for doing visual updates in GUIs?

Also, with CFrame calculations we’re currently doing in RenderStepped with the Camera, should we be doing calculations in Heartbeat, while applying stuff relative to the Camera during RenderStepped?

Maybe I’m misunderstanding the differences between the two, idk.

1 Like

As always: you don’t necessarily have to optimize if you don’t see any need to do so.

Ideally for performance you would run some logic in Heartbeat that outputs visual data which is used in the next frame in RenderStepped. So you could calculate the UI transformations, CFrames and positions and whatnot of all your visuals in Heartbeat, and then set them in the next RenderStepped cycle. Keep in mind that if you do that, you have a 1 frame delay between input and the visual response. Probably not a huge issue, up to you entirely to decide if you want/need to do that.

2 Likes

Still trying to figure out how much of this effects me with the way I handle projectile rendering… I currently use a single while true do loop uses delta times to handle the CFrame positon of the round and it updates every frame… Should I be using heartbeat instead? What about calling :Lerp() on a part every frame? Should that be heartbeat too?

Also, are we able to set the frequency of the heartbeat? if not… geez, I don’t like the idea of a heart skipping beats or slowing down.

The frequency of Heartbeat is the same as the framerate. If your game slows down, so will Heartbeat. The step argument from Heartbeat tells you how much time it’s been since the last frame, so if you have time-dependent things you can multiply by step to make sure they stay consistent.

but i don’t bind things to heartbeat… just

while true do
runserv.Heartbeat:wait()
end

:c gonna have to get used to this new function stuff aren’t I?

That’s basicly the same (during-which-phase-of-a-step-wise)