RunService.Heartbeat switching to variable frequency

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)

Yeah, try something like
function foo(step)
--blah blah blah
end

RunService.Heartbeat:connect(foo)

I prefer Heartbeat:wait(), as you’ll only use one thread that you yield.
If you use :connect(), it’ll spawn a new thread every time it fires.
(Not that you should car too much about performance in ROBLOX though)

Somehow I don’t think that’s true, but I don’t really know enough to dispute it…

> x = Instance.new("BindableEvent")
> x.Event:connect(function() print(coroutine.running()) end)
> x:Fire()
thread: 0D1325B4
> x:Fire()
thread: 0D1325B4
> x:Fire()
thread: 0D1325B4
> x:Fire()
thread: 0D1325B4
> y = Instance.new("BindableEvent")
> y.Event:connect(function() wait(5) print(coroutine.running()) end)
> y:Fire()
> y:Fire()
> y:Fire()
thread: 0D132714
thread: 0D132874
thread: 0D132454

Well what do you know, looks like the event system re-uses coroutines (“threads”) when it can.

Only seems to happen if the function doesn’t yield and doesn’t error.
This is definitly something weird and unexpected, never knew that happens.
(Which makes me wonder for some sandboxes that use coroutines for data storage or identification…)

One more wrinkle to keep in mind from a game design perspective: you aren’t running your game on just one client at a particular frame rate, but across multiple clients which potentially have varying frame rates.

If you put game logic in methods bound by the local frame rate you will have players who are literally playing the game at different speeds (and a server trying desperately to interpolate between these different states). Imagine 10 people playing Tetris at 3 different speeds all on the same game board, it can cause all kinds of strange issues.

In short: separate your display logic from your game logic and have the game logic bound by time while the display logic is bound by framerate.

1 Like

I think it would be appropriate to adjust this wiki article:
http://wiki.roblox.com/index.php?title=API:Class/RunService/Heartbeat

It still says 1/30th of a second, it should probably include something about it being linked to FPS now.

@ProfBeetle: Yeah, both the RenderStepped and Heartbeat events have an argument that indicates the time since the last event, so people should definitely be using those values and they shouldn’t assume the passed time is always 1/60s.

2 Likes

Done, thanks for the suggestion.

@zeuxcg

Phantom Forces just switched to using heartbeat for all game logic and third person player animations, while keeping renderstepped for first person animations and camera movement.

We want to know whether this has helped improved performance for anyone, and whether the load in each frame has been reduced or not?

My computer is most likely too powerful to notice any appreciable difference.

I’m not gonna lie I just started lagging recently on my $1200 gaming PC.

I’ve NEVER lagged before that. I’m not saying it was that change, but I am saying it never lagged before on my i5 6600+16GB Ram + R9 390.

I get occasional lag spikes, not 24/7 FPS drops.

2 Likes

If devs are encouraged to put logic in heartbeat and rendering in renderstepped, what purpose does stepped have?

Right now in my vehicle physics code, I use stepped for all of the physics calculations.

1 Like