Design: Proposta: Aguardar

Criado em 18 mai. 2020  ·  96Comentários  ·  Fonte: WebAssembly/design

@rreverser e eu gostaríamos de propor uma nova proposta para WebAssembly: Await .

A motivação da proposta é ajudar código " síncrono " compilado para WebAssembly, que faz algo como uma leitura de um arquivo:

fread(buffer, 1, num, file);
// the data is ready to be used right here, synchronously

Esse código não pode ser implementado facilmente em um ambiente de host que é principalmente assíncrono e que implementaria "ler de um arquivo" de forma assíncrona, por exemplo, na Web,

const result = fetch("http://example.com/data.dat");
// result is a Promise; the data is not ready yet!

Em outras palavras, o objetivo é ajudar com o problema de sincronização/assíncrono que é tão comum com o wasm na Web.

O problema de sincronização/assíncrona é um problema sério. Embora o novo código possa ser escrito com isso em mente, grandes bases de código existentes geralmente não podem ser refatoradas para contorná-lo, o que significa que não podem ser executadas na Web. Nós temos o Asyncify que instrumenta um arquivo wasm para permitir pausar e retomar, e que permitiu que algumas dessas bases de código fossem portadas, então não estamos completamente bloqueados aqui. No entanto, instrumentar o wasm tem uma sobrecarga significativa, algo como um aumento de 50% no tamanho do código e uma desaceleração de 50% em média (mas às vezes muito pior), porque adicionamos instruções para escrever / ler de volta no estado local e na pilha de chamadas e assim por diante. Essa sobrecarga é uma grande limitação e exclui o Asyncify em muitos casos!

O objetivo desta proposta é permitir pausar e retomar a execução de forma eficiente (em particular, sem sobrecarga como o Asyncify) para que todos os aplicativos que encontrem o problema de sincronização/assíncrono possam evitá-lo facilmente. Pessoalmente, pretendemos isso principalmente para a Web, onde pode ajudar o WebAssembly a se integrar melhor com as APIs da Web, mas casos de uso fora da Web também podem ser relevantes.

A ideia em resumo

O problema central aqui é entre o código wasm ser síncrono e o ambiente host que é assíncrono. Nossa abordagem é, portanto, focada na fronteira de uma instância wasm e no exterior. Conceitualmente, quando uma nova instrução await é executada, a instância wasm "espera" por algo de fora. O que "wait" significa seria diferente em diferentes plataformas e pode não ser relevante em todas as plataformas (como nem todas as plataformas podem achar a proposta wasm atomics relevante), mas na plataforma da Web especificamente, a instância wasm aguardaria uma Promise e pausaria até que isso resolva ou rejeite. Por exemplo, uma instância wasm pode pausar em uma operação de rede fetch e ser escrita algo assim em .wat :

;; call an import which returns a promise
call $do_fetch
;; wait for the promise just pushed to the stack
await
;; do stuff with the result just pushed to the stack

Observe a semelhança geral com await em JS e outras linguagens. Embora isso não seja idêntico a eles (veja os detalhes abaixo), o principal benefício é que ele permite escrever código de aparência síncrona (ou melhor, compilar código de aparência síncrona em wasm).

Os detalhes

Especificação principal do wasm

As alterações na especificação principal do wasm são mínimas:

  • Adicione um tipo waitref .
  • Adicione uma instrução await .

Um tipo é especificado para cada instrução await (como call_indirect ), por exemplo:

;; elaborated wat from earlier, now with full types

(type $waitref_=>_i32 (func (param waitref) (result i32)))
(import "env" "do_fetch" (func $do_fetch (result waitref)))

;; call an import which returns a promise
call $do_fetch
;; wait for the promise just pushed to the stack
await (type $waitref_=>_i32)
;; do stuff with the result just pushed to the stack

O tipo deve receber um waitref e pode retornar qualquer tipo (ou nada).

await é definido apenas em termos de fazer o ambiente do host fazer algo. É semelhante nesse sentido à instrução unreachable , que na Web faz o host lançar um RuntimeError , mas isso não está na especificação principal. Da mesma forma, a especificação principal do wasm diz apenas que await deve esperar por algo do ambiente host, mas não como realmente faríamos isso, o que pode ser muito diferente em diferentes ambientes host.

Isso é tudo para a especificação do núcleo wasm!

Especificação Wasm JS

As mudanças na especificação wasm JS (que afetam apenas ambientes JS como a Web) são mais interessantes:

  • Um waitref válido é uma promessa JS.
  • Quando um await é executado em um Promise, toda a instância wasm pausa e espera que esse Promise seja resolvido ou rejeitado.
  • Se o Promise for resolvido, a instância retoma a execução após enviar para a pilha o valor recebido do Promise (se houver)
  • Se a Promise for rejeitada, retomamos a execução e lançamos uma exceção wasm do local do await .

Por "toda a instância wasm pausa" queremos dizer que todo o estado local é preservado (a pilha de chamadas, valores locais, etc.) como a Memória pode ter sido gravada). Enquanto esperamos, o event loop JS funciona normalmente, e outras coisas podem acontecer. Quando retomamos mais tarde (se não rejeitarmos a Promise, caso em que uma exceção seria lançada), continuamos exatamente de onde paramos, basicamente como se nunca tivéssemos pausado (mas, enquanto isso, outras coisas aconteceram, e o estado global pode ter alterado, etc).

Como é o JS quando ele chama uma instância wasm que é pausada? Para explicar isso, vamos primeiro dar uma olhada em um exemplo comum encontrado ao portar aplicativos nativos para wasm, um loop de eventos:

void event_loop_iteration() {
  // ..
  while (auto task = getTask()) {
    task.run(); // this *may* be a network fetch
  }
  // ..
}

Imagine que esta função é chamada uma vez por requestAnimationFrame . Ele executa as tarefas que lhe são atribuídas, que podem incluir: renderização, física, áudio e busca de rede. Se tivermos um evento de busca de rede, então e só então acabamos executando uma instrução await fetch na promessa de fetch . Podemos fazer isso 0 vezes para uma chamada de event_loop_iteration , ou 1 vez, ou muitas vezes. Só sabemos se acabamos fazendo isso durante a execução deste wasm - não antes e, em particular, não no chamador JS desta exportação wasm. Portanto, esse chamador deve estar pronto para a instância pausar ou não.

Uma situação um tanto análoga pode acontecer em JavaScript puro:

function foo(bar) {
  // ..
  let result = bar(42);
  // ..
}

foo obtém uma função JS bar e a chama com alguns dados. Em JS bar pode ser uma função assíncrona ou normal. Se for assíncrono, ele retorna um Promise e só termina a execução depois. Se for normal, ele executa antes de retornar e retorna o resultado real. foo pode assumir que sabe que tipo bar é (nenhum tipo é escrito em JS, na verdade bar pode nem ser uma função!), ou pode manipular ambos os tipos de funções sejam totalmente gerais.

Agora, normalmente você sabe exatamente qual conjunto de funções bar pode ser! Por exemplo, você pode ter escrito foo e os possíveis bar s em coordenação, ou documentado exatamente quais são as expectativas. Mas a interação wasm/JS sobre a qual estamos falando aqui é realmente mais semelhante ao caso em que você não tem um acoplamento tão estreito entre as coisas e, na verdade, você precisa lidar com os dois casos. Como mencionado anteriormente, o exemplo event_loop_iteration requer isso. Mas, de maneira ainda mais geral, geralmente o wasm é seu aplicativo compilado enquanto o JS é um código genérico de "tempo de execução", de modo que o JS precisa lidar com todos os casos. JS pode fazer isso facilmente, é claro, por exemplo, usando result instanceof Promise para verificar o resultado, ou use JS await :

async function runEventLoopIteration() {
  // await in JavaScript can handle Promises as well as regular synchronous values
  // in the same way, so the log is guaranteed to be written out consistently after
  // the operation has finished (note: this handles 0 or 1 iterations, but could be
  // generalized)
  await wasm.event_loop_iteration();
  console.log("the event loop iteration is done");
}

(observe que, se não precisarmos desse console.log , não precisaríamos do JS await neste exemplo e teríamos apenas uma chamada normal para uma exportação wasm)

Para resumir o acima, propomos que o comportamento de uma instância wasm em pausa seja modelado no caso JS de uma função que pode ou não ser assíncrona, que podemos afirmar como:

  • Quando um await é executado, a instância wasm sai imediatamente de volta para quem a chamou (normalmente seria JS chamando uma exportação wasm, mas veja as notas mais adiante). O chamador recebe um Promise que pode ser usado para saber quando a execução do wasm é concluída e para obter um resultado, se houver.

Suporte a cadeia de ferramentas/biblioteca

Em nossa experiência com o Asyncify e ferramentas relacionadas, é fácil (e divertido!) escrever um pequeno JS para lidar com uma instância wasm em espera. Além das opções mencionadas anteriormente, uma biblioteca pode fazer o seguinte:

  1. Envolva uma instância wasm para fazer com que suas exportações sempre retornem uma Promise. Isso fornece uma interface simples e agradável para o exterior (no entanto, adiciona sobrecarga às chamadas rápidas para o wasm que não pausam). Isso é o que a biblioteca auxiliar Asyncify independente faz, por exemplo.
  2. Escreva algum estado global quando uma instância pausar e verifique isso no JS que chamou a instância. É isso que a integração Asyncify do Emscripten faz, por exemplo.

Muito mais pode ser construído em cima de tais abordagens, ou outras. Preferimos deixar tudo isso para cadeias de ferramentas e bibliotecas para evitar complexidade na proposta e nas VMs.

Implementação e Desempenho

Vários fatores devem ajudar a manter as implementações de VM simples:

  1. Uma pausa/retomada ocorre apenas em um await e conhecemos suas localizações estaticamente dentro de cada função.
  2. Quando retomamos, continuamos exatamente de onde deixamos as coisas, e só o fazemos uma vez. Em particular, nunca "fork" a execução: nada aqui retorna duas vezes, ao contrário do setjmp do C ou uma corrotina em um sistema que permite clonagem/bifurcação.
  3. É aceitável se a velocidade de um await for mais lenta do que uma chamada normal para JS, pois estaremos aguardando um Promise, o que no mínimo implica que um Promise foi alocado e que esperamos no loop de eventos ( que tem sobrecarga mínima mais potencialmente esperando por outras coisas atualmente em execução ). Ou seja, os casos de uso aqui não exigem que os implementadores de VM encontrem maneiras de tornar await incrivelmente rápido. Queremos apenas que await seja eficiente em comparação com os requisitos aqui e, em particular, esperamos que seja muito mais rápido do que a grande sobrecarga do Asyncify.

