Design: Proposition : attendre

Créé le 18 mai 2020  ·  96Commentaires  ·  Source: WebAssembly/design

@rreverser et moi aimerions proposer une nouvelle proposition pour WebAssembly : Await .

La motivation de la proposition est d'aider le code " synchrone " compilé sur WebAssembly, qui fait quelque chose comme une lecture à partir d'un fichier :

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

Ce code ne peut pas être facilement implémenté dans un environnement hôte qui est principalement asynchrone , et qui implémenterait "lire à partir d'un fichier" de manière asynchrone, par exemple sur le Web,

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

En d'autres termes, l'objectif est d'aider à résoudre le problème de synchronisation/asynchrone qui est si courant avec wasm sur le Web.

Le problème de synchronisation/asynchrone est un problème sérieux. Bien que le nouveau code puisse être écrit en pensant à cela, les grandes bases de code existantes ne peuvent souvent pas être refactorisées pour le contourner, ce qui signifie qu'elles ne peuvent pas s'exécuter sur le Web. Nous avons Asyncify qui instrumente un fichier wasm pour permettre la pause et la reprise, et qui a permis le portage de certaines de ces bases de code, nous ne sommes donc pas complètement bloqués ici. Cependant, l'instrumentation du wasm a une surcharge importante, quelque chose comme une augmentation de 50 % de la taille du code et un ralentissement de 50 % en moyenne (mais parfois bien pire), car nous ajoutons des instructions pour écrire/relire dans l'état local et appeler la pile et ainsi de suite. Cette surcharge est une grande limitation et elle exclut Asyncify dans de nombreux cas !

L'objectif de cette proposition est de permettre de suspendre et de reprendre l'exécution de manière efficace (en particulier, sans surcharge comme Asyncify) afin que toutes les applications qui rencontrent le problème sync/async puissent facilement l'éviter. Personnellement, nous l'avons prévu principalement pour le Web, où cela peut aider WebAssembly à mieux s'intégrer aux API Web, mais des cas d'utilisation en dehors du Web peuvent également être pertinents.

L'idée en bref

Le problème principal ici est entre le code wasm étant synchrone et l'environnement hôte qui est asynchrone. Notre approche est donc focalisée sur la frontière d'une instance wasm et l'extérieur. Conceptuellement, lorsqu'une nouvelle instruction await est exécutée, l'instance wasm "attend" quelque chose de l'extérieur. Ce que « attendre » signifie serait différent sur différentes plates-formes, et peut ne pas être pertinent sur toutes les plates-formes (comme toutes les plates-formes peuvent ne pas trouver la proposition wasm atomics pertinente), mais sur la plate-forme Web en particulier, l'instance wasm attendrait une promesse et une pause jusqu'à ce que cela résout ou rejette. Par exemple, une instance wasm pourrait s'arrêter sur une opération réseau fetch et être écrite quelque chose comme ceci dans .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

Notez la similitude générale avec await en JS et dans d'autres langages. Bien que cela ne leur soit pas identique (voir les détails ci-dessous), le principal avantage est qu'il permet d'écrire du code d'apparence synchrone (ou plutôt, de compiler du code d'apparence synchrone dans wasm).

Les détails

Spécification wasm de base

Les modifications apportées à la spécification wasm de base sont très minimes :

  • Ajoutez un type waitref .
  • Ajoutez une instruction await .

Un type est spécifié pour chaque instruction await (comme call_indirect ), par exemple :

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

Le type doit recevoir un waitref et peut retourner n'importe quel type (ou rien).

await n'est défini qu'en termes de faire faire quelque chose à l'environnement hôte. C'est similaire en ce sens à l'instruction unreachable , qui sur le Web oblige l'hôte à lancer un RuntimeError , mais ce n'est pas dans la spécification de base. De même, la spécification wasm de base indique seulement que await est destiné à attendre quelque chose de l'environnement hôte, mais pas comment nous le ferions réellement, ce qui peut être très différent dans différents environnements hôte.

C'est tout pour la spécification wasm de base !

Spécification Wasm JS

Les modifications apportées à la spécification wasm JS (qui n'affectent que les environnements JS comme le Web) sont plus intéressantes :

  • Une waitref valide est une promesse JS.
  • Lorsqu'un await est exécuté sur une promesse, l'instance entière de wasm s'interrompt et attend que cette promesse soit résolue ou rejetée.
  • Si la promesse se résout, l'instance reprend l'exécution après avoir poussé vers la pile la valeur reçue de la promesse (s'il y en a une)
  • Si la promesse est rejetée, nous reprenons l'exécution et lançons une exception wasm à partir de l'emplacement du await .

Par "toute l'instance de wasm s'interrompt", nous entendons que tout l'état local est préservé (la pile d'appels, les valeurs locales, etc.) afin que nous puissions reprendre l'exécution en cours plus tard, comme si nous n'avions jamais fait de pause (bien sûr, l'état global peut avoir changé, comme la mémoire peut avoir été écrite). Pendant que nous attendons, la boucle d'événements JS fonctionne normalement et d'autres choses peuvent se produire. Lorsque nous reprenons plus tard (si nous ne rejetons pas la promesse, auquel cas une exception serait levée), nous continuons exactement là où nous nous sommes arrêtés, essentiellement comme si nous n'avions jamais fait de pause (mais entre-temps, d'autres choses se sont produites, et l'état global peut avoir modifié, etc).

À quoi ressemble JS lorsqu'il appelle une instance wasm qui s'interrompt ensuite ? Pour expliquer cela, examinons d'abord un exemple courant rencontré lors du portage d'applications natives vers wasm, une boucle d'événements :

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

Imaginez que cette fonction soit appelée une fois par requestAnimationFrame . Il exécute les tâches qui lui sont confiées, qui peuvent inclure : le rendu, la physique, l'audio et la récupération du réseau. Si nous avons un événement de récupération de réseau, alors et seulement alors, nous finissons par exécuter une instruction await sur la promesse fetch . Nous pouvons le faire 0 fois pour un appel de event_loop_iteration , ou 1 fois, ou plusieurs fois. Nous savons seulement si nous finissons par le faire lors de l'exécution de ce wasm - pas avant, et en particulier pas dans l'appelant JS de cet export wasm. Cet appelant doit donc être prêt pour que l'instance soit en pause ou non.

Une situation quelque peu analogue peut se produire en JavaScript pur :

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

foo obtient une fonction JS bar et l'appelle avec des données. Dans JS bar peut être une fonction asynchrone ou normale. S'il est asynchrone, il renvoie une Promise et ne termine l'exécution que plus tard. Si c'est normal, il s'exécute avant de revenir et renvoie le résultat réel. foo peut soit supposer qu'il connaît le type bar (aucun type n'est écrit en JS, en fait bar n'est peut-être même pas une fonction !), soit il peut gérer les deux types de fonctions doivent être parfaitement généraux.

Maintenant, normalement, vous savez exactement quel ensemble de fonctions bar pourrait être ! Par exemple, vous avez peut-être écrit foo et les bar possibles en coordination, ou documenté exactement quelles sont les attentes. Mais l'interaction wasm/JS dont nous parlons ici est en fait plus similaire au cas où vous n'avez pas un couplage aussi étroit entre les choses, et où en fait vous devez gérer les deux cas. Comme mentionné précédemment, l'exemple event_loop_iteration l'exige. Mais encore plus généralement, le wasm est souvent votre application compilée tandis que le JS est un code "d'exécution" générique, de sorte que JS doit gérer tous les cas. JS peut facilement le faire, bien sûr, par exemple en utilisant result instanceof Promise pour vérifier le résultat, ou en utilisant 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");
}

(notez que si nous n'avons pas besoin de ce console.log , nous n'aurions pas besoin du JS await dans cet exemple, et nous n'aurions qu'un appel normal à une exportation wasm)

Pour résumer ce qui précède, nous proposons que le comportement d'une instance wasm en pause soit modélisé sur le cas JS d'une fonction qui peut ou non être asynchrone, que nous pouvons énoncer comme suit :

  • Lorsqu'un await est exécuté, l'instance wasm revient immédiatement à celui qui l'a appelé (généralement, ce serait JS appelant une exportation wasm, mais voir les notes plus tard). L'appelant reçoit une promesse qu'il peut utiliser pour savoir quand l'exécution du wasm se termine et pour obtenir un résultat s'il y en a un.

Prise en charge de la chaîne d'outils / bibliothèque

D'après notre expérience avec Asyncify et les outils associés, il est facile (et amusant !) d'écrire un petit JS pour gérer une instance wasm en attente. Outre les options mentionnées précédemment, une bibliothèque peut effectuer l'une des opérations suivantes :

  1. Enveloppez une instance wasm pour que ses exportations renvoient toujours une promesse. Cela donne une belle interface simple à l'extérieur (cependant, cela ajoute une surcharge aux appels rapides dans wasm qui ne s'arrêtent pas ). C'est ce que fait la bibliothèque d'assistance autonome Asyncify , par exemple.
  2. Écrivez un état global lorsqu'une instance s'arrête et vérifiez-le à partir du JS qui a appelé l'instance. C'est ce que fait l'intégration Asyncify d'Emscripten, par exemple.

Beaucoup plus peuvent être construits sur de telles approches, ou d'autres. Nous préférons laisser tout cela aux chaînes d'outils et aux bibliothèques pour éviter la complexité dans la proposition et dans les VM.

Mise en œuvre et performances

Plusieurs facteurs devraient contribuer à simplifier les implémentations de VM :

  1. Une pause/reprise se produit uniquement sur une attente, et nous connaissons leurs emplacements de manière statique à l'intérieur de chaque fonction.
  2. Lorsque nous reprenons, nous continuons exactement là où nous avons laissé les choses, et nous ne le faisons qu'une seule fois. En particulier, nous ne forkons jamais l'exécution : rien ici ne revient deux fois, contrairement à setjmp de C ou à une coroutine dans un système qui permet le clonage/forking.
  3. Il est acceptable si la vitesse d'un await est plus lente qu'un appel normal à JS, puisque nous attendrons une promesse, ce qui implique au minimum qu'une promesse a été allouée et que nous attendons la boucle d'événement ( qui a un minimum de frais généraux et attend potentiellement d' autres choses en cours d'exécution ). Autrement dit, les cas d'utilisation ici n'exigent pas que les implémenteurs de VM trouvent des moyens de rendre await incroyablement rapide. Nous voulons seulement que await soit efficace par rapport aux exigences ici, et nous nous attendons en particulier à ce qu'il soit beaucoup plus rapide que la surcharge importante d'Asyncify.

Compte tenu de ce qui précède, une implémentation naturelle consiste à copier la pile lorsque nous faisons une pause. Bien que cela ait des frais généraux, compte tenu des performances attendues ici, cela devrait être très raisonnable. Et si nous copions la pile uniquement lorsque nous faisons une pause, nous pouvons éviter de faire un travail supplémentaire pour nous préparer à la pause. Autrement dit, il ne devrait pas y avoir de surcharge générale supplémentaire (ce qui est très différent d'Asyncify !)

Notez que bien que la copie de la pile soit une approche naturelle ici, ce n'est pas une opération complètement triviale, car la copie peut ne pas être un simple memcpy, selon les composants internes de la VM. Par exemple, si la pile contient des pointeurs vers elle-même, alors ceux-ci doivent être ajustés ou la pile doit être relocalisable. Alternativement, il pourrait être possible de recopier la pile à sa position d'origine avant de la reprendre, car comme mentionné précédemment, elle n'est jamais "forkée".

Notez également que rien dans cette proposition n'exige de copier la pile. Peut-être que certaines implémentations peuvent faire autre chose, grâce aux facteurs de simplification mentionnés dans les 3 points du début de cette section. Le comportement observable ici est assez simple et la gestion explicite de la pile n'en fait pas partie.

Nous sommes très intéressés par les commentaires des implémenteurs de VM sur cette section !

Précisions

Cette proposition suspend uniquement l'exécution de WebAssembly vers l'appelant de l'instance wasm. Il ne permet pas de mettre en pause les cadres de pile de l'hôte (JS ou navigateur). await fonctionne sur une instance wasm, n'affectant que les cadres de pile à l'intérieur.

Il est correct d'appeler l'instance WebAssembly pendant qu'une pause s'est produite, et plusieurs événements de pause/reprise peuvent être en cours à la fois. (Notez que si la machine virtuelle adopte l'approche de copier la pile, cela ne signifie pas qu'une nouvelle pile doit être allouée chaque fois que nous entrons dans le module, car nous n'avons besoin de la copier que si nous faisons une pause.)

Connexion à d'autres propositions

Des exceptions

Le rejet de la promesse lançant une exception signifie que cette proposition dépend de la proposition d'exceptions wasm.

Coroutines

La proposition de coroutines d'Andreas Rossberg traite également de la pause et de la reprise de l'exécution. Cependant, bien qu'il y ait un certain chevauchement conceptuel, nous ne pensons pas que les propositions se concurrencent. Les deux sont utiles car ils se concentrent sur différents cas d'utilisation. En particulier, la proposition de coroutines permet aux coroutines d'être commutées entre wasm intérieur , tandis que la proposition d'attente permet à une instance entière d'attendre l'environnement extérieur . Et la manière dont les deux choses sont faites conduit à des caractéristiques différentes.

Plus précisément, la proposition de coroutines gère la création de pile de manière explicite (des instructions sont fournies pour créer une coroutine, en mettre une en pause, etc.). La proposition await ne parle que de pause et de reprise et donc la gestion de la pile est implicite . La gestion explicite de la pile est appropriée lorsque vous savez que vous créez des coroutines spécifiques, tandis que la gestion implicite est appropriée lorsque vous savez seulement que vous devez attendre quelque chose pendant l'exécution (voir l'exemple précédent avec event_loop_iteration ).

Les caractéristiques de performance de ces deux modèles peuvent être très différentes. Si, par exemple, nous créons une coroutine chaque fois que nous exécutons du code qui pourrait s'arrêter (encore une fois, souvent nous ne le savons pas à l'avance), cela pourrait allouer de la mémoire inutilement. Le comportement observé de await est plus simple que ce que les coroutines générales peuvent faire et il peut donc être plus simple à implémenter.

Une autre différence significative est que await est une seule instruction qui fournit tout ce dont un module wasm a besoin pour corriger l'incompatibilité de synchronisation/asynchrone de wasm avec le Web (voir le premier exemple .wat du tout début). Il est également très facile à utiliser du côté JS qui peut simplement fournir et/ou recevoir une Promise (bien qu'un peu de code de bibliothèque puisse être utile à ajouter, comme mentionné précédemment, cela peut être très minime).

