Custom wait - the best solution to yielding!

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

I had some rare use cases of disabling the script (with a loop inside) from the outside, but they were a long time ago and were perhaps just bad coding practice.

But the script performance one is still kind of relevant when measuring efficiency of scripts.

Amazing module! Could you also add an custom implementation of the DebrisService AddItem function?

Already done!

1 Like

Hey, I’m having an issue with the module. It seems whenever I use multiple wait() at the same time, all of them will wait for the amount of time that was longest. I dumped the source code into a module called Wait2 and then ran this code:

local t = tick() 
local wait2 = require(game.ReplicatedStorage.Modules.Wait2) 
coroutine.wrap(function() 
      wait2(3) 
end)()
wait2(1) 
print(tick() - t)

The print will always spout a number around 3.

The code used in the module:

local heap = {}
local currentSize = 0

function insert(yieldTime, data)
	currentSize += 1
	local start = time()
	heap[currentSize] = {
		pos = start - yieldTime,
		data = data,
		time = start
	}

	-- bubble up
	local pos = currentSize

	local parentIdx = math.floor(pos/2)
	local currentIdx = pos
	while currentIdx > 1 and start-heap[parentIdx].pos < start-heap[currentIdx].pos do
		heap[currentIdx], heap[parentIdx] = heap[parentIdx], heap[currentIdx]
		currentIdx = parentIdx
		parentIdx = math.floor(parentIdx/2)
	end
end
function extractMin()
	if currentSize < 2 then
		heap[1] = nil
		currentSize = 0
		return
	end

	heap[1], heap[currentSize] = heap[currentSize], nil
	-- sink down
	local k = 1
	local start = time()
	while k < currentSize do
		local smallest = k

		local leftChildIdx = 2*k
		local rightChildIdx = 2*k+1

		if leftChildIdx < currentSize and start-heap[smallest].pos < start-heap[leftChildIdx].pos then
			smallest = leftChildIdx
		end
		if rightChildIdx < currentSize and start-heap[smallest].pos < start-heap[rightChildIdx].pos then
			smallest = rightChildIdx
		end

		if smallest == k then
			break
		end

		heap[k], heap[smallest] = heap[smallest], heap[k]
		k = smallest
	end
	currentSize -= 1
end

game:GetService('RunService').Stepped:Connect(function()
	local PrioritizedThread = heap[1]
	if not PrioritizedThread then
		return
	end

	local start = time()
	-- while true do loops could potentially trigger script exhaustion, if you were to have >50k yields for some reason...
	for _ = 1, 10000 do
		local YieldTime = start - PrioritizedThread.time
		if PrioritizedThread.data[2] - YieldTime <= 0 then
			extractMin()
			coroutine.resume(PrioritizedThread.data[1], YieldTime, start)

			PrioritizedThread = heap[1]
			if not PrioritizedThread then 
				break 
			end
		else
			break
		end
	end
end)

return function(Time)
	Time = (type(Time) ~= 'number' or Time <= 0) and 0.001 or Time
	insert(Time, {coroutine.running(), Time})
	return coroutine.yield()
end