Dado o exposto, uma implementação natural é copiar a pilha quando pausamos. Embora isso tenha alguma sobrecarga, dadas as expectativas de desempenho aqui, deve ser muito razoável. E se copiarmos a pilha apenas quando pausarmos, poderemos evitar fazer trabalho extra para preparar a pausa. Ou seja, não deve haver sobrecarga geral extra (que é muito diferente de Asyncify!)

Observe que, embora copiar a pilha seja uma abordagem natural aqui, não é uma operação completamente trivial, pois a cópia pode não ser um memcpy simples, dependendo dos componentes internos da VM. Por exemplo, se a pilha contiver ponteiros para si mesma, eles precisariam ser ajustados ou a pilha ser relocável. Alternativamente, pode ser possível copiar a pilha de volta para sua posição original antes de retomá-la, já que, como mencionado anteriormente, ela nunca é "bifurcada".

Observe também que nada nesta proposta requer a cópia da pilha. Talvez algumas implementações possam fazer outras coisas, graças aos fatores de simplificação mencionados nos 3 pontos anteriores nesta seção. O comportamento observável aqui é bastante simples, e o manuseio explícito da pilha não faz parte dele.

Estamos muito interessados ​​em ouvir os comentários dos implementadores de VM sobre esta seção!

Esclarecimentos

Esta proposta apenas pausa a execução do WebAssembly de volta para o chamador da instância wasm. Ele não permite pausar quadros de pilha do host (JS ou navegador). await opera em uma instância wasm, afetando apenas os quadros de pilha dentro dela.

Não há problema em chamar a instância WebAssembly enquanto uma pausa ocorreu e vários eventos de pausa/retomada podem estar em andamento ao mesmo tempo. (Observe que, se a VM adotar a abordagem de copiar a pilha, isso não significa que uma nova pilha deva ser alocada toda vez que entrarmos no módulo, pois só precisamos copiá-la se realmente pausarmos.)

Conexão com outras propostas

Exceções

A rejeição da promessa lançando uma exceção significa que esta proposta depende da proposta de exceções wasm.

Corrotinas

A proposta de corrotinas de Andreas Rossberg também trata de pausar e retomar a execução. No entanto, embora haja alguma sobreposição conceitual, não achamos que as propostas sejam concorrentes. Ambos são úteis porque estão focados em diferentes casos de uso. Em particular, a proposta de corrotinas permite que as corrotinas sejam alternadas entre dentro do wasm, enquanto a proposta await permite que uma instância inteira espere pelo ambiente externo . E a maneira como as duas coisas são feitas leva a características diferentes.

Especificamente, a proposta de corrotinas trata a criação de pilha de maneira explícita (são fornecidas instruções para criar uma corrotina, pausar uma, etc.). A proposta await fala apenas sobre pausar e retomar e, portanto, o manuseio da pilha está implícito . O manuseio explícito de pilha é apropriado quando você sabe que está criando corrotinas específicas, enquanto o implícito é apropriado quando você só sabe que precisa esperar por algo durante a execução (veja o exemplo anterior com event_loop_iteration ).

As características de desempenho desses dois modelos podem ser muito diferentes. Se, por exemplo, criamos uma corrotina toda vez que executamos um código que pode pausar (novamente, muitas vezes não sabemos com antecedência) que pode alocar memória desnecessariamente. O comportamento observado de await é mais simples do que as corrotinas gerais podem fazer e, portanto, pode ser mais simples de implementar.

Outra diferença significativa é que await é uma única instrução que fornece todas as necessidades de um módulo wasm para corrigir a incompatibilidade de sincronização/assíncrona que o wasm tem com a Web (veja o primeiro exemplo .wat do próprio começo). Também é muito fácil de usar no lado JS, que pode apenas fornecer e/ou receber uma promessa (enquanto um pequeno código de biblioteca pode ser útil para adicionar, como mencionado anteriormente, pode ser muito mínimo).

Em teoria, as duas propostas poderiam ser concebidas para serem complementares. Talvez await possa ser uma das instruções na proposta de corrotinas de alguma forma? Outra opção é permitir que um await opere em uma corrotina (basicamente dando a uma instância wasm uma maneira fácil de esperar pelos resultados da corrotina).

WASI#276

Por coincidência , o WASI #276 foi postado por @tqchen quando estávamos terminando de escrever isso. Estamos muito felizes em ver isso, pois compartilha nossa crença de que as corrotinas e o suporte assíncrono são funcionalidades separadas.

Acreditamos que uma instrução await poderia ajudar a implementar algo muito semelhante ao que é proposto lá (opção C3), com a diferença de que não precisaria haver syscalls assíncronas especiais, mas sim algumas syscalls poderiam retornar um waitref que pode então ser await -ed.

Para JavaScript, definimos esperar como pausar uma instância wasm, o que faz sentido porque podemos ter várias instâncias, bem como JavaScript na página. No entanto, em alguns ambientes de servidor, pode haver apenas o host e uma única instância wasm e, nesse caso, a espera pode ser muito mais simples, talvez literalmente esperando em um descritor de arquivo ou na GPU. Ou a espera pode pausar toda a wasm VM, mas continuar executando um loop de eventos. Nós não temos ideias específicas aqui, mas com base na discussão nessa edição pode haver possibilidades interessantes aqui, estamos curiosos para saber o que as pessoas pensam!

Caso de canto: instância wasm => instância wasm => await

Em um ambiente JS, quando uma instância wasm pausa, ela retorna imediatamente para quem a chamou. Descrevemos o que acontece se o chamador for do JS, e a mesma coisa acontece se o chamador for o navegador (por exemplo, se fizermos um setTimeout em uma exportação wasm que pausa; mas nada de interessante acontece lá, pois a promessa retornada é simplesmente ignorada). Mas há outro caso, da chamada proveniente de wasm, ou seja, onde wasm instance A chama diretamente uma exportação da instância B e B pausa. A pausa nos faz sair imediatamente de B e retornar um Promise .

Quando o chamador é JavaScript, como uma linguagem dinâmica, isso é um problema menor e, na verdade, é razoável esperar que o chamador verifique o tipo conforme discutido anteriormente. Quando o chamador é WebAssembly, que é digitado estaticamente, isso é estranho. Se não fizermos algo na proposta para isso, o valor será convertido, em nosso exemplo de uma Promise para qualquer instância que A espera (se um i32 , seria convertido para 0 ). Em vez disso, sugerimos que ocorra um erro:

  • Se uma instância wasm chamar (diretamente ou usando call_indirect ) uma função de outra instância wasm e, durante a execução na outra instância, um await for executado, uma exceção RuntimeError será lançado do local do await .

É importante ressaltar que isso pode ser feito sem sobrecarga, a menos que seja pausado, ou seja, mantendo as chamadas wasm instance -> wasm instance normais em velocidade máxima, verificando a pilha apenas ao fazer uma pausa.

Observe que os usuários que desejam algo como uma instância wasm para chamar outra e fazer a última pausa podem fazê-lo, mas precisam adicionar algum JS entre as duas.

Outra opção aqui é uma pausa para propagar também para o wasm de chamada, ou seja, todo o wasm pausaria até o JS, potencialmente abrangendo várias instâncias do wasm. Isso tem algumas vantagens, como os limites do módulo wasm pararem de importar, mas também desvantagens, como a propagação ser menos intuitiva (o autor da instância de chamada pode não esperar tal comportamento) e que adicionar JS no meio pode alterar o comportamento (também potencialmente inesperadamente). Exigir que os usuários tenham JS no meio, como mencionado anteriormente, parece menos arriscado.

Outra opção pode ser que algumas exportações wasm sejam marcadas como assíncronas enquanto outras não, e então poderíamos saber estaticamente o que é o quê, e não permitir chamadas impróprias; mas veja o exemplo event_loop_iteration anterior que é um caso comum que não seria resolvido marcando exportações, e também existem chamadas indiretas, então não podemos evitar o problema dessa maneira.

Abordagens alternativas consideradas

Talvez não precisemos de uma nova instrução await , se wasm pausar sempre que uma importação JS retornar uma promessa? O problema é que agora quando JS retorna uma Promise que não é um erro. Uma mudança tão incompatível com versões anteriores significaria que o wasm não pode mais receber uma Promise sem pausar, mas isso também pode ser útil.

Outra opção que consideramos é marcar as importações de alguma forma para dizer "esta importação deve pausar se retornar uma promessa". Pensamos em várias opções de como marcá-los, tanto no lado JS quanto no wasm, mas não encontramos nada que parecesse certo. Por exemplo, se marcarmos importações no lado JS, o módulo wasm não saberá se uma chamada para uma importação pausa ou não até a etapa de link, quando as importações chegam. Ou seja, chamadas para importações e pausar seriam "misturadas". Parece que a coisa mais direta é apenas ter uma nova instrução para isso, await , que é explícita sobre espera. Em teoria, esse recurso também pode ser útil fora da Web (veja as notas anteriores), portanto, ter uma instrução para todos pode tornar as coisas mais consistentes em geral.

Discussões relacionadas anteriores

https://github.com/WebAssembly/design/issues/1171
https://github.com/WebAssembly/design/issues/1252
https://github.com/WebAssembly/design/issues/1294
https://github.com/WebAssembly/design/issues/1321

Obrigado por ler, comentários são bem-vindos!

Comentários muito úteis

Eu esperava ter mais discussão publicamente aqui, mas para economizar tempo, entrei em contato diretamente com alguns implementadores de VM, pois poucos se envolveram aqui até agora. Dado o feedback deles junto com a discussão aqui, infelizmente acho que devemos pausar esta proposta.

Await tem um comportamento observável muito mais simples do que as corrotinas gerais ou a comutação de pilha, mas as pessoas da VM com quem conversei concordam com @rossberg que o trabalho da VM no final provavelmente seria semelhante para ambos. E pelo menos algumas pessoas de VM acreditam que obteremos corrotinas ou alternância de pilha de qualquer maneira, e que podemos suportar os casos de uso do await usando isso. Isso significará criar uma nova corrotina/pilha em cada chamada no wasm (ao contrário desta proposta), mas pelo menos algumas pessoas de VM acham que isso pode ser feito rápido o suficiente.

