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

11 KiB

id ticket title status created accepted ref_agenda plans tags
DEC-0034 perf-async-background-work-lanes-for-assets-and-fs Async Work Lane and Asset Backlog Contract accepted 2026-06-28 2026-06-28 AGD-0008
PLN-0123
PLN-0124
PLN-0125
PLN-0126
PLN-0127
PLN-0128
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:

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:

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:

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:

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:

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.