En théorie, les deux propositions pourraient être conçues pour être complémentaires. Peut-être que await pourrait être l'une des instructions de la proposition de coroutines d'une manière ou d'une autre ? Une autre option consiste à autoriser un await à fonctionner sur une coroutine (en donnant essentiellement à une instance wasm un moyen simple d'attendre les résultats de la coroutine).

WASI#276

Par coïncidence, WASI # 276 a été posté par @tqchen juste au moment où nous terminions d'écrire ceci. Nous sommes très heureux de voir cela car cela partage notre conviction que les coroutines et le support asynchrone sont des fonctionnalités distinctes.

Nous pensons qu'une instruction await pourrait aider à implémenter quelque chose de très similaire à ce qui est proposé ici (option C3), à la différence qu'il n'y aurait pas besoin d'appels système asynchrones spéciaux, mais certains appels système pourraient renvoyer un waitref qui peut alors être await -ed.

Pour JavaScript, nous avons défini l'attente comme la mise en pause d'une instance wasm, ce qui est logique car nous pouvons avoir plusieurs instances ainsi que JavaScript sur la page. Cependant, dans certains environnements de serveur, il peut n'y avoir que l'hôte et une seule instance de wasm, et dans ce cas, l'attente peut être beaucoup plus simple, peut-être attendre littéralement sur un descripteur de fichier ou sur le GPU. Ou attendre pourrait mettre en pause l'intégralité de la machine virtuelle wasm mais continuer à exécuter une boucle d'événements. Nous n'avons pas d'idées précises ici nous-mêmes, mais sur la base de la discussion dans ce numéro, il peut y avoir des possibilités intéressantes ici, nous sommes curieux de savoir ce que les gens pensent !

Cas d'angle : instance wasm => instance wasm => attendre

Dans un environnement JS, lorsqu'une instance wasm s'interrompt, elle revient immédiatement à celui qui l'a appelée. Nous avons décrit ce qui se passe si l'appelant vient de JS, et la même chose se produit si l'appelant est le navigateur (par exemple, si nous avons fait un setTimeout sur une exportation wasm qui s'interrompt ; mais rien d'intéressant ne s'y passe, comme la promesse retournée est simplement ignorée). Mais il existe un autre cas, l'appel provenant de wasm, c'est-à-dire où l'instance wasm A appelle directement une exportation à partir de l'instance B et B fait une pause. La pause nous fait immédiatement sortir de B et renvoie un Promise .

Lorsque l'appelant est JavaScript, en tant que langage dynamique, cela pose moins de problème et, en fait, il est raisonnable de s'attendre à ce que l'appelant vérifie le type, comme indiqué précédemment. Lorsque l'appelant est WebAssembly, qui est typé statiquement, c'est gênant. Si nous ne faisons rien dans la proposition pour cela, la valeur sera convertie, dans notre exemple d'une promesse à n'importe quelle instance que A attend (si un i32 , il serait converti en un 0 ). Au lieu de cela, nous suggérons qu'une erreur se produise :

  • Si une instance de wasm appelle (directement ou en utilisant call_indirect ) une fonction d'une autre instance de wasm, et lors de l'exécution dans l'autre instance, un await est exécuté, alors une exception RuntimeError est jeté de l'emplacement du await .

Surtout, cela pourrait être fait sans surcharge à moins de faire une pause, c'est-à-dire en gardant les appels wasm instance -> wasm instance normaux à pleine vitesse, en vérifiant la pile uniquement lors d'une pause.

Notez que les utilisateurs qui veulent quelque chose comme une instance wasm en appellent une autre et que cette dernière pause peut le faire, mais ils doivent ajouter du JS entre les deux.

