prometeu-runtime/discussion/workflow/decisions/DEC-0034-async-work-lane-and-asset-backlog-contract.md
bQUARKz 8f37d971cd
Some checks failed
Intrepid/Prometeu/Runtime/pipeline/head There was a failure building this commit
Intrepid/Prometeu/Runtime/pipeline/pr-master This commit looks good
[PERF] Async Background Work Lanes for Assets and FS
2026-06-28 09:35:08 +01:00

337 lines
11 KiB
Markdown

---
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`.