Além da falta de interesse do pessoal da VM, tivemos algumas fortes objeções a esta proposta aqui de @fgmccabe e @RossTate , conforme discutido acima. Discordamos em algumas coisas, mas aprecio esses pontos de vista e o tempo gasto para explicá-los.

Concluindo, no geral, parece que seria uma perda de tempo de todos tentar avançar aqui. Mas obrigado a todos que participaram da discussão! E espero que pelo menos isso motive a priorização de corrotinas / troca de pilha.

Observe que a parte JS desta proposta pode ser relevante no futuro, como açúcar JS basicamente para integração conveniente do Promise. Precisamos esperar pela mudança de pilha ou corrotinas e ver se isso pode funcionar em cima disso. Mas acho que não vale a pena manter o assunto em aberto para isso, então fechando.

Todos 96 comentários

Excelente redação! Eu gosto da ideia de suspensão controlada pelo host. A proposta do @rossberg também discute sistemas de efeitos funcionais, e eu admito que não sou especialista neles, mas à primeira vista parece que eles podem atender à mesma necessidade de fluxo de controle não local.

Com relação a: "Dado o exposto, uma implementação natural é copiar a pilha quando pausamos." Como isso funcionaria para a pilha de execução? Imagino que a maioria dos mecanismos JIT compartilhem a pilha de execução C nativa entre JS e wasm, então não tenho certeza do que salvar e restaurar significaria nesse contexto. Essa proposta significa que a pilha de execução wasm precisaria ser virtualizada de alguma forma? IIUC evitar o uso da pilha C como esta foi bastante complicado quando o python tentou fazer algo semelhante: https://github.com/stackless-dev/stackless/wiki.

Compartilho uma preocupação semelhante a @sbc100. Copiar a pilha é inerentemente uma operação bastante difícil, especialmente se sua VM ainda não tiver uma implementação de GC.

@sbc100

Essa proposta significa que a pilha de execução wasm precisaria ser virtualizada de alguma forma?

Eu tenho que deixar isso para os implementadores de VM, pois não sou especialista nisso. E eu não entendo a conexão com o python sem pilha, mas talvez eu não saiba o que é bom o suficiente para entender a conexão, desculpe!

Mas em geral: várias abordagens de corrotina funcionam manipulando o ponteiro de pilha em um nível baixo . Essas abordagens podem ser uma opção aqui. Queríamos salientar que, mesmo que a pilha tenha que ser copiada como parte de tal abordagem, fazer isso tem uma sobrecarga aceitável nesse contexto.

(Não temos certeza se essas abordagens podem funcionar em wasm VMs ou não - esperando ouvir os implementadores se sim ou não e se existem opções melhores!)

@lachlansneff

Você pode explicar com mais detalhes o que você quer dizer com GC tornando as coisas mais fáceis? Eu não sigo.

@kripken GCs geralmente (mas nem sempre) têm a capacidade de percorrer uma pilha, o que é necessário se você precisar reescrever ponteiros na pilha para apontar para a nova pilha. Talvez alguém que saiba mais sobre JSC possa confirmar ou negar isso.

@lachlansneff

Obrigado, agora eu vejo o que você está dizendo.

Não sugerimos que percorrer a pilha de maneira tão completa (identificando cada local até o fim, etc.) seja necessário para fazer isso. (Para outras abordagens possíveis, veja o link no meu último comentário sobre métodos de implementação de corrotina de baixo nível.)

Peço desculpas pela terminologia de "copiar a pilha" na proposta - vejo que não ficou claro o suficiente, com base no feedback seu e do @sbc100 . Novamente, não queremos sugerir uma abordagem de implementação de VM específica. Nós só queríamos dizer que se copiar a pilha for necessário em alguma abordagem, isso não seria um problema para a velocidade.

Em vez de sugerir uma abordagem de implementação específica, esperamos ouvir do pessoal da VM como eles acham que isso poderia ser feito!

Estou muito animado para ver esta proposta. O Lucet tem os operadores yield e resume há algum tempo, e os usamos precisamente para interagir com o código assíncrono em execução no ambiente do host Rust.

Isso foi bastante simples de adicionar ao Lucet, já que nosso design já estava comprometido em manter uma pilha separada para a execução do Wasm, mas posso imaginar que isso poderia apresentar algumas dificuldades de implementação para VMs que não o fazem.

Essa proposta parece ótima! Estamos tentando entrar em uma boa maneira de gerenciar código assíncrono em wasmer-js por um tempo (já que não temos acesso aos internos da VM em um contexto de navegador).

Em vez de sugerir uma abordagem de implementação específica, esperamos ouvir do pessoal da VM como eles acham que isso poderia ser feito!

Acho que talvez usar a estratégia de retorno de chamada para funções assíncronas possa ser a maneira mais fácil de fazer as coisas acontecerem e também de maneira agnóstica de linguagem.

Parece que .await pode ser chamado em um JsPromise dentro de uma função Rust usando wasm-bindgen-futures ? Como isso pode funcionar sem a instrução await proposta aqui? Desculpe minha ignorância, estou procurando soluções para chamar fetch dentro do wasm e estou aprendendo sobre o Asyncify, mas parece que a solução Rust é mais simples. O que estou perdendo aqui? Alguém pode me esclarecer?

Estou muito animado com esta proposta. A principal vantagem da proposta é sua simplicidade, pois podemos construir APIs que são sincronizadas com o POV do wasm, e facilita muito a portabilidade de aplicativos sem ter que pensar explicitamente em callbacks e async/await. Isso nos permitiria trazer aprendizado de máquina baseado em WASM e WebGPU para wasm vms nativos usando uma única API nativa e executado na Web e nativo.

Uma coisa que acho que vale a pena discutir é a assinatura das funções que potencialmente as chamadas esperam. Imagine que temos a seguinte função

int test() {
   await();
   return 1;
}

A assinatura da função correspondente é () => i32 . De acordo com a nova proposta, as chamadas para test podem retornar i32 ou Promise<i32> . Observe que é mais difícil pedir ao usuário que declare estaticamente uma nova assinatura (por causa do custo de portabilidade de código e pode ser chamadas indiretas dentro da função que não sabemos que as chamadas aguardam).

Devemos ter um modo de chamada separado na função exportada (por exemplo, chamada assíncrona) para indicar que o await é permitido durante o tempo de execução?

Em termos de terminologia, a operação proposta é como uma operação de rendimento em sistemas operacionais. Uma vez que cede o controle para o sistema operacional (neste caso, a wasm VM) para aguardar a syscall terminar.

Se entendi bem esta proposta, acho que é aproximadamente equivalente a remover a restrição de que o await em JS seja utilizável apenas em funções async . Ou seja, no lado wasm waitref poderia ser externref e ao invés de uma instrução await você poderia ter uma função importada $await : [externref] -> [] , e no lado JS você pode fornecer foo(promise) => await promise como a função para importar. Na outra direção, se você fosse um código JS que quisesse await em uma Promise fora da função async , você poderia fornecer essa promessa para um módulo wasm que simplesmente chama await na entrada. Esse é um entendimento correto?

@RossTate Não exatamente, AIUI. O código wasm pode await uma promessa (chame-o promise1 ), mas apenas a execução wasm produzirá, não o JS. O código wasm retornará uma promessa diferente (chame promise2 ) para o chamador JS. Quando promise1 é resolvido, a execução do wasm continua. Finalmente, quando esse código wasm sair normalmente, promise2 será resolvido com o resultado da função wasm.

@tqchen

Devemos ter um modo de chamada separado na função exportada (por exemplo, chamada assíncrona) para indicar que o await é permitido durante o tempo de execução?

Interessante - onde você vê o benefício? Como você disse, realmente não há como ter certeza se uma exportação vai acabar fazendo await ou não, em situações comuns de portabilidade, então na melhor das hipóteses ela só poderia ser usada algumas vezes. Isso ajudaria as VMs internamente, talvez?

Ter uma declaração explícita pode garantir que o usuário declare sua intenção claramente, e a VM pode gerar uma mensagem de erro adequada se a intenção do usuário não estiver fazendo uma chamada que seja executada de forma assíncrona.

A partir do ponto de vista do usuário também torna a escrita do código mais consistente. Por exemplo, o usuário poderia escrever o código a seguir, mesmo que test não chame um await e a interface do sistema retorne Promise.resolve(test()) automaticamente.

await inst.exports_async.test();

A partir do ponto de vista do usuário também torna a escrita do código mais consistente. Por exemplo, o usuário poderia escrever o código a seguir, mesmo que test não chame um await e a interface do sistema retorne Promise.resolve(test()) automaticamente.

@tqchen Observe que o usuário já pode fazer isso conforme mostrado no exemplo no teste da proposta. Ou seja, JavaScript já suporta e trata valores síncronos e assíncronos em um operador await da mesma forma.

Se a sugestão for impor um único tipo estático, acreditamos que isso pode ser feito no nível do lint ou do sistema de tipos ou em um nível de wrapper JavaScript sem introduzir complexidade no lado principal do WebAssembly ou restringir os implementadores de tais wrappers.

Ah, obrigado pela correção, @binji.

Nesse caso, o seguinte é aproximadamente equivalente? Adicione uma função WebAssembly.instantiateAsync(moduleBytes, imports, "name1", "name2") à API JS. Suponha que moduleBytes tenha um número de importações mais uma importação adicional import "name1" "name2" (func (param externref)) . Então esta função instancia as importações com os valores dados por imports e instancia a importação adicional com o que é conceitualmente await . Quando as funções exportadas são criadas a partir deste módulo, elas são protegidas para que, quando este await for chamado, ele caminhe pela pilha para encontrar o primeiro guarda e, em seguida, copie o conteúdo da pilha para uma nova Promise que é então imediatamente voltou.

Isso funcionaria? Minha percepção é que esta proposta pode ser feita apenas modificando a API JS sem a necessidade de modificar o próprio WebAssembly. Claro, mesmo assim, ele ainda adiciona muitas funcionalidades úteis.

@kripken Como a função start seria tratada? Será que estaticamente não permitiria await , ou de alguma forma interagiria com a instanciação Wasm?

@malbarbo wasm-bindgen-futures permite que você execute o código async no Rust. Isso significa que você tem que escrever seu programa de forma assíncrona: você tem que marcar suas funções como async , e você precisa usar .await . Mas esta proposta permite que você execute código assíncrono sem usar async ou .await , em vez disso, parece uma chamada de função síncrona regular.

