Open someoneidoknow opened 4 months ago
womp womp (im someoneidoknow)
no more boilerplate to write your own scheduler, since hexged has peak programming skills he can use the interrupt functions built in to create a scheduler library which has peak scheduling. people can make OS's easier without hard coding their terrible scheduler which crashes
what does this mean exactly? What would a "scheduler" be?
no more boilerplate to write your own scheduler, since hexged has peak programming skills he can use the interrupt functions built in to create a scheduler library which has peak scheduling. people can make OS's easier without hard coding their terrible scheduler which crashes
what does this mean exactly? What would a "scheduler" be?
this basically means a way to scheduler tasks preemptively without lots of code to make in the background stuff that needs to be done in sub 16 ms timings, such as timing specific events across micro's (tickrate can be 1tps but stuff like logic can maybe be decoupled to 60tps) something like bool Success = scheduler.addTask(coro:thread, priority:number) and then while scheduler.run() do end (scheduler can default to 16 ms per call of work before exiting to do handling work or whatever) this can also work together with the pilot class with ring levels or whatever
@someoneidoknow
If you're simply looking to limit how much work lower ring threads can do overall you can just set a timeout:
local WORK_TIME_LIMIT = 16 / 1000
-- Define a function that spawns a low privilege user thread with a timeout
local function spawnUserCode<A...>(payload: (A...) -> (), ...: A...)
-- Create a thread for the payload function
local thread = coroutine.create(payload)
-- Set a timeout & raise the thread's ring
pilot.setTimeout(WORK_TIME_LIMIT, thread)
pilot.setRing(thread, 1)
-- Start the thread
task.spawn(thread, ...)
end
spawnUserCode(function()
trySomethingExpensive()
end)
You can probably implement the specific behaviour as you described it using something like this:
-- How long the microcontroller can do work for at once before we stop doing whatever we were doing
local WORK_TIME_LIMIT = 16 / 1000
local waitingForSignalGate = false
local workThread = task.spawn(function()
-- Optionally, lower the ring of the work thread so it can't interrupt our privileged code
pilot.setRing(1)
while true do
-- Keep a gate so we know when the thread is waiting to be signaled to do more work and not just yielding
waitingForSignalGate = true
coroutine.yield()
waitingForSignalGate = false
doWork()
end
end)
-- Set a per-tick timeout on the work thread
pilot.setTimeout(WORK_TIME_LIMIT, workThread)
-- Main scheduler
task.spawn(function()
while true do
-- Start the work thread so it does work, but only if it's waiting to be signaled
if waitingForSignalGate then
task.spawn(workThread)
end
-- Wait one frame
task.wait()
end
end)
Another thing you could do would be to try to approximate per-thread CPU times yourself but this has a lot of flaws and is probably kind of overcomplicated:
local WORK_TIME_LIMIT = 16 / 1000
local lastUpdate = os.clock()
local lastThread
-- Keep track of the CPU time of every running thread (without holding onto them so they can GC)
local cpuTimes = setmetatable({}, {__mode = "k"})
-- Interrupt as often as possible
-- This implementation has one flaw because interrupts are global
-- Very busy threads will be statistically more likely to be interrupted, so if we have busy privileged threads, we will be less likely to interrupt the threads we want to throttle
-- Another issue is that if we have a particularly busy non-privileged thread that talks to another non-privileged thread that thread might get attributed the 1st thread's CPU time
-- There is not currently a way around this without implementing your own custom interrupts like pilot does
pilot.setInterrupt(0, function()
-- If the thread is privileged, cancel and set the last update time
if pilot.hasRing(0) then
lastUpdate = os.clock()
return
end
-- Grab the running thread
local thread = coroutine.running()
-- If there is no last thread, set it to the interrupted thread
if not lastThread then
lastThread = thread
end
-- Prefer attributing CPU time to this thread
local preferredThread = thread
-- If the interrupted thread has changed we don't know how much CPU time each thread used
-- So we have to pick one to give the CPU time to (this could use thread priorities for example)
if lastThread ~= thread and pilot.getRing(lastThread) < pilot.getRing(ring) then
preferredThread = thread
end
-- Determine the time elapsed since the last update
local timeElapsed = os.clock() - lastUpdate
-- Add to the thread's CPU time
cpuTimes[preferredThread] = (cpuTimes[preferredThread] or 0) + timeElapsed
-- Set the last update time & the last thread
lastThread = thread
lastUpdate = os.clock()
-- If the CPU time of the interrupted thread is too high, pause it for one second so we have our new CPU budget
if cpuTimes[thread] > WORK_TIME_LIMIT then
task.wait(1)
end
end)
The above exploration of how to solve this problem without a new feature (that I admittedly kinda still don't understand how you expect it to work exactly) does make me realize that pilot.setInterrupt
needs to work per-thread to be more useful/precise for most of its intended use cases... That would fix a variety of issues with all of the examples above.
I will consider implementing individual per-thread CPU tracking. The same kinds of issues are present with setTimeout
currently since CPU tracking is global instead of per-thread.
Guidelines
Version
Development (Unstable)
Topic
A programming feature (e.g. APIs, tweaks, changes, etc)
Why are you making your suggestion?
no more boilerplate to write your own scheduler, since hexged has peak programming skills he can use the interrupt functions built in to create a scheduler library which has peak scheduling. people can make OS's easier without hard coding their terrible scheduler which crashes
What do you propose? What should change, or what should be added?
to add the scheduler i guess ^^, would help OS's