Custom wait - the best solution to yielding!

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

Hey! I just updated the source. Let me know if the issue still occurs!

1 Like

Seems like it’s still broken for me;

Has it worked for you yet?

Update [1.0.9]

  • Since I’ve kept experiencing issues with binary heaps, which to be fair I am not competent with, I have switched the module back to using a plain old array - this makes the source much smaller and compact, and arguably slightly faster (unless you have 10^5 or more yields…). This fixes all the issues with the module.

cc. @mi54321

6 Likes

I thought it used time()? I do prefer os.clock(), at least in my experience it seems to go a bit… not as “correct”? You can’t get small precise numbers from it, it will just give you 0, not sure, was a problem for me.

I also saw people doing wait functions by getting the delta time from Heartbeat as well, to the point where I’m not sure what is better :P

local function wait(n: number?)
    if not n then
        return runService.Heartbeat:Wait()
    else
        local lasted = 0
        repeat
            lasted += runService.Heartbeat:Wait()
        until lasted >= n
        return lasted
     end
end
1 Like

It has been switched back to os.clock(). It’s pretty easy to edit the source anyway.

You would be creating a new loop that calls a yielding function (Heartbeat:Wait()) for every single wait call - this module stores all threads to yield and updates all of them in the same loop, thus making it much faster.

2 Likes

Thanks, I’ll probably be using this in my next module. (session locking, so there’s auto save loops which are pretty ew) I even tried making my own version based on yours.

local runService = game:GetService("RunService")
local running = coroutine.running
local c_yield = coroutine.yield
local c_resume = coroutine.resume
local c_create = coroutine.create
local os_clock = os.clock

local beat = runService.Heartbeat

if false and runService:IsClient() then
	beat = runService.RenderStepped
end

local yields = {}

beat:Connect(function(delta)
	local Clock = os_clock()
	for i = 1, #yields do
		local data = yields[i]
		local spent = Clock - data[1]
		if spent >= data[2] then
			yields[i] = yields[#yields]
			yields[#yields] = nil
			c_resume(data[3], spent)
		end
	end
end)

return function(n: number?)
	if not n then n = 0 end
	
	local thread = running()
	yields[#yields + 1] = {os_clock(), n, thread}
	return c_yield()
end

Tried using some tips people gave me, I’m still not sure if I should be using RenderStepped when it’s a client but yeah :D

1 Like

Amazing! I love this. Great job.

am using Artificial Heartbeat

function CreateArtificialHB(name:string?,fps:number?,parent:instance?)
	local ArtificialHB = Instance.new("BindableEvent")
	ArtificialHB.Name = "ArtificialHB" .. tostring(math.random(1,100))

	if name then
		pcall(function()
			ArtificialHB.Name = name
		end)
	end

	if parent then
		pcall(function()
			ArtificialHB.Parent = parent
			parent:WaitForChild(ArtificialHB.Name)
		end)
	end
	
	if not fps or fps < 1 then
		fps = 60
	end
	
	frame = 1/fps or 1/60
	local tf = 0
	local allowframeloss = false
	local tossremainder = false
	local lastframe = tick()

	ArtificialHB:Fire()

	local connection = game:GetService("RunService").Heartbeat:Connect(function(s,p)
		tf = tf + s
		if tf >= frame then
			if allowframeloss then
				ArtificialHB:Fire()
				lastframe = tick()
			else
				for i = 1,math.floor(tf/frame) do
					ArtificialHB:Fire()
				end
				lastframe = tick()
			end
			if tossremainder then
				tf = 0
			else
				tf = tf - frame * math.floor(tf/frame)
			end
		end
	end)

	return ArtificialHB,connection,function(number:number?)
		if not number or number < 1 then
			ArtificialHB.Event:Wait()
			return true
		end
		for _ = 1,number do
			ArtificialHB.Event:Wait()
		end
		return true;
	end
end

local ArtificialHB,Signal,Await = CreateArtificialHB("ArtificialHB",60)

while true do
--ur stuff
Await()
end

Hey,
thanks for amazing resource but I have experienced problem the custom wait does not working in ModuleScripts inside ServerScriptService is it intended?

In what way is it not working? could you show me the code? It’s been working for me and many others, so please thoroughly verify that this is not an issue on your end. If it is indeed an issue on our end, please make a reproduction script which reproduces the bug in as few lines as possible.

Sorry I probably can’t (not on pc atm) but it happened to me in multiple places with different scripts.
Basically it yielded infinitely anything after the line with customwait does not worked.

Example:

local cwait = require(game:GetService("ServerScriptService").CustomWait) 

function module:New(n) 
print("x") 
cwait(n) 
print("y") 
end

And yeah I required both module scripts :smiley:.

Edit: Hmm thats odd maybe can this be caused that I actually clone the ModuleScript?

That example script works for me - I’m afraid this would be an issue on your end.
image

local Test = require(script.Parent)

print(1)
Test:New(5)
print(2)

Where can I take this module? or is it just the source code