Eggs-D-Studios / wos-issues

Waste of Space bugs, suggestions, and other feedback
6 stars 0 forks source link

Built in scheduler to the pilot class #14

Open someoneidoknow opened 4 months ago

someoneidoknow commented 4 months ago

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

someoneidoknow commented 4 months ago

womp womp (im someoneidoknow)

Hexcede commented 4 months ago

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?

someoneidoknow commented 4 months ago

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

Hexcede commented 4 months ago

@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)
Hexcede commented 4 months ago

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.