--- id: DEC-0034 ticket: perf-async-background-work-lanes-for-assets-and-fs title: Async Work Lane and Asset Backlog Contract status: accepted created: 2026-06-28 accepted: 2026-06-28 ref_agenda: AGD-0008 plans: [PLN-0123, PLN-0124, PLN-0125, PLN-0126, PLN-0127, PLN-0128] tags: [perf, asset, fs, async, scheduler, runtime] --- # DEC-0034 - Async Work Lane and Asset Backlog Contract ## Status Accepted. ## Contexto `asset.load()` currently creates an OS thread per non-resident asset request. That behavior does not match the target runtime model: the machine should have explicit execution lanes, bounded ownership, observable backpressure, and no uncontrolled thread creation from guest-visible operations. Render worker concurrency is already handled by separate render decisions. This decision covers the third runtime lane for asynchronous IO/decode/persistence work: ```text Lane 1: VM, firmware, SystemOS, logical frame Lane 2: render worker / asynchronous rasterization Lane 3: async work lane for asset IO/decode and game persistence work ``` The concrete first consumer is asset loading. Game persistence through memcard may also consume this lane. FS is allowed to consume this lane, but public FS API shape belongs to the app-home filesystem agenda. ## Decisao Prometeu MUST introduce a third async work lane as the runtime-owned execution place for asset IO/decode work and compatible persistence IO work. The async work lane MUST be serial: it has exactly one active job at a time. The runtime MUST NOT create one OS thread per `asset.load` request in the normal path. Asset loading MUST use a backlog keyed by target `bank_type/slot`. A target can have at most one current request. A newer request for the same `bank_type/slot` MUST supersede the previous request for that target. Asset handles MUST represent stable bank slots, not transient worker threads. A handle MUST remain queryable even when the slot has no valid loaded asset and even when there is no active request for that slot. The handle state MUST separate resident slot state from request/backlog state. Asset IO/read and decode/materialization MUST run on the async work lane. Asset install/commit into resident banks MUST happen on the main runtime lane at predictable ownership points. Memcard work MAY consume the same async work lane and has higher priority than ordinary asset loads when contention exists. FS MAY consume the same lane for IO-style work, but this decision does not define the public FS API. ## Rationale A serial lane maps cleanly to the intended third-core mental model and avoids a desktop-biased generic thread pool. It gives the runtime one place to observe IO/decode backlog, progress, priority, cancellation, and telemetry. Keying requests by `bank_type/slot` makes the backlog naturally bounded by the sum of bank slots. There is no need for a guest-visible `queue_full` state: there cannot be unbounded distinct pending targets if each target has at most one current request. Keeping commit/install on the main lane preserves deterministic publication semantics and avoids letting the async lane mutate resident graphics/audio/scene state while the VM, render handoff, or frame boundary is observing it. Separating `slot_state` and `request_state` inside the handle avoids ambiguity: callers can inspect the slot at any time, while mutating operations still remain protected by request generation. ## Invariantes / Contrato ### Async Work Lane - The async work lane MUST be runtime-owned. - The async work lane MUST be separate from the render worker. - The async work lane MUST execute at most one active job at a time. - The runtime MUST NOT use `thread::spawn` per asset request in the normal asset loading path. - The host may implement the lane as a fixed worker thread. Hardware targets may map it to a dedicated core when available. - If a physical third core is unavailable, the implementation MUST preserve the same logical lane contract. ### Asset Backlog - Asset requests MUST target a concrete `bank_type/slot`. - The backlog is ordered and serial. - Each `bank_type/slot` MUST have at most one current request. - A newer request for the same `bank_type/slot` MUST supersede any earlier pending request for that target. - If the target already contains the requested `asset_id` as a valid resident asset, `asset.load` MUST return a handle in ready state without adding a job to the backlog. - The effective backlog size is bounded by the sum of targetable bank slots, even though the contract does not expose a fixed queue limit. - Implementations MUST collect stale metadata for canceled or superseded requests so the handle/request history does not grow without bound. ### Handles An asset handle MUST represent a stable bank slot target. The handle state MUST include at least: ```text handle: bank_type slot slot_state: loaded_asset_id resident_state slot_generation request_state: requested_asset_id request_generation state backlog_position progress ``` The handle MUST be queryable when: - the slot is empty; - the slot contains a valid resident asset; - a request is queued; - a request is active; - a request is ready for commit; - a request has been canceled; - a request has been superseded; - a request ended in error. Mutating operations such as commit, cancel, promote, demote, or move MUST act on the current request generation. They MUST NOT accidentally mutate a newer request through an older handle view. ### Superseding `superseded` means a request was replaced by a newer request for the same `bank_type/slot`. - Superseding is an operational state, not a fault. - If the old request is queued, it MUST be removed from the backlog. - If the old request is active, the lane SHOULD cancel cooperatively when the current phase supports cheap cancellation. - If active work cannot be interrupted cheaply, it MAY finish, but its result MUST be discarded when its generation no longer matches the target's current request generation. ### Asset Backlog API Direction The existing asset operations remain the base surface: ```text asset.load(asset_id, slot) -> (status, handle) asset.status(handle) -> status asset.commit(handle) -> status asset.cancel(handle) -> status ``` The asset backlog surface SHOULD add a small status-first API: ```text asset.backlog_info() -> (status, pending_count, active_handle, active_asset_id, active_bank_type, active_slot, active_progress) asset.backlog_position(handle) -> (status, state, position, progress) asset.backlog_move(handle, new_position) -> status asset.backlog_promote(handle) -> status asset.backlog_demote(handle) -> status asset.target_status(bank_type, slot) -> (status, asset_id, handle, state, position, progress) ``` `asset.backlog_promote(handle)` is an official shortcut for moving a queued request to position `1`, the first pending position after the active job. `asset.backlog_demote(handle)` is an official shortcut for moving a queued request to the end of the pending backlog. The final ABI names and exact return shapes may be refined during planning, but the implementation MUST preserve the capabilities above. ### Progress Progress MUST be represented without floating point. The recommended scale is `0..10000`. The initial phase model is: ```text queued -> 0 read -> 0..4000 decode -> 4000..9000 stage -> 9000..10000 ready -> 10000 ``` If a phase cannot report internal progress, it MUST keep the previous progress mark and advance at phase completion. Implementations MUST NOT invent false precision for non-linear decode phases. ### Telemetry Telemetry MUST be cheap and must not add hot-path per-item decode cost. Minimum telemetry: - current backlog depth; - target/request position; - active job progress; - jobs submitted; - jobs completed; - jobs failed; - jobs canceled; - jobs superseded; - job duration; - percentiles by `bank_type`; - lightweight percentiles or small-window samples by `asset_id`. Percentiles MUST be updated when a job closes, not inside the inner decode loop. ### Priority When async lane consumers contend, the initial priority order is: 1. memcard commit/write; 2. FS write/config work; 3. asset visual/audio/scene load; 4. non-critical list/read work. This priority order is a lane arbitration rule. It does not define public FS syscall semantics. ### FS Boundary FS MAY consume the async work lane for IO-style work. This decision MUST NOT define the public FS API, app-home FS semantics, or whether FS is exposed as request/poll or sync-appearing operations. Those questions belong to `AGD-0006`. `AGD-0006` MUST be updated or interpreted with the existence of this async IO lane in mind. ## Impactos ### Spec - `docs/specs/runtime/15-asset-management.md` must absorb the async lane, backlog, handle, superseding, progress, and telemetry contract. - `docs/specs/runtime/16-host-abi-and-syscalls.md` must absorb any final public asset backlog syscall names and return shapes. - `docs/specs/runtime/16a-syscall-policies.md` may need status catalog updates for superseded/canceled/backend unavailable states if not already covered by asset domain status. - `docs/specs/runtime/09-events-and-concurrency.md` should mention the async work lane as an implementation-side lane that does not introduce guest callbacks. - `AGD-0006` / future FS spec work must account for FS as a possible consumer of the async IO lane. ### Runtime - Asset loading must stop spawning a thread per request. - AssetManager needs a serial backlog keyed by `bank_type/slot`. - Asset handles need stable slot identity plus separated `slot_state` and `request_state`. - Commit/install remains a main-lane operation. - Superseding and generation checks become required correctness mechanisms. ### Host - Desktop host may implement the async work lane with one fixed worker thread. - Host presentation/render worker remains separate and unaffected. ### Firmware - No direct firmware behavior is required for asset backlog mechanics. - Firmware flows that rely on preload or memcard may later use lane telemetry or priority policy if needed. ### Tooling - Test tools may need support for inspecting backlog state and forcing request ordering. - Asset fixtures should cover queued, active, ready, canceled, superseded, and already-resident paths. ## Referencias - `AGD-0008` - Async Background Work Lanes for Assets and FS. - `docs/specs/runtime/15-asset-management.md` - `docs/specs/runtime/16-host-abi-and-syscalls.md` - `docs/specs/runtime/16a-syscall-policies.md` - `docs/specs/runtime/09-events-and-concurrency.md` - `docs/specs/runtime/08-save-memory-and-memcard.md` - `AGD-0006` - App Home Filesystem Surface and Semantics. ## Propagacao Necessaria 1. Update asset management specs with the lane/backlog/handle contract. 2. Update syscall specs if backlog APIs become public ABI. 3. Update FS agenda/spec work to acknowledge the async IO lane without deciding FS public API in this decision. 4. Plan implementation of a serial async work lane and asset backlog. 5. Plan tests for superseding, stable slot handles, immediate-ready resident assets, progress, telemetry, and main-lane commit. ## Revision Log - 2026-06-28: Initial draft from `AGD-0008`.