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

24 KiB

id ticket title status created resolved decision tags
AGD-0008 perf-async-background-work-lanes-for-assets-and-fs Agenda - [PERF] Async Background Work Lanes for Assets and FS accepted 2026-03-27 2026-06-28 DEC-0034
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:

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 - AssetManager::load.
  • asset.rs - thread::spawn por load.
  • fs.rs - fs e mem marcados como superficies custosas/nao deterministicas.
  • dispatch.rs - chamadas fs.* executam no dispatch.
  • virtual_fs.rs - 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.
  1. 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.
  1. 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.
  1. 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.
  1. 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.
  1. 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:

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:

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

  • A agenda esta madura para virar decisao normativa ou ainda falta algum caso de asset/memcard/lane?
  • 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.