prometeu-runtime/discussion/workflow/agendas/AGD-0008-perf-async-background-work-lanes-for-assets-and-fs.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

558 lines
24 KiB
Markdown

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