Em outras palavras, atualmente você não pode usar APIs de SO síncronas (como std::fs ) porque a Web só tem APIs assíncronas. Mas com esta proposta você poderia usar APIs de SO síncronas: elas usariam internamente Promises, mas pareceriam síncronas com Rust.

Mesmo que esta proposta seja implementada, wasm-bindgen-futures ainda existirá e ainda será útil, porque está lidando com um caso de uso diferente (executando funções async ). E as funções async são úteis porque podem ser facilmente paralelizadas.

@RossTate Parece que sua sugestão é bastante semelhante à abordada em "Abordagens alternativas consideradas":

Outra opção que consideramos é marcar as importações de alguma forma para dizer "esta importação deve pausar se retornar uma promessa". Pensamos em várias opções de como marcá-los, tanto no lado JS quanto no wasm, mas não encontramos nada que parecesse certo. Por exemplo, se marcarmos importações no lado JS, o módulo wasm não saberá se uma chamada para uma importação pausa ou não até a etapa de link, quando as importações chegam. Ou seja, chamadas para importações e pausar seriam "misturadas". Parece que a coisa mais direta é apenas ter uma nova instrução para isso, await, que é explícita sobre espera. Em teoria, esse recurso também pode ser útil fora da Web (veja as notas anteriores), portanto, ter uma instrução para todos pode tornar as coisas mais consistentes em geral.

Como a função start seria tratada? Será que estaticamente não permitiria esperar, ou de alguma forma interagiria com a instanciação Wasm?

@Pauan Não abordamos isso especificamente, mas acho que não há nada que nos impeça de permitir await em start também. Neste caso, a Promise retornada de instantiate{Streaming} ainda seria resolvida/rejeitada naturalmente quando a função start terminasse de executar completamente, com a única diferença sendo que ela esperaria por await ed promessas.

Dito isto, aplicam-se as mesmas limitações de hoje e, por enquanto, não seria muito útil para casos que exigem acesso, por exemplo, à memória exportada.

@RReverser Como isso funcionaria para o new WebAssembly.Instance síncrono (que é usado em trabalhadores)?

Ponto interessante @Pauan sobre o início!

Sim, para instanciação síncrona parece arriscado - se await for permitido, é estranho se alguém chamar as exportações enquanto estiver pausado. Não permitir await pode ser mais simples e seguro. (Talvez também no início assíncrono para consistência, não pareça haver casos de uso importantes que impeçam? Precisa de mais reflexão.)

(que é usado em trabalhadores)?

Hmm bom ponto; Não acho que deva ser usado em Workers, mas como essa API já existe, talvez ela possa retornar uma Promise? Eu vi isso como um padrão emergente semi-popular para retornar thenables de um construtor de várias bibliotecas, embora não tenha certeza se é uma boa ideia fazer isso em uma API padrão.

Concordo que não permitir isso em start (como na armadilha) é o mais seguro por enquanto, e sempre podemos mudar isso no futuro de maneira compatível com versões anteriores, caso algo mude.

Talvez eu tenha perdido alguma coisa, mas não há discussão sobre o que acontece quando a execução do WASM é pausada com uma instrução await e uma promessa retornada ao JS, então o JS chama de volta ao WASM sem aguardar a promessa.

Esse é um caso de uso válido? Se for, pode permitir que aplicativos de "loop principal" recebam eventos de entrada sem ceder ao navegador manualmente. Em vez disso, eles poderiam ceder esperando uma promessa que fosse resolvida imediatamente.

E quanto ao cancelamento? Não é implementado nas promessas JS e isso causa alguns problemas.

@Kangz

Talvez eu tenha perdido alguma coisa, mas não há discussão sobre o que acontece quando a execução do WASM é pausada com uma instrução await e uma promessa retornada ao JS, então o JS chama de volta ao WASM sem esperar pela promessa.

Esse é um caso de uso válido? Se for, pode permitir que aplicativos de "loop principal" recebam eventos de entrada sem ceder ao navegador manualmente. Em vez disso, eles poderiam ceder esperando uma promessa que fosse resolvida imediatamente.

O texto atual talvez não seja suficientemente claro quanto a isso. Para o primeiro parágrafo, sim, isso é permitido, veja a seção "Esclarecimentos": It is ok to call into the WebAssembly instance while a pause has occurred, and multiple pause/resume events can be in flight at once.

Para o segundo parágrafo, não - você não pode obter eventos mais cedo e não pode fazer com que o JS resolva uma promessa antes do que faria. Deixe-me tentar resumir as coisas de outra maneira:

  • Quando wasm pausa na Promise A, ele sai de volta para o que o chamou e retorna uma nova Promise B.
  • Wasm recomeça quando a Promessa A é resolvida. Isso acontece no horário normal , o que significa que tudo está normal no loop de eventos JS.
  • Depois que o wasm recomeça e também termina a execução, só então a Promise B é resolvida.

Então, em particular, a Promise B tem que ser resolvida após a Promise A. Você não pode obter o resultado da Promise A antes que JS possa obtê-lo.

Dito de outra forma: o comportamento desta proposta pode ser polyfilled por Asyncify + algum JS que use Promises em torno dele.

@RReverser , acho que não são os mesmos, mas primeiro acho que precisamos esclarecer algo (se ainda não tiver sido esclarecido, nesse caso, desculpe-me por perder).

Pode haver várias chamadas de JS para a mesma instância wasm na mesma pilha ao mesmo tempo. Se await for executado pela instância, qual chamada será pausada e retornará uma promessa?

Para o segundo parágrafo, não - você não pode obter eventos mais cedo e não pode fazer com que o JS resolva uma promessa antes do que faria.

Desculpe, acho que minha pergunta não foi clara. No momento, os aplicativos de "loop principal" em C++ usam emscripten_set_main_loop para que, entre cada execução da função de quadro, o controle seja devolvido ao navegador e a entrada ou outros eventos possam ser processados.

Com esta proposta, parece que o seguinte deve funcionar para traduzir aplicativos "main-loop". (embora eu não conheça bem o loop de eventos JS)

int main() {
  while (true) {
    frame();
    processEvents();
  }
}

// polyfillable with ASYNCIFY!
void processEvents() {
  __builtin_await(EM_ASM(
    new Promise((resolve, reject) => {
      setTimeout(0, () => resolve());
    })
  ))
}

@Kangz Isso deve funcionar, sim (exceto que você tem um pequeno problema com a ordem dos argumentos no seu código setTimeout, além de poder ser simplificado):

int main() {
  while (true) {
    frame();
    processEvents();
  }
}

// polyfillable with ASYNCIFY!
void processEvents() {
  __builtin_await(EM_ASM_WAITREF(
    return new Promise(resolve => setTimeout(resolve));
  ));
}

Pode haver várias chamadas de JS para a mesma instância wasm na mesma pilha ao mesmo tempo. Se await for executado pela instância, qual chamada será pausada e retornará uma promessa?

O mais íntimo. É trabalho do wrapper JS coordenar o resto, se desejar fazê-lo.

@Kangz Desculpe, entendi mal antes disso. Sim, como o @RReverser disse que deve funcionar, e é um bom exemplo de um caso de uso pretendido aqui!

Como você disse, é polyfillable com Asyncify e, na verdade, é equivalente ao mesmo código com Asyncify hoje, substituindo o __builtin_await por uma chamada para emscripten_sleep(0) (que faz um setTimeout(0) ) .

Obrigado, @RReverser , pelo esclarecimento. Acho que ajudaria reformular a descrição para dizer que a chamada (mais recente) na instância pausa, em vez da própria instância.

Nesse caso, isso soa quase equivalente a adicionar as duas funções primitivas a seguir ao JS: promise-on-await(f) e await-for-promise(p) . O primeiro chama f() mas, se durante a execução de f() uma chamada é feita para await-for-promise(p) , em vez disso, retorna um novo Promise que retoma a execução após p ser resolvido e ele mesmo é resolvido após a conclusão da execução (ou chama await-for-promise novamente). Se uma chamada para await-for-promise for feita no contexto de vários promise-on-await s, a mais recente retornará uma promessa. Se uma chamada para await-for-promise for feita fora de qualquer promise-on-await , algo ruim acontecerá (como se o código start uma instância executasse await ).

Isso faz sentido?

@RossTate Isso é bem próximo, sim, e captura a ideia geral. (Mas, como você disse, apenas quase equivalente, pois não pode ser usado para polyfill isso e está faltando o manuseio específico de limites wasm/JS.)

Obrigado pela sugestão de reformular esse texto. Estou mantendo uma lista de tais notas da discussão aqui. (Não tenho certeza se vale a pena aplicá-los ao primeiro post, pois parece menos confuso não alterá-lo ao longo do tempo?)

@RossTate Interessante... Eu gosto disso! Isso torna a natureza assíncrona da chamada explícita ( promise-on-await é necessário para qualquer chamada potencialmente assíncrona) e não requer nenhuma alteração no Wasm. Também faz (algum) sentido se você remover o Wasm do meio - se promise-on-await chamar await-for-promise diretamente, ele retornará um Promise .

@kripken você pode entrar em mais detalhes sobre por que isso seria diferente? Não entendo muito bem por que o limite Wasm/JS é importante aqui.

@binji Eu só quis dizer que essas funções no JS não permitiriam que eu fizesse algo semelhante. Chamá-los como importações do wasm não funcionaria. Ainda precisamos de uma maneira de fazer com que eu saia para a fronteira etc. de uma maneira recuperável, não é?

@kripken certo, acho que nesse ponto a importação await-for-promise teria que estar funcionando como um intrínseco do Wasm.

Meu pensamento era que, em vez de adicionar uma instrução await ao wasm, esse módulo importaria await-for-promise e chamaria isso. Da mesma forma, em vez de alterar as funções exportadas, o código JS as chamaria dentro de um promise-on-await . Isso significa que as primitivas JS lidariam com todo o trabalho da pilha, incluindo a pilha WebAssembly. Também seria mais flexível, por exemplo, se você quiser, pode dar ao módulo um retorno de chamada JS que pode chamar de volta para o módulo e fazer a chamada externa pausar em vez da cláusula interna - tudo depende se o código JS escolhe encerrar a chamada em promise-on-await ou não. Eu não acho que você precisa mudar nada para o próprio wasm.

Eu estaria interessado em ouvir o que @syg pensa sobre esses potenciais primitivos JS.

Oh ok, desculpe - eu tomei seu comentário @RossTate como "para ter certeza de que entendi, deixe-me reformular assim e me diga se isso tem a forma certa", e não uma sugestão concreta.