Une autre option ici est qu'une pause se propage également au wasm appelant, c'est-à-dire que tous les wasm s'arrêteraient jusqu'à JS, couvrant potentiellement plusieurs instances de wasm. Cela présente certains avantages, comme les limites du module wasm cessent d'avoir de l'importance, mais aussi des inconvénients, comme la propagation étant moins intuitive (l'auteur de l'instance appelante peut ne pas s'attendre à un tel comportement) et que l'ajout de JS au milieu pourrait changer le comportement (également potentiellement de manière inattendue). Exiger que les utilisateurs aient JS entre les deux, comme mentionné précédemment, semble moins risqué.

Une autre option pourrait être que certaines exportations wasm soient marquées comme asynchrones alors que d'autres ne le sont pas, et nous pourrions alors savoir statiquement ce qui est quoi, et ne pas autoriser les appels inappropriés ; mais voyez l'exemple event_loop_iteration précédent qui est un cas courant qui ne serait pas résolu en marquant les exportations, et il y a aussi des appels indirects, donc nous ne pouvons pas éviter le problème de cette façon.

Approches alternatives envisagées

Peut-être n'avons-nous pas du tout besoin d'une nouvelle instruction await , si wasm s'interrompt chaque fois qu'une importation JS renvoie une promesse ? Le problème est qu'en ce moment, lorsque JS renvoie une promesse qui n'est pas une erreur. Un tel changement rétro-incompatible signifierait que wasm ne peut plus recevoir de promesse sans faire de pause, mais cela pourrait également être utile.

Une autre option que nous avons envisagée consiste à marquer les importations d'une manière ou d'une autre pour dire "cette importation doit s'arrêter si elle renvoie une promesse". Nous avons réfléchi à diverses options pour les marquer, du côté JS ou du côté wasm, mais nous n'avons rien trouvé de bien. Par exemple, si nous marquons les importations du côté JS, le module wasm ne saurait pas si un appel à une importation s'interrompt ou non jusqu'à l'étape de liaison, lorsque les importations arrivent. C'est-à-dire que les appels aux importations et à la pause seraient "mélangés". Il semble que la chose la plus simple soit simplement d'avoir une nouvelle instruction pour cela, await , qui est explicite sur l'attente. En théorie, une telle capacité peut également être utile en dehors du Web (voir les notes précédentes), donc avoir une instruction pour tout le monde peut rendre les choses plus cohérentes dans l'ensemble.

Discussions connexes précédentes

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

Merci d'avoir lu, les commentaires sont les bienvenus!

Commentaire le plus utile

J'espérais avoir plus de discussions publiquement ici, mais pour gagner du temps, j'ai contacté directement certains implémenteurs de VM, car peu se sont engagés ici jusqu'à présent. Compte tenu de leurs commentaires et de la discussion ici, je pense malheureusement que nous devrions suspendre cette proposition.

Await a un comportement observable beaucoup plus simple que les coroutines générales ou la commutation de pile, mais les personnes de VM à qui j'ai parlé sont d'accord avec @rossberg que le travail de VM à la fin serait probablement similaire pour les deux. Et au moins certaines personnes VM pensent que nous obtiendrons de toute façon des coroutines ou une commutation de pile, et que nous pouvons prendre en charge les cas d'utilisation d'attente en utilisant cela. Cela signifiera créer une nouvelle coroutine/pile à chaque appel dans le wasm (contrairement à cette proposition), mais au moins certaines personnes VM pensent que cela pourrait être fait assez rapidement.

En plus du manque d'intérêt des gens de VM, nous avons eu de fortes objections à cette proposition ici de @fgmccabe et @RossTate , comme indiqué ci-dessus. Nous ne sommes pas d'accord sur certaines choses, mais j'apprécie ces points de vue et le temps qu'il a fallu pour les expliquer.

En conclusion, dans l'ensemble, j'ai l'impression que ce serait une perte de temps pour tout le monde d'essayer d'avancer ici. Mais merci à tous ceux qui ont participé à la discussion ! Et j'espère qu'au moins cela motivera la priorisation des coroutines / commutation de pile.

Notez que la partie JS de cette proposition peut être pertinente à l'avenir, car JS sucre essentiellement pour une intégration pratique de Promise. Nous devrons attendre le changement de pile ou les coroutines et voir si cela pourrait fonctionner en plus de cela. Mais je ne pense pas que cela vaille la peine de garder le problème ouvert pour cela, alors fermez.

Tous les 96 commentaires

Excellente rédaction ! J'aime l'idée d'une suspension contrôlée par l'hôte. La proposition de @rossberg traite également des systèmes d'effets fonctionnels, et je ne suis certes pas un expert avec eux, mais à première vue, il semble que ceux-ci pourraient répondre au même besoin de flux de contrôle non local.

Concernant : "Compte tenu de ce qui précède, une implémentation naturelle consiste à copier la pile lorsque nous faisons une pause." Comment cela fonctionnerait-il pour la pile d'exécution ? J'imagine que la plupart des moteurs JIT partagent la pile d'exécution C native entre JS et wasm, donc je ne sais pas ce que signifieraient la sauvegarde et la restauration dans ce contexte. Cette proposition signifie-t-elle que la pile d'exécution wasm devrait être virtualisée d'une manière ou d'une autre ? IIUC évitant l'utilisation de la pile C comme celle-ci était assez délicat lorsque python a essayé de faire quelque chose de similaire : https://github.com/stackless-dev/stackless/wiki.

Je partage une inquiétude similaire à @ sbc100. La copie de la pile est par nature une opération assez difficile, surtout si votre machine virtuelle n'a pas déjà une implémentation GC.

@sbc100

Cette proposition signifie-t-elle que la pile d'exécution wasm devrait être virtualisée d'une manière ou d'une autre ?

Je dois laisser cela aux implémenteurs de VM car je ne suis pas un expert en la matière. Et je ne comprends pas le lien avec le python sans pile, mais peut-être que je ne sais pas ce qui est assez bien pour comprendre le lien, désolé !

Mais en général : diverses approches de coroutine fonctionnent en manipulant le pointeur de pile à un niveau bas . Ces approches peuvent être une option ici. Nous voulions souligner que même si la pile doit être copiée dans le cadre d'une telle approche, cela a une surcharge acceptable dans ce contexte.

(Nous ne savons pas si ces approches peuvent fonctionner dans les machines virtuelles wasm ou non - en espérant avoir des nouvelles des implémenteurs si oui ou non, et s'il existe de meilleures options !)

@lachlansneff

Pouvez-vous s'il vous plaît expliquer plus en détail ce que vous entendez par GC facilitant les choses ? Je ne suis pas.

Les GC @kripken ont souvent (mais pas toujours) la capacité de parcourir une pile, ce qui est nécessaire si vous devez réécrire des pointeurs sur la pile pour pointer vers la nouvelle pile. Peut-être que quelqu'un qui en sait plus sur JSC peut confirmer ou infirmer cela.

@lachlansneff

Merci, maintenant je vois ce que tu veux dire.

Nous ne suggérons pas que parcourir la pile d'une manière aussi complète (identifier chaque local jusqu'au bout, etc.) soit nécessaire pour ce faire. (Pour d'autres approches possibles, voir le lien dans mon dernier commentaire sur les méthodes d'implémentation de coroutine de bas niveau.)

Je m'excuse pour la terminologie de "copier la pile" dans la proposition - je vois que ce n'était pas assez clair, sur la base de vos commentaires et de ceux de @ sbc100 . Encore une fois, nous ne voulons pas suggérer une approche de mise en œuvre de machine virtuelle spécifique. Nous voulions juste dire que si la copie de la pile est nécessaire dans une approche, cela ne poserait pas de problème de vitesse.

Plutôt que de suggérer une approche de mise en œuvre spécifique, nous espérons entendre les gens de VM comment ils pensent que cela pourrait être fait !

Je suis très excité de voir cette proposition. Lucet utilise depuis un certain temps les opérateurs yield et resume , et nous les utilisons précisément pour interagir avec le code asynchrone exécuté dans l'environnement hôte Rust.

C'était assez simple à ajouter à Lucet, puisque notre conception s'était déjà engagée à maintenir une pile distincte pour l'exécution de Wasm, mais je pouvais imaginer que cela pourrait présenter des difficultés de mise en œuvre pour les machines virtuelles qui ne le font pas.

Cette proposition sonne bien ! Nous essayons depuis un moment de trouver un bon moyen de gérer le code asynchrone sur wasmer-js (puisque nous n'avons pas accès aux composants internes de la machine virtuelle dans un contexte de navigateur).

Plutôt que de suggérer une approche de mise en œuvre spécifique, nous espérons entendre les gens de VM comment ils pensent que cela pourrait être fait !

Je pense que l'utilisation de la stratégie de rappel pour les fonctions asynchrones pourrait être le moyen le plus simple de faire avancer les choses et également d'une manière indépendante de la langue.

Il semble .await puisse être appelé dans un JsPromise à l'intérieur d'une fonction Rust en utilisant wasm-bindgen-futures ? Comment cela peut-il fonctionner sans l'instruction await proposée ici ? Je suis désolé pour mon ignorance, je cherche des solutions pour appeler fetch inside wasm et j'apprends Asyncify, mais il semble que la solution Rust soit plus simple. Qu'est-ce qui me manque ici ? Quelqu'un peut-il m'éclaircir?

Je suis très excité par cette proposition. Le principal avantage de la proposition est sa simplicité, car nous pouvons créer des API qui se synchronisent avec le POV du wasm, et il est beaucoup plus facile de porter des applications sans avoir à penser explicitement aux rappels et async/wait. Cela nous permettrait d'apporter l'apprentissage automatique basé sur WASM et WebGPU aux machines virtuelles wasm natives à l'aide d'une seule API native et de fonctionner à la fois sur le Web et en natif.

Une chose dont je pense qu'il vaut la peine d'être discutée est la signature des fonctions qui appellent potentiellement wait. Imaginons que nous ayons la fonction suivante

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

La signature de la fonction correspondante est () => i32 . Dans le cadre de la nouvelle proposition, les appels à test pourraient soit renvoyer i32, soit a Promise<i32> . Notez qu'il est plus difficile de demander à l'utilisateur de déclarer statiquement une nouvelle signature (à cause du coût du portage du code et des appels indirects à l'intérieur de la fonction dont nous ne savons pas que les appels attendent).

Devrions-nous avoir un mode d'appel séparé dans la fonction exportée (par exemple, un appel asynchrone) pour indiquer que l'attente est autorisée pendant l'exécution ?

D'un point de vue terminologique, l'opération proposée s'apparente à une opération de rendement dans les systèmes d'exploitation. Puisqu'il cède le contrôle au système d'exploitation (dans ce cas, la machine virtuelle wasm) pour attendre que l'appel système finisse.

Si je comprends bien cette proposition, je pense que cela équivaut à peu près à supprimer la restriction selon laquelle le await dans JS ne peut être utilisé que dans les fonctions async . Autrement dit, du côté wasm, waitref pourrait être externref et plutôt qu'une instruction await , vous pourriez avoir une fonction importée $await : [externref] -> [] , et du côté JS vous pouvez fournir foo(promise) => await promise comme fonction à importer. Dans l'autre sens, si vous étiez du code JS qui voulait await sur une promesse en dehors de la fonction async , vous pourriez fournir cette promesse à un module wasm qui appelle simplement await sur l'entrée. Est-ce une compréhension correcte?

@RossTate Pas tout à fait, AIUI. Le code wasm peut await une promesse (appelez-le promise1 ), mais seule l'exécution wasm donnera, pas le JS. Le code wasm renverra une promesse différente (appelez-la promise2 ) à l'appelant JS. Lorsque promise1 est résolu, l'exécution de wasm continue. Enfin, lorsque ce code wasm se termine normalement, alors promise2 sera résolu avec le résultat de la fonction wasm.

@tqchen

Devrions-nous avoir un mode d'appel séparé dans la fonction exportée (par exemple, un appel asynchrone) pour indiquer que l'attente est autorisée pendant l'exécution ?

Intéressant - où voyez-vous l'avantage ? Comme vous l'avez dit, il n'y a vraiment aucun moyen de savoir si une exportation finira par faire un await ou non, dans des situations de portage courantes, donc au mieux, elle ne peut être utilisée que parfois. Est-ce que cela aiderait peut-être les machines virtuelles en interne ?

Avoir une déclaration explicite peut garantir que l'utilisateur énonce clairement son intention, et la VM peut générer un message d'erreur approprié si l'intention de l'utilisateur n'effectue pas un appel qui s'exécute de manière asynchrone.

À partir du POV de l'utilisateur, cela rend également l'écriture du code plus cohérente. Par exemple, l'utilisateur pourrait écrire le code suivant, même si test n'appelle pas une attente, et l'interface système renvoie Promise.resolve(test()) automatiquement.

await inst.exports_async.test();

À partir du POV de l'utilisateur, cela rend également l'écriture du code plus cohérente. Par exemple, l'utilisateur pourrait écrire le code suivant, même si test n'appelle pas une attente, et l'interface système renvoie Promise.resolve(test()) automatiquement.

@tqchen Notez que l'utilisateur peut déjà le faire comme indiqué dans l'exemple du test de proposition. Autrement dit, JavaScript prend déjà en charge et gère les valeurs synchrones et asynchrones dans un opérateur await de la même manière.

Si la suggestion est d' appliquer un seul type statique, nous pensons que cela peut être fait soit au niveau du système de caractères ou de type, soit au niveau de l'encapsuleur JavaScript sans introduire de complexité du côté principal de WebAssembly ni restreindre les implémenteurs de ces encapsuleurs.

Ah, merci pour la correction, @binji.

Dans ce cas, est-ce que ce qui suit est à peu près équivalent ? Ajoutez une fonction WebAssembly.instantiateAsync(moduleBytes, imports, "name1", "name2") à l'API JS. Supposons que moduleBytes ait un certain nombre d'importations plus une importation supplémentaire import "name1" "name2" (func (param externref)) . Ensuite, cette fonction instancie les importations avec les valeurs données par imports et instancie l'importation supplémentaire avec ce qui est conceptuellement await . Lorsque des fonctions exportées sont créées à partir de ce module, elles sont protégées de sorte que lorsque ce await est appelé, il remonte la pile pour trouver le premier garde, puis copie le contenu de la pile dans une nouvelle Promise qui est ensuite immédiatement retourné.

Est-ce que ça marcherait ? Mon sentiment est que cette proposition peut être réalisée uniquement en modifiant l'API JS sans qu'il soit nécessaire de modifier WebAssembly lui-même. Bien sûr, même dans ce cas, il ajoute encore beaucoup de fonctionnalités utiles.

@kripken Comment la fonction start serait-elle gérée ? Interdirait-il statiquement await , ou interagirait-il d'une manière ou d'une autre avec l'instanciation Wasm?

@malbarbo wasm-bindgen-futures vous permet d'exécuter du code async dans Rust. Cela signifie que vous devez écrire votre programme de manière asynchrone : vous devez marquer vos fonctions comme async , et vous devez utiliser .await . Mais cette proposition vous permet d'exécuter du code asynchrone sans utiliser async ou .await , à la place, cela ressemble à un appel de fonction synchrone normal.

En d'autres termes, vous ne pouvez pas actuellement utiliser d'API de système d'exploitation synchrones (comme std::fs ) car le Web n'a que des API asynchrones. Mais avec cette proposition, vous pourriez utiliser des API de système d'exploitation synchrones : elles utiliseraient en interne Promises, mais elles auraient l' air synchrones avec Rust.

Même si cette proposition est implémentée, wasm-bindgen-futures existera toujours et sera toujours utile, car il gère un cas d'utilisation différent (fonctions async exécutées). Et les fonctions async sont utiles car elles peuvent être facilement parallélisées.

@RossTate Il semble que votre suggestion soit assez similaire à celle couverte dans "Approches alternatives envisagées":

Une autre option que nous avons envisagée consiste à marquer les importations d'une manière ou d'une autre pour dire "cette importation doit s'arrêter si elle renvoie une promesse". Nous avons réfléchi à diverses options pour les marquer, du côté JS ou du côté wasm, mais nous n'avons rien trouvé de bien. Par exemple, si nous marquons les importations du côté JS, le module wasm ne saurait pas si un appel à une importation s'interrompt ou non jusqu'à l'étape de liaison, lorsque les importations arrivent. C'est-à-dire que les appels aux importations et à la pause seraient "mélangés". Il semble que la chose la plus simple soit simplement d'avoir une nouvelle instruction pour cela, wait, qui est explicite sur l'attente. En théorie, une telle capacité peut également être utile en dehors du Web (voir les notes précédentes), donc avoir une instruction pour tout le monde peut rendre les choses plus cohérentes dans l'ensemble.

Comment la fonction de démarrage serait-elle gérée ? Interdirait-il statiquement l'attente, ou interagirait-il d'une manière ou d'une autre avec l'instanciation Wasm ?

@Pauan Nous n'avons pas couvert cela spécifiquement, mais je pense que rien ne nous empêche d'autoriser await dans start également. Dans ce cas, la promesse renvoyée par instantiate{Streaming} serait toujours naturellement résolue/rejetée lorsque la fonction de démarrage aurait fini de s'exécuter complètement, la seule différence étant qu'elle attendrait les promesses await ed.

Cela dit, les mêmes limitations qu'aujourd'hui s'appliquent et pour l'instant, cela ne serait pas très utile pour les cas nécessitant un accès, par exemple, à la mémoire exportée.

@RReverser Comment cela fonctionnerait-il pour le new WebAssembly.Instance synchrone (qui est utilisé dans les travailleurs) ?

Point intéressant @Pauan à propos du départ !

Oui, pour l'instanciation synchrone, cela semble risqué - si await est autorisé, il est étrange que quelqu'un appelle les exportations pendant qu'elles sont en pause. Interdire await peut être le plus simple et le plus sûr. (Peut-être aussi dans le démarrage asynchrone pour la cohérence, il ne semble pas y avoir de cas d'utilisation importants qui empêcheraient ? Nécessite plus de réflexion.)

(qui est utilisé chez les travailleurs) ?

Bon point ; Je ne pense pas qu'il doive être utilisé dans Workers, mais puisque cette API existe déjà, peut-être pourrait-elle renvoyer une Promise ? J'ai vu cela comme un modèle émergent semi-populaire pour renvoyer des thenables à partir d'un constructeur de diverses bibliothèques, bien que je ne sois pas sûr que ce soit une bonne idée de le faire dans une API standard.

Je conviens que l'interdire dans start (comme dans le piégeage) est le plus sûr pour l'instant, et nous pouvons toujours changer cela à l'avenir d'une manière rétrocompatible si quelque chose change.

J'ai peut-être manqué quelque chose, mais il n'y a aucune discussion sur ce qui se passe lorsque l'exécution de WASM est interrompue avec une instruction await et une promesse renvoyée à JS, puis JS rappelle WASM sans attendre la promesse.

Est-ce un cas d'utilisation valide ? Si c'est le cas, cela pourrait permettre aux applications de "boucle principale" de recevoir des événements d'entrée sans céder manuellement au navigateur. Au lieu de cela, ils pourraient céder en attendant une promesse résolue immédiatement.

Qu'en est-il de l'annulation ? Il n'est pas implémenté dans les promesses JS et cela pose quelques problèmes.

@Kangz

J'ai peut-être manqué quelque chose, mais il n'y a aucune discussion sur ce qui se passe lorsque l'exécution de WASM est interrompue avec une instruction d'attente et une promesse renvoyée à JS, puis JS rappelle WASM sans attendre la promesse.

Est-ce un cas d'utilisation valide ? Si c'est le cas, cela pourrait permettre aux applications de "boucle principale" de recevoir des événements d'entrée sans céder manuellement au navigateur. Au lieu de cela, ils pourraient céder en attendant une promesse résolue immédiatement.

Le texte actuel n'est peut-être pas assez clair à ce sujet. Pour le premier paragraphe, oui, c'est autorisé, voir la section "Clarifications" : 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.

Pour le deuxième paragraphe, non - vous ne pouvez pas obtenir d'événements plus tôt, et vous ne pouvez pas faire en sorte que JS résolve une promesse plus tôt qu'il ne le ferait. Essayons de résumer les choses autrement :

  • Lorsque wasm s'arrête sur la promesse A, il revient à ce qui l'a appelé et renvoie une nouvelle promesse B.
  • Wasm reprend lorsque la promesse A se résout. Cela se produit à l'heure normale , ce qui signifie que tout est normal dans la boucle d'événements JS.
  • Après la reprise de wasm et la fin de son exécution, la promesse B n'est résolue qu'à ce moment-là.

Donc, en particulier, la promesse B doit être résolue après la promesse A. Vous ne pouvez pas obtenir le résultat de la promesse A avant que JS ne puisse l'obtenir.

En d'autres termes : le comportement de cette proposition peut être rempli par Asyncify + certains JS qui utilisent Promises autour d'elle.

@RReverser , je ne pense pas que ce soit la même chose, mais je pense d'abord que nous devons clarifier quelque chose (si cela n'a pas déjà été clarifié, auquel cas je suis désolé de l'avoir manqué).

Il peut y avoir plusieurs appels de JS dans la même instance wasm sur la même pile en même temps. Si await est exécuté par l'instance, quel appel est mis en pause et renvoie une promesse ?

Pour le deuxième paragraphe, non - vous ne pouvez pas obtenir d'événements plus tôt, et vous ne pouvez pas faire en sorte que JS résolve une promesse plus tôt qu'il ne le ferait.

Désolé, je pense que ma question n'était pas claire. Pour le moment, les applications de "boucle principale" en C++ utilisent emscripten_set_main_loop afin qu'entre chaque exécution de la fonction frame, le contrôle soit rendu au navigateur et que les entrées ou autres événements puissent être traités.

Avec cette proposition, il semble que ce qui suit devrait fonctionner pour traduire les applications "en boucle principale". (bien que je ne connaisse pas bien la boucle d'événements JS)

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

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

@Kangz Cela devrait fonctionner, oui (sauf que vous avez un petit problème avec l'ordre des arguments dans votre code setTimeout et cela pourrait être simplifié):

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

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

Il peut y avoir plusieurs appels de JS dans la même instance wasm sur la même pile en même temps. Si await est exécuté par l'instance, quel appel est mis en pause et renvoie une promesse ?

Le plus intime. C'est le travail du wrapper JS de coordonner le reste s'il le souhaite.

@Kangz Désolé, je vous ai mal compris avant. Oui, comme @RReverser l' a dit, cela devrait fonctionner, et c'est un bon exemple de cas d'utilisation prévu ici !

Comme vous l'avez dit, il est polyfillable avec Asyncify, et en fait c'est équivalent au même code avec Asyncify aujourd'hui en remplaçant le __builtin_await par un appel à emscripten_sleep(0) (qui fait un setTimeout(0) ) .

Merci, @RReverser , pour la clarification. Je pense qu'il serait utile de reformuler la description pour dire que l'appel (le plus récent) dans l'instance s'interrompt, plutôt que l'instance elle-même.

Dans ce cas, cela semble presque équivalent à ajouter les deux fonctions primitives suivantes à JS : promise-on-await(f) et await-for-promise(p) . Le premier appelle f() mais, si pendant l'exécution de f() un appel est fait à await-for-promise(p) , retourne à la place une nouvelle Promise qui reprend l'exécution après la résolution p et se résout lui-même une fois cette exécution terminée (ou appelle à nouveau await-for-promise ). Si un appel à await-for-promise est effectué dans le contexte de plusieurs promise-on-await s, alors le plus récent renvoie une Promise. Si un appel à await-for-promise est effectué en dehors de tout promise-on-await , alors quelque chose de grave se produit (comme si le code start d'une instance exécute await ).

Cela a-t-il du sens?

@RossTate C'est assez proche, oui, et capture l'idée générale. (Mais comme vous l'avez dit, seulement presque équivalent, car il ne pouvait pas être utilisé pour remplir ceci, et il manque la gestion spécifique des limites wasm/JS.)

Merci pour la suggestion de reformuler ce texte. Je garde une liste de ces notes de la discussion ici. (Je ne sais pas si cela vaut la peine de les appliquer au premier message, car cela semble moins déroutant de ne pas le changer au fil du temps ?)

@RossTate Intéressant... J'aime ça ! Il rend explicite la nature asynchrone de l'appel ( promise-on-await est requis pour tout appel potentiellement asynchrone) et ne nécessite aucune modification de Wasm. Cela a également (un peu) de sens si vous supprimez Wasm du milieu - si promise-on-await appelle await-for-promise directement, alors il renvoie un Promise .

@kripken pouvez-vous expliquer plus en détail pourquoi cela serait différent ? Je ne comprends pas très bien pourquoi la limite Wasm/JS est importante ici.

@binji, je voulais juste dire que de telles fonctions dans JS ne laisseraient pas wasm faire quelque chose de similaire. Les appeler comme des importations de wasm ne fonctionnerait pas. Nous avons encore besoin d'un moyen de faire sortir le wasm vers la frontière, etc. de manière récapitulative, n'est-ce pas ?

@kripken à droite, je suppose qu'à ce stade, l'importation await-for-promise devrait fonctionner comme un Wasm intrinsèque.

Ma pensée était qu'au lieu d'ajouter une instruction await à wasm, un tel module importerait à la place await-for-promise et l'appellerait. De même, au lieu de modifier les fonctions exportées, le code JS les appellerait dans un promise-on-await . Cela signifie que les primitives JS géreraient tout le travail de la pile, y compris la pile WebAssembly. Ce serait également plus flexible, par exemple, si vous le souhaitez, vous pouvez donner au module un rappel JS qui peut ensuite rappeler dans le module et faire mettre en pause l'appel externe au lieu de la clause interne --- tout dépend si le code JS choisit pour encapsuler l'appel dans promise-on-await ou non. Je ne pense pas que vous ayez besoin de changer quoi que ce soit à wasm lui-même.

Je serais intéressé d'entendre ce que @syg pense de ces primitives JS potentielles.

Oh ok, désolé - j'ai pris votre commentaire @RossTate pour être "pour m'assurer que je comprends, laissez-moi le reformuler comme ça, et dites-moi si cela a la bonne forme", et non une suggestion concrète.

En y réfléchissant, votre idée veut mettre en pause non seulement les cadres JS mais aussi wasm, mais il existe également des cadres hôte/navigateur. (La proposition actuelle évite cela en travaillant uniquement sur wasm jusqu'à la frontière où il a été appelé.) Voici un exemple :

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

Si forEach est implémenté dans le code du navigateur, cela signifie mettre en pause les cadres du navigateur. Il est également significatif que s'arrêter au milieu d'une telle boucle, et reprendre plus tard, serait un nouveau pouvoir que JS peut faire, et votre idée le permettrait également pour une boucle normale :

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

Et tout cela peut avoir de curieuses interactions de spécifications avec les fonctions async JS. Tout cela semble être de grandes discussions à avoir avec les navigateurs et les personnes JS.

Mais aussi, cela évite seulement d'ajouter await et waitref dans la spécification wasm de base, mais ce sont de minuscules ajouts - car ils ne font rien dans la spécification de base. La proposition actuelle a déjà 99 % de la complexité du côté JS. Et IIUC, votre proposition échange ce petit ajout à la spécification wasm avec des ajouts beaucoup plus importants du côté JS - cela rend donc la plate-forme Web dans son ensemble plus complexe, et inutilement puisque tout cela est pour wasm. De plus, il y a en fait un avantage à définir await dans la spécification wasm de base, car cela peut être utile en dehors du Web.

J'ai peut-être oublié quelque chose dans votre suggestion, excusez-moi si c'est le cas. Dans l'ensemble, je suis curieux de savoir quelle est votre motivation pour essayer d'éviter un ajout à la spécification wasm de base ?

Je ne pense pas que ces primitives aient beaucoup de sens pour js, et je pense que plus d'implémentations wasm que celles des navigateurs peuvent en bénéficier. Je suis toujours curieux de savoir pourquoi les exceptions pouvant être reprises (en gros les effets) ne rempliraient pas ce cas d'utilisation.

Mon commentaire était une combinaison des deux. À un niveau élevé, j'essaie de comprendre s'il existe un moyen de reformuler la proposition comme un simple enrichissement de l'API JS (et de la même manière, comment d'autres hôtes interagiraient avec les modules wasm). L'exercice permet d'évaluer si wasm doit vraiment être changé et aide à déterminer si réellement la proposition ajoute secrètement de nouvelles primitives au JS que les gens du JS peuvent ou non approuver. Autrement dit, s'il n'est pas possible de le faire avec un await : func (param externref) (result externref) importé, il est fort probable que cela ajoute de nouvelles fonctionnalités à JS.

En ce qui concerne la simplicité des modifications apportées à wasm, il y a encore beaucoup de choses à considérer comme quoi faire à propos des appels de module à module, quoi faire lorsque les fonctions exportées renvoient des valeurs GC contenant des pointeurs vers des fonctions pouvant exécuter await après la fin de l'appel, et ainsi de suite.

Pour en revenir à l'exercice, comme vous l'avez souligné, il existe de bonnes raisons de ne capturer que la pile wasm. Cela me ramène à ma suggestion précédente, bien que légèrement révisée avec une nouvelle perspective. Ajoutez une fonction WebAssembly.instantiateAsync(moduleBytes, imports, "name1", "name2") à l'API JS. Supposons que moduleBytes ait un certain nombre d'importations plus une importation supplémentaire import "name1" "name2" (func (param externref) (result externref)) . Ensuite, instantiateAsync instancie les autres importations de moduleBytes simplement avec les valeurs données par imports et instancie l'importation supplémentaire avec ce qui est conceptuellement await-for-promise . Lorsque des fonctions exportées sont créées à partir de cette instance, elles sont protégées (conceptuellement par promise-on-await ) de sorte que lorsque ce await-for-promise est appelé, il remonte la pile pour trouver le premier garde, puis copie le contenu de la pile dans une nouvelle promesse qui est ensuite immédiatement renvoyée. Nous avons maintenant les mêmes primitives que celles que j'ai mentionnées ci-dessus, mais elles ne sont plus de première classe, et ce modèle restreint garantit que seule la pile wasm sera jamais capturée. Dans le même temps, WebAssembly n'a pas besoin d'être modifié pour prendre en charge le modèle.

Les pensées?

@devsnek

Je suis toujours curieux de savoir pourquoi les exceptions pouvant être reprises (en gros les effets) ne rempliraient pas ce cas d'utilisation.

Ils sont une option dans cet espace, bien sûr.

Ma compréhension de la dernière présentation de @rossberg est qu'il voulait initialement suivre cette voie, mais a ensuite changé de direction pour adopter une approche coroutine. Voir la diapositive avec le titre "Problèmes". Après cette diapositive, les coroutines sont décrites, qui sont une autre option dans cet espace. Alors peut-être que votre question s'adresse davantage à @rossberg qui peut peut-être clarifier ?

Cette proposition se concentre sur la résolution du problème de synchronisation/asynchrone qui ne nécessite pas autant de puissance que les exceptions ou les coroutines pouvant être reprises. Ceux-ci se concentrent sur les interactions internes à l'intérieur d'un module wasm, tandis que nous nous concentrons sur l'interaction entre un module wasm et l'extérieur (car c'est là que se produit le problème de synchronisation/asynchrone). C'est pourquoi nous n'avons besoin que d'une seule nouvelle instruction dans la spécification wasm de base, et presque toute la logique de cette proposition se trouve dans la spécification wasm JS. Et cela signifie que vous pouvez attendre une promesse comme celle-ci :

call $get_promise
await
;; use it!

Cette simplicité dans le wasm est utile pour elle-même, mais signifie également qu'il est très clair pour la machine virtuelle ce qui se passe, ce qui peut également avoir des avantages.

@RossTate

C'est-à-dire que s'il n'est pas possible de faire avec un await : func (param externref) (result externref) importé, il est fort probable que cela ajoute de nouvelles fonctionnalités à JS.

Je ne suis pas cette inférence, désolé. Mais ça me semble détourné. Si vous pensez que cette proposition ajoute de nouvelles fonctionnalités à JS, pourquoi ne pas le montrer directement ? (Je crois fermement que ce n'est pas le cas, mais je suis curieux de savoir si nous avons fait une erreur !)

Quant à la simplicité des changements apportés à wasm, il y a encore beaucoup de choses à considérer comme quoi faire à propos des appels de module à module

La spécification wasm de base dit-elle quelque chose sur les appels de module à module? Je ne me souviens pas qu'il l'ait fait, et en parcourant les sections pertinentes maintenant, je ne le vois pas. Mais peut-être ai-je raté quelque chose ?

Ma conviction est que les ajouts de base aux spécifications wasm seraient essentiellement de lister await , disons qu'il est destiné à "attendre quelque chose", et c'est tout. C'est pourquoi j'ai écrit That's it for the core wasm spec! dans la proposition. Si je me trompe, montrez-moi s'il vous plaît dans la spécification wasm de base où nous aurions besoin d'en ajouter plus.

Supposons et disons qu'un jour la spécification wasm principale aura une nouvelle instruction pour créer un module wasm et appeler une méthode dessus. Dans ce cas, j'imagine que nous dirions await juste des pièges parce que le but est d'attendre quelque chose à l'extérieur, sur l'hôte.

Cela me ramène à ma suggestion précédente, bien que légèrement révisée avec une nouvelle perspective [nouvelle idée]

Cette idée n'est-elle pas fonctionnellement la même que le deuxième paragraphe de Alternative approaches considered dans la proposition ? Une telle chose peut être faite, mais nous avons expliqué pourquoi nous pensons que c'est moins bon.

@kripken l'a compris. pour être clair, je pense que await résout les cas d'utilisation présentés de manière très pratique et élégante. J'espère aussi que nous pourrons peut-être utiliser cet élan pour résoudre d'autres cas d'utilisation en élargissant un peu la conception.

Je pense que la suggestion de @RossTate ressemble en effet beaucoup à ce qui est mentionné dans "Approches alternatives envisagées". Je pense donc que nous devrions discuter plus en détail de la raison pour laquelle cette approche a été rejetée. Je pense que nous pouvons tous convenir qu'une solution qui n'implique aucun changement de spécification wasm serait préférable, si nous pouvons rendre le côté JS fonctionnel. J'essaie de comprendre les inconvénients que vous exposez dans cette section et pourquoi ils rendent la solution JS uniquement si inacceptable.

Je pense que nous pouvons tous convenir qu'une solution qui n'impliquait aucun changement de spécification wasm serait préférable

Non! Voir les cas d'utilisation non Web abordés ici. Sans await dans la spécification wasm, nous nous retrouverions avec chaque plate-forme faisant quelque chose ad hoc : l'environnement JS fait une chose d'importation, d'autres endroits créent de nouvelles API marquées "synchrones", etc. L'écosystème wasm serait serait moins cohérent, il serait plus difficile de déplacer un wasm du Web vers d'autres endroits, etc.

Mais oui, nous devrions rendre la partie principale de la spécification wasm aussi simple que possible. Je pense que ça fait ça ? 99% de la logique est du côté JS (mais @RossTate semble être en désaccord, et nous essayons toujours de comprendre cela - j'ai posé des questions concrètes dans ma dernière réponse qui, je l'espère, feront avancer les choses).

Ma conviction est que les ajouts de base aux spécifications wasm seraient essentiellement de lister await , disons qu'il est destiné à "attendre quelque chose", et c'est tout.

À moins que cette sémantique ne puisse être formalisée plus précisément, cela introduit une ambiguïté ou un comportement défini par l'implémentation dans la spécification. Nous avons jusqu'à présent évité cela (à un coût important dans le cas de SIMD), donc c'est certainement quelque chose que j'aimerais voir épinglé. Je ne pense pas que la proposition elle-même doive changer pour rendre cela plus formel, mais "attendre quelque chose" devrait être reformulé dans la terminologie précise déjà utilisée par la spécification.

La spécification wasm de base dit-elle quelque chose sur les appels de module à module?

Les importations d'une instance peuvent être instanciées avec les exportations d'une autre instance. D'après ce que je comprends de l'API JS (et du principe de compositionnalité de wasm), un appel à une telle importation est conceptuellement un appel direct à la fonction exportée par l'autre instance. Il en va de même pour les appels (indirects) sur des valeurs fonctionnelles telles que funcref qui sont transmises entre les deux instances.

Supposons et disons qu'un jour la spécification wasm principale aura une nouvelle instruction pour créer un module wasm et appeler une méthode dessus. Dans ce cas, j'imagine que nous dirions attendre juste des pièges parce que le but est d'attendre quelque chose à l'extérieur, sur l'hôte.

Sur la base du principe de composition du module discuté lors de la réunion en personne, il ne devrait pas y avoir de piège. Cela devrait être comme s'il n'y avait qu'une seule instance de module (composée) et qu'elle s'exécutait await . Autrement dit, await emballerait la pile jusqu'au cadre de pile JS le plus récent.

Notez que cela implique que si f était la valeur d'une fonction unaire exportée d'une instance de wasm, alors l'objet instanciation-parameters {"some" : {"import" : f}} serait sémantiquement différent de {"some" : {"import" : (x) => f(x)}} car les appels au premier restera dans la pile wasm tandis que les appels au second entreront dans la pile JS, même à peine. Jusqu'à présent, ces objets de paramètre d'instanciation seraient considérés comme équivalents. Je peux expliquer pourquoi c'est utile du point de vue de la migration du code/de l'interopérabilité du langage, mais ce serait une digression pour le moment.

Cette idée n'est-elle pas fonctionnellement la même que le deuxième paragraphe des approches alternatives envisagées dans la proposition ? Une telle chose peut être faite, mais nous avons expliqué pourquoi nous pensons que c'est moins bon.

Désolé, j'ai lu cette alternative comme signifiant quelque chose de différent, mais cela n'a plus d'importance maintenant, sauf pour expliquer ma confusion. Il semble que vous vouliez dire la même chose que ma suggestion, auquel cas cela vaut la peine de discuter des avantages et des inconvénients.

Le fait que cette proposition soit si légère du côté wasm est dû au fait que l'instruction await semble être sémantiquement identique à un appel à une fonction importée. Bien sûr, les conventions comptent, comme vous le soulignez ! Mais await n'est pas la seule fonctionnalité pour laquelle cela est valable ; il en va de même pour la plupart des fonctions importées. Dans le cas de await , j'ai l'impression que le problème de la convention pourrait être résolu en ayant des modules avec cette fonctionnalité ayant une clause import "control" "await" (func (param externref) (result externref)) , et d'avoir des environnements qui prennent en charge cette fonctionnalité instancient toujours cette importation avec le rappel approprié.

Cela semble donner une solution qui économise une tonne de travail en ne changeant pas de wasm tout en offrant la portabilité multiplateforme que vous recherchez. Mais je travaille toujours pour comprendre les nuances de la proposition, et j'en ai déjà raté un tas jusqu'à présent !

Le fait que cette proposition soit si légère du côté wasm est dû au fait que l'instruction await semble être sémantiquement identique à un appel à une fonction importée.

FWIW c'est là que cette proposition a commencé à l'origine, mais l'utilisation d'intrinsèques comme celle-ci semble plus opaque pour les machines virtuelles et généralement découragée (je pense que @binji a suggéré de s'en éloigner dans les discussions originales).

Par exemple, suivant votre argument, quelque chose comme memory.grow ou atomic.wait pourrait également être fait comme import "control" "memory_grow" ou import "control" "atomic_wait" en conséquence, mais ils ne sont pas comme ils le font 't fournir le même niveau d'opportunités d'interopérabilité et d'analyse statique (à la fois du côté de la machine virtuelle et de l'outillage) que l'instruction réelle.

Vous pourriez dire que memory.grow en tant qu'instruction est toujours utile dans les cas où la mémoire n'est pas exportée, mais que atomic.wait pourrait certainement être implémentée en dehors du noyau. En fait, c'est très similaire à await , sauf pour le niveau auquel la pause/reprise se produit et pour le fait que await en tant que fonction nécessiterait bien plus de magie que atomic.wait car il doit pouvoir interagir avec la pile de machines virtuelles et ne pas simplement bloquer le thread actuel jusqu'à ce qu'une valeur change.

@tlively

"attendre quelque chose" devrait être reformulé dans la terminologie précise déjà utilisée par la spécification.

Définitivement oui. Je peux suggérer un texte plus spécifique maintenant si cela peut être utile :

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.

Au fait, une autre comparaison qui m'est venue en écrivant ceci concerne les conseils d'alignement sur les charges et les magasins. Wasm prend en charge les charges et les magasins non alignés, de sorte que les conseils ne peuvent pas conduire à un comportement différent observable par le module wasm (même si le conseil est faux), mais pour l'hôte, ils suggèrent une implémentation très différente sur certaines plates-formes (ce qui peut être plus efficace). C'est donc un exemple d'instructions différentes sans sémantique différente observable en interne, comme le dit la spécification : The alignment in load and store instructions does not affect the semantics .

@RossTate

Sur la base du principe de composition du module discuté lors de la réunion en personne, il ne devrait pas y avoir de piège. Cela devrait être comme s'il n'y avait qu'une seule instance de module (composée) et qu'elle s'exécutait en attente. Autrement dit, await emballerait la pile jusqu'au cadre de pile JS le plus récent.

Ça sonne bien, et bon à savoir, merci, j'ai raté cette partie.

Je pense que cela m'explique une partie de notre incompréhension. Module => les appels de module ne sont pas dans la spécification wasm atm, ce qui était mon point plus tôt. Mais on dirait que vous pensez à une future spécification où ils pourraient être. Dans tous les cas, cela ne semble pas être un problème ici, car la composition détermine exactement comment un await doit se comporter dans cette situation (ce qui n'est pas ce que j'ai suggéré plus tôt ! mais cela a plus de sens).

La spécification wasm de base dit-elle quelque chose sur les appels de module à module? Je ne me souviens pas qu'il l'ait fait, et en parcourant les sections pertinentes maintenant, je ne le vois pas. Mais peut-être ai-je raté quelque chose ?

Oui, la spécification wasm principale fait la distinction entre les fonctions qui ont été importées d'autres modules wasm et les fonctions hôtes (§ 4.2.6). La sémantique de l'invocation de fonction (§ 4.4.7) ne dépend pas du module qui a défini la fonction, et en particulier les appels de fonction intermodules sont actuellement spécifiés pour se comporter de manière identique aux appels de fonction de même module.

Si await s sous les appels inter-modules sont définis pour intercepter, cela nécessiterait alors de spécifier une traversée vers le haut de la pile d'appels pour vérifier si un appel inter-module existe avant la dernière trame factice créée par une invocation de l'hôte (§ 4.5.5). Ce serait une complication malheureuse dans la spécification. Mais je suis d'accord avec Ross que le fait d'avoir un piège d'appels intermodules serait une violation de la compositionnalité, donc je préférerais la sémantique où la pile entière est gelée jusqu'à la dernière invocation de l'hôte. La façon la plus simple de spécifier cela serait de rendre await similaire à une invocation de fonction hôte (§ 4.4.7.3), comme vous le dites, @kripken. Mais les invocations de fonction hôte sont complètement non déterministes, donc un meilleur nom pour l'instruction du point de vue de la spécification de base pourrait être undefined . Et à ce stade, je commence en fait à préférer une importation intrinsèque qui sera toujours fournie par la plate-forme Web (et WASI pour la portabilité) car la spécification de base, à elle seule, ne bénéficie pas d'une instruction undefined IMO.

Sémantiquement, un appel à l'environnement hôte qui renvoie un waitref plus un await n'est qu'un appel bloquant, n'est-ce pas ?

Quelle valeur cela apporte-t-il aux incorporations non Web qui n'ont pas d'environnement asynchrone comme le fait un navigateur et qui peuvent nativement prendre en charge le blocage des appels ?

@RReverser , je vois le point que vous faites sur les intrinsèques. Il y a un jugement à prendre pour décider quand une opération doit être définie par des fonctions non interprétées par rapport à des instructions. Je pense qu'un facteur dans ce jugement est de considérer comment il interagit avec d'autres instructions. memory.grow affecte le comportement des autres instructions de mémoire. Je n'ai pas eu l'occasion de parcourir la proposition Threads, mais j'imagine que atomic.wait affecte ou est affecté par le comportement d'autres instructions de synchronisation. La spec doit alors être mise à jour pour formaliser ces interactions.

Mais avec await tout seul, il n'y a aucune interaction avec d'autres instructions. Les seules interactions sont avec l'hôte, c'est pourquoi mon intuition serait que cette proposition devrait être faite via des fonctions d'hôte importées.

Je pense qu'une grande différence entre atomic.wait et ce await proposé est que le module ne peut pas être ré-entré avec atomic.wait . L'agent est suspendu dans son intégralité.

@kripken :

Ma compréhension de la dernière présentation de @rossberg est qu'il voulait initialement suivre cette voie, mais a ensuite changé de direction pour adopter une approche coroutine. Voir la diapositive avec le titre "Problèmes". Après cette diapositive, les coroutines sont décrites, qui sont une autre option dans cet espace. Alors peut-être que votre question s'adresse davantage à @rossberg qui peut peut-être clarifier ?

Oui, donc la factorisation coroutine-ish peut être considérée comme une généralisation de la précédente conception d'exceptions pouvant être reprises. Il a toujours la même notion d'événements/exceptions pouvant être repris, mais l'instruction try est décomposée en primitives plus petites, ce qui rend la sémantique plus simple et le modèle de coût plus explicite. C'est aussi un peu plus expressif.

L'intention est toujours que cela puisse exprimer toutes les abstractions de contrôle pertinentes, et async est l'un des cas d'utilisation motivants. Pour interagir avec JS async, l'API JS pourrait vraisemblablement fournir un événement await prédéfini (portant une promesse JS en tant qu'externref) qu'un module Wasm pourrait importer et throw à suspendre. Bien sûr, il y a beaucoup de détails qui devraient être étoffés, mais en principe cela devrait être possible.

Quant à la proposition actuelle, j'essaie toujours de comprendre. :)

En particulier, il semble autoriser await dans n'importe quelle ancienne fonction Wasm, ai-je bien compris ? Si c'est le cas, c'est très différent de JS, qui n'autorise await que dans les fonctions asynchrones. Et c'est une contrainte très centrale, car elle permet aux moteurs de compiler await par transformation _locale_ d'une seule fonction (asynchrone) !

Sans cette contrainte, les moteurs auraient soit besoin d'effectuer une transformation de programme _globale_ (comme le fait supposément Asyncify), où chaque appel deviendrait potentiellement beaucoup plus cher (vous ne pouvez généralement pas savoir si un appel peut atteindre une attente). Ou, de manière équivalente, les moteurs devraient pouvoir créer plusieurs piles et basculer entre elles !

Maintenant, c'est exactement la fonctionnalité que l'idée de coroutine / gestionnaires d'effets essaie d'introduire dans Wasm. Mais évidemment, il s'agit d'un ajout hautement non trivial à la plate-forme et à son modèle d'exécution, une complication que JS a pris soin d'éviter pour ses abstractions de contrôle (telles que async et les générateurs).

@rossberg

En particulier, il semble autoriser l'attente dans n'importe quelle ancienne fonction Wasm, est-ce que je lis correctement? Si c'est le cas, c'est très différent de JS, qui n'autorise l'attente que dans les fonctions asynchrones.

Oui, le modèle ici est très différent. JS await est par fonction, alors que cette proposition fait une attente d'une instance wasm entière (car le but est de résoudre l'incompatibilité sync/async entre JS et wasm, qui est entre JS et wasm). JS await est également destiné au code manuscrit, tandis que cela permet le portage du code compilé.

Et c'est une contrainte très centrale, car elle permet aux moteurs de compiler l'attente par transformation locale d'une seule fonction (asynchrone) ! Sans cette contrainte, les moteurs auraient soit besoin d'effectuer une transformation globale du programme (comme le fait soi-disant Asyncify), où chaque appel deviendrait potentiellement beaucoup plus cher (vous ne pouvez généralement pas savoir si un appel peut atteindre une attente). Ou, de manière équivalente, les moteurs devraient pouvoir créer plusieurs piles et basculer entre elles !

Une transformation globale du programme n'est certainement pas prévue ici ! Désolé si ce n'était pas clair.

Comme mentionné dans la proposition, la commutation entre les piles est une option d'implémentation possible, mais notez que ce n'est pas la même chose que la commutation de pile de style coroutine :

  • Seule l'instance wasm entière peut s'arrêter. Ce n'est pas pour la commutation de pile à l'intérieur du module. (En particulier, c'est pourquoi cette proposition ne pourrait pas avoir d'ajouts à la spécification wasm de base et être entièrement du côté wasm JS ; jusqu'à présent, certaines personnes préfèrent cela, et je pense que l'une ou l'autre peut fonctionner.)
  • Les coroutines déclarent explicitement les piles, ce n'est pas le cas de await.
  • les piles d'attente ne peuvent être reprises qu'une seule fois, il n'y a pas de bifurcation/retour plus d'une fois (vous ne savez pas si vous l'aurez dans votre proposition ou non ?).
  • Le modèle de performance est très différent ici. await va attendre une promesse dans JS, qui a déjà une surcharge et une latence minimales. Donc, ce n'est pas grave si l'implémentation a des frais généraux lorsque nous faisons une pause, et nous nous en soucions moins que les coroutines ne le feraient probablement.

Compte tenu de ces facteurs, et du fait que le comportement observable de cette proposition est qu'une instance entière de wasm s'interrompt, il peut y avoir différentes façons de l'implémenter. Par exemple, hors du Web, dans une machine virtuelle exécutant une seule instance de wasm, elle pourrait littéralement simplement exécuter sa boucle d'événements jusqu'à ce qu'il soit temps de reprendre le wasm. Sur le Web, une approche de mise en œuvre pourrait être : lorsqu'une attente se produit, copiez l'intégralité de la pile wasm, de la position actuelle à l'endroit où nous avons appelé le wasm ; gardez cela sur le côté; pour reprendre, recopiez-le et continuez à partir de là. Il peut également y avoir d'autres approches ou variantes de celles-ci (certaines peut-être sans copier, mais encore une fois, éviter la surcharge de copie n'est pas réellement crucial ici !).

Désolé pour le long message et quelques répétitions du texte de la proposition lui-même, mais j'espère que cela aide à clarifier certains des points auxquels vous avez fait référence ?

Je pense qu'il y a beaucoup à discuter ici en termes de mise en œuvre. Jusqu'ici le commentaire d' @acfoltzer sur Lucet est encourageant !

Juste pour clarifier certaines phrases dans le commentaire le plus récent de @kripken , ce n'est pas toute l'instance de wasm qui s'arrête. C'est juste l'appel le plus récent d'un cadre hôte dans wasm sur la pile qui est mis en pause, puis le cadre hôte reçoit à la place une promesse correspondante (ou l'analogue approprié pour l'hôte). Voir ici pour la clarification précédente pertinente.

Hum, je ne vois pas en quoi cela fait une différence. Lorsque vous attendez quelque part au plus profond de Wasm, vous devrez capturer toute la pile d'appels depuis au moins l'entrée de l'hôte jusqu'à ce point. Et vous pouvez garder cette suspension (c'est-à-dire ce segment de pile) en vie aussi longtemps que vous le souhaitez, tout en effectuant d'autres appels d'en haut ou en créant plus de suspensions. Et vous pouvez reprendre d'ailleurs (je pense ?). Cela ne nécessite-t-il pas toute la machinerie de mise en œuvre de suites délimitées ? Seulement que l'invite est définie lors de l'entrée Wasm au lieu d'une construction distincte.

@rossberg

Cela peut être vrai sur certaines machines virtuelles, oui. Si l'attente et les coroutines finissent par nécessiter exactement le même travail de machine virtuelle, alors au moins aucun travail supplémentaire n'est nécessaire. Dans ce cas, l'avantage de la proposition await serait l'intégration JS pratique.

Je pense que vous pouvez obtenir une intégration JS pratique sans transformation complète du programme si vous n'autorisez pas la rentrée du module.

Je pense que vous pouvez obtenir une intégration JS pratique sans transformation complète du programme si vous n'autorisez pas la rentrée du module.

Cela semble être un moyen plus simple de le faire, mais cela nécessiterait de bloquer tout module visité dans la pile des appels (ou, dans un premier temps, tous les modules WebAssembly).

Cela semble être un moyen plus simple de le faire, mais cela nécessiterait de bloquer tout module visité dans la pile des appels (ou, dans un premier temps, tous les modules WebAssembly).

Correct, tout comme atomic.wait .

@taralx

Je pense que vous pouvez obtenir une intégration JS pratique sans transformation complète du programme si vous n'autorisez pas la rentrée du module.

D'une part, la rentrée peut être utile, par exemple, un moteur de jeu peut télécharger un fichier et ne pas vouloir que l'interface utilisateur soit complètement interrompue pendant cette opération (Asyncify le permet aujourd'hui). Mais d'un autre côté, peut-être que la rentrée pourrait être interdite, mais une application pourrait créer plusieurs instances du même module pour cela (toutes important la même mémoire, des variables globales mutables, etc. ?), donc une rentrée serait un appel à une autre instance. Je pense que nous pouvons faire en sorte que cela fonctionne dans les chaînes d'outils (il y aurait une limite effective sur le nombre de rentrées actives à la fois - égale au nombre d'instances - ce qui semble bien).

Donc, si votre simplification aide les machines virtuelles, cela vaut vraiment la peine d'être considéré !

(Notez cependant que, comme indiqué précédemment, je ne pense pas que nous ayons besoin d'une transformation complète du programme ici avec l'une des options discutées. Vous n'en avez besoin que si vous êtes dans la mauvaise situation où se trouve Asyncify, où c'est tout ce que vous pouvez faire au niveau de la chaîne d'outils. Pour await, dans le pire des cas, comme discuté avec @rossberg , vous pouvez faire ce que la proposition de coroutines ferait en interne. Mais votre idée est potentiellement très intéressante si elle rend les choses plus simples que cela !)

D'une part, la rentrée peut être utile, par exemple, un moteur de jeu peut télécharger un fichier et ne pas vouloir que l'interface utilisateur soit complètement interrompue pendant cette opération (Asyncify le permet aujourd'hui).

Je ne suis pas sûr que ce soit une fonctionnalité sonore. Il me semble que cela introduirait cependant une _concurrence inattendue_ dans l'application. Une application native qui charge des actifs lors du rendu utiliserait 2 threads en interne, et chaque thread serait mappé à un WebWorker + SharedArrayBuffer. Si une application utilise des threads, elle peut également utiliser des primitives Web synchrones de WebWorkers (comme elles sont autorisées, du moins dans certains cas). Sinon, il est toujours possible de mapper des opérations asynchrones dans le thread principal sur des opérations de blocage dans un travailleur utilisant Atomics.wait (par exemple).

Je me demande si tout le cas d'utilisation n'est pas déjà résolu par le multithreading en général. En utilisant des primitives de blocage dans un travailleur, toute la pile (JS/Wasm/natif du navigateur) est préservée, ce qui semble être beaucoup plus simple et robuste.

En utilisant des primitives de blocage dans un travailleur, toute la pile (JS/Wasm/natif du navigateur) est préservée, ce qui semble être beaucoup plus simple et robuste.

C'est en fait une autre implémentation alternative du wrapper Asyncify JS autonome que j'ai expérimenté, mais, bien qu'il résolve le problème de taille de code, la surcharge de performances était encore beaucoup plus élevée que l'actuel Asyncify qui utilise la transformation Wasm.

@alexp-sssup

Il me semble que cela introduirait cependant une simultanéité inattendue dans l'application.

Certainement, oui - cela doit être fait très soigneusement et peut casser des choses. Nous avons une expérience mitigée avec cela en utilisant Asyncify, bon et mauvais (pour un exemple de cas d'utilisation valide : un fichier est téléchargé dans JS, et JS appelle wasm pour allouer de l'espace dans lequel le copier, avant de reprendre). Mais dans tous les cas, la rentrée n'est pas un élément crucial de cette proposition de toute façon.

Pour ajouter à ce que @RReverser a dit, un autre problème avec les threads est que leur prise en charge n'est pas et ne sera pas universelle. Mais wait pourrait être partout.

Dans d'autres langages où async/wait ont été introduits, la rentrée est absolument essentielle. C'est en quelque sorte le fait que d'autres événements peuvent se produire pendant que l'on (a) attend. Il me semble que la rentrée est assez importante.

De plus, n'est-il pas vrai que chaque fois qu'un module fait un appel à une fonction externe, il doit supposer qu'il pourrait être réintroduit via l'une de ses exportations (dans l'exemple ci-dessus, même sans attente, tout appel à et externe la fonction est libre (sans jeu de mots) pour appeler malloc).

une application pourrait créer plusieurs instances du même module pour cela (toutes important la même mémoire, variables globales mutables, etc. ?), donc une rentrée serait un appel à une autre instance

Uniquement pour les mémoires partagées du module. Les autres mémoires doivent être réinstanciées, ce qui est important pour éviter qu'une opération écrase une autre opération en cours de changement.

Je note que la version non réentrante de ceci est polyremplissable sur n'importe quelle intégration avec prise en charge des threads, au cas où quelqu'un voudrait jouer avec et voir à quel point c'est utile.

Je note que la version non réentrante de ceci est polyremplissable sur n'importe quelle intégration avec prise en charge des threads, au cas où quelqu'un voudrait jouer avec et voir à quel point c'est utile.

Comme mentionné ci-dessus, c'est quelque chose avec lequel nous avons déjà joué, mais que nous avons abandonné car il offre des performances encore pires que la solution actuelle, n'est pas universellement pris en charge et rend en outre très difficile le partage WebAssembly.Global ou WebAssembly.Table avec le fil principal sans hacks supplémentaires, ce qui en fait un mauvais choix pour un polyfill transparent.

La solution actuelle qui réécrit le module Wasm ne souffre pas de ces problèmes, mais a plutôt un coût de taille de fichier important.

En tant que tel, aucun de ceux-ci n'est idéal pour les grandes applications du monde réel, ce qui nous motive à rechercher un support natif pour l'intégration asynchrone comme décrit ici.

moins bonnes performances

Avez-vous une sorte de référence?

Oui, je peux le partager quand je suis de retour au travail mardi (ou, plus probablement, mercredi), ou il est assez facile d'en créer un qui appelle simplement vous-même une fonction JS asynchrone vide.

Merci. Je pourrais créer un microbenchmark, mais ce ne serait pas très instructif.

Oh oui, le mien est également un microbenchmark puisque nous étions uniquement intéressés par la comparaison des frais généraux.

Le problème avec un microbenchmark est que nous ne savons pas quelle est la latence acceptable pour une application réelle. Si cela prend 1 ms supplémentaire, est-ce vraiment un problème si l'application n'effectue que des opérations d'attente à un taux de 1/s, par exemple ?

Je pense que l'accent mis sur la vitesse d'une approche basée sur l'atome peut être une distraction. Comme mentionné précédemment, les atomes ne fonctionnent pas et ne fonctionneront pas partout (en raison de COOP / COEP) et seul un travailleur peut utiliser l'approche atomique car le fil principal ne peut pas bloquer. C'est une bonne idée, mais pour une solution universelle, nous avons besoin de quelque chose comme Await.

Je ne le propose pas comme une solution à long terme. Je suggère qu'un polyfill qui l'utilise pourrait être utilisé pour voir si une solution non réentrante fonctionnera pour les gens.

@taralx Oh, ok, maintenant je vois, merci.

@taralx :

Je pense que vous pouvez obtenir une intégration JS pratique sans transformation complète du programme si vous n'autorisez pas la rentrée du module.

Ce serait mauvais. Cela signifie que la fusion de plusieurs modules pourrait casser leur comportement. Ce serait l'antithèse de la modularité.

En tant que principe de conception général, le comportement opérationnel ne doit jamais dépendre des limites du module (autre qu'une simple portée). Les modules ne sont qu'un mécanisme de regroupement et de portée dans Wasm, et vous souhaitez conserver la possibilité de regrouper des éléments (lier/fusionner/diviser des modules) sans que cela ne modifie le comportement d'un programme.

@rossberg : ceci est généralisable comme bloquant l'accès à n'importe quel module Wasm, comme proposé précédemment. Mais alors c'est probablement trop limitatif.

Ce serait mauvais. Cela signifie que la fusion de plusieurs modules pourrait casser leur comportement. Ce serait l'antithèse de la modularité.

C'était mon point avec l'argument polyfilling - atomic.wait ne brise pas la modularité, donc cela ne devrait pas non plus.

@taralx , atomic.wait fait référence à un emplacement spécifique dans une mémoire spécifique. Quelle mémoire et quel emplacement le blocage await utiliserait-il, et comment contrôler les modules qui partagent cette mémoire ?

@rossberg pouvez-vous élaborer sur un scénario que vous pensez que cela casse? Je soupçonne que nous avons des idées différentes sur la façon dont la version non réentrante fonctionnerait.

@taralx , envisagez de charger deux modules A et B, chacun fournissant une fonction d'exportation, disons A.f et B.g . Les deux peuvent effectuer await lorsqu'ils sont appelés. Deux morceaux de code client reçoivent chacun une de ces fonctions, respectivement, et ils les appellent indépendamment. Ils ne s'interfèrent pas et ne se bloquent pas. Ensuite, quelqu'un fusionne ou refactorise A et B en C, sans rien changer au code. Soudain, les deux morceaux de code client pourraient commencer à se bloquer de manière inattendue. Action effrayante à distance grâce à un état partagé caché.

Ça a du sens. Mais autoriser la rentrée risque la concurrence dans des modules qui ne s'y attendent pas, c'est donc une action effrayante à distance dans les deux sens.

Mais les modules sont déjà ré-entrables, non ? Chaque fois qu'un module fait un appel à une importation, le code externe peut entrer à nouveau dans le module qui pourrait changer d'état global avant de revenir. Je ne vois pas en quoi la rentrée pendant l'attente proposée est plus effrayante ou simultanée que l'appel d'une fonction importée. Peut-être qu'il me manque quelque chose ?

(édité)

Hum, oui. D'accord, donc une fonction importée pourrait réintégrer le module. J'ai clairement besoin d'y réfléchir plus sérieusement.

Lorsque le code est en cours d'exécution et qu'il appelle une fonction, il y a deux possibilités : il sait que la fonction n'appellera pas des choses aléatoires, ou que la fonction peut appeler des choses aléatoires. Dans ce dernier cas, la réentrée est toujours possible. Les mêmes règles s'appliquent à await .

(j'ai édité mon commentaire plus haut)

Merci à tous pour la discussion jusqu'ici !

Pour résumer, il semble qu'il y ait un intérêt général ici, mais il y a de grandes questions ouvertes comme si cela devrait être 100% du côté JS ou juste 99% - il semble que le premier supprimerait les principales inquiétudes de certaines personnes, et ce serait être bien pour le cas du Web, donc c'est probablement ok. Une autre grande question ouverte est de savoir dans quelle mesure cela serait faisable dans les machines virtuelles sur lesquelles nous avons besoin de plus d'informations.

Je suggérerai un point à l'ordre du jour de la prochaine réunion du CG dans 2 semaines pour discuter de cette proposition et l'examiner pour l'étape 1, ce qui signifierait ouvrir un référentiel et discuter plus en détail des questions ouvertes dans des questions distinctes. (Je pense que c'est le bon processus, mais corrigez-moi si je me trompe.)

Juste pour info
Nous mettrons en place une proposition de commutation complète de la pile dans un processus similaire
Plage de temps. Je pense que cela pourrait rendre inutile votre variante de cas spécial -
Qu'est-ce que tu penses?
Francis

Le jeudi 28 mai 2020 à 15h51, Alon Zakai [email protected] a écrit :

Merci à tous pour la discussion jusqu'ici !

Pour résumer, il semble qu'il y ait ici un intérêt général, mais il y a
de grandes questions ouvertes comme si cela devrait être à 100 % du côté JS ou simplement
99 % - il semble que le premier supprime les principaux soucis de certaines personnes
avoir, et ce serait bien pour le cas du Web, donc c'est probablement ok.
Une autre grande question ouverte est de savoir dans quelle mesure cela serait faisable dans des machines virtuelles qui
nous avons besoin de plus d'informations sur.

Je proposerai un point à l'ordre du jour de la prochaine réunion du CG dans 2 semaines pour en discuter
cette proposition et l'examiner pour l'étape 1, ce qui signifierait l'ouverture d'un repo
et discuter plus en détail des questions ouvertes dans des questions distinctes.
(Je pense que c'est le bon processus, mais corrigez-moi si je me trompe.)


Vous recevez ceci parce que vous êtes abonné à ce fil.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/WebAssembly/design/issues/1345#issuecomment-635649331 ,
ou désabonnez-vous
https://github.com/notifications/unsubscribe-auth/AAQAXUCLZ4CJVQYEUBK23BLRT3TFLANCNFSM4NEJW2PQ
.

>

François McCabe
SUÉ

@fgmccabe

Nous devrions en discuter à coup sûr.

En général cependant, à moins que votre proposition ne se concentre sur le côté JS, je suppose que cela ne rendrait pas celle-ci discutable (qui est de 99% à 100% du côté JS).

Maintenant que la discussion sur les détails de la mise en œuvre est terminée, je voudrais soulever à nouveau une préoccupation de niveau supérieur que j'ai exprimée plus tôt mais que j'ai abandonnée pour avoir une discussion à la fois.

Un programme est composé de plusieurs composants. Du point de vue de l'ingénierie logicielle, il est important que le fractionnement de composants en parties ou la fusion de composants ne modifient pas de manière significative le comportement du programme. C'est le raisonnement derrière le principe de composition de modules discuté lors de la dernière réunion de CG en personne, et il est implicite dans la conception de nombreuses langues.

Dans le cas des programmes web, maintenant avec WebAssembly ces différents composants pourraient même être écrits dans des langages différents : JS ou wasm. En fait, de nombreux composants pourraient tout aussi bien être écrits dans l'une ou l'autre langue ; Je les appellerai des composants "ambivalents". À l'heure actuelle, la plupart des composants ambivalents sont écrits en JS, mais j'imagine que nous espérons tous que de plus en plus d'entre eux seront réécrits en wasm. Pour faciliter cette "migration de code", nous devons essayer de nous assurer que la réécriture d'un composant de cette manière ne modifie pas la façon dont il interagit avec l'environnement. À titre d'exemple, le fait qu'un composant de programme "apply" particulier (f, x) => f(x) soit écrit en JS ou en wasm ne devrait pas affecter le comportement de l'ensemble du programme. Il s'agit d'un principe de migration de code.

Malheureusement, toutes les variantes de cette proposition semblent violer soit le programme de composition de modules, soit le principe de migration de code. Le premier est violé lorsque await capture la pile jusqu'à l'endroit où le module wasm actuel a été entré le plus récemment, car cette limite change lorsque les modules sont séparés ou combinés ensemble. Ce dernier est violé lorsque await capture la pile jusqu'à l'endroit où wasm a été entré le plus récemment, car cette limite change lorsque le code est migré de JS vers wasm (de sorte que la migration de quelque chose d'aussi simple que (f, x) => f(x) de JS en wasm peut modifier considérablement le comportement de l'ensemble du programme).

Je ne pense pas que ces violations soient dues à de mauvais choix de conception de cette proposition. Le problème semble plutôt être que cette proposition essaie d'éviter de rendre indirectement JS plus puissant, et cet objectif le force à imposer des frontières artificielles qui violent ces principes. Je comprends parfaitement cet objectif, mais je soupçonne que ce problème se posera de plus en plus : ajouter des fonctionnalités à WebAssembly d'une manière qui respecte ces principes nécessitera souvent d'ajouter indirectement des fonctionnalités à JS car JS est le langage d'intégration. Ma préférence serait de s'attaquer à ce problème de front (que je n'ai vraiment aucune idée de comment résoudre). Sinon, ma préférence secondaire serait d'apporter cette modification uniquement dans l'API JS, car c'est JS qui est le facteur limitant ici, plutôt que d'ajouter des instructions à WebAssembly pour lesquelles wasm n'a aucune interprétation.

Je ne pense pas que ces violations soient dues à de mauvais choix de conception de cette proposition. Au contraire, le problème semble être que cette proposition essaie d'éviter de rendre indirectement JS plus puissant

C'est important, mais ce n'est pas la principale raison de la conception ici.

La principale raison de cette conception est que si je suis entièrement d'accord que le principe de composition a du sens pour wasm , le problème fondamental que nous avons sur le Web est qu'en fait JS et wasm ne sont pas équivalents dans la pratique. Nous avons un JS manuscrit qui est asynchrone et un wasm porté qui est synchronisé. En d'autres termes, la frontière entre eux est en fait le problème exact que nous essayons de résoudre. Dans l'ensemble, je ne suis pas sûr d'être d'accord que le principe de composition devrait être appliqué à wasm et JS (mais peut-être que cela devrait être un débat intéressant).

J'espérais avoir plus de discussions publiquement ici, mais pour gagner du temps, j'ai contacté directement certains implémenteurs de VM, car peu se sont engagés ici jusqu'à présent. Compte tenu de leurs commentaires et de la discussion ici, je pense malheureusement que nous devrions suspendre cette proposition.

Await a un comportement observable beaucoup plus simple que les coroutines générales ou la commutation de pile, mais les personnes de VM à qui j'ai parlé sont d'accord avec @rossberg que le travail de VM à la fin serait probablement similaire pour les deux. Et au moins certaines personnes VM pensent que nous obtiendrons de toute façon des coroutines ou une commutation de pile, et que nous pouvons prendre en charge les cas d'utilisation d'attente en utilisant cela. Cela signifiera créer une nouvelle coroutine/pile à chaque appel dans le wasm (contrairement à cette proposition), mais au moins certaines personnes VM pensent que cela pourrait être fait assez rapidement.

En plus du manque d'intérêt des gens de VM, nous avons eu de fortes objections à cette proposition ici de @fgmccabe et @RossTate , comme indiqué ci-dessus. Nous ne sommes pas d'accord sur certaines choses, mais j'apprécie ces points de vue et le temps qu'il a fallu pour les expliquer.

En conclusion, dans l'ensemble, j'ai l'impression que ce serait une perte de temps pour tout le monde d'essayer d'avancer ici. Mais merci à tous ceux qui ont participé à la discussion ! Et j'espère qu'au moins cela motivera la priorisation des coroutines / commutation de pile.

Notez que la partie JS de cette proposition peut être pertinente à l'avenir, car JS sucre essentiellement pour une intégration pratique de Promise. Nous devrons attendre le changement de pile ou les coroutines et voir si cela pourrait fonctionner en plus de cela. Mais je ne pense pas que cela vaille la peine de garder le problème ouvert pour cela, alors fermez.

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

badumt55 picture badumt55  ·  8Commentaires

void4 picture void4  ·  5Commentaires

Artur-A picture Artur-A  ·  3Commentaires

spidoche picture spidoche  ·  4Commentaires

cretz picture cretz  ·  5Commentaires