Xterm.js: Des parties de xterm.js doivent s'exécuter dans un web worker

Créé le 15 juin 2018  ·  26Commentaires  ·  Source: xtermjs/xterm.js

Je n'ai pas encore vu de problème de suivi.

Bloqué par https://github.com/xtermjs/xterm.js/issues/1507
Connexe : https://github.com/xtermjs/xterm.js/issues/444

areperformance typenhancement

Tous les 26 commentaires

Y a-t-il encore des projets/réflexions dans cette direction ? De ma vue centrée sur l'analyseur, il semble que le moteur de rendu et l'entrée de données + l'analyse se battent pour le temps du processeur, donc ma première supposition est de déplacer toutes les parties principales (entrée de données + analyseur + la plupart de la classe de terminal) dans un webworker seuls les éléments liés au DOM résident dans le thread principal. Cela nécessitera une traduction d'événements, mais cela fait déjà partie de la nouvelle proposition en couches. La partie délicate consistera à déplacer les données du tampon vers le thread principal sans trop de délai d'exécution (SharedArrayBuffer pourrait résoudre ce problème à l'avenir sans copie mais est actuellement désactivé dans tous les moteurs en raison de Spectre, nous allons donc devoir le faire "le dur manière").

Ma pensée est de déplacer core/base dans un worker, puis ui/base/public s'exécute dans le thread UI. La façon dont l'état du tampon est synchronisé nécessite encore une réflexion.

Une idée est de conserver le tampon uniquement dans le travailleur, puis d'avoir un modèle de vue plus convivial pour le rendu dans le thread principal qui peut être mis à jour de manière incrémentielle par le travailleur.

Nous pourrions également réécrire le noyau en rouille en tant que module WebAssembly, mais je pense que c'est hors de portée pour le moment 😅

Une idée est de conserver le tampon uniquement dans le travailleur, puis d'avoir un modèle de vue plus convivial pour le rendu dans le thread principal qui peut être mis à jour de manière incrémentielle par le travailleur.

Oui, ce serait ma préférence, je pense que Monaco a cependant besoin d'une vue plus complète du tampon.

Nous pourrions également réécrire le noyau en rouille en tant que module WebAssembly, mais je pense que c'est hors de portée pour le moment 😅

Ouais, ça semble être tout un effort à entreprendre . Le nettoyage du TS l'emporte au moins pour l'instant imo

J'ai donné plus de réflexions à cela, voici un aperçu approximatif de la façon dont cela pourrait fonctionner. L'interface utilisateur peut souscrire à une vue et peut spécifier le nombre de lignes qu'elle souhaite voir, ainsi que le décalage des lignes en haut ou en bas. Le noyau suivra ensuite les modifications de la mémoire tampon qui affectent cette fenêtre et émettra des mises à jour avec des instructions de mise à jour. L'objectif devrait être que l'interface utilisateur n'ait pas besoin de stocker d'état, elle devrait simplement consommer les mises à jour et dessiner / redessiner en fonction de celles-ci.

screen shot 2018-06-20 at 12 20 04

// subscribe to a new view
const view = terminal.createViewport({ rows: 20, offset: 0, sticky: true });

// listen for view updates and modify the UI accordingly
view.onUpdate((changeDecorations /*changes*/, terminalState /*infos*/) => {

  // TODO: 
  // specify how changeDecorations could look like, maybe like this:
  for (let change of changeDecorations) {
     switch (change.type) {
        case ChangeType.DELETE: {
           // some rows have left the viewport
        }
        case ChangeType.ADD: {
           // some rows have been added to the viewport
        }
        case ChangeType.MODIFY: {
           // exstinig viewport rows have been modified
        }
     }
  }
});

// terminal viewport is scrolled in the UI, update the viewport offset
myViewportElement.addEventListener('wheel', (evt) => {
  // calculate the new offset
  let offset = ...

  // NOTE: this will likely trigger the onUpdate callback if things have changed
  view.setOffset({ offset, sticky: offset > 0 });
})

@mofux Hmm pas sûr de ça - en théorie, le noyau du terminal interne n'a pas besoin de contenir des données de lignes, qui dépassent la hauteur réelle du terminal, pour fonctionner correctement. La seule exception à cette règle est le redimensionnement, qui ne se produira pas très souvent. Peut-être pouvons-nous en faire un avantage et ne conserver que les "parties chaudes" du tampon dans le noyau et déléguer la mise en mémoire tampon des données de défilement au moteur de rendu ? Il faudra cependant une rétropropagation pour les événements de redimensionnement. Je n'ai pas réfléchi plus profondément à cela, juste quelques pensées aléatoires jusqu'à présent pour moi.

@jerch Le
À un moment donné, je veux que des addons comme le linkifier vivent dans le noyau et s'abonnent aux changements de tampon, puis créent des décorations dessus. Une décoration est comme une plage collante (startRow, startColumn, endRow, endColumn) qui fournit des informations de rendu au moteur de rendu frontal (via l'écouteur onUpdate ). Les styles de cellules comme l'arrière-plan, le premier plan, le gras, etc. finiraient également comme décoration. Cela nous donne de très gros avantages : ils n'ont besoin d'être calculés que si une ligne de tampon chaud est mise à jour. Et comme ces calculs finiront par s'exécuter dans WebWorker, cela débloquera le thread de l'interface utilisateur.

Peut-être que cette conception a plus de sens : nous ne maintenons le tampon que pour la hauteur du pty, puis nous conservons nos décorations pour la hauteur de l'écran pty + scrollback :

screen shot 2018-06-20 at 13 22 28

@mofux Yupp vous avez raison, j'ai totalement oublié les addons. Du point de vue de la conception de l'API, il semble également plus pratique de conserver les données de défilement dans la partie hors écran. Ce qui me dérange avec la conception actuelle du tampon, c'est le fait que nous détenons principalement des informations de rendu dans le tampon pour chaque cellule, même pour les données de défilement, qui ne sont plus accessibles par des "termes terminaux" normaux (donc ne changera plus, sauf redimensionner une fois qu'il devrait prendre en charge la refusion). C'est une mise en page très gourmande en mémoire.

Avec votre deuxième feuille de mise en page, nous pourrions peut-être créer un tampon de défilement moins cher (la partie violette) qui est plus proche des besoins de rendu (attributs et cellules fusionnés peut-être ?), tandis que la partie orange donne toujours un accès rapide à tous les attributs d'une cellule par base de cellules. Je suis toujours inquiet de la quantité de données que nous devons envoyer entre les threads de travail pour afficher quelque chose de nouveau.

Je ne sais pas encore comment les addons joueront dans cela, une intuition me dit que nous devrons peut-être créer différentes entrées au niveau de l'API si nous voulons que les parties critiques soient personnalisables de l'extérieur (comme une API de base, avant et après le rendu).

Une dernière réflexion concernant l'architecture (et le titre du numéro) : au lieu de simplement exécuter le noyau dans un webworker, ce serait bien (et dans la plupart des cas encore plus utile) si nous envisageons d'exécuter le noyau dans un processus de nœud côté serveur (proche de l'endroit où se trouve le processus pty), qui communique ensuite avec l'interface utilisateur (navigateur) via RPC / websockets. Techniquement, il ne devrait pas y avoir beaucoup de différence entre communiquer avec un webworker et communiquer avec un processus serveur, il ne s'agit que d'une couche de transport différente.

screen shot 2018-06-25 at 14 56 04

@mofux Imho c'est une chose

avantages:

  • les versions unicode peuvent être synchronisées plus facilement entre pty et le noyau du terminal, affectent le calcul de la largeur de wc, la gestion du graphème et du bidi (pas encore pris en charge)
  • la restauration d'anciennes sessions de terminal peut être implémentée beaucoup plus facilement
  • la consommation de mémoire du navigateur n'est plus un problème
  • la partie frontale peut être vraiment mince, jusqu'à l'extrême jusqu'à une image en pixels en tant que représentation terminale (comme un gratte-écran)

les inconvénients:

  • xterm.js dépend d'une partie serveur, plus de déploiement de navigateur uniquement possible à moins que quelqu'un porte à nouveau la partie principale de nodejs sur les moteurs de navigateur
  • impact négatif sur les performances du serveur (cpu et mem), ce n'est pas un problème pour les applications locales comme hyper ou vscode, mais cela nuira vraiment aux applications de serveur distant avec 1k+ terminaux - note: l'alimentation frontale est un peu gratuite pour un fournisseur de services
  • en fonction de la "stubiness" du frontend, la communication serveur-client explosera (nous devrons demander et transmettre les données de vue encore et encore pour de petits changements)
  • la latence pour les applications distantes va augmenter
  • Pourquoi s'embêter avec une partie de base auto-écrite? Réinventer la roue carrée ? Il suffit de prendre une bibliothèque de terminal c bien établie, de gratter la sortie et de la transmettre au navigateur avec une gestion des touches et de la souris comme couche d'interaction. Terminé.

Ne prenez pas le dernier point au sérieux, c'est plus un discours qu'un argument objectif. Je m'intéresse toujours au cœur du client pour une raison simple : créer une couche de transport pour les chaînes d'octets est beaucoup plus facile/rapide que de gérer différents états d'API sur des composants avec différents emplacements de machine, à mon humble avis. Mais je me trompe peut-être.

@jerch Merci pour vos réflexions, vraiment apprécié! Veuillez considérer les idées mentionnées ci-dessus comme un banc d'essai pour la discussion afin de développer une vision pour les améliorations futures.

L'objectif principal que je vois est de séparer l'interface utilisateur du noyau, afin que nous puissions décharger le travail effectué par l'analyseur sur un thread séparé, débloquant le thread de l'interface utilisateur.

Le deuxième objectif est de façonner une interface (contrat) qui permet au noyau et à l'interface utilisateur de se parler via une sorte d'IPC (que ce soit worker.postMessage ou un WebSocket , ou même un traditional direct communication si les deux s'exécutent dans le même thread comme c'est le cas maintenant). Le défi ici est de garder l'empreinte mémoire minimale (nous ne voulons pas maintenir le tampon + le défilement deux fois) et la surcharge de communication et la charge utile aussi faibles que possible. Si nous obtenons cela correctement, cela ouvre également la possibilité de prendre en charge plus facilement différents moteurs de rendu (par exemple monaco-editor)

xterm.js dépend d'une partie serveur, plus de déploiement de navigateur uniquement possible à moins que quelqu'un porte à nouveau la partie principale de nodejs sur les moteurs de navigateur

Je pense que nous avons mal compris ici. Je pense au noyau comme étant un morceau de code JS indépendant du DOM qui peut s'exécuter dans le navigateur, un WebWorker ou Node.js sans aucun portage. Il devrait certainement appartenir au développeur de décider où le noyau doit s'exécuter (en fonction de son cas d'utilisation). Ma pensée était de découpler le noyau et l'interface utilisateur autant que possible, afin que nous puissions potentiellement prendre en charge tous ces scénarios.

en fonction de la "stubiness" du frontend, la communication serveur-client explosera (nous devrons demander et transmettre les données de vue encore et encore pour de petits changements)

Ça dépend. Si nous n'envoyons que des mises à jour incrémentielles à l'interface utilisateur, cela ne devrait pas poser de problème.

la latence pour les applications distantes va augmenter

Pour le moment, nous envoyons toutes les données pty à l'interface du navigateur (avec la même charge de latence), ce qui est vraiment pénible dans les scénarios où xterm.js s'exécute dans un navigateur éloigné du serveur. J'ai vu des situations où le flux de données du pty ne ferait que spammer le websocket si fort qu'il finirait par se déconnecter car les pings ne passaient plus. La capture de toutes les données sur le serveur et l'envoi uniquement de mises à jour de vue au navigateur (peut-être limité) pourraient améliorer cette situation. Le fait est que nous ne pouvons pas ignorer le traitement de parties du flux de données pty car il doit constamment mettre à jour le tampon - nous devons tout manger. Mais nous pouvons ignorer / réguler les mises à jour d'une vue qui lit à partir d'un état de tampon cohérent. Et c'est là que je vois le gros avantage d'exécuter le noyau côté serveur. Nous ne sommes plus obligés d'envoyer l'intégralité du flux pty au client, nous n'envoyons que des mises à jour de vue. Des commandes telles que ls -lR / qui font pivoter le tampon + faire défiler plusieurs fois une image ne nuisent plus à l'interface utilisateur.

Je pense que nous avons mal compris ici. Je pense au noyau comme étant un morceau de code JS indépendant du DOM qui peut s'exécuter dans le navigateur, un WebWorker ou Node.js sans aucun portage. Il devrait certainement appartenir au développeur de décider où le noyau doit s'exécuter (en fonction de son cas d'utilisation). Ma pensée était de découpler le noyau et l'interface utilisateur autant que possible, afin que nous puissions potentiellement prendre en charge tous ces scénarios.

:+1:

Ça dépend. Si nous n'envoyons que des mises à jour incrémentielles à l'interface utilisateur, cela ne devrait pas poser de problème.

Ouais. Il y a un petit _mais_ cependant - si nous n'envoyons que des mises à jour incrémentielles, la partie frontale a besoin d'un moyen de conserver les anciennes données et de fusionner les nouvelles. Oh - et la partie backend a besoin d'une certaine abstraction pour filtrer le contenu mis à jour à partir de vieux trucs. Peut-être que nous pouvons établir une clé de cache tampon au niveau de la ligne ou même mieux (pour la quantité de communication) / pire (pour le temps d'exécution + l'utilisation de la mémoire) au niveau de la cellule (un vrai truc de données de cellule "à écriture directe").

À propos de la mise à jour de la latence / serveur-client-comm :
Cela dépend beaucoup de la granularité des mises à jour que nous visons. Le charme de l'approche actuelle réside dans la simplicité - nous n'avons pas besoin d'un protocole spécial, nous pompons simplement le flux d'octets pty. Une fois que nous décidons d'opter pour un transport d'API plus élevé, nous devons créer une couche de protocole pour prendre en charge ceci et cela. Je ne suis pas contre une telle approche, c'est tout de même un travail de la mettre en page de manière à ce que les utilisateurs tiers puissent l'intégrer facilement dans leurs environnements très différents.

Enfin, je pense que les changements possibles à partir de #791 devraient faire partie des considérations, certaines de mes suggestions pourraient augmenter le fardeau de la livraison de mises à jour faciles et bon marché (en particulier mes approches de stockage et de pointeur pourraient finir par être contradictoires).

Ouais. Il y a un petit mais cependant - si nous n'envoyons que des mises à jour incrémentielles, la partie frontale a besoin d'un moyen de conserver les anciennes données et de fusionner les nouvelles.

Le moteur de rendu de canevas fait déjà cela pour la fenêtre :

https://github.com/xtermjs/xterm.js/blob/5620da49d8590efd79ca06e995c89866c239e53e/src/renderer/TextRenderLayer.ts#L21

Clôture car hors de portée dans l'intérêt de garder la liste des problèmes petite car cela ne se produira probablement pas avant des années, voire pas du tout.

Rouvrir ceci car ce serait une bonne direction à suivre et s'assurer que le thread principal reste réactif pendant les charges de travail lourdes.

J'ai joué un peu avec les travailleurs Web ces derniers temps et j'imagine que cela fonctionne en utilisant un tampon de tableau partagé s'il est pris en charge, et s'il remet les données du tampon d'écriture au travailleur (et ne persiste pas dans le thread principal). Le principal défi imo est de savoir comment vous êtes censé obtenir facilement des intégrateurs pour tirer parti des travailleurs car plusieurs fichiers sont requis, alors, xterm.js peut également être regroupé dans une position différente.

@Tyriar Quelques enquêtes que j'ai faites dans ce domaine :

Le tampon de tableau partagé (SAB) avec ses propres verrous atomiques en lecture/écriture est le moyen le plus rapide de « déplacer » les données entre les threads de travail. Il perd encore 20 à 30% pour la synchronisation / les verrous de thread par rapport au code de promesse à thread unique déplaçant les données (testé sans aucune charge de travail sur les données, donc ces chiffres ne concernent que la partie "mobile" des données). Inconvénient de cette solution : difficile à faire correctement (et à entretenir ?), peut ne pas fonctionner dans tous les navigateurs en raison des correctifs de sécurité de Spectre.

Le transfert d'objet normal est ok et la seule solution de repli valide si les atomes SAB + ne sont pas disponibles. Fonctionne environ 50 % plus lentement qu'une solution SAB, ce qui pourrait pénaliser la latence de mise à jour de l'écran (aucune idée si cela sera perceptible à la fin).

Étant donné que le tampon est constitué de tableaux typés, une solution SAB pourrait être facilement réalisée. Néanmoins, une solution de secours pourrait être nécessaire pour couvrir les moteurs sans SAB. Enfin, l'interface worker-mainthread sera un défi car nous avons tellement d'événements/rappels dans le code du navigateur, qui doivent être branchés sans conditions de concurrence.

Oui, nous aurions certainement besoin d'une solution de secours car Safari n'a pas de SAB par exemple. Mes explorations dans les travailleurs et SAB sont qu'ils sont assez géniaux, vous avez juste besoin de penser à la façon dont les solutions de secours et l'emballage fonctionneraient (2 cibles de construction ? s'appuyer sur des importations dynamiques ? comment les embedders se regroupent-ils ?).

Bon point avec les rappels et les événements, ce que nous tirerions exactement dans le travailleur n'est pas clair du tout. Vous pouvez voir en zoomant sur un profil que l'analyse syntaxique discutable n'est pas la partie chère :

image

Donc, peut-être que dans un monde idéal, le tampon devrait également être un tampon de tableau partagé appartenant au thread de travail et seuls les événements qui indiquent expliquer quelles plages ont changé ou quelque chose serait envoyé au thread principal.

Une autre chose à laquelle il faut penser est de savoir comment transferControlToOffscreen s'intègre dans tout cela. Ce serait extrêmement cool si nous avions un fil de rendu, un fil d'analyse/tampon et le fil principal ne fait presque rien, gardant l'application extrêmement réactive même lorsque l'analyse/le rendu lourd est en cours.

Il est clair que ce serait un changement assez radical cependant, je n'aurai pas le temps de jouer avec ça de sitôt, mais c'est toujours amusant d'en parler. Nous pouvons y penser pendant que nous façonnons l'architecture et peut-être en faisons-nous d'abord de petites parties (comme si le moteur de rendu webgl était éventuellement exécuté dans un ouvrier). Je souhaite également qu'un problème pointe les doublons pour que le code VS perde de nombreuses trames lorsque le terminal est occupé.

Une autre chose à laquelle il faut penser est de savoir comment transferControlToOffscreen s'intègre dans tout cela. Ce serait extrêmement cool si nous avions un fil de rendu, un fil d'analyse/tampon et le fil principal ne fait presque rien, gardant l'application extrêmement réactive même lorsque l'analyse/le rendu lourd est en cours.

En effet. Cela pourrait même être rendu sans verrouillage pour l'action PRINT normale (qui, conjointement avec Terminal.scroll couvre comme 95% de l'activité lourde du terminal) si nous recourons à la copie sur écriture pour une ligne en accrochant la ligne mise à jour comme une véritable action atomique (possible tant que l'on s'en tient à un seul écrivain - un seul/multiple lecteur). Les verrous seront probablement toujours nécessaires pour d'autres actions comme ED, redimensionner et autres (des choses qui manipulent plus d'une ligne à la fois).

Oui, le moteur de rendu webgl semble être parfaitement adapté pour tester de nouveaux chemins dans cette direction, car seul le contexte webgl est actuellement autorisé pour le offcanvas. Je ne sais même pas si nous pouvons gagner quelque chose pour le moteur de rendu DOM ici - Serait-il plus rapide de pré-construire les chaînes DOM dans un travailleur et de déplacer le contenu en tant que gros lot de mise à jour vers le thread principal ? Idk, jamais testé quelque chose comme ça.

Éditer:
Au niveau du code, je me demande si nous pourrions nous en sortir avec un modèle de décorateur, qui « mappe » la fonctionnalité sur différentes cibles de thread. Quelque chose comme ca:

<strong i="12">@bufferWorker</strong>
<strong i="13">@main</strong>
class Terminal ... {
  // not decorated things get spawned on all thread targets listed on the class
  public doXY(...) {...}

  // only in bufferWorker
  <strong i="14">@bufferWorker</strong>
  public doSomethingOnBuffer(...) {...}
}

C'est juste une idée approximative, je ne sais même pas si les décorateurs peuvent être mal utilisés pour ce type de trucs de macro-précompilation. Cela introduirait certainement une autre étape de précompilation, tout en facilitant beaucoup ces définitions au niveau du code. Eh bien, cela romprait également avec IntelliSense, donc ce n'est peut-être pas du tout une bonne idée. Y a-t-il quelque chose dans TS pour effectuer des tâches de précompilation de type macro ?

Étant donné que tous les problèmes liés dans le référentiel vscode sont fermés et qu'ils pointent vers ce problème, je vais demander ici : existe-t-il une solution de contournement pour que vscode puisse imprimer de longues lignes sans gel ?

Ce problème rend parfois vscode inutilisable, car certains packages affichent leurs informations de débogage dans une séquence de très longues lignes.

@arsinclair C'est la déf. un bogue dans un gestionnaire de consommation de tampon de terminal dans vscode, xterm.js lui-même n'a pas ce problème. À mon humble avis, la bonne façon de résoudre ce problème serait de résoudre le problème dans la fonction slowpoke.

Solution de contournement piratée :
Vous pouvez essayer d'identifier le slowpoke et le supprimer de l'événement. Mais ne faites cela que si vous savez ce que vous faites, car cela pourrait casser l'intégration du terminal vscode jusqu'à des effets secondaires indésirables étranges (ce qui pourrait s'avérer vraiment mauvais si le terminal est connecté à un système réel). Je ne connais pas les gestionnaires là-bas, je ne peux donc pas dire s'il est sûr de simplement supprimer l'un d'entre eux.

@arsinclair vous voyez probablement https://github.com/microsoft/vscode/issues/100338

@Tyriar , je ne pense pas - il n'y a pas d'URL dans ma sortie. C'est juste un ensemble d'UUID séparés par des virgules.

@jerch , je ne suis pas sûr de ce qu'est une fonction _slowpoke_ à laquelle vous faites référence. Pourriez-vous détailler ?

@arcanis Avec la fonction slowpoke, je veux dire un code qui mange vos précieux cycles de processeur pour des raisons non évidentes. Le linkifier serait un candidat, si vous avez de très longues lignes d'emballage automatique. Peu importe s'il y a des liens dans votre sortie, les identifier lui-même peut prendre assez de temps.

Qu'en est-il ?!

si vous avez de très longues lignes d'emballage automatique

Oui, c'est exactement le cas.

Peu importe s'il y a des liens dans votre sortie, les identifier lui-même peut prendre assez de temps

Le problème est qu'une fois que je fais défiler jusqu'au bloc de longues lignes, la charge du processeur passe à 100% et ne baisse jamais. J'ai essayé d'attendre plusieurs heures pour voir si cela se termine un jour.

Je ne suis ni xterm.js, ni développeur vscode, donc je ne connais pas la base de code et je ne sais pas pourquoi cela se produit et comment y remédier.
Comme je n'avais pas le temps de m'en occuper, j'ai simplement redirigé toutes les sorties vers un fichier .log, puis je lisais ce fichier séparément.

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