Pensando nisso, sua ideia quer pausar não apenas os frames JS, mas também o wasm, mas também os frames do host/navegador. (A proposta atual evita isso trabalhando apenas no wasm até o limite onde foi chamado.) Aqui está um exemplo:

myList.forEach((item) => {
  .. call something which ends up pausing ..
});

Se forEach for implementado no código do navegador, isso significa pausar os quadros do navegador. Também significativo é que pausar no meio de um loop desse tipo e retomar mais tarde seria um novo poder que o JS pode fazer, e sua ideia também permitiria isso para um loop normal:

for (let i of something) {
  .. call something which ends up pausing ..
}

E tudo isso pode ter interações de especificações curiosas com funções JS async . Tudo isso parece grandes discussões para se ter com o pessoal do navegador e do JS.

Mas também, isso evita apenas adicionar await e waitref na especificação principal do wasm, mas essas são pequenas adições - já que elas não fazem nada na especificação principal. A proposta atual já tem 99% da complexidade do lado JS. E a sua proposta IIUC troca essa pequena adição à especificação wasm com adições muito maiores no lado JS - por isso torna a plataforma da Web como um todo mais complexa e desnecessariamente, pois isso é tudo para wasm. Além disso, há realmente um benefício em definir await na especificação principal do wasm, que pode ser útil fora da Web.

Talvez eu tenha perdido alguma coisa na sua sugestão, desculpe se sim. No geral, estou curioso para saber qual é a sua motivação para tentar evitar uma adição à especificação principal do wasm?

Não acho que essas primitivas façam muito sentido para js, e acho que mais implementações de wasm do que as de navegadores podem se beneficiar disso. Ainda estou curioso por que exceções retomáveis ​​(aproximadamente efeitos) não atenderiam a esse caso de uso.

Meu comentário foi uma combinação de ambos. Em alto nível, estou tentando descobrir se há uma maneira de reformular a proposta como puramente um enriquecimento da API JS (e da mesma forma como outros hosts interagiriam com os módulos wasm). O exercício ajuda a avaliar se o wasm realmente precisa ser alterado e ajuda a determinar se realmente a proposta está adicionando secretamente novas primitivas ao JS que as pessoas do JS podem ou não aprovar. Ou seja, se não for possível fazer apenas com um await : func (param externref) (result externref) importado, então é bem provável que isso esteja adicionando novas funcionalidades ao JS.

Quanto à simplicidade das alterações no wasm, ainda há muitas coisas a considerar, como o que fazer sobre chamadas de módulo para módulo, o que fazer quando funções exportadas retornam valores GC que contêm ponteiros para funções que podem executar await após o término da chamada, e assim por diante.

Voltando ao exercício, como você apontou, há boas razões para capturar apenas a pilha wasm. Isso me traz de volta à minha sugestão anterior, embora ligeiramente revisada com alguma nova perspectiva. Adicione uma função WebAssembly.instantiateAsync(moduleBytes, imports, "name1", "name2") à API JS. Suponha que moduleBytes tenha um número de importações mais uma importação adicional import "name1" "name2" (func (param externref) (result externref)) . Então instantiateAsync instancia as outras importações de moduleBytes simplesmente com os valores dados por imports e instancia a importação adicional com o que é conceitualmente await-for-promise . Quando as funções exportadas são criadas a partir dessa instância, elas são protegidas (conceitualmente por promise-on-await ) de modo que, quando este await-for-promise é chamado, ele percorre a pilha para encontrar o primeiro guarda e, em seguida, copia o conteúdo de a pilha em uma nova Promise que é imediatamente retornada. Agora temos os mesmos primitivos que mencionei acima, mas eles não são mais de primeira classe, e esse padrão restrito garante que apenas a pilha wasm seja capturada. Ao mesmo tempo, o WebAssembly não precisa ser alterado para dar suporte ao padrão.

Pensamentos?

@devsnek

Ainda estou curioso por que exceções retomáveis ​​(aproximadamente efeitos) não atenderiam a esse caso de uso.

Eles são uma opção neste espaço, com certeza.

Meu entendimento da última apresentação de @rossberg é que ele inicialmente queria seguir esse caminho, mas depois mudou de direção para fazer uma abordagem de corrotina. Veja o slide com o título "Problemas". Após esse slide, são descritas as corrotinas, que são outra opção neste espaço. Então, talvez sua pergunta seja mais para @rossberg, que talvez possa esclarecer?

Esta proposta está focada em resolver o problema de sincronização/assíncrona que não requer tanto poder quanto exceções ou corrotinas retomáveis. Esses se concentram nas interações internas dentro de um módulo wasm, enquanto estamos focados na interação entre um módulo wasm e o exterior (porque é onde acontece o problema de sincronização/assíncrona). É por isso que precisamos apenas de uma única nova instrução na especificação principal do wasm, e quase toda a lógica nesta proposta está na especificação wasm JS. E isso significa que você pode esperar por uma promessa como esta:

call $get_promise
await
;; use it!

Essa simplicidade no wasm é útil por si só, mas também significa que é muito claro para a VM o que está acontecendo, o que também pode trazer benefícios.

@RossTate

Ou seja, se não for possível fazer apenas com um await : func importado (param externref) (result externref), então é bem provável que isso esteja adicionando novas funcionalidades ao JS.

Eu não sigo essa inferência, desculpe. Mas parece-me um rodeio. Se você acha que esta proposta adiciona novas funcionalidades ao JS, por que não mostrar isso diretamente? (Acredito firmemente que não, mas estou curioso se você descobrir que cometemos um erro!)

Quanto à simplicidade das mudanças no wasm, ainda há muitas coisas a considerar, como o que fazer sobre chamadas de módulo para módulo

A especificação principal do wasm diz alguma coisa sobre chamadas de módulo para módulo? Não me lembro de ter feito isso, e passando os olhos pelas seções relevantes agora, não vejo isso. Mas talvez eu tenha perdido alguma coisa?

Minha crença é que as adições de especificações principais do wasm seriam basicamente listar await , dizer que se destina a "esperar por algo", e é isso. Por isso escrevi That's it for the core wasm spec! na proposta. Se eu estiver errado, por favor, mostre-me na especificação principal do wasm onde precisaríamos adicionar mais.

Vamos especular e dizer que algum dia a especificação principal do wasm terá uma nova instrução para criar um módulo wasm e chamar um método nele. Nesse caso, imagino que diríamos await apenas armadilhas porque o objetivo é esperar por algo do lado de fora, no host.

Isso me traz de volta à minha sugestão anterior, embora ligeiramente revisada com uma nova perspectiva [nova ideia]

Essa ideia não é funcionalmente a mesma do segundo parágrafo em Alternative approaches considered na proposta? Tal coisa pode ser feita, mas explicamos por que achamos que é menos bom.

@kripken entendeu. para ser claro acho que await resolve os casos de uso apresentados de uma forma muito prática e elegante. também espero que possamos usar esse momento para resolver outros casos de uso, ampliando um pouco o design.

Eu acho que a sugestão de @RossTate realmente soa muito como o que é mencionado em "Abordagens alternativas consideradas". Então, acho que devemos discutir com mais detalhes por que essa abordagem foi descartada. Acho que todos podemos concordar que uma solução que não envolvesse alterações nas especificações do wasm seria preferível, se pudermos tornar o lado JS viável. Estou tentando entender as desvantagens que você expõe nessa seção e por que elas tornam a solução somente JS tão inaceitável.

Acho que todos podemos concordar que uma solução que não envolvesse alterações nas especificações do wasm seria preferível

Não! Veja os casos de uso não Web discutidos aqui. Sem await na especificação wasm, acabaríamos com cada plataforma fazendo algo ad-hoc: o ambiente JS faz alguma coisa de importação, outros lugares criam novas APIs marcadas como "síncronas", etc. O ecossistema wasm ser menos consistente, seria mais difícil mover um wasm da Web para outros lugares, etc.

Mas sim, devemos tornar a parte de especificação do núcleo wasm o mais simples possível. Eu acho que isso faz isso? 99% da lógica está no lado JS (mas @RossTate parece discordar, e ainda estamos tentando descobrir isso - fiz perguntas concretas na minha última resposta que espero que avancem).

Minha crença é que as adições de especificações principais do wasm seriam basicamente listar await , dizer que se destina a "esperar por algo", e é isso.

A menos que essas semânticas possam ser formalizadas com mais precisão, isso introduz ambiguidade ou comportamento definido pela implementação na especificação. Até agora evitamos isso (com um custo significativo no caso do SIMD), então isso é definitivamente algo que eu gostaria de ver definido. Eu não acho que a proposta em si tenha que mudar para tornar isso mais formal, mas "esperar por algo" deve ser reformulado na terminologia precisa já usada pela especificação.

A especificação principal do wasm diz alguma coisa sobre chamadas de módulo para módulo?

As importações de uma instância podem ser instanciadas com as exportações de outra instância. Pelo que entendi da API JS (e do princípio de composicionalidade do wasm), uma chamada para tal importação é conceitualmente uma chamada direta para qualquer função que a outra instância exportou. O mesmo vale para chamadas (indiretas) em valores funcionais como funcref que são passados ​​entre as duas instâncias.

Vamos especular e dizer que algum dia a especificação principal do wasm terá uma nova instrução para criar um módulo wasm e chamar um método nele. Nesse caso, imagino que diríamos esperar apenas armadilhas porque o objetivo é esperar por algo do lado de fora, no host.

Com base no princípio de composicionalidade do módulo discutido na reunião presencial, não deve ser uma armadilha. Deve ser como se houvesse apenas uma instância de módulo (composta) e executasse await . Ou seja, await compactaria a pilha até o quadro de pilha JS mais recente.

Observe que isso implica que se f fosse o valor de uma função unária exportada de alguma instância wasm, então o objeto de parâmetros de instanciação {"some" : {"import" : f}} seria semanticamente diferente de {"some" : {"import" : (x) => f(x)}} porque as chamadas para o primeiro permanecerá na pilha wasm, enquanto as chamadas para o último entrarão na pilha JS, mesmo que apenas um pouco. Até agora, esses objetos de parâmetro de instanciação seriam considerados equivalentes. Posso explicar por que isso é útil do ponto de vista de migração de código/interoperabilidade de idioma, mas isso seria uma digressão no momento.

