Custom wait - the best solution to yielding!

Update [1.0.8]

  • Addressed issue where a yield wouldn’t work properly (@GrayzcaIe)
  • Optimization of code to function faster (I’ll show a benchmark to compare all solutions in a moment)

Glad you figured out a solution. I will check it out when I get back on. But otherwise, exceptional job making this module. Looking forward to more of your potential projects. :grin:

So I wrote my own custom yield script, just for fun, and after doing a benchmark it seems to be functioning better than this one.

Benchmark script:

-- // By StrategicPlayZ \\ --

local FastWait = require(workspace.FastWait)
local CustomWait = require(workspace.CustomWait)

for i = 5,1,-1 do
	print("[STARTING BENCHMARK IN " .. i .. "s]")
	wait(1)
end

local total = 50000
local wait_time = 1

local benchmark_func = FastWait do
	print("...[BENCHMARK BEGIN]...")
	
	local num_failed = 0
	local total_wait = 0
	local highest_wait = 0
	local lowest_wait = math.huge
	
	local completed
	
	game:GetService("RunService").Stepped:Wait()
	
	for index = 1, total do
		coroutine.wrap(function()
			local taken_time = benchmark_func(wait_time)
			if ((taken_time > (wait_time + 0.04)) or (taken_time < (wait_time - 0.04))) then
				num_failed += 1
			end
			if (taken_time > highest_wait) then
				highest_wait = taken_time
			end
			if (taken_time < lowest_wait) then
				lowest_wait = taken_time
			end
			total_wait += taken_time
			completed()
		end)()
	end
	
	do
		local n = 0
		function completed()
			n += 1
			if (n ~= total) then return end
			
			local average_wait = (total_wait / total)
			
			print("[% NUM FAILED]: " .. (num_failed / total * 100) .. "%")
			print("[AVERAGE WAIT]: " .. average_wait)
			print("[HIGHEST WAIT]: " .. highest_wait)
			print("[LOWEST  WAIT]: " .. lowest_wait)
		end
	end
end

Result (custom wait):
Screenshot_10

Result (my wait module):
Screenshot_11

https://www.roblox.com/library/6356332088/FastWait

After slightly altering my code, the end result was this:

  • Yours
    image
  • Mine
    image

All I altered was the for loop in the Stepped event, editing it from 10,000 to 100,000. By doing so, it effectively unyielded all the yields at once, rather than taking 5 Stepped events to unyield them, since it only unyielded a maximum of 10,000 per Stepped. I set this limit with script exhaustion time in mind, which your script does not take into account for. By raising the for loop limit, this module effectively addresses the flawed benchmark.
Either way, I will not be raising the unyield limit from 10,000 since as stated, it could cause havoc when you have 100+ while true do wait() end loops.

3 Likes

Screenshot_8

I set mine to 100k, it still shows similar result, I even set it to while true loop, it still shows some fails.

By the way forgot to mention I have edited the module with os.clock instead of time since it is much more accurate.

This is the result when I changed os.clock back to time().

Screenshot_9

You too try with os.clock instead.

Here is the result of my module:
Screenshot_10

And no it’s not os.clock being inefficient, as you can confirm it from the following code in the command bar:
(Run this when the game is running.)

local a = time()
print("a")
print(time()-a) -- OUTPUTS 0s
local a = os.clock()
print("a")
print(os.clock()-a) -- OUTPUTS SOMETHING DIFFERENT, IN MY CASE: 0.000154
1 Like

This seems like an issue of having different hardware in that case. I don’t really bother comparing stuff at such a level, since it’s very prone to illusionary and misleading results - the only reason I compared this module with Roblox’s is that the margin of error is small enough in comparison to Roblox’s “failure rate”, and as such the margin of error can be ignored completely. With modules that provide very similar results with only the slightest differences, it’s hard to say what’s truly the better option. I’ll run both scripts though the micro-profiler and also boatbomber’s benchmarker plugin instead, as they provide with more accurate results.

image

3 Likes

So I tried to use it CustomWait() instead of CustomWait(.1) on while true loop and my Computer hated me, should definitely add a default minimum

1 Like

…Could you elaborate? If no time is added, it will yield for 1 Stepped frame before unyielding shortly after.

Weird, I did this code and it exhausted the script

while true do
wait()
-- Code
end

That’s weird. I’ve confirmed this behavior just now, and it does seem like wait(0) or wait() achieve this effect. An easy solution would be to just do wait(0.01) for now. I’ll fix this next update and clamp the minimum value to 0.01.

1 Like

That doesn’t happen with my module. Seems like using a binary heap is not a good way of implementing custom yields.

Does your post have any other relevancy to this discussion other than to brag and advertise your own module?

1 Like

It isn’t bragging. I am just saying that your module isn’t the best solution to yielding (as it claims in the title), nor do I claim mine is the best.

Best != perfect. Don’t mix up the words. Thus far I have not seen any solution which other modules offer better solutions than this module. You mentioned your own module with no other relevancy other than to imply it is better than mine because the issue mentioned above does not occur with yours. Before you state that you didn’t say that, I meant imply. Intent is enough, and saying that was not your intent would be delusional.

1 Like

Update [1.0.9]

  • Addressed bug where wait() or wait(x) where x <= 0 would not yield the thread. cc @LiISk8rBoi

According to my benchmarking the module of CloneTrooper mentioned in the main thread performs better than this one.

This loops us back to the previous debate - code performs and scales differently on different machines. There is no one absolute, and I’m not that interested in results that could be biased depending on what machine it was ran.

Either way, since both utilize custom task schedulers they are already optimized out well enough. The only difference you would see between the two would be when running both > 10^7 times, which would never occur in any productive code.
Also, sorry for being rude in the previous reply. I was rather heated towards the reply.

1 Like

That’s a satisfactory reply. Thanks.

1 Like

I have found a bug (in Roblox).

When you use this module, you can not stop it from the outside, i.e deleting/disabling the script will not stop it.

Also when you are using custom wait modules the Script Performance tab does not show the usage % of it.

We can fix this by checking if a script is deleted or disabled in loops etc. This bug is probably because of how Roblox works internally.

EDIT: Tested it again, it stopped this time. The outcome is variable, or it is dependent on the code in the loop.

I mean… I’m unsure as to why you’d wanna do that, but a quick workaround would be:

local Yielded = {}
local function WrappedWait(n)
    Yielded[coroutine.running()] = true
    local a,b = RBXWait(n)
    Yielded[coroutine.running()] = nil
    return a, b
end
local function UnyieldAll()
    for Thread in next, Yielded do
        coroutine.resume(Thread)
    end
    table.clear(Yielded)
end
2 Likes