Custom wait - the best solution to yielding!

UPDATE - task.wait or this module?

The old wait had restrictions in that it had an internal throttler which caused wait calls to become extremely unrelieable past a certain point, which is no longer the case with task.wait. The performance difference between task.wait and this module has gotten smaller, which means this module should only be used if you either seek extreme precision, or to yields hundreds of threads in parallel. 99% of the time, you won’t need this module anymore :slightly_smiling_face:


Hello all.
I’ve recently been working on a custom implementation of Roblox’s wait function, and my main source of motivation for this was due to the fact how poorly Roblox’s yields scale when using wait. Basically, this is a custom task scheduler.
This is a pure Lua implementation so sadly it doesn’t have a huge edge against Roblox’s, but through my benchmarking it is still way more performant.
This module uses uses os.clock instead of tick for performance reasons, which as a bonus would return more accurate yield times (7 or more decimals).
As this uses the .Stepped event, this supports faster yield times than Roblox’s default (1 / 30).

Why should I use this, rather than default wait?

The main concern in a lot of Roblox games is that they use the wait function. Like, a lot. This is more bad than you may think. Firstly, this can clog up the task scheduler very easily, and deteriorate the state of your game very quickly. @Maximum_ADHD has demonstrated this issue, as seen here:
https://twitter.com/CloneTeee1019/status/1264764207498657792
Yeah… Pretty bad.
So, how does this module address this? Simple! This module manages your yields for you, and instead of letting it update and check for every yield, it simply stores them in an arrays and iterates over them. This is a better solution since you would converse back and forth less with the C API, thus sparing Roblox it’s resources.

TLDR This module makes sure your game will always have only one yield updating at once, making your game not die from having a filled task scheduler.

Source code can be found here: https://github.com/flareo/RBXWait/blob/master/Source.lua

Usage:
wait(number Time)
Returns:
number YieldTime, number CPU Time

Check out my other modules!

159 Likes

Would this still be useful in games that don’t have a lot of wait functions?

1 Like

Absolutely. Using wait() itself can take around .1 seconds, and using RenderStepped can take longer based off of FPS. A custom wait is always useful.

1 Like

I’ve tried creating a wait alternative with these events as well, and there’s a major problem with them: Stepped and Heartbeat fire a lot slower when there is heavy physics simulation. Your code is complex though, and I can’t be sure right now that it slows down. I’ll do a test when I’ll get back home.

This uses a priority queue, so it actually just updates one yield at a time (meaning this runs at O(1) always), rather than looping through every yield and checking if you should resume them or not.
This removes a ton of lag, and I’m hoping that will affect the heavy physics simulation bit?
If it doesn’t, then there’s nothing you can do about it as this relies on Stepped.

1 Like

By the way, your thing refuses to work, sadly.
image

Something just to keep in mind here, tweening a non existent number and then waiting for that tween to be completed is alot more efficient then doing .Stepped over and over till it reaches a certain point in time.

1 Like

What was your original problem that resulted in this library? Why do you need a better implementation of wait? I think if you’re hitting some limit related to the implementation of wait you may be doing something wrong. What are you trying to optimize? Anyway, great resource!

I only update one index at a time, so I’m unsure how performing a function call to the C side (or however Roblox handles it), have roblox lerp that value, and then invoke an rbx script signal is more efficient.
Also, that would scale horribly with more yields.

1 Like

Actually, there’s no particular reason: I wanted to learn about containers (linked lists, priority queues …) more, and thought contributing at the same time would be a good idea.

@UnpalatabIe sorry for that, I’ll look into it tomorrow.

1 Like

Update [1.0.1]

  • Since this module is gaining attraction, I’ve revised and cleaned up the source. Mainly, I’ve removed all the localizations since due to Roblox’s custom implementation of Lua (Luau), localization (of globals, at least) is slower.

Love this module, but sometimes I face the problem of it waiting infinitely… Do you know what causes this?

Do you have a repro script for this? Otherwise, I’m unsure what the issue would be.

EDIT:

Found the issue!
If you have two wait calls right next to each other (e.g print(Wait(1), Wait(1))), this module would first resume the yielded thread and then remove the already unyielded thread from the priority queue.
What happens in some cases is that a new thread was added to the priority queue before the unyielded thread was removed, disrupting the other and messing up #t, because getting the length of a table such as this:

{
    [2] = ...,
    [3] = ...
}

would return 0, because that’s actually a dictionary and not an array, since the first index is 2 and not 1.

1 Like

Alright, so apparently Roblox deprecated elapsedTime, which this module uses.
What’s especially weird is the message it provides:
image
elapsedTime returns the elapsed time the Roblox instance has been running for whilst os.clock returns CPU time, which are two different components. I won’t be using os.clock in favor of elapsedTime for now, because even Roblox’s wait uses elapsedTime internally, as indicated by this script:

print(select(2, wait(1)), elapsedTime())

This would output:
image
Obviously there is a margin of error, but the meaning behind what I mean is clear.

Another reason I won’t be switching to os.clock would be that running os.clock and elapsedTime in studio would return two completely different values:
image

I’m unsure why Roblox decided to deprecate elapsedTime, not even updating the API and stating it’s been deprecated, but I won’t be switching elapsedTime over to os.clock for now.

7 Likes

Update [1.0.2]

  • Removed and cleaned up source once more, removing functions that were no longer used, or only used once. Also switched from table.insert(Table, FindBestSpot(Time), Value) to Table[FindBestSpot(Time)] = Value.

  • Fixed an issue where if you had two wait calls right next to each other (e.g print(Wait(1), Wait(1))), this module would first resume the yielded thread and then remove the already unyielded thread from the priority queue.
    What happened in some cases is that a new thread was added to the priority queue before the unyielded thread was removed, disrupting the array’s order and thus messing up #t, because getting the length of a table such as this:

{
    [2] = ...,
    [3] = ...
}

would return 0, because that’s actually a dictionary and not an array, since the first index is 2 and not 1.

3 Likes

Same person, same issue~
It still yields infinitely sometimes. Although the problem could be in my own code, I’m pretty confident it isn’t, as my problem was fixed once I used the normal wait instead of your custom one. Any ideas on what the problem could be?

[This has been addressed in the new update.]

Update [1.0.3]

  • I have no idea what I was thinking, using t[i] = v rather than table.insert(t, i, v), because t[i] = v wouldnt shift any elements above i by one, thus replacing the thread currently yielded that was stored in that index. This is what caused seemingly random infinite yields. Sorry @Sweet_Smoothies and others for the issues caused.

  • Fixed issue with stack overflow; I’ve switched to a much more performant while true do loop rather than a recursive function when updating a yielded thread.

  • Source is now more compact and clean.

5 Likes

Update [1.0.4]

I am pleased to announce that this module now uses binary heaps - probably the best solution when it comes to speed. A binary heap averages at a whopping O(1) insertion time and O(1) find-min time, but most notably it has an O(log n) delete-min time! This is a huge improvement from the previous version of the module. Thank you CntKillMe and Jiramide for helping me understand how to properly implement binary heaps :slight_smile: .

I’ve also switched from elapsedTime to os.clock - since Roblox deprecated out elapsedTime and said to use os.clock instead, I’ll be doing that. The second return parameter has been renamed accordingly from elapsedTime to CPU Time, since that is what os.clock returns.

4 Likes

Update [1.0.4.5]

  • Fixed issues where if you start several yields at once, the script may not be able to keep up.
1 Like