Essa ideia não é funcionalmente a mesma do segundo parágrafo em Abordagens alternativas consideradas na proposta? Tal coisa pode ser feita, mas explicamos por que achamos que é menos bom.

Desculpe, eu li essa alternativa como significando algo diferente, mas isso não importa agora, exceto para explicar minha confusão. Parece que você quis dizer o mesmo que minha sugestão, nesse caso vale a pena discutir os prós e contras.

O fato dessa proposta ser tão leve no lado wasm é porque a instrução await parece ser semanticamente idêntica a uma chamada para uma função importada. Claro, as convenções são importantes, como você aponta! Mas await não é a única funcionalidade válida; o mesmo vale para a maioria das funções importadas. No caso de await , minha percepção é que a preocupação com a convenção poderia ser resolvida fazendo com que módulos com essa funcionalidade tenham uma cláusula import "control" "await" (func (param externref) (result externref)) , e ter ambientes que suportem essa funcionalidade sempre instanciam essa importação com o retorno de chamada apropriado.

Isso parece fornecer uma solução que economiza muito trabalho ao não alterar o wasm e ainda fornecer a portabilidade entre plataformas que você está procurando. Mas ainda estou trabalhando para entender as nuances da proposta, e já perdi muita coisa até agora!

O fato dessa proposta ser tão leve no lado do wasm é porque a instrução await parece ser semanticamente idêntica a uma chamada para uma função importada.

FWIW foi aqui que essa proposta começou originalmente, mas usar intrínsecos como esse parece mais opaco para as VMs e geralmente desencorajado (acho que @binji sugeriu se afastar disso nas discussões originais).

Por exemplo, seguindo seu argumento, algo como memory.grow ou atomic.wait também pode ser feito como import "control" "memory_grow" ou import "control" "atomic_wait" correspondentemente, mas eles não são como eles não fornece o mesmo nível de oportunidades de interoperabilidade e análise estática (tanto na VM quanto no lado das ferramentas) como instrução real.

Você poderia argumentar que memory.grow como uma instrução ainda é útil para casos onde a memória não é exportada, mas atomic.wait definitivamente poderia ser implementado fora do núcleo. Na verdade, é muito semelhante a await , exceto pelo nível em que a pausa/retomada ocorre e pelo fato de que await como uma função exigiria muito mais mágica do que atomic.wait uma vez que ele precisa ser capaz de interagir com a pilha da VM e não apenas bloquear o thread atual até que um valor seja alterado.

@tlively

"esperar por algo" deve ser reformulado na terminologia precisa já usada pela especificação.

Definitivamente sim. Posso sugerir um texto mais específico agora, se isso for útil:

When an await instruction is executed on a waitref, the host environment is requested to do some work. Typically there would be a natural meaning to what that work is based on what a waitref is on a specific host (in particular, waiting for some form of host event), but from the wasm module's point of view, the semantics of an await are similar to a call to an imported host function, that is: we don't know exactly what the host will do, but at least expect to give it certain types and receive certain results; after the instruction executes, global state (the store) may change; and an exception may be thrown.

The behavior of an await from the host's perspective may be very different, however, from a call to an imported host function, and might involve something like pausing and resuming the wasm module. It is for this reason that this instruction is defined. For the instruction to be usable on a particlar host, the host would need to define the proper behavior.

Aliás, outra comparação que me ocorreu enquanto escrevia isso são as dicas de alinhamento em cargas e lojas. O Wasm suporta cargas e armazenamentos desalinhados, portanto, as dicas não podem levar a um comportamento diferente observável pelo módulo wasm (mesmo que a dica esteja errada), mas para o host elas sugerem uma implementação muito diferente em determinadas plataformas (o que pode ser mais eficiente). Então, esse é um exemplo de instruções diferentes sem semântica diferente observável internamente, como diz a especificação: The alignment in load and store instructions does not affect the semantics .

@RossTate

Com base no princípio de composicionalidade do módulo discutido na reunião presencial, não deve ser uma armadilha. Deve ser como se houvesse apenas uma instância de módulo (composta) e executasse await. Ou seja, await empacotaria a pilha até o quadro de pilha JS mais recente.

Parece bom, e bom saber, obrigado, eu perdi essa parte.

Acho que isso me explica parte do nosso mal-entendido. Módulo => chamadas de módulo não estão no atm spec wasm, que foi o meu ponto anterior. Mas parece que você está pensando em uma especificação futura onde eles podem estar. De qualquer forma, isso não parece um problema aqui, já que a composicionalidade determina exatamente como um await deve se comportar nessa situação (o que não é o que sugeri anteriormente!, mas faz mais sentido).

A especificação principal do wasm diz alguma coisa sobre chamadas de módulo para módulo? Não me lembro de ter feito isso, e passando os olhos pelas seções relevantes agora, não vejo isso. Mas talvez eu tenha perdido alguma coisa?

Sim, a especificação principal do wasm distingue entre funções que foram importadas de outros módulos wasm e funções do host (§ 4.2.6). A semântica da chamada de função (§ 4.4.7) não depende do módulo que definiu a função e, em particular, as chamadas de função entre módulos são especificadas atualmente para se comportarem de forma idêntica às chamadas de função do mesmo módulo.

Se await s abaixo das chamadas de módulo cruzado forem definidos para interceptar, isso exigiria a especificação de uma travessia na pilha de chamadas para inspecionar se existe uma chamada de módulo cruzado antes do último quadro fictício criado por uma invocação do host (§ 4.5.5). Esta seria uma complicação infeliz na especificação. Mas eu concordo com Ross que ter chamadas de módulo cruzado trap seria uma violação da composicionalidade, então eu preferiria a semântica onde toda a pilha é congelada de volta para a última invocação do host. A maneira mais simples de especificar isso seria tornar await semelhante a uma invocação de função do host (§ 4.4.7.3), como você diz, @kripken. Mas as invocações de funções do host são completamente não determinísticas, então um nome melhor para a instrução do ponto de vista da especificação principal pode ser undefined . E neste ponto eu realmente começo a preferir uma importação intrínseca que sempre será fornecida pela plataforma Web (e WASI para portabilidade) porque a especificação principal, por si só, não se beneficia de ter uma instrução undefined IMO.

Semanticamente, uma chamada para o ambiente host que retorna um waitref mais um await é apenas uma chamada de bloqueio, certo?

Que valor isso fornece para incorporações não Web que não têm um ambiente assíncrono como um navegador e podem oferecer suporte nativo a chamadas de bloqueio?

@RReverser , vejo o ponto em que você está falando sobre intrínsecos. Há um julgamento envolvido na decisão de quando uma operação deve ser definida por meio de funções não interpretadas versus instruções. Eu acho que um fator neste julgamento é considerar como ele interage com outras instruções. memory.grow afeta o comportamento de outras instruções de memória. Eu não tive a chance de examinar a proposta de Threads, mas imagino que atomic.wait afeta ou é afetado pelo comportamento de outras instruções de sincronização. A especificação então precisa ser atualizada para formalizar essas interações.

Mas com await por si só, não há interações com outras instruções. As únicas interações são com o host, razão pela qual minha intuição seria que esta proposta deveria ser feita através de funções do host importadas.

Eu acho que uma grande diferença entre atomic.wait e este await proposto é que o módulo não pode ser reinserido com atomic.wait . O agente está suspenso em sua totalidade.

@kripken :

Meu entendimento da última apresentação de @rossberg é que ele inicialmente queria seguir esse caminho, mas depois mudou de direção para fazer uma abordagem de corrotina. Veja o slide com o título "Problemas". Após esse slide, são descritas as corrotinas, que são outra opção neste espaço. Então, talvez sua pergunta seja mais para @rossberg, que talvez possa esclarecer?

Sim, então a fatoração de corrotina pode ser pensada como uma generalização do projeto anterior de exceções recuperáveis. Ele ainda tem a mesma noção de eventos/exceções retomáveis, mas a instrução try é decomposta em primitivas menores -- o que torna a semântica mais simples e o modelo de custo mais explícito. Também é um pouco mais expressivo.

A intenção ainda é que isso possa expressar todas as abstrações de controle relevantes, e o assíncrono é um dos casos de uso motivadores. Para interoperar com JS assíncrono, a API JS poderia fornecer um evento await predefinido (carregando uma promessa JS como uma referência externa) que um módulo Wasm poderia importar e throw para suspender. Claro, há muitos detalhes que teriam que ser detalhados, mas em princípio isso deveria ser possível.

Quanto à proposta atual, ainda estou tentando entender. :)

Em particular, parece permitir await em qualquer função Wasm antiga, estou lendo isso corretamente? Se sim, isso é muito diferente do JS, que permite await apenas em funções assíncronas. E essa é uma restrição muito central, porque permite que os mecanismos compilem await por transformação _local_ de uma única função (assíncrona)!

Sem essa restrição, os mecanismos precisariam realizar uma transformação de programa _global_ (como supostamente o Asyncify faz), onde cada chamada se tornaria potencialmente muito mais cara (geralmente você não pode saber se alguma chamada pode alcançar um await). Ou, de forma equivalente, os mecanismos precisariam ser capazes de criar várias pilhas e alternar entre elas!

Agora, esse é exatamente o recurso que a ideia dos manipuladores de corrotina/efeito tenta introduzir no Wasm. Mas, obviamente, é uma adição altamente não trivial à plataforma e seu modelo de execução, uma complicação que JS teve muito cuidado em evitar para suas abstrações de controle (como assíncrono e geradores).

@rossberg

Em particular, parece permitir aguardar em qualquer função Wasm antiga, estou lendo isso corretamente? Se sim, isso é muito diferente do JS, que permite esperar apenas em funções assíncronas.

Sim, o modelo aqui é muito diferente. A espera de JS é por função, enquanto esta proposta faz uma espera de uma instância inteira do wasm (pois o objetivo é resolver a incompatibilidade de sincronização/assíncrona entre JS e wasm, que é entre JS e wasm). Também JS await é para código manuscrito, enquanto isso é para habilitar a portabilidade de código compilado.

E essa é uma restrição muito central, porque permite que os mecanismos compilem await pela transformação local de uma única função (assíncrona)! Sem essa restrição, os mecanismos precisariam realizar uma transformação global do programa (como supostamente o Asyncify faz), onde cada chamada se tornaria potencialmente muito mais cara (geralmente você não pode saber se alguma chamada pode alcançar um await). Ou, de forma equivalente, os mecanismos precisariam ser capazes de criar várias pilhas e alternar entre elas!

