Join the RoClicks Discord
RoClicks

Scheduler.luau

Luau, 97 lines from Engineering overview

Download Scheduler.luau
--!nocheck
-- Scheduler: time-budgeted coroutine jobs, so heavy work (streaming chunks, cloning big models, building a
-- world, baking meshes) spreads over frames instead of one long hitch. Client or server.
--
--   local S = require(Scheduler)
--   S.budgetMs = 4                                   -- per frame, for ALL jobs together
--   local job = S.add(function()
--       for i, piece in pieces do
--           piece:Clone().Parent = folder
--           S.step()                                 -- yields if this frame's budget is used up
--       end
--   end, 10, "chunk")                                -- priority (lower runs first), tag
--   S.cancel(job) / S.cancelTag("chunk")             -- drops the coroutine at its last step()
--   S.drain(5)                                       -- run everything now (edit-mode harnesses, tests)
--
-- RULE: never cancel a job in the middle of a RELEASE (destroy / return-to-pool). Cancel drops the coroutine
-- where it yielded, so a half-released model is orphaned forever: duplicate ground, stale props, leaked parts.
-- Publish the new thing first, then release the old one in its own job under a tag nobody cancels.
local S = {}
S.budgetMs = 4
S.worstSliceMs = 0 -- longest uninterrupted slice seen (find the loop that forgot to call step())

local jobs = {}
local frameStart = os.clock()
local inJob, draining = false, false

function S.add(fn, priority, tag)
	local job = { co = coroutine.create(fn), pri = priority or 0, tag = tag }
	table.insert(jobs, job)
	return job
end

function S.cancel(job) if job then job.dead = true end end

function S.cancelTag(tag)
	for _, j in jobs do if j.tag == tag then j.dead = true end end
end

function S.pending()
	local n = 0
	for _, j in jobs do if not j.dead then n += 1 end end
	return n
end

local function over()
	if draining then return false end
	return (os.clock() - frameStart) * 1000 > S.budgetMs
end

-- cooperative yield point: call it inside every loop iteration of a job
function S.step()
	if inJob and over() then coroutine.yield() end
end

local function runFrame()
	frameStart = os.clock()
	if #jobs == 0 then return end
	table.sort(jobs, function(a, b) return a.pri < b.pri end)
	local i = 1
	while i <= #jobs and not over() do
		local j = jobs[i]
		if j.dead then
			table.remove(jobs, i)
		else
			inJob = true
			local t0 = os.clock()
			local ok, err = coroutine.resume(j.co)
			S.worstSliceMs = math.max(S.worstSliceMs, (os.clock() - t0) * 1000)
			inJob = false
			if not ok then
				warn("[Scheduler] job error: " .. tostring(err) .. "\n" .. debug.traceback(j.co))
				table.remove(jobs, i)
			elseif coroutine.status(j.co) == "dead" then
				table.remove(jobs, i)
			else
				i += 1
			end
		end
	end
end

function S.drain(maxSeconds)
	local t0 = os.clock()
	draining = true
	while #jobs > 0 and os.clock() - t0 < (maxSeconds or 20) do runFrame() end
	draining = false
	return #jobs
end

-- one Heartbeat hook drives everything (pass a fake signal in plain-luau tests)
function S.start(heartbeat)
	heartbeat = heartbeat or game:GetService("RunService").Heartbeat
	return heartbeat:Connect(runFrame)
end

return S