Accurate Yields Down To Thousandths Of A Second

A while back I created a system for accurate yielding. It was really broken, and showed that at the time I had little understanding of roblox task scheduling.

Now after lots of learning through experience, I’ve created a system that ACTUALLY yields more accurate than any system on roblox thus far. Even more accurate than the original wait(). This system is useful for waiting very short periods of time, or in situations where you need a wait more precise than a fraction of a frame length.

local RS = game:GetService('RunService')

local OpTime = 0

local function OperationCheck()
	local st = tick()
	for k = 0,50 do
		local num = 4294967296
		num = num ^ 0.5
		if tick()-st >= 200 then break end
	end
	if tick()-st >= 200 then end
	OpTime = tick()-st
end
local FrameDelta = 0

RS.RenderStepped:connect(function(delta)
	FrameDelta = delta
	OperationCheck()
end)
OperationCheck()

function RealWait(Duration)
	if Duration == nil then Duration = 0.1/3 end
	local st = tick()
	while tick()-st < Duration-FrameDelta-OpTime do
		RS.RenderStepped:wait()
	end
	local n = Duration - (tick()-st)
	for i = 0,n,OpTime do
		for k = 0,50 do
			local num = 4294967296
			num = num ^ 0.5
			if tick()-st >= Duration then break end
		end
		if tick()-st >= Duration then break end
	end
end

Simply call RealWait like you would normal wait.

How does it work? Assuming your desired duration is longer than a single frame, the function will yield for that amount of time minus 1 framelength. Then for the last frame, it’ll use a for loop operation to stretch the last framelength to fill the desired duration. This means super accurate wait times even for users on the slowest of machines.

4 Likes

Few things to note, os.clock() is more precise compared to tick() meaning it’ll have more precision accuracy.

Secondly, custom waits are usually extremely expensive to use, it shouldn’t be used unless it’s a must. Roblox’s wait(n) should be suffice unless accuracy is necessary.

4 Likes

Could I know how you benchmarked to make sure the yield times don’t go above 1^-3th of a second?
I ran this, and 7503 out of 10,000 yields failed (took more than 1.01 seconds to run, i.e a margin of error of 0.01 seconds):

local Failed = 0
for i = 1, 10 ^ 4 do
	coroutine.wrap(function()
		local Time = os.clock()
		RealWait(1)
		if os.clock() - Time > 1.01 then
			Failed += 1
		end
	end)()
end

wait(1.5)
print('Num failed (inaccurate):', Failed)

Console output:
image
This means there’s circa a 75% fail rate.

My PC can run GTA V at ~80 FPS or so, so I’m not inclined to believe this is an issue with insufficient specs.

1 Like

I’d like to add onto my reply:
I’m not the type of person that likes advertising my own products, but what is different between your module and mine?
I’ve benchmarked both by checking total yield times, and with the margin of error being 0.03 seconds, your module failed 8299 out of 10,000 times, whilst mine failed 3053 out of 10,000 times.
This means not only is my module much better since it utilizes a priority queue, it beats your module at something it wasn’t even coded specifically to target.

2 Likes

The difference between my code and yours, is yours requires a new frame to begin to verify the timestamps and end the yield. My code stretches the closest frame to fill the in-between frame gap. At 80 fps, the difference is negligible, but at low and varying framerate, my function retains accuracy down to 0.001 seconds. My testing was done in studio at 5 fps. Normal wait would get the variance down to 0.1 off target, your system could get 0.06, mine could get 0.001.

Also I would hope you’re performing tests after the game loads, since running code during the loading screen (first 3 seconds or so after game start) can be wildly unstable timing wise.

2 Likes

…I don’t even want to know what you would be doing wrong to get 5 FPS in your game. It is literally unplayable at that point. I’m unsure why you would even bother benchmarking something at 5 FPS.

Yes, I’ve added a 5-second yield prior to running the benchmarks.

1 Like

I simply did

couroutine.wrap() 
 while true do
 for i = 0,500000000 do
end
RS.stepped:wait()
end
 end)

Before my test code to achieve the lag for benchmarking. Obviously users would only get this laggy due to their own hardware limitations, but at least my function can keep the timing accurate under extreme conditions.

Note, you may need to adjust the number of the for loop there for your own hardware. Mine is pretty beefy so that number might seem like overkill.

This would imply it is just as good to use the default Roblox wait function, unless your game is constantly running on 5 FPS. Even then, busy waiting is not something you should use frequently, especially considering how this does not have any sort of queue system - this scales up extremely quickly.

Also, running your code whilst on ~6 FPS still makes it fail 9427 / 10,000 times.
image

Under normal circumstances I would be inclined to agree with you, however my method utilizes a busy wait for only the last fraction of a frame before the desired end time. Queuing wouldnt be an issue unless you setup several waits to end at literally the exact same frame. Even so I remeasure how long my operating code takes the user’s CPU to handle every frame, and there’s no reason I couldn’t increase the frequency of the measurement by binding it multiple times to renderstep.

Also I’m not sure what your testing method is like, but in mine I’m literally manually looking at the before and after tick regarding each wait to see the difference in time.

I don’t know why you would need inter-frame accuracy for a yield. Regardless, why stop here? What if you used Stepped, RenderStepped, and Heartbeat all at the same time to determine the most most accurate place to stop?

Because of how roblox handles tasks, you can see the gap very easily in the microprofiler. It’s because of this tiny gap in time, where roblox can’t run any developer’s code, that causes systems like avozzo’s to be consistently off target, how much depends on user performance.

My code was made specifically to address that issue and achieve accurate yielding regardless of user framerate.