Definitivamente, não se pretende aqui uma transformação global do programa! Desculpe se não ficou claro.

Conforme mencionado na proposta, alternar entre pilhas é uma opção de implementação possível, mas observe que não é o mesmo que alternância de pilha de estilo de corrotina:

  • Somente a instância wasm inteira pode pausar. Isso não é para comutação de pilha dentro do módulo. (Em particular, é por isso que esta proposta não pode ter adições à especificação principal do wasm e estar inteiramente do lado do wasm JS; até agora, algumas pessoas preferem isso, e acho que qualquer uma das formas pode funcionar.)
  • Coroutines declaram pilhas explicitamente, await não.
  • await stacks só pode ser retomado uma vez, não há bifurcação/retorno mais de uma vez (não tenho certeza se você terá isso em sua proposta ou não?).
  • O modelo de desempenho é muito diferente aqui. await vai esperar por um Promise em JS, que já tem uma sobrecarga e latência mínimas. Portanto, não há problema se a implementação tiver alguma sobrecarga quando realmente pausarmos e nos importarmos menos do que as corrotinas provavelmente dariam.

Dados esses fatores, e que o comportamento observável desta proposta é que toda uma instância wasm pausa, pode haver várias maneiras de implementá-la. Por exemplo, fora da Web em uma VM executando uma única instância wasm, ele poderia literalmente executar seu loop de eventos até que seja hora de retomar o wasm. Na Web, uma abordagem de implementação pode ser: quando ocorrer um await, copie toda a pilha wasm, da posição atual para onde chamamos o wasm; salve isso do lado; para retomar, copie-o de volta e continue a partir daí. Também pode haver outras abordagens ou variações dessas (algumas talvez sem copiar, mas, novamente, evitar a sobrecarga de cópia não é realmente crucial aqui!).

Desculpe o post longo, e algumas repetições do próprio texto da proposta, mas espero que isso ajude a esclarecer alguns dos pontos que você se referiu?

Acho que há muito o que discutir aqui em termos de implementação. Até agora, o comentário de @acfoltzer sobre Lucet é encorajador!

Apenas para esclarecer algumas frases no comentário mais recente de @kripken , não é toda a instância wasm que pausa. É apenas a chamada mais recente de um quadro de host para wasm na pilha que é pausada e, em vez disso, o quadro de host recebe uma promessa correspondente (ou o análogo apropriado para o host). Veja aqui o esclarecimento anterior relevante.

Hm, eu não vejo como isso faz a diferença. Quando você espera em algum lugar no interior do Wasm, você precisará capturar toda a pilha de chamadas de pelo menos a entrada do host até esse ponto. E você pode manter essa suspensão (ou seja, esse segmento de pilha) ativa pelo tempo que quiser, enquanto faz outras chamadas de cima ou cria mais suspensões. E você pode retomar de outro lugar (eu acho?). Isso não requer toda a maquinaria de implementação de continuações delimitadas? Apenas que o prompt é definido na entrada Wasm em vez de uma construção separada.

@rossberg

Isso pode ser verdade em algumas VMs, sim. Se aguardar e corrotinas acabarem precisando exatamente do mesmo trabalho de VM, pelo menos nenhum trabalho extra será necessário. Nesse caso, o benefício da proposta await seria a integração JS conveniente.

Acho que você pode obter uma integração JS conveniente sem a transformação de todo o programa se não permitir que o módulo seja reinserido.

Acho que você pode obter uma integração JS conveniente sem a transformação de todo o programa se não permitir que o módulo seja reinserido.

Isso parece uma maneira mais fácil de fazer isso, mas isso exigiria o bloqueio de qualquer módulo visitado na pilha de chamadas (ou, como primeira etapa, todos os módulos WebAssembly).

Isso parece uma maneira mais fácil de fazer isso, mas isso exigiria o bloqueio de qualquer módulo visitado na pilha de chamadas (ou, como primeira etapa, todos os módulos WebAssembly).

Correto, assim como atomic.wait .

@taralx

Acho que você pode obter uma integração JS conveniente sem a transformação de todo o programa se não permitir que o módulo seja reinserido.

Por um lado, a reentrada pode ser útil, por exemplo, um mecanismo de jogo pode baixar um arquivo e não querer que a interface do usuário seja completamente pausada ao fazê-lo (o Asyncify permite isso hoje). Mas, por outro lado, talvez a reentrada não seja permitida, mas um aplicativo pode criar várias instâncias do mesmo módulo para isso (todas importando a mesma memória, globais mutáveis ​​etc.?), então uma reentrada seria uma chamada para outra instância. Acho que podemos fazer isso funcionar em cadeias de ferramentas (haveria um limite efetivo no número de reentradas ativas de uma só vez - igual ao número de instâncias - o que parece bom).

Portanto, se sua simplificação ajudar as VMs, definitivamente vale a pena considerar!

(Observe que, como discutido anteriormente, não acho que precisamos de uma transformação de programa inteira aqui com nenhuma das opções sendo discutidas. Você só precisa disso se estiver na situação ruim em que o Asyncify está, onde é tudo o que você pode fazer no nível de cadeia de ferramentas. Para await, no pior caso, conforme discutido com @rossberg , você pode fazer o que a proposta de corrotinas faria internamente. Mas sua ideia é potencialmente muito interessante se tornar as coisas mais simples do que isso!)

Por um lado, a reentrada pode ser útil, por exemplo, um mecanismo de jogo pode baixar um arquivo e não querer que a interface do usuário seja completamente pausada ao fazê-lo (o Asyncify permite isso hoje).

Não tenho certeza se este é um recurso de som embora. Parece-me que isso introduziria uma simultaneidade inesperada no aplicativo. Um aplicativo nativo que carrega ativos durante a renderização usaria 2 threads internamente e cada thread seria mapeado para um WebWorker + SharedArrayBuffer. Se um aplicativo usa threads, ele também pode usar primitivos da Web síncronos de WebWorkers (como são permitidos, pelo menos em alguns casos). Caso contrário, sempre é possível mapear operações assíncronas no thread principal para operações de bloqueio em um trabalhador usando Atomics.wait (por exemplo).

Gostaria de saber se todo o caso de uso já não foi resolvido por multithreading em geral. Ao usar primitivos de bloqueio em um trabalhador, toda a pilha (JS/Wasm/nativo do navegador) é preservada, o que parece ser muito mais simples e robusto.

Ao usar primitivos de bloqueio em um trabalhador, toda a pilha (JS/Wasm/nativo do navegador) é preservada, o que parece ser muito mais simples e robusto.

Na verdade, essa é outra implementação alternativa do wrapper Asyncify JS autônomo que experimentei, mas, embora resolva o problema do tamanho do código, a sobrecarga de desempenho foi ainda muito maior do que o Asyncify atual que usa a transformação Wasm.

@alexp-sssup

Parece-me que isso introduziria simultaneidade inesperada no aplicativo.

Definitivamente, sim - isso precisa ser feito com muito cuidado e pode quebrar as coisas. Temos experiência mista com isso usando o Asyncify, bom e ruim (para um exemplo de caso de uso válido: um arquivo é baixado em JS e JS chama wasm para malloc algum espaço para copiá-lo, antes de continuar). Mas em qualquer caso, a reentrada não é uma parte crucial desta proposta de qualquer maneira.

Para adicionar ao que o @RReverser disse, outro problema com os threads é que o suporte para eles não é e não será universal. Mas esperar pode estar em todos os lugares.

Em outros idiomas onde o async/await foi introduzido, a reentrada é absolutamente fundamental. É meio que o ponto principal de que outros eventos podem acontecer enquanto se está (a) esperando. Parece-me que a reentrada é muito importante.

Além disso, não é verdade que sempre que um módulo faz qualquer chamada para uma função externa ele tem que assumir que ele pode ser reinserido através de qualquer uma de suas exportações (no exemplo acima, mesmo sem esperar, qualquer chamada para e externo função é livre (sem trocadilhos) para chamar malloc).

um aplicativo pode criar várias instâncias do mesmo módulo para isso (todas importando a mesma memória, globais mutáveis, etc.?), então uma reentrada seria uma chamada para outra instância

Apenas para as memórias compartilhadas do módulo. As outras memórias precisam ser reinstanciadas, o que é importante para evitar que uma operação interfira em outras alterações durante o voo.

Observo que a versão não reentrante disso é polipreenchível em qualquer incorporação com suporte a thread, caso alguém queira brincar com ela e ver como é útil.

Observo que a versão não reentrante disso é polipreenchível em qualquer incorporação com suporte a thread, caso alguém queira brincar com ela e ver como é útil.

Como mencionado acima, isso é algo com o qual já brincamos, mas descartado, pois traz um desempenho ainda pior do que a solução atual, não é universalmente suportado e, além disso, dificulta muito o compartilhamento de WebAssembly.Global ou WebAssembly.Table com o thread principal sem hacks adicionais, tornando-se uma má escolha para um polyfill transparente.

A solução atual que reescreve o módulo Wasm não sofre com esses problemas, mas tem um custo significativo de tamanho de arquivo.

Como tal, nenhum deles é ótimo para grandes aplicativos do mundo real, o que nos motiva a procurar suporte nativo para integração assíncrona, conforme descrito aqui.

pior desempenho

Você tem algum tipo de benchmark?

Sim, posso compartilhá-lo quando voltar ao trabalho na terça-feira (ou, mais provavelmente, na quarta-feira), ou é bastante fácil criar um que apenas chame para uma função JS assíncrona vazia.

Obrigado. Eu poderia criar um microbenchmark, mas não seria muito instrutivo.

Ah, sim, o meu também é um microbenchmark, pois estávamos interessados ​​apenas na comparação de despesas gerais.

O problema com um microbenchmark é que não sabemos quanta latência é aceitável para um aplicativo real. Se demorar mais 1ms, isso é realmente um problema se o aplicativo só executa operações de espera na taxa de 1/s, por exemplo?

Acho que o foco na velocidade de uma abordagem baseada em átomos pode ser uma distração. Como mencionado anteriormente, os atomics não funcionam e não funcionarão em todos os lugares (devido ao COOP/COEP) e também apenas um trabalhador pode usar a abordagem atomics, pois o thread principal não pode bloquear. É uma boa ideia, mas para uma solução universal precisamos de algo como Await.

Não estou sugerindo isso como uma solução de longo prazo. Estou sugerindo que um polyfill que o use pode ser usado para ver se uma solução não reentrante funcionará para as pessoas.

