Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit3bb1da2647

README.md

frame_budget

The share of a frame that cooperative engine work spends before it hands the frame back.

Some engine work is one long sequence of small units run on a task: a scene load applies one record after another, and the world-load onRegister sweep fires one instance's hook after another. Run back to back in one frame, a long sequence holds that frame for its whole length, and nothing else the engine does (answering a call, drawing, input) happens until it is through. Asking due() before each unit and yielding when it answers true runs the same work across as many frames as it needs, a slice of each.

local FrameBudget = require("@builtin::modules.frame_budget")

for _, item in ipairs(items) do
    FrameBudget.yield()
    process(item)
end
  • One slice per frame, shared. Each caller is charged the time it runs between its readings of the slice in a frame (task.runTime), whoever runs it, and the charges of every caller add up to the frame's slice. What the rest of the frame runs (systems, components, other tasks) is not charged, so a busy frame still gives the slice to the work. Two long pieces of work running at once share a frame's slice rather than taking one each.
  • A unit is never split. The first unit of a frame always runs, so a frame holds at most the slice plus the one unit that crossed it. A unit that is itself long stays one piece of work.
  • Code that cannot yield runs in its frame. due() answers false outside a coroutine that can yield, so a caller called from such code does all its units in the frame it was called in, as it would without the budget.

A caller that does work of its own around the yield asks due() and calls wait(), which yields to the next frame and opens that frame's slice as the caller resumes; yield() is the two together.

Work whose unit is a native call that bounds itself by time is told how long it may run: remainingMs() answers what is left of the current frame's slice, or nil for code that cannot yield, which then runs the whole of its work in the frame it was called in. The publish gate's script check runs this way:

local block, checking = __spacetime_vcs.scriptPublishBlock(FrameBudget.remainingMs())
while checking do
    FrameBudget.wait()
    block, checking = __spacetime_vcs.scriptPublishBlock(FrameBudget.remainingMs())
end

The background lane

Some work is started early so it is ready when asked for, and no picture waits on it, such as the search text an editor reads ahead of the first tools.search, the skills and procedural packs an agent or a build reads later, the procedural nodes loaded ahead of a build's first plan, and the language server's re-check of the files a play flip reads. background(fn) runs fn on the background lane: every yield, due and wait its work makes on that coroutine hands the frame back for as long as a hold on the lane stands, and takes its slice as usual once none does.

FrameBudget.background(function()
    for _, path in ipairs(paths) do
        FrameBudget.yield()
        warm(path)
    end
end)

Work the picture waits on holds the lane while it runs: holdBackground(key) until releaseBackground(key). A world's entry holds it under "world entry" from the moment the world is ready (its onRegister sweep) until the world load reads visually_complete on the boot timeline (its scene drained, the content it draws resident, nothing owed to its picture and no loading screen over it), so the frames of the entry carry the world's load and its first picture, and the background work runs in the frames before and after them. A hold no one releases lapses after BACKGROUND_HOLD_SECONDS (or the seconds it was taken for), with a warning naming its key, so work on the lane always runs in the end. backgroundHolds() lists the keys holding the lane now, and backgroundWaits() answers whether the calling code's next unit waits for it (a walk with a clock of its own asks it between its pieces).

FrameBudget.DEFAULT_MS is the slice a call that names none spends. spent() answers how much of the current frame's slice is gone.

A caller with a budget of its own reads mark() where its work starts. ranMs(mark) answers how long it has run since, none of the time it spent suspended; cpuMs(mark) answers the CPU time of its work since: those stretches and the stretches of every coroutine it resumes, and every one they resume, while it waits on them, which a machine whose cores are taken by other work does not stretch, so a bound that refuses work for running too long refuses the same work on a loaded machine as on an idle one, and work handed to a coroutine as work run in place. A task it starts and waits for runs while it is suspended, which cpuMs does not count.