--- id: AGD-0008 ticket: perf-async-background-work-lanes-for-assets-and-fs title: Agenda - [PERF] Async Background Work Lanes for Assets and FS status: accepted created: 2026-03-27 resolved: 2026-06-28 decision: DEC-0034 tags: [perf, asset, fs, async, scheduler, runtime] --- # Agenda - [PERF] Async Background Work Lanes for Assets and FS ## Contexto Depois das decisoes de render worker, a concorrencia de render saiu do escopo desta agenda. O problema restante e a lane de trabalho assincrono para IO, decode e persistencia. O modelo de execucao que queremos discutir agora assume uma terceira core/lane dedicada a trabalhos assincronos de runtime: ```text Core/lane 1: VM, firmware, SystemOS e frame logico Core/lane 2: render worker / rasterizacao assincrona Core/lane 3: async work lane para asset IO/decode e possivelmente FS ``` Essa terceira lane nao deve virar um pool grande e solto. A intencao e ter uma fila previsivel, com capacidade limitada, telemetria e regras de saturacao. Para assets, a necessidade e concreta: `asset.load()` ainda cria uma thread do SO por requisicao. Para FS, a motivacao provavel era evitar que operacoes de leitura/escrita/listagem/delete, hoje chamadas de forma sincrona pelo dispatch, roubem tempo do frame quando o backend real envolver IO. Precisamos verificar se FS deve usar a mesma lane agora ou apenas reservar compatibilidade. ## Problema `asset.load()` hoje dispara `thread::spawn` por requisicao de asset nao residente. Isso escala mal, cria jitter e deixa o runtime sem uma politica unica de backpressure. O dominio `fs` tambem tem operacoes potencialmente bloqueantes (`read`, `write`, `list_dir`, `delete`, memcard commit/read/write) e a tabela de syscalls ja as marca como nao deterministicas/custosas. Ainda assim, a implementacao atual executa essas chamadas diretamente no caminho de dispatch. Sem um contrato explicito, cada dominio pode inventar sua propria concorrencia: asset criando threads, FS bloqueando o frame, e memcard herdando comportamento acidental do backend. ## Pontos Criticos - `thread::spawn` por request escala mal e cria jitter. - A terceira core/lane deve ser um recurso de plataforma controlado, nao uma permissao geral para criar threads. - Asset loading precisa de um pool/fila limitada para multiplos loads pendentes. - FS pode justificar a mesma lane quando envolver IO real, mas nao devemos assinar uma API assincrona ampla sem entender a semantica de retorno. - Commit/install em bancos residentes pode ter restricoes de determinismo e ownership; decode pode ser assincrono, mas publicacao/instalacao talvez tenha que cruzar de volta para a lane principal. - A estrategia precisa funcionar em hardware alvo com poucos cores e tambem no host desktop sem transformar desktop em referencia de custo errada. - Render worker ja tem seu proprio contrato e nao deve compartilhar essa lane. ## Hotspots Atuais - [asset.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-drivers/src/asset.rs#L438) - `AssetManager::load`. - [asset.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-drivers/src/asset.rs#L507) - `thread::spawn` por load. - [fs.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-hal/src/syscalls/domains/fs.rs#L4) - `fs` e `mem` marcados como superficies custosas/nao deterministicas. - [dispatch.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs#L549) - chamadas `fs.*` executam no dispatch. - [virtual_fs.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-system/src/services/fs/virtual_fs.rs#L69) - VFS normaliza path e chama backend diretamente. ## Alvo da Discussao Fechar um modelo de terceira core/lane para trabalho assincrono de runtime, comecando por asset loading e avaliando se FS deve entrar na mesma lane no v1. O alvo nao e criar multitarefa geral, callbacks para guest, ou um scheduler paralelo da VM. A lane deve executar trabalho de IO/decode/sync e publicar resultados observaveis por status/polling em pontos previsiveis. ## O Que Precisa Ser Definido 1. Topologia da terceira lane. Definir: - uma async work lane dedicada, separada da VM e do render worker; - se a lane e uma thread fixa no host e uma core dedicada no hardware alvo; - fallback quando nao houver core fisico dedicado; - proibicao explicita de `thread::spawn` por request no caminho normal. 2. Pool/fila de asset loading. Definir: - backlog de pedidos sem limite operacional fixo; - exatamente um job ativo por vez na lane; - o "pool" significa backlog e estados de job, nao multiplos workers reais; - politica de substituicao/cancelamento para pedidos que miram o mesmo bank/slot; - status para pending/backlog, ativo, progresso, cancelamento, erro de decode e sucesso. 3. Fases do asset load. Separar: - request e alocacao de handle; - leitura de bytes; - decode/materializacao; - staging de resultado; - install/commit em banco residente. Ponto a fechar: quais fases rodam na terceira lane e quais voltam para a lane principal por determinismo/ownership. 4. Inclusao de FS na mesma lane. Verificar se `fs` deve usar a terceira lane no v1 para: - leituras grandes; - escritas; - listagem; - delete; - memcard commit/read/write. Ponto a fechar: FS sync publico continua bloqueante/status-first, vira request/poll, ou ganha apenas uma implementacao interna que drena trabalho em pontos de frame. 5. Separacao por dominio. Decidir se `asset` e `fs` compartilham uma fila com classes de prioridade ou se a terceira lane contem subfilas dedicadas. 6. Politica de prioridade. Definir: - prioridade de loads visuais vs audio; - prioridade de save/config; - prioridade de memcard commit; - starvation prevention; - limite de trabalho de instalacao/publicacao por frame. 7. Modelo de retorno e telemetria. Fechar como guest/runtime observa: - `pending`; - `loading`; - `ready`; - `canceled`; - `backend_unavailable`; - backlog; - posicao de um asset no backlog; - progresso percentual do asset ativo; - drops/retries quando aplicavel. 8. Orcamento para hardware barato. Definir o baseline: - VM principal; - render worker; - terceira async work lane; - limites de memoria para jobs pendentes e buffers temporarios. ## Opcoes ### Opcao A - Terceira lane unica com subfilas e pool limitado de assets - **Abordagem:** criar uma async work lane dedicada. Assets usam um pool/fila de jobs nessa lane; FS tambem pode submeter jobs na mesma lane, com prioridade menor ou classe separada. - **Pro:** modelo simples de budget; evita explosao de threads; encaixa no alvo de terceira core; permite compartilhar backpressure e telemetria. - **Contra:** assets e FS podem competir por IO; precisa politica clara para evitar que save/config fique atras de loads longos. - **Manutenibilidade:** boa se a lane tiver tipos de job e estados explicitos. ### Opcao B - Lane de assets agora, FS apenas reservado - **Abordagem:** fechar v1 somente para asset loading, removendo `spawn` por request. FS continua sincrono por enquanto, mas o contrato da lane reserva compatibilidade para FS no futuro. - **Pro:** menor escopo; resolve a dor concreta de assets rapidamente; evita alterar semantica de FS antes da agenda de app home filesystem. - **Contra:** nao resolve stalls reais de FS/memcard; pode exigir refatorar a lane depois para aceitar FS. - **Manutenibilidade:** boa no curto prazo, com risco de segunda migracao. ### Opcao C - Lanes separadas para asset e FS - **Abordagem:** criar uma lane/pool para assets e outra para FS. - **Pro:** isolamento forte; FS/save nao compete diretamente com loads de textura/audio/cena. - **Contra:** consome mais threads/cores; contraria a premissa de terceira core unica; pior para hardware simples. - **Manutenibilidade:** media; isolamento ajuda, mas a topologia fica pesada. ### Opcao D - Pool global pequeno sem nocao de terceira lane - **Abordagem:** trocar `spawn` por request por um pool global fixo. - **Pro:** simples de implementar em host desktop. - **Contra:** perde a arquitetura de lanes por core; pode mascarar custo em desktop; fica menos claro como mapear para hardware alvo. - **Manutenibilidade:** baixa para o modelo de console se virar dependencia acidental de scheduler do SO. ## Sugestao / Recomendacao Recomendo seguir com a Opcao A como direcao de arquitetura, mas fechar a decisao em duas camadas: 1. Contrato obrigatorio agora: - terceira async work lane como recurso runtime; - assets deixam de usar `thread::spawn` por requisicao; - asset loading passa por backlog consultavel/reordenavel; - status, progresso e telemetria de backlog ficam explicitos. 2. Contrato condicionado para FS: - FS deve ser analisado como consumidor legitimo da mesma lane porque suas operacoes podem envolver IO real e ja sao custosas/nao deterministicas; - a decisao precisa escolher entre incluir FS no v1 ou reservar a lane para FS sem mudar ainda a superficie publica; - memcard precisa ser considerado junto com FS, porque usa a mesma capacidade e backend de persistencia. O ponto importante: a terceira core/lane deve ser a unidade de concorrencia. O pool de assets deve existir dentro desse modelo, nao como criacao livre de threads por load. ## Respostas Consolidadas Ate Aqui 1. O pool de assets deve ser uma lane serial. - Ha exatamente um job ativo na async work lane. - O pool representa backlog, estados e controle de ordem, nao multiplos workers executando em paralelo. 2. O backlog de pedidos deve ser semanticamente ilimitado. - Nao existe `queue_full` como status normal do dominio. - Como cada pedido mira um `bank_type/slot` e pedidos novos para o mesmo alvo substituem os anteriores, o backlog efetivo fica limitado pela soma dos slots dos bancos. - O contrato nao precisa expor limite de fila, mas a implementacao deve garantir que metadados de handles cancelados/superseded nao crescam sem coleta. 3. Asset loading precisa expor consulta e reordenacao do backlog. - Deve existir API publica/runtime para consultar onde um asset esta no backlog. - Deve existir capacidade de adiantar ou atrasar um pedido de asset na fila. - O asset ativo deve expor progresso percentual. - Requests novos para o mesmo `bank_type/slot` devem cancelar pedidos anteriores pendentes para aquele alvo, evitando carregar asset que ja foi substituido antes de executar. - Se o alvo ja estiver carregado com o mesmo `asset_id`, o novo request nao entra no backlog; ele retorna imediatamente pronto com handle disponivel. - Se um novo request substituir o alvo atualmente ativo, a lane deve cancelar em ponto cooperativo quando possivel; se o trabalho nao puder ser interrompido, o resultado antigo deve ser descartado ao final por geracao superseded. - `superseded` significa que um request foi substituido por outro request mais novo para o mesmo `bank_type/slot`. Nao e erro e nao e cancelamento manual; e obsolescencia operacional do pedido antigo. - Handles representam o slot do bank como alvo estavel. Dentro do estado do handle deve existir uma secao de request/backlog para aquele slot, informando se ha pedido pendente/ativo, qual asset foi solicitado, progresso, posicao e geracao. - Handles devem ser capazes de consultar o estado do alvo `bank_type/slot` a qualquer momento. Se nao houver asset valido carregado e tambem nao houver request pendente para aquele alvo, a consulta deve retornar estado explicito de vazio/invalido, nao falhar por ausencia de request ativo. 4. Decode/materializacao fria rodam na terceira lane. - Leitura e decode de asset podem acontecer fora da lane principal. - O resultado deve voltar como material pronto/staged. 5. Install/commit em bancos residentes continua na lane principal. - A async lane prepara trabalho. - A lane principal publica/instala em pontos previsiveis por ownership, determinismo e integracao com bancos/render/audio. 6. FS e memcard entram como consumidores possiveis da mesma lane, mas precisam ser melhor amarrados com a agenda de app home filesystem. - A `DSC-0007` / `AGD-0006` deve ser ajustada para estabelecer melhor a superficie publica e o modelo async/sync de FS. - Esta agenda deve afirmar a existencia da lane para trabalhos async de IO desse tipo e permitir que FS a consuma. - Questoes de API publica de FS, request/poll e semantica de app home devem ficar a cargo da agenda 6. - FS, memcard e assets nao devem operar sobrepostos quando isso gerar concorrencia real de backend; se houver competicao, a fila unica resolve a ordem. 7. Prioridade quando houver competicao: - memcard commit/write; - FS write/config; - asset visual/audio load; - list/read nao critico. 8. Status-first continua valendo. - `backend_unavailable`, `canceled`, erro de decode e estados de job sao status operacionais. - Cancelamento por superseding de pedido no mesmo bank/slot e resultado operacional normal, nao fault. - Nao ha `queue_full` no contrato esperado. - Trap permanece para violacao estrutural, como argumento invalido, capability ausente ou handle estruturalmente invalido. 9. Telemetria minima: - backlog atual; - posicao por asset; - progresso percentual do asset ativo; - jobs submetidos, concluidos, falhos e cancelados; - tempo por job; - percentis por asset enquanto estiver carregando na lane. 10. Escopo da agenda. - O foco desta agenda e a lane, assets e memcard/game persistence. - FS entra como consumidor permitido da lane de IO, mas as decisoes de API publica de FS pertencem a `AGD-0006`. 11. API de backlog. - A API inicial sugerida cobre os casos esperados. - `asset.backlog_promote(handle)` deve ser mantido como atalho oficial para `asset.backlog_move(handle, 1)`. - `asset.backlog_demote(handle)` deve ser mantido como atalho oficial para mover o pedido para o fim dos pendentes. - Os atalhos nao criam semantica nova; apenas evitam que o caller calcule posicoes comuns. - O handle deve permitir consulta estavel do slot do bank mesmo quando nao ha asset valido carregado. - O estado do handle deve separar: - estado do slot/residencia atual; - estado do request/backlog atual para aquele slot. - Operacoes que mudam estado, como commit/cancel/reorder, atuam sobre a secao de request/backlog do handle e precisam validar geracao/request atual para nao agir sobre um pedido substituido. 12. Cancelamento por substituicao. - Quando um request novo chega para o mesmo `bank_type/slot`, o request anterior e marcado imediatamente como `superseded`. - Se o request anterior ainda estiver no backlog, ele sai da fila. - Se o request anterior estiver ativo, a lane tenta cancelar cooperativamente quando a fase atual permitir. - Se a fase ativa nao puder ser interrompida com baixo custo, ela termina e o resultado e descartado ao final porque a geracao do alvo ja mudou. - Como o handle pertence ao slot do bank, ele continua consultavel; sua secao de request deve indicar que a geracao anterior foi `superseded` ou que nao ha request ativo para aquele alvo. 13. Fases de progresso. - `read/decode/stage` e suficiente como modelo minimo para `GLYPH`, `SOUND` e `SCENE`. - Subfases futuras podem detalhar progresso dentro de `decode` sem mudar o contrato geral. 14. Percentis. - Percentis devem existir por `bank_type` como telemetria principal. - Percentis por `asset_id` tambem sao uteis, mas podem usar janela pequena ou agregador leve. - Percentis sao atualizados no fechamento do job, nao durante o hot path. 15. Ajuste esperado na agenda de FS. - `AGD-0006` deve registrar que existe uma async IO lane compartilhavel por FS/memcard/assets. - Esta agenda so decide que FS pode consumir a lane. - A API publica de FS, incluindo request/poll ou sync aparente, pertence a `AGD-0006`. ## API Inicial Sugerida Para Assets Esta agenda ainda nao fecha nomes finais de ABI, mas a direcao inicial sugerida e manter `asset.load/status/commit/cancel` e adicionar uma superficie pequena para backlog: 1. `asset.backlog_info() -> (status, pending_count, active_handle, active_asset_id, active_bank_type, active_slot, active_progress)` - Consulta o estado geral da lane de assets. - `active_progress` usa escala inteira, preferencialmente `0..10000` para permitir duas casas decimais sem float. 2. `asset.backlog_position(handle) -> (status, state, position, progress)` - Consulta um pedido especifico. - `position = 0` significa job ativo. - `position > 0` significa posicao no backlog. - `position = -1` pode representar nao enfileirado/pronto/cancelado, dependendo do `state`. 3. `asset.backlog_move(handle, new_position) -> status` - Move um pedido pendente para uma posicao absoluta. - `new_position = 1` significa primeiro pendente depois do ativo. - Nao interrompe diretamente o job ativo; substituicao do mesmo `bank_type/slot` usa a regra de superseding. 4. `asset.backlog_promote(handle) -> status` - Atalho para mover o pedido para a frente do backlog pendente. 5. `asset.backlog_demote(handle) -> status` - Atalho para mover o pedido para o fim do backlog pendente. 6. `asset.target_status(bank_type, slot) -> (status, asset_id, handle, state, position, progress)` - Consulta por alvo operacional, que e o que realmente controla superseding. - Ajuda quando o caller quer saber "o que esta programado para este slot". Estados sugeridos para `state`: - `empty`; - `invalid`; - `queued`; - `active`; - `ready`; - `committed`; - `canceled`; - `superseded`; - `error`; - `backend_unavailable`. Regra de design: APIs de backlog sao operacionais e status-first. Reordenar um handle inexistente, ja concluido ou ja superseded retorna status operacional apropriado quando o handle e conhecido; violações estruturais continuam Trap. Handles sao observaveis e representam slots de banks. Um handle conhecido pode ser consultado mesmo quando o slot alvo nao contem asset valido carregado; nesse caso a consulta deve retornar estado como `empty` ou `invalid`. O estado do handle deve conter pelo menos duas partes: ```text slot_state: loaded_asset_id resident_state request_state: requested_asset_id generation queued/active/ready/canceled/superseded/error backlog_position progress ``` Mutacoes seguem regras mais estritas e devem falhar operacionalmente quando a secao de request/backlog do handle nao representa a geracao atual. ## Progresso e Percentis ### Progresso percentual Sugestao: - usar progresso em base inteira `0..10000`; - quando o tamanho total de bytes for conhecido, calcular progresso por fases ponderadas; - quando o decode nao for linear ou nao houver tamanho confiavel, expor progresso por fase com marcos conservadores. Modelo inicial de fases: ```text queued -> 0 read -> 0..4000 decode -> 4000..9000 stage -> 9000..10000 ready -> 10000 ``` Se uma fase nao consegue reportar progresso interno, ela deve manter o ultimo marco e saltar para o proximo marco ao concluir. Isso evita progresso falso sem perder visibilidade de fase. ### Percentis por asset Sugestao: - registrar duracoes por asset no fim de cada job, nao por tick/frame; - manter agregadores pequenos por `asset_id` e por `bank_type`; - calcular percentis a partir de janela/ring buffer pequeno ou histograma barato por bucket de duracao; - nunca atualizar percentis no loop quente de decode por item/pixel; - o job ativo pode expor tempo decorrido e fase atual; percentis sao atualizados apenas no fechamento do job. ## Perguntas em Aberto - [x] A agenda esta madura para virar decisao normativa ou ainda falta algum caso de asset/memcard/lane? - [x] A estrutura minima de `slot_state` e `request_state` no handle esta completa ou falta algum campo para commit/cancel/reorder seguro? ## Resolucao A agenda esta madura para decisao normativa. A decisao deve formalizar: - a terceira async work lane serial; - um job ativo por vez; - backlog efetivamente limitado pela unicidade de `bank_type/slot`; - handles como observadores estaveis de slots de bank; - `slot_state` e `request_state` dentro do handle; - cancelamento/superseding por novo request para o mesmo alvo; - load pronto imediato quando o asset ja esta residente no alvo; - decode/materializacao na lane async; - commit/install na lane principal; - memcard como consumidor prioritario da mesma lane; - FS como consumidor permitido, deixando API publica de FS para `AGD-0006`; - progresso por fases e percentis atualizados no fechamento do job. ## Dependencias - `docs/specs/runtime/09-events-and-concurrency.md` - `docs/specs/runtime/15-asset-management.md` - `docs/specs/runtime/16a-syscall-policies.md` - `discussion/workflow/agendas/AGD-0006-app-home-filesystem-surface-and-semantics.md` ## Criterio para Encerrar Pode virar PR quando houver decisao escrita sobre: - existencia e papel da terceira async work lane; - relacao dessa lane com a terceira core no hardware alvo e fallback no host; - desenho do backlog serial de asset loading; - regra de cancelamento/substituicao para requests no mesmo bank/slot; - proibicao de `thread::spawn` por load no caminho normal; - fases de asset load que rodam na lane vs lane principal; - inclusao condicionada de FS/memcard nessa mesma lane; - API de consulta e reordenacao do backlog de assets; - comportamento de request imediato quando o asset ja esta residente no alvo; - regra de bounded backlog por unicidade de `bank_type/slot`; - politica de prioridade entre asset, FS e memcard; - status e telemetria para backlog, progresso, percentis, cancelamento e falha. ## Discussao - 2026-06-28: Agenda atualizada para remover render worker do escopo, assumir uma terceira core/lane para trabalhos assincronos, exigir pool/fila limitada para asset loading e reavaliar o papel de FS/memcard como consumidores da mesma lane. - 2026-06-28: Discussao consolidou que a async work lane deve ser serial, com um job ativo, backlog semanticamente ilimitado, consulta/reordenacao publica de backlog de assets, progresso percentual do asset ativo, decode na lane async, install/commit na lane principal, e FS/memcard como consumidores a alinhar com a agenda de app home filesystem. - 2026-06-28: Acrescentada regra de asset pipe: requests para o mesmo bank/slot devem cancelar pedidos anteriores pendentes para aquele alvo, tratando o pedido mais novo como substituto operacional. - 2026-06-28: Discussao refinou que o alvo canonico e `bank_type/slot`; um asset ja residente deve retornar handle pronto sem entrar no backlog; o backlog efetivo e limitado pela soma dos slots dos bancos; FS fica como consumidor permitido da lane, enquanto API publica de FS permanece na agenda 6; progresso deve usar fases conservadoras e percentis devem ser atualizados no fechamento do job, nao no hot path. - 2026-06-28: Open questions restantes foram consolidadas: API de backlog aceita com `promote/demote` como atalhos; superseding marca requests substituidos por outro request para o mesmo `bank_type/slot`; cancelamento do ativo e cooperativo quando barato e descarte por geracao quando nao for; `read/decode/stage` cobre GLYPH/SOUND/SCENE; percentis ficam por `bank_type` e por `asset_id`; API publica de FS permanece em `AGD-0006`. - 2026-06-28: Adicionada a expectativa de que handles possam consultar o alvo `bank_type/slot` a qualquer momento, inclusive quando nao houver asset valido carregado; mutacoes devem continuar protegidas por request/geracao para evitar commit/cancel acidental de pedidos substituidos. - 2026-06-28: Refinado o modelo de handle: o handle pertence ao slot do bank e contem uma secao de estado de request/backlog para aquele slot. Consulta e residencia ficam estaveis por alvo; mutacoes atuam sobre a geracao atual do request dentro do handle.