@taralx Oh, ok, agora entendi, obrigado.

@taralx :

Acho que você pode obter uma integração JS conveniente sem a transformação de todo o programa se não permitir que o módulo seja reinserido.

Isso seria ruim. Isso significa que a mesclagem de vários módulos pode interromper seu comportamento. Essa seria a antítese da modularidade.

Como princípio geral de projeto, o comportamento operacional nunca deve depender dos limites do módulo (além do escopo simples). Os módulos são meramente um mecanismo de agrupamento e escopo no Wasm, e você deseja manter a capacidade de reagrupar coisas (link/mesclar/dividir módulos) sem que isso altere o comportamento de um programa.

@rossberg : isso é generalizável como bloqueando o acesso a qualquer módulo Wasm, conforme proposto anteriormente. Mas então é provavelmente muito limitante.

Isso seria ruim. Isso significa que a mesclagem de vários módulos pode interromper seu comportamento. Essa seria a antítese da modularidade.

Esse foi o meu ponto com o argumento polyfilling - atomic.wait não quebra a modularidade, então isso também não deveria.

@taralx , atomic.wait referencia um local específico em uma memória específica. Que memória e localização o bloqueio await usaria e como controlar quais módulos compartilham essa memória?

@rossberg você pode elaborar um cenário que você acha que isso quebra? Suspeito que temos ideias diferentes sobre como a versão não reentrante funcionaria.

@taralx , considere carregar dois módulos A e B, cada um fornecendo alguma função de exportação, digamos A.f e B.g . Ambos podem executar await quando chamados. Duas partes do código do cliente são passadas para uma dessas funções, respectivamente, e elas as chamam de forma independente. Eles não interferem ou bloqueiam um ao outro. Então alguém mescla ou refatora A e B em C, sem alterar nada no código. De repente, ambas as partes do código do cliente podem começar a se bloquear inesperadamente. Ação assustadora à distância através do estado compartilhado oculto.

Isso faz sentido. Mas permitir a reentrada arrisca a simultaneidade em módulos que não a esperam, então é uma ação assustadora à distância de qualquer maneira.

Mas os módulos já podem ser reinseridos, não? Sempre que um módulo faz uma chamada de importação, o código externo pode entrar novamente no módulo, o que pode alterar o estado global antes de retornar. Não consigo ver como a reentrada durante a espera proposta é mais assustadora ou simultânea do que chamar uma função importada. Talvez eu esteja perdendo alguma coisa?

(editado)

Hum, sim. Ok, então uma função importada pode entrar novamente no módulo. Eu claramente preciso pensar mais sobre isso.

Quando o código está em execução e chama uma função, há duas possibilidades: ele sabe que a função não chamará coisas aleatórias, ou a função pode chamar coisas aleatórias. Neste último caso, a reentrada é sempre possível. As mesmas regras se aplicam a await .

(editei meu comentário acima)

Obrigado a todos pela discussão até agora!

Para resumir, parece que há interesse geral aqui, mas há grandes questões em aberto, como se isso deveria ser 100% do lado JS ou apenas 99% - parece que o primeiro removeria as principais preocupações que algumas pessoas têm, e isso ser bom para o caso da Web, então provavelmente está ok. Outra grande questão em aberto é quão viável isso seria fazer em VMs sobre as quais precisamos de mais informações.

Vou sugerir um item da agenda para a próxima reunião do CG em 2 semanas para discutir esta proposta e considerá-la para o estágio 1, o que significaria abrir um repositório e discutir as questões em aberto em questões separadas com mais detalhes. (Acredito que esse é o processo correto, mas corrija-me se estiver errado.)

Apenas para FYI
Estaremos montando uma proposta de troca de pilha completa em um
prazo. Eu sinto que isso pode tornar sua variante de caso especial discutível -
O que você acha?
Francisco

Em qui, 28 de maio de 2020 às 15h51 Alon Zakai [email protected] escreveu:

Obrigado a todos pela discussão até agora!

Para resumir, parece que há interesse geral aqui, mas há
grandes questões em aberto, como se isso deve ser 100% do lado JS ou apenas
99% - parece que o primeiro removeria as principais preocupações de algumas pessoas
tem, e isso seria bom para o caso da Web, então provavelmente está tudo bem.
Outra grande questão em aberto é quão viável isso seria fazer em VMs que
precisamos de mais informações sobre.

Vou sugerir um item da agenda para a próxima reunião do CG em 2 semanas para discutir
esta proposta e considerá-la para o estágio 1, o que significaria abrir um repo
e discutir as questões em aberto em questões separadas com mais detalhes.
(Acredito que esse é o processo correto, mas corrija-me se estiver errado.)


Você está recebendo isso porque está inscrito neste tópico.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/WebAssembly/design/issues/1345#issuecomment-635649331 ,
ou cancelar
https://github.com/notifications/unsubscribe-auth/AAQAXUCLZ4CJVQYEUBK23BLRT3TFLANCNFSM4NEJW2PQ
.

>

Francis McCabe
SWE

@fgmccabe

Devemos discutir isso com certeza.

No entanto, em geral, a menos que sua proposta se concentre no lado JS, acho que isso não seria discutível (que é 99%-100% no lado JS).

Agora que a discussão sobre os detalhes da implementação foi concluída, gostaria de levantar novamente uma preocupação de nível superior que expressei anteriormente , mas abandonei para ter uma discussão de cada vez.

Um programa é composto de muitos componentes. De uma perspectiva de engenharia de software, é importante que dividir componentes em partes ou mesclar componentes não altere significativamente o comportamento do programa. Esse é o raciocínio por trás do princípio de composição de módulos discutido na última reunião presencial de CG e está implícito no design de muitas linguagens.

No caso de programas web, agora com WebAssembly esses diferentes componentes podem até ser escritos em linguagens diferentes: JS ou wasm. Na verdade, muitos componentes poderiam ser escritos em qualquer uma das linguagens; Vou me referir a eles como componentes "ambivalentes". No momento, a maioria dos componentes ambivalentes são escritos em JS, mas imagino que todos esperamos que mais e mais deles sejam reescritos em wasm. Para facilitar essa "migração de código", devemos tentar garantir que reescrever um componente dessa maneira não altere a forma como ele interage com o ambiente. Como um exemplo de brinquedo, se um componente de programa "apply" específico (f, x) => f(x) é escrito em JS ou em wasm não deve afetar o comportamento do programa geral. Este é um princípio de migração de código.

Infelizmente, todas as variantes desta proposta parecem violar o programa de composição de módulos ou o princípio de migração de código. O primeiro é violado quando await captura a pilha até onde o módulo wasm atual foi inserido mais recentemente, porque esse limite muda conforme os módulos são separados ou combinados. O último é violado quando await captura a pilha até onde o wasm foi inserido mais recentemente, porque esse limite muda conforme o código é migrado de JS para wasm (de modo que migrar algo tão simples quanto (f, x) => f(x) de JS para wasm pode alterar significativamente o comportamento do programa geral).

Não acho que essas violações se devam a más escolhas de design desta proposta. Em vez disso, o problema parece ser que essa proposta está tentando evitar indiretamente tornar o JS mais poderoso, e esse objetivo é forçá-lo a impor limites artificiais que violam esses princípios. Entendo totalmente esse objetivo, mas suspeito que esse problema surgirá cada vez mais: adicionar funcionalidade ao WebAssembly de uma maneira que respeite esses princípios geralmente exigirá a adição indireta de funcionalidade ao JS devido ao JS ser a linguagem de incorporação. Minha preferência seria enfrentar esse problema de frente (que eu realmente não tenho ideia de como resolver). Se não for isso, minha preferência secundária seria fazer essa alteração apenas na API JS, porque é o JS que é o fator limitante aqui, em vez de adicionar instruções ao WebAssembly para o qual o wasm não tem interpretação.

Não acho que essas violações se devam a más escolhas de design desta proposta. Em vez disso, o problema parece ser que esta proposta está tentando evitar indiretamente tornar o JS mais poderoso

Isso é importante, mas não é a principal razão para o design aqui.

A principal razão para esse design é que, embora eu concorde plenamente que o princípio da composição faz sentido para wasm , o problema fundamental que temos na Web é que, de fato, JS e wasm não são equivalentes na prática. Temos JS manuscrito que é assíncrono e wasm portado que é sincronizado. Em outras palavras, o limite entre eles é, na verdade, o problema exato que estamos tentando resolver. No geral, não tenho certeza se concordo que o princípio da composição deve ser aplicado ao wasm e JS (mas talvez devesse, poderia ser um debate interessante).

Eu esperava ter mais discussão publicamente aqui, mas para economizar tempo, entrei em contato diretamente com alguns implementadores de VM, pois poucos se envolveram aqui até agora. Dado o feedback deles junto com a discussão aqui, infelizmente acho que devemos pausar esta proposta.

Await tem um comportamento observável muito mais simples do que as corrotinas gerais ou a comutação de pilha, mas as pessoas da VM com quem conversei concordam com @rossberg que o trabalho da VM no final provavelmente seria semelhante para ambos. E pelo menos algumas pessoas de VM acreditam que obteremos corrotinas ou alternância de pilha de qualquer maneira, e que podemos suportar os casos de uso do await usando isso. Isso significará criar uma nova corrotina/pilha em cada chamada no wasm (ao contrário desta proposta), mas pelo menos algumas pessoas de VM acham que isso pode ser feito rápido o suficiente.

Além da falta de interesse do pessoal da VM, tivemos algumas fortes objeções a esta proposta aqui de @fgmccabe e @RossTate , conforme discutido acima. Discordamos em algumas coisas, mas aprecio esses pontos de vista e o tempo gasto para explicá-los.

Concluindo, no geral, parece que seria uma perda de tempo de todos tentar avançar aqui. Mas obrigado a todos que participaram da discussão! E espero que pelo menos isso motive a priorização de corrotinas / troca de pilha.

Observe que a parte JS desta proposta pode ser relevante no futuro, como açúcar JS basicamente para integração conveniente do Promise. Precisamos esperar pela mudança de pilha ou corrotinas e ver se isso pode funcionar em cima disso. Mas acho que não vale a pena manter o assunto em aberto para isso, então fechando.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

cretz picture cretz  ·  5Comentários

jfbastien picture jfbastien  ·  6Comentários

frehberg picture frehberg  ·  6Comentários

konsoletyper picture konsoletyper  ·  6Comentários

void4 picture void4  ·  5Comentários