Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Publicodes engine in web worker #375

Closed
wants to merge 20 commits into from
Closed

Publicodes engine in web worker #375

wants to merge 20 commits into from

Conversation

wiinxt
Copy link
Collaborator

@wiinxt wiinxt commented Aug 22, 2023

Ajoute les packages @publicodes/worker et @publicodes/worker-react

  • @publicodes/worker permet de créer un Engine dans un web worker afin de ne plus bloquer le thread principale lors du chargement des règles et lors des calculs, il export createWorkerEngine et createWorkerEngineClient, le premier wrap un Engine afin de communiquer avec le second qui est le client, le client possède les même fonction qu'un Engine mais sont asynchrone (ex: asyncEvaluate ou asyncSetSituation).
  • @publicodes/worker-react ajoute des hooks react qui s'actualise après un asyncSetSituation. Il ajoute aussi usePromise et useLazyPromise afin de simplifier l'utilisation des fonction asynchrone du client.
  • publicodes-react à été refacto pour qu'il accepte les Engine et les nouveaux WorkerEngine créé par le client.

Il y a deux méthodes pour convertir une App react qui utilise une Engine :

  • Grâce au hooks de @publicodes/worker-react : useAsyncShallowCopy, useAsyncSetSituation, useAsyncGetRule, useAsyncParsedRules et useWorkerEngine pour faire du code plus complexe (asyncEvaluate, etc.)
  • En ajoutant des actions à createWorkerEngine, les fonctions seront exécuté dans le worker avec l'Engine directement

cette PR est utilisé par betagouv/mon-entreprise#2744

@laem
Copy link
Collaborator

laem commented Aug 23, 2023

Je suis actuellement confronté au problème de l'Engine dans une app Next : n'étant pas sérialisable, le serveur ne peut réhydrater le client avec l'engine en props.

Peut-être que ton travaille résolverait ce problème ? Très intéressant en tout cas :)

@wiinxt
Copy link
Collaborator Author

wiinxt commented Aug 24, 2023

Si il y a moyen d'utiliser un web worker pendant le ssr ça pourrait peut être fonctionner, comme l'engine ne serait plus utiliser directement par l'app react alors il n'y aurait aucun souci de sérialisation j'imagine (à moins qu'il essaye de sérialiser le worker...).

Ca m'a fait penser à autre chose, coté mon-entreprise on fait du prerendering simple de quelques page avec renderToString, ca posera problèmes car cette fonction n'exécute pas les effets donc on aura que des valeurs par défaut partout ou on utilise l'engine.

Mais il existe d'autres fonctions plus évolué pour faire du ssr (renderToPipeableStream ou renderToReadableStream qui doivent être utilisé par next sans doute) qui peuvent attendre que les Suspense soit résolu avant de retourner la page.
De ce que j'ai vue il suffit de throw une promesse pour que Suspense attende qu'elle soit résolu, il faut que je test ça mais si c'est le cas ça serait parfait !

@wiinxt
Copy link
Collaborator Author

wiinxt commented Sep 5, 2023

J'ai réussi à faire notre prerender via renderToPipeableStream pour mon-entreprise, je n'ai pas encore testé hydrateRoot coté client mais je ne pense pas que ça posera problème.

Fonctionnement pendant le SSR

Actuellement Suspense (avec react 18) fonctionne de cette manière : un/des enfants throw une promesse (via lazy()) qui est catch par le Suspense parent le plus proche, celui ci attend que la/les promesse soit résolu puis essaye de les render à nouveau. Chaque lazy à son propre cache qu'il va utiliser pour retourner le composant qu'il a téléchargé.

Le problème c'est qu'on ne peut pas utiliser lazy dans un composant car le cache serait reset à chaque fois.
Pour contourner ça j'ai créé un composant SuspensePromise pour faire un cache dans un context afin de garder les promesses exécuté ainsi que leurs résultats.

Le problème qui viens alors c'est qu'il faudrait une clé unique pour associer un usePromise à un cache, j'ai fait plusieurs tests (utiliser useId de react ou la call stack par exemple) mais malheureusement je n'en n'est pas trouvé.
J'ai donc fait un simple tableau auquel on ajoute une par une chaque promesse après que la dernière soit résolu afin d'avoir un ordre stable des appels.
Comme ce n'est pas très opti pour l'instant ce n'est activé que pour le SSR.

Les futur version de react aiderons a mieux opti ça normalement, notamment le hooks use potentiellement.

Pour ce qui est du worker coté serveur, Node implémente leur propre version des worker, on est donc obligé d'utiliser une librarie (@eshaz/web-worker) pour que ca soit compatible (vous pouvez upvote cette issue pour que Node implémente les web worker nativement nodejs/node#43583).

@mquandalle
Copy link
Collaborator

React Async à l'air vraiment complexe, c'est cool que tu réussisses à avancer !

Question : est-ce que l'API asynchrone exposée permet de bien garder une UI toujours cohérente et pas une partie des valeurs calculées avec une situation, et une autre partie avec l'ancienne situation en attendant le calcul asynchrone ? J'imagine que c'est géré par une frontière unique <Suspense> ?

@wiinxt
Copy link
Collaborator Author

wiinxt commented Sep 5, 2023

Pour garder l'UI actualisé il y a un state situationVersion (dans @publicodes/worker-react) qui s'incrémente à chaque fois qu'un setSituation a été fait, cette variable est ensuite ajouté au workerEngine qui est passé dans un context accessible avec useWorkerEngine().

Ensuite quand on l'utilise on le passe en dépendance de usePromise qui réexécutera la fonction des que workerEngine aura été modifié:

const workerEngine = useWorkerEngine()
const resultSmic = usePromise(
	() => workerEngine.asyncEvaluate('SMIC'),
	[workerEngine],
	'loading...'
)

Pour ce qui est de la cohérence je n'est rien fait qui empêcherais que des calculs soit avec une nouvelle situation et d'autre avec une précédente si il y a un calcul très long, mais dans ce cas on peut faire un state isLoading qu'on met a true ou false avant et après la promise et afficher un loader

Il y a quand même une chose qui limite ce comportement sans que ça soit le but premier, pour optimiser un peu la vitesse entre le worker et le tread principal, le worker attend 50ms à partir de la première action demandé et ajoute toutes les actions pendant ce temps dans une file d'attente pour ensuite toutes les traiter d'un coup. Donc si il y a un long calcul dans un batch alors les autres action de se batch seront aussi impacter, ça limitera les différence dans l'UI je pense.

L'autre opti c'est que si il y a un setSituation dans un batch alors on abort tous les evaluate qui sont fait avant car il seront sans doute redemandé ensuite.

Il y a aussi d'autre amélioration qui sont possible, par exemple on pourrait mettre en cache coté thread principal toutes les rule et parsedRules des le début pour gagner du temps si besoin

Pour ce qui est de Suspense il ne sont géré que lors du SSR car trop lent coté client

@wiinxt
Copy link
Collaborator Author

wiinxt commented Sep 28, 2023

Je ferme, voir compte rendu ici : betagouv/mon-entreprise#2744

@wiinxt wiinxt closed this Sep 28, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants