Xterm.js: Améliorations des performances du tampon

Créé le 13 juil. 2017  ·  73Commentaires  ·  Source: xtermjs/xterm.js

Problème

Mémoire

À l'heure actuelle, notre tampon prend trop de mémoire, en particulier pour une application qui lance plusieurs terminaux avec de grands scrollbacks définis. Par exemple, la démo utilisant un terminal 160x24 avec 5000 scrollback rempli prend environ 34 Mo de mémoire (voir https://github.com/Microsoft/vscode/issues/29840#issuecomment-314539964), rappelez-vous qu'il ne s'agit que d'un seul terminal et que les moniteurs 1080p utiliser probablement des terminaux plus larges. De plus, afin de prendre en charge truecolor (https://github.com/sourcelair/xterm.js/issues/484), chaque personnage devra stocker 2 types number supplémentaires qui doubleront presque la consommation de mémoire actuelle du tampon.

Extraction lente du texte d'une ligne

Il y a l'autre problème de devoir récupérer le texte réel d'une ligne rapidement. La raison pour laquelle cela est lent est due à la façon dont les données sont présentées ; une ligne contient un tableau de caractères, chacun ayant une seule chaîne de caractères. Nous allons donc construire la chaîne, puis elle sera prête pour le ramasse-miettes immédiatement après. Auparavant, nous n'avions pas du tout besoin de le faire car le texte est extrait du tampon de ligne (dans l'ordre) et rendu dans le DOM. Cependant, cela devient de plus en plus utile à mesure que nous améliorons encore xterm.js, des fonctionnalités telles que la sélection et les liens extraient toutes deux ces données. Toujours en utilisant l'exemple de défilement 160x24/5000, il faut 30 à 60 ms pour copier l'intégralité du tampon sur un Macbook Pro mi-2014.

Soutenir l'avenir

Un autre problème potentiel à l'avenir est que lorsque nous envisageons d'introduire un modèle de vue qui peut avoir besoin de dupliquer tout ou partie des données dans le tampon, ce genre de chose sera nécessaire pour implémenter le reflow (https://github.com/sourcelair /xterm.js/issues/622) correctement (https://github.com/sourcelair/xterm.js/pull/644#issuecomment-298058556) et peut -

Cette discussion a commencé dans https://github.com/sourcelair/xterm.js/issues/484 , cela va plus en détail et propose une solution supplémentaire.

Je penche vers la solution 3 et je me dirige vers la solution 5 s'il reste du temps et que cela montre une nette amélioration. J'adorerais n'importe quel commentaire ! /cc @jerch , @mofux , @rauchg , @parisk

1. Solution simple

C'est essentiellement ce que nous faisons maintenant, juste avec truecolor fg et bg ajoutés.

// [0]: charIndex
// [1]: width
// [2]: attributes
// [3]: truecolor bg
// [4]: truecolor fg
type CharData = [string, number, number, number, number];

type LineData = CharData[];

Avantages

  • Très simple

Les inconvénients

  • Trop de mémoire consommée doublerait presque notre utilisation actuelle de la mémoire qui est déjà trop élevée.

2. Extraire le texte de CharData

Cela stockerait la chaîne contre la ligne plutôt que la ligne, cela entraînerait probablement des gains très importants en matière de sélection et de liaison et serait plus utile au fil du temps pour avoir un accès rapide à la chaîne entière d'une ligne.

interface ILineData {
  // This would provide fast access to the entire line which is becoming more
  // and more important as time goes on (selection and links need to construct
  // this currently). This would need to reconstruct text whenever charData
  // changes though. We cannot lazily evaluate text due to the chars not being
  // stored in CharData
  text: string;
  charData: CharData[];
}

// [0]: charIndex
// [1]: attributes
// [2]: truecolor bg
// [3]: truecolor fg
type CharData = Int32Array;

Avantages

  • Pas besoin de reconstruire la ligne chaque fois que nous en avons besoin.
  • Moins de mémoire qu'aujourd'hui en raison de l'utilisation d'un Int32Array

Les inconvénients

  • Lent à mettre à jour les caractères individuels, la chaîne entière devrait être régénérée pour les changements de caractère unique.

3. Stockez les attributs dans des plages

Extraire les attributs et les associer à une plage. Puisqu'il ne peut jamais y avoir de chevauchement d'attributs, cela peut être disposé de manière séquentielle.

type LineData = CharData[]

// [0]: The character
// [1]: The width
type CharData = [string, number];

class CharAttributes {
  public readonly _start: [number, number];
  public readonly _end: [number, number];
  private _data: Int32Array;

  // Getters pull data from _data (woo encapsulation!)
  public get flags(): number;
  public get truecolorBg(): number;
  public get truecolorFg(): number;
}

class Buffer extends CircularList<LineData> {
  // Sorted list since items are almost always pushed to end
  private _attributes: CharAttributes[];

  public getAttributesForRows(start: number, end: number): CharAttributes[] {
    // Binary search _attributes and return all visible CharAttributes to be
    // applied by the renderer
  }
}

Avantages

  • Moins de mémoire qu'aujourd'hui même si nous stockons également des données TrueColor
  • Peut optimiser l'application des attributs, plutôt que de vérifier l'attribut de chaque caractère et de le différencier du précédent
  • Encapsule la complexité du stockage des données dans un tableau ( .flags au lieu de [0] )

Les inconvénients

  • Changer les attributs d'une plage de caractères à l'intérieur d'une autre plage est plus complexe

4. Mettre les attributs dans un cache

L'idée ici est de tirer parti du fait qu'il n'y a généralement pas autant de styles dans une même session de terminal, nous ne devons donc pas en créer aussi peu que nécessaire et les réutiliser.

// [0]: charIndex
// [1]: width
type CharData = [string, number, CharAttributes];

type LineData = CharData[];

class CharAttributes {
  private _data: Int32Array;

  // Getters pull data from _data (woo encapsulation!)
  public get flags(): number;
  public get truecolorBg(): number;
  public get truecolorFg(): number;
}

interface ICharAttributeCache {
  // Never construct duplicate CharAttributes, figuring how the best way to
  // access both in the best and worst case is the tricky part here
  getAttributes(flags: number, fg: number, bg: number): CharAttributes;
}

Avantages

  • Utilisation de la mémoire similaire à celle d'aujourd'hui, même si nous stockons également des données en couleurs vraies
  • Encapsule la complexité du stockage des données dans un tableau ( .flags au lieu de [0] )

Les inconvénients

  • Moins d'économies de mémoire que l'approche des plages

5. Hybride de 3 et 4

type LineData = CharData[]

// [0]: The character
// [1]: The width
type CharData = [string, number];

class CharAttributes {
  private _data: Int32Array;

  // Getters pull data from _data (woo encapsulation!)
  public get flags(): number;
  public get truecolorBg(): number;
  public get truecolorFg(): number;
}

interface CharAttributeEntry {
  attributes: CharAttributes,
  start: [number, number],
  end: [number, number]
}

class Buffer extends CircularList<LineData> {
  // Sorted list since items are almost always pushed to end
  private _attributes: CharAttributeEntry[];
  private _attributeCache: ICharAttributeCache;

  public getAttributesForRows(start: number, end: number): CharAttributeEntry[] {
    // Binary search _attributes and return all visible CharAttributeEntry's to
    // be applied by the renderer
  }
}

interface ICharAttributeCache {
  // Never construct duplicate CharAttributes, figuring how the best way to
  // access both in the best and worst case is the tricky part here
  getAttributes(flags: number, fg: number, bg: number): CharAttributes;
}

Avantages

  • Protentialement le plus rapide et le plus économe en mémoire
  • Très économe en mémoire lorsque le tampon contient de nombreux blocs avec des styles mais seulement à partir de quelques styles (le cas courant)
  • Encapsule la complexité du stockage des données dans un tableau ( .flags au lieu de [0] )

Les inconvénients

  • Plus complexe que les autres solutions, cela ne vaut peut-être pas la peine d'inclure le cache si on garde déjà un seul CharAttributes par bloc ?
  • Frais généraux supplémentaires dans l'objet CharAttributeEntry
  • Changer les attributs d'une plage de caractères à l'intérieur d'une autre plage est plus complexe

6. Hybride de 2 & 3

Cela prend la solution de 3 mais ajoute également une chaîne de texte qui évalue paresseusement pour un accès rapide au texte de la ligne. Puisque nous stockons également les caractères dans CharData nous pouvons l'évaluer paresseusement.

type LineData = {
  text: string,
  CharData[]
}

// [0]: The character
// [1]: The width
type CharData = [string, number];

class CharAttributes {
  public readonly _start: [number, number];
  public readonly _end: [number, number];
  private _data: Int32Array;

  // Getters pull data from _data (woo encapsulation!)
  public get flags(): number;
  public get truecolorBg(): number;
  public get truecolorFg(): number;
}

class Buffer extends CircularList<LineData> {
  // Sorted list since items are almost always pushed to end
  private _attributes: CharAttributes[];

  public getAttributesForRows(start: number, end: number): CharAttributes[] {
    // Binary search _attributes and return all visible CharAttributes to be
    // applied by the renderer
  }

  // If we construct the line, hang onto it
  public getLineText(line: number): string;
}

Avantages

  • Moins de mémoire qu'aujourd'hui même si nous stockons également des données TrueColor
  • Peut optimiser l'application des attributs, plutôt que de vérifier l'attribut de chaque caractère et de le différencier du précédent
  • Encapsule la complexité du stockage des données dans un tableau ( .flags au lieu de [0] )
  • Accès plus rapide à la chaîne de ligne réelle

Les inconvénients

  • Mémoire supplémentaire due à l'accrochage aux chaînes de ligne
  • Changer les attributs d'une plage de caractères à l'intérieur d'une autre plage est plus complexe

Des solutions qui ne fonctionneront pas

  • Stocker la chaîne en tant qu'entier dans un Int32Array ne fonctionnera pas car il faut beaucoup trop de temps pour reconvertir l'entier en caractère.
areperformance typplan typproposal

Commentaire le plus utile

État actuel :

Après:

Tous les 73 commentaires

Une autre approche qui pourrait être mélangée: utilisez indexeddb, websql ou l'API du système de fichiers pour extraire les entrées de défilement inactives sur le disque 🤔

Excellente proposition. Je suis d'accord avec le fait que 3. est la meilleure voie à suivre pour le moment car il nous permet d'économiser de la mémoire tout en prenant également en charge les couleurs vraies.

Si nous y arrivons et que les choses continuent de bien se passer, nous pouvons alors optimiser comme proposé en 5. ou de toute autre manière qui nous vient à l'esprit à ce moment-là et qui a du sens.

3. c'est super .

@mofux , bien qu'il existe certainement un cas d'utilisation pour utiliser des techniques de stockage sur disque pour réduire l'empreinte mémoire, cela peut dégrader l'expérience utilisateur de la bibliothèque dans les environnements de navigateur qui demandent à l'utilisateur l'autorisation d'utiliser le stockage sur disque.

Concernant Accompagner l'avenir :
Plus j'y pense, plus l'idée d'avoir un WebWorker qui fait tout le gros travail d'analyse des données tty, de maintenance des tampons de ligne, de correspondance des liens, de correspondance des jetons de recherche et autres m'attire. En gros, faire le gros du travail dans un thread d'arrière-plan séparé sans bloquer l'interface utilisateur. Mais je pense que cela devrait faire partie d'une discussion séparée peut-être vers une version 4.0 😉

+100 sur WebWorker à l'avenir, mais je pense que nous devons modifier les versions de liste des navigateurs que nous prenons en charge, car tous ne peuvent pas l'utiliser...

Quand je dis Int32Array , ce sera un tableau normal s'il n'est pas pris en charge par l'environnement.

@mofux bonne réflexion avec WebWorker dans le futur 👍

@AndrienkoAleksandr oui, si nous voulions utiliser WebWorker nous aurions également besoin de prendre en charge l'alternative via la détection de fonctionnalités.

Wow belle liste :)

J'ai également tendance à pencher vers 3. car il promet une forte réduction de la consommation de mémoire pour plus de 90% de l'utilisation typique du terminal. L'optimisation de la mémoire à mon humble avis devrait être l'objectif principal à ce stade. Une optimisation supplémentaire pour des cas d'utilisation spécifiques pourrait être applicable en plus de cela (ce qui me vient à l'esprit: "canvas like apps" comme ncurses et autres utilisera des tonnes de mises à jour de cellule unique et dégradera un peu la liste [start, end] fil du temps) .

@AndrienkoAleksandr ouais, j'aime aussi l'idée du webworker car elle pourrait alléger _certain_ fardeau du fil principal. Le problème ici (outre le fait qu'il pourrait ne pas être pris en charge par tous les systèmes cibles recherchés) est le _some_ - la partie JS n'est plus un gros problème avec toutes les optimisations que xterm.js a vues au fil du temps. Le vrai problème en termes de performances est la mise en page/le rendu du navigateur...

@mofux La pagination vers une "mémoire étrangère" est une bonne idée, même si elle devrait faire partie d'une abstraction supérieure et non de la "donne-moi un widget de terminal interactif" qu'est xterm.js. Cela pourrait être réalisé par un addon à mon humble avis.

Offtopic: J'ai fait des tests avec des tableaux contre des typesdarrays contre asm.js. Tout ce que je peux dire - OMG, c'est comme 1 : 1,5 : 10 pour des charges et des ensembles variables simples (sur FF encore plus). Si la vitesse JS pure commence vraiment à faire mal, "utiliser asm" pourrait être là pour le sauvetage. Mais je considérerais cela comme un dernier recours car cela impliquerait des changements fondamentaux. Et l'assemblage Web n'est pas encore prêt à être expédié.

Offtopic: J'ai fait des tests avec des tableaux contre des typesdarrays contre asm.js. Tout ce que je peux dire - OMG, c'est comme 1 : 1,5 : 10 pour des charges et des ensembles variables simples (sur FF encore plus)

@jerch pour clarifier, est-ce que les tableaux vs typedarrays sont de 1:1 à 1:5?

Oups belle prise avec la virgule - je voulais dire 10:15:100 termes de vitesse. Mais seuls les tableaux de type FF étaient légèrement plus rapides que les tableaux normaux. asm est au moins 10 fois plus rapide que les tableaux js sur tous les navigateurs - testé avec FF, webkit (Safari), blink/V8 (Chrome, Opera).

@jerch cool, une accélération de 50% des typesdarrays en plus d'une meilleure mémoire vaudrait certainement la peine d'investir pour le moment.

Idée pour économiser de la mémoire - peut-être pourrions-nous nous débarrasser des width pour chaque personnage. Je vais essayer d'implémenter une version wcwidth moins chère.

@jerch, nous devons y accéder un peu, et nous ne pouvons pas le charger paresseux ou quoi que ce soit, car lorsque le reflow viendra, nous aurons besoin de la largeur de chaque caractère dans le tampon. Même si c'était rapide, nous pourrions toujours vouloir le garder.

Il serait peut-être préférable de le rendre facultatif, en supposant 1 s'il n'est pas spécifié :

type CharData = [string, number?]; // not sure if this is valid syntax

[
  // 'a'
  ['a'],
  // '文'
  ['文', 2],
  // after wide
  ['', 0],
  ...
]

@Tyriar Ouais - eh bien puisque je l'ai déjà écrit, s'il vous plaît regardez-le dans PR # 798
L'accélération est de 10 à 15 fois sur mon ordinateur pour le coût de 16k octets pour la table de recherche. Peut-être qu'une combinaison des deux est possible si nécessaire.

D'autres drapeaux que nous prendrons en charge à l'avenir : https://github.com/sourcelair/xterm.js/issues/580

Autre réflexion : seule la partie inférieure du terminal ( Terminal.ybase à Terminal.ybase + Terminal.rows ) est dynamique. Le défilement qui constitue la majeure partie des données est complètement statique, nous pouvons peut-être en tirer parti. Je ne le savais pas jusqu'à récemment, mais même des choses comme des lignes de suppression (DL, CSI Ps M) ne ramènent pas le défilement vers le bas mais insèrent plutôt une autre ligne. De même, faire défiler vers le haut (SU, CSI Ps S) supprime l'élément à Terminal.scrollTop et insère un élément à Terminal.scrollBottom .

Gérer la partie dynamique inférieure du terminal de manière indépendante et pousser pour revenir en arrière lorsque la ligne est sortie pourrait entraîner des gains importants. Par exemple, la partie inférieure pourrait être plus verbeuse pour favoriser la modification des attributs, un accès plus rapide, etc. tandis que le scrollback peut être plus un format d'archivage comme proposé dans ce qui précède.

Une autre idée : c'est probablement une meilleure idée de restreindre CharAttributeEntry aux lignes car c'est ainsi que la plupart des applications semblent fonctionner. De plus, si le terminal est redimensionné, un remplissage "vide" est ajouté à droite qui ne partage pas les mêmes styles.

par exemple:

screen shot 2017-08-07 at 8 51 52 pm

À droite des différences rouge/vert se trouvent des cellules « vides » sans style.

@Tyriar
Une chance de remettre cette question à l'ordre du jour ? Au moins pour les programmes intensifs en sortie, une manière différente de conserver les données du terminal peut économiser beaucoup de mémoire et de temps. Certains hybrides de 2/3/4 donneront un énorme coup de pouce au débit, si nous pouvions éviter de diviser et d'enregistrer des caractères uniques de la chaîne d'entrée. De plus, enregistrer les attributs uniquement une fois qu'ils ont été modifiés permet d'économiser de la mémoire.

Exemple:
Avec le nouvel analyseur, nous pourrions enregistrer un tas de caractères d'entrée sans déranger les attributs, car nous savons qu'ils ne changeront pas au milieu de cette chaîne. Les attributs de cette chaîne pourraient être enregistrés dans une autre structure de données ou attribut avec les largeurs de wc (oui, nous en avons toujours besoin pour trouver les sauts de ligne) et les sauts de ligne et les arrêts. Cela abandonnerait essentiellement le modèle de cellule lorsque des données sont entrantes.
Le problème survient si quelque chose intervient et veut avoir une représentation détaillée des données du terminal (par exemple, le moteur de rendu ou une séquence d'échappement/l'utilisateur veut déplacer le curseur). Nous devons toujours effectuer les calculs de cellule si cela se produit, mais cela devrait être suffisant de le faire uniquement pour le contenu des colonnes et des lignes du terminal. (Je ne suis pas encore sûr du contenu défilé, qui pourrait être encore plus cache et peu coûteux à redessiner.)

@jerch Je rencontrerai @mofux un jour à Prague dans quelques semaines et nous allions faire/commencer des améliorations internes sur la façon dont les attributs de texte sont gérés, ce qui couvre cela 😃

De https://github.com/xtermjs/xterm.js/pull/1460#issuecomment -390500944

L'algo est un peu cher car chaque caractère doit être évalué deux fois

@jerch si vous avez des idées sur un accès plus rapide au texte à partir du tampon, faites-le nous savoir. Actuellement, la plupart ne sont qu'un seul caractère, comme vous le savez, mais il pourrait s'agir d'un ArrayBuffer , d'une chaîne, etc.

Eh bien, j'ai beaucoup expérimenté avec ArrayBuffers dans le passé :

  • ils sont légèrement pires que Array ce qui concerne le temps d'exécution pour les méthodes typiques (peut-être encore moins optimisés par les fournisseurs de moteurs)
  • new UintXXArray est bien pire que la création de tableau littéral avec []
  • ils sont rentables plusieurs fois si vous pouvez préallouer et réutiliser la structure de données (jusqu'à 10 fois), c'est là que la nature de la liste chaînée des tableaux mixtes mange les performances en raison de l'allocation lourde et du gc en coulisses
  • pour les données de chaîne, la conversion aller-retour consomme tous les avantages - dommage que JS ne fournisse pas de chaîne native aux convertisseurs Uint16Array (en partie faisable avec TextEncoder cependant)

Mes découvertes sur ArrayBuffer suggèrent de ne pas les utiliser pour les données de chaîne en raison de la pénalité de conversion. En théorie, le terminal pourrait utiliser ArrayBuffer de node-pty jusqu'aux données du terminal (cela permettrait d'économiser plusieurs conversions sur le chemin du frontend), je ne sais pas si le rendu peut être fait de cette façon, je pense rendre il a toujours besoin d'une conversion finale de uint16_t à string . Mais même cette dernière création de chaîne consommera la majeure partie du temps d'exécution enregistré - et en outre, transformerait le terminal en interne en une vilaine bête C-ish. J'ai donc abandonné cette approche.

TL;DR ArrayBuffer est supérieur si vous pouvez préallouer et réutiliser la structure de données. Pour tout le reste, les tableaux normaux sont meilleurs. Les chaînes ne valent pas la peine d'être compressées dans ArrayBuffers.

Une nouvelle idée que j'ai proposée essaie de réduire autant que possible la création de cordes, en particulier. essaie d'éviter les divisions et les jointures désagréables. C'est un peu basé sur votre 2ème idée ci-dessus avec la nouvelle méthode InputHandler.print , wcwidth et les arrêts de ligne à l'esprit :

  • print obtient maintenant des chaînes entières jusqu'à plusieurs lignes terminales
  • enregistrer ces chaînes dans une simple liste de pointeurs sans aucune altération (pas d'allocation de chaîne ou de gc, l'allocation de liste peut être évitée si elle est utilisée avec une structure préallouée) avec les attributs actuels
  • avance le curseur de wcwidth(string) % cols
  • cas spécial \n (saut de ligne dur) : avance le curseur d'une ligne, marque la position dans la liste des pointeurs comme un saut de ligne
  • cas spécial débordement de ligne avec wrapAround : marque la position dans la chaîne comme un saut de ligne progressif
  • cas spécial \r : charge le contenu de la dernière ligne (de la position actuelle du curseur au dernier saut de ligne) dans un tampon de ligne pour être écrasé
  • les flux de données comme ci-dessus, malgré le cas \r aucune abstraction de cellule ni division de chaîne n'est nécessaire
  • les changements d'attributs ne posent aucun problème, tant que personne ne demande la vraie représentation cols x rows (ils modifient simplement l'indicateur attr qui est enregistré avec la chaîne entière)

Btw les wcwidths sont un sous-ensemble de l'algorithme graphème, donc cela pourrait être interchangeable à l'avenir.

Maintenant, la partie dangereuse 1 - quelqu'un veut déplacer le curseur dans le cols x rows :

  • déplacer cols arrière dans les sauts de ligne - le début du contenu actuel du terminal
  • chaque saut de ligne indique une vraie ligne terminale
  • charger des éléments dans le modèle de cellule pour une seule page (je ne sais pas encore si cela peut également être omis avec un positionnement intelligent des chaînes)
  • faire le sale boulot : si des modifications d'attributs sont demandées, nous n'avons pas de chance et devons nous rabattre sur le modèle de cellule complète ou sur un modèle de division et d'insertion de chaîne (ce dernier pourrait introduire de mauvaises performances)
  • les données circulent à nouveau, maintenant avec des chaînes et des données attrs dégradées dans le tampon pour cette page

Maintenant, la partie dangereuse 2 - le moteur de rendu veut dessiner quelque chose :

  • dépend un peu du moteur de rendu si nous devons explorer un modèle de cellule ou si nous pouvons simplement fournir les décalages de chaîne avec des sauts de ligne et des attributs de texte

Avantages:

  • flux de données très rapide
  • optimisé pour la méthode InputHandler la plus courante - print
  • rend possible la refusion des lignes lors du redimensionnement du terminal

Les inconvénients:

  • presque toutes les autres méthodes InputHandler seront dangereuses dans le sens d'interrompre ce modèle de flux et de nécessiter une abstraction de cellule intermédiaire
  • intégration du moteur de rendu pas claire (pour moi au moins atm)
  • peut dégrader les performances des malédictions comme les applications (elles contiennent généralement des séquences plus "dangereuses")

Eh bien, c'est un brouillon de l'idée, loin d'être utilisable en atmosphère car de nombreux détails ne sont pas encore couverts. Esp. les parties "dangereuses" pourraient devenir désagréables avec de nombreux problèmes de performances (comme la dégradation du tampon avec un comportement gc encore pire, etc. pp)

@jerch

Les chaînes ne valent pas la peine d'être compressées dans ArrayBuffers.

Je pense que Monaco stocke son tampon dans des ArrayBuffer s et est assez performant. Je n'ai pas encore approfondi la mise en œuvre.

esp. essaie d'éviter les divisions et les jointures désagréables

Lesquels?

J'ai pensé que nous devrions penser à tirer davantage parti du fait que le défilement est immuable d'une manière ou d'une autre.

Une idée était de séparer le scrollback de la section viewport. Une fois qu'une ligne revient en arrière, elle est poussée dans la structure de données de défilement. Vous pourriez imaginer 2 objets CircularList , un dont les lignes sont optimisées pour ne jamais changer, un pour le contraire.

@Tyriar À propos du défilement - oui, car il n'est jamais accessible par le curseur, cela peut économiser de la mémoire pour simplement supprimer l'abstraction de la cellule pour les lignes défilées.

@Tyriar
Il est logique de stocker des chaînes dans ArrayBuffer si nous pouvons limiter la conversion à une (peut-être la dernière pour la sortie de rendu). C'est légèrement mieux que la manipulation des cordes partout. Ce serait faisable car node-pty peut également fournir des données brutes (et le websocket peut également nous fournir des données brutes).

esp. essaie d'éviter les divisions et les jointures désagréables

Lesquels?

L'approche globale est d'éviter _minimize_ scissions du tout. Si personne ne demande que le curseur saute dans les données mises en mémoire tampon, les chaînes ne seraient jamais divisées et pourraient aller directement au moteur de rendu (si pris en charge). Aucune cellule ne se divise et ne se joint plus tard.

@jerch eh bien, si la fenêtre d'affichage est étendue, je pense que nous pouvons également

@Tyriar Ah d'accord. Pas sûr de ce dernier non plus, je pense que xterm natif ne permet cela que pour le défilement réel à la souris ou à la barre de défilement. Même SD/SU ne déplace pas le contenu du tampon de défilement dans la fenêtre d'affichage du terminal "actif".

Pourriez-vous m'indiquer la source de l'éditeur monaco, où l'ArrayBuffer est utilisé ? Apparemment, je ne le trouve pas moi-même :blush:

Hmm vient de relire la spécification TextEncoder/Decoder, avec ArrayBuffers de node-pty jusqu'à l'interface, nous sommes essentiellement bloqués avec utf-8, à moins que nous ne le traduisions à la dure à un moment donné. Faire en sorte que xterm.js utf-8 soit conscient ? Idk, cela impliquerait de nombreux calculs de points de code intermédiaires pour les caractères unicode supérieurs. Proside - cela économiserait de la mémoire pour les caractères ascii.

@rebornix pouvez-vous nous donner des indications sur l'endroit où Monaco stocke le tampon ?

Voici quelques chiffres pour les tableaux typés et le nouvel analyseur (était plus facile à adopter) :

  • UTF-8 (Uint8Array) : l'action print passe de 190 Mo/s à 290 Mo/s
  • UTF-16 (Uint16Array) : l'action print passe de 190 Mo/s à 320 Mo/s

Dans l'ensemble, UTF-16 fonctionne beaucoup mieux, mais c'était prévu puisque l'analyseur est optimisé pour cela. UTF-8 souffre du calcul du point de code intermédiaire.

La conversion de chaîne en tableau typé consomme ~4% du temps d'exécution JS de mon benchmark ls -lR /usr/lib (toujours bien en dessous de 100 ms, effectué via une boucle dans InputHandler.parse ). Je n'ai pas testé la conversion inverse (c'est implicitement fait atm dans InputHandller.print au niveau cellule par cellule). Le temps d'exécution global est légèrement moins bon qu'avec les chaînes (le temps économisé dans l'analyseur ne compense pas le temps de conversion). Cela peut changer lorsque d'autres parties sont également typées en fonction des tableaux.

Et les captures d'écran correspondantes (testées avec ls -lR /usr/lib ):

avec des cordes :
grafik

avec Uint16Array :
grafik

Notez la différence pour EscapeSequenceParser.parse , qui peut bénéficier d'un tableau typé (~30% plus rapide). Le InputHandler.parse effectue la conversion, donc c'est pire pour la version du tableau typé. De plus, GC Minor a plus à faire pour le tableau typé (puisque je jette le tableau).

Edit : un autre aspect peut être vu dans les captures d'écran - le GC devient pertinent avec environ 20% d'exécution, les longues images (signalées en rouge) sont toutes liées au GC.

Juste une autre idée un peu radicale :

  1. Créez votre propre mémoire virtuelle basée sur un buffer, quelque chose de gros (> 5 Mo)
    Si le buffer de tableau a une longueur de multiples de 4, des commutateurs transparents de int8 à int16 à int32 types sont possibles. L'allocateur retourne un index libre sur le Uint8Array , ce pointeur peut être converti en une position Uint16Array ou Uint32Array par un simple décalage de bit.
  2. Écrivez les chaînes entrantes dans la mémoire en tant que type uint16_t pour UTF-16.
  3. L'analyseur s'exécute sur les pointeurs de chaîne et appelle les méthodes dans InputHandler avec des pointeurs vers cette mémoire au lieu de tranches de chaîne.
  4. Créez le tampon de données du terminal à l'intérieur de la mémoire virtuelle en tant que tableau de tampon en anneau d'un type de type struct au lieu d'objets JS natifs, peut-être comme ceci (toujours basé sur des cellules):
struct Cell {
    uint32_t *char_start;  // start pointer of cell content (JS with pointers hurray!)
    uint8_t length;        // length of content (8 bit here is sufficient)
    uint32_t attr;         // text attributes (might grow to hold true color someday)
    uint8_t width;         // wcwidth (maybe merge with other member, always < 4)
    .....                  // some other cell based stuff
}

Avantages:

  • omet les objets JS et donc GC lorsque cela est possible (seuls quelques objets locaux resteront)
  • une seule copie initiale des données dans la mémoire virtuelle nécessaire
  • presque pas malloc frais de free (dépend de l'intelligence de l'allocateur/désallocateur)
  • économisera beaucoup de mémoire (évite la surcharge de mémoire des objets JS)

Les inconvénients:

  • Bienvenue au Cavascript Horror Show :scream:
  • difficile à mettre en œuvre, change un peu tout
  • l'avantage de la vitesse n'est pas clair jusqu'à ce qu'il soit vraiment mis en œuvre

:le sourire:

dur amusant à mettre en œuvre, change un peu tout 😉

C'est plus proche du fonctionnement de Monaco, je me suis souvenu de cet article de blog qui traite de la stratégie de stockage des métadonnées des personnages https://code.visualstudio.com/blogs/2017/02/08/syntax-highlighting-optimizations

Oui c'est en gros la même idée.

J'espère que ma réponse à l' endroit où Monaco stocke le tampon n'est pas trop tard.

Alex et moi sommes en faveur d'Array Buffer et la plupart du temps, cela nous donne de bonnes performances. Certains endroits où nous utilisons ArrayBuffer :

Nous utilisons des chaînes simples pour le tampon de texte au lieu de Array Buffer car la chaîne V8 est plus facile à manipuler

  • Nous effectuons l'encodage/décodage au tout début du chargement d'un fichier, les fichiers sont donc convertis en chaîne JS. V8 décide d'utiliser un ou deux octets pour stocker un caractère.
  • Nous effectuons très souvent des modifications sur le tampon de texte, les chaînes sont plus faciles à manipuler.
  • Nous utilisons le module natif de nodejs et avons accès aux composants internes V8 si nécessaire.

La liste suivante n'est qu'un bref résumé des concepts intéressants sur lesquels je suis tombé et qui pourraient aider à réduire l'utilisation de la mémoire et/ou le temps d'exécution :

  • FlatJS (https://github.com/lars-t-hansen/flatjs) - méta-langage pour aider à coder avec des tas basés sur un tampon de tableau
  • http://2ality.com/2017/01/shared-array-buffer.html (annoncé dans le cadre d'ES2017, l'avenir pourrait être incertain en raison de Spectre, à côté de cette idée très prometteuse avec une réelle concurrence et de vrais atomes)
  • webassembly/asm.js (état actuel ? encore utilisable ? Je n'ai pas suivi son développement depuis un certain temps, j'ai utilisé emscripten à asm.js il y a des années avec une lib C pour un jeu d'IA avec des résultats impressionnants cependant)
  • https://github.com/AssemblyScript/assemblyscript

Pour faire avancer les choses ici, voici un hack rapide sur la façon dont nous pourrions "fusionner" les attributs de texte.

Le code est principalement motivé par l'idée d'économiser de la mémoire pour les données du tampon (le temps d'exécution en souffrira, pas encore testé combien). Esp. les attributs de texte avec RVB pour le premier plan et l'arrière-plan (une fois pris en charge) feront que xterm.js consommera des tonnes de mémoire par la disposition cellule par cellule actuelle. Le code essaie de contourner cela en utilisant un atlas de comptage de références redimensionnable pour les attributs. C'est à mon humble avis une option car un seul terminal contiendra à peine plus de 1 million de cellules, ce qui porterait l'atlas à 1M * entry_size si toutes les cellules diffèrent.

La cellule elle-même a juste besoin de contenir l'index de l'atlas d'attributs. Lors des changements de cellule, l'ancien index doit être non ref'd et le nouveau ref'd. L'index de l'atlas remplacerait l'attribut attribut de l'objet terminal et sera lui-même modifié dans SGR.

L'atlas ne traite actuellement que des attributs de texte, mais pourrait être étendu à tous les attributs de cellule si nécessaire. Alors que le tampon de terminal actuel contient 2 nombres de 32 bits pour les données d'attribut (4 avec RVB dans la conception de tampon actuelle), l'atlas le réduirait à un seul nombre de 32 bits. Les entrées de l'atlas peuvent également être emballées davantage.

interface TextAttributes {
    flags: number;
    foreground: number;
    background: number;
}

const enum AtlasEntry {
    FLAGS = 1,
    FOREGROUND = 2,
    BACKGROUND = 3
}

class TextAttributeAtlas {
    /** data storage */
    private data: Uint32Array;
    /** flag lookup tree, not happy with that yet */
    private flagTree: any = {};
    /** holds freed slots */
    private freedSlots: number[] = [];
    /** tracks biggest idx to shortcut new slot assignment */
    private biggestIdx: number = 0;
    constructor(size: number) {
        this.data = new Uint32Array(size * 4);
    }
    private setData(idx: number, attributes: TextAttributes): void {
        this.data[idx] = 0;
        this.data[idx + AtlasEntry.FLAGS] = attributes.flags;
        this.data[idx + AtlasEntry.FOREGROUND] = attributes.foreground;
        this.data[idx + AtlasEntry.BACKGROUND] = attributes.background;
        if (!this.flagTree[attributes.flags])
            this.flagTree[attributes.flags] = [];
        if (this.flagTree[attributes.flags].indexOf(idx) === -1)
            this.flagTree[attributes.flags].push(idx);
    }

    /**
     * convenient method to inspect attributes at slot `idx`.
     * For better performance atlas idx and AtlasEntry
     * should be used directly to avoid number conversions.
     * <strong i="10">@param</strong> {number} idx
     * <strong i="11">@return</strong> {TextAttributes}
     */
    getAttributes(idx: number): TextAttributes {
        return {
            flags: this.data[idx + AtlasEntry.FLAGS],
            foreground: this.data[idx + AtlasEntry.FOREGROUND],
            background: this.data[idx + AtlasEntry.BACKGROUND]
        };
    }

    /**
     * Returns a slot index in the atlas for the given text attributes.
     * To be called upon attributes changes, e.g. by SGR.
     * NOTE: The ref counter is set to 0 for a new slot index, thus
     * values will get overwritten if not referenced in between.
     * <strong i="12">@param</strong> {TextAttributes} attributes
     * <strong i="13">@return</strong> {number}
     */
    getSlot(attributes: TextAttributes): number {
        // find matching attributes slot
        const sameFlag = this.flagTree[attributes.flags];
        if (sameFlag) {
            for (let i = 0; i < sameFlag.length; ++i) {
                let idx = sameFlag[i];
                if (this.data[idx + AtlasEntry.FOREGROUND] === attributes.foreground
                    && this.data[idx + AtlasEntry.BACKGROUND] === attributes.background) {
                    return idx;
                }
            }
        }
        // try to insert into a previously freed slot
        const freed = this.freedSlots.pop();
        if (freed) {
            this.setData(freed, attributes);
            return freed;
        }
        // else assign new slot
        for (let i = this.biggestIdx; i < this.data.length; i += 4) {
            if (!this.data[i]) {
                this.setData(i, attributes);
                if (i > this.biggestIdx)
                    this.biggestIdx = i;
                return i;
            }
        }
        // could not find a valid slot --> resize storage
        const data = new Uint32Array(this.data.length * 2);
        for (let i = 0; i < this.data.length; ++i)
            data[i] = this.data[i];
        const idx = this.data.length;
        this.data = data;
        this.setData(idx, attributes);
        return idx;
    }

    /**
     * Increment ref counter.
     * To be called for every terminal cell, that holds `idx` as text attributes.
     * <strong i="14">@param</strong> {number} idx
     */
    ref(idx: number): void {
        this.data[idx]++;
    }

    /**
     * Decrement ref counter. Once dropped to 0 the slot will be reused.
     * To be called for every cell that gets removed or reused with another value.
     * <strong i="15">@param</strong> {number} idx
     */
    unref(idx: number): void {
        this.data[idx]--;
        if (!this.data[idx]) {
            let treePart = this.flagTree[this.data[idx + AtlasEntry.FLAGS]];
            treePart.splice(treePart.indexOf(this.data[idx]), 1);
        }
    }
}

let atlas = new TextAttributeAtlas(2);
let a1 = atlas.getSlot({flags: 12, foreground: 13, background: 14});
atlas.ref(a1);
// atlas.unref(a1);
let a2 = atlas.getSlot({flags: 12, foreground: 13, background: 15});
atlas.ref(a2);
let a3 = atlas.getSlot({flags: 13, foreground: 13, background: 16});
atlas.ref(a3);
let a4 = atlas.getSlot({flags: 13, foreground: 13, background: 16});
console.log(atlas);
console.log(a1, a2, a3, a4);
console.log('a1', atlas.getAttributes(a1));
console.log('a2', atlas.getAttributes(a2));
console.log('a3', atlas.getAttributes(a3));
console.log('a4', atlas.getAttributes(a4));

Éditer:
La pénalité d'exécution est presque nulle, pour mon benchmark avec ls -lR /usr/lib cela ajoute moins de 1 ms au temps d'exécution total d'environ 2,3 s. Remarque intéressante : la commande définit moins de 64 emplacements d'attributs de texte différents pour la sortie de 5 Mo de données et économisera plus de 20 Mo une fois entièrement implémentée.

Réalisation de prototypes de PR pour tester certaines modifications apportées au tampon (voir https://github.com/xtermjs/xterm.js/pull/1528#issue-196949371 pour l'idée générale derrière les modifications) :

  • PR #1528 : atlas des attributs
  • PR #1529 : supprime wcwidth et charCode du tampon
  • PR #1530 : remplacer la chaîne dans le tampon par des points de code / valeur d'index de stockage de cellule

@jerch ce pourrait être une bonne idée de rester à l'écart du mot atlas pour cela afin que "atlas" signifie toujours "atlas de texture". Quelque chose comme store ou cache serait probablement mieux ?

oh ok, "cache" c'est bien.

Je suppose que j'en ai fini avec les PR du banc d'essai. Veuillez également consulter les commentaires des relations publiques pour obtenir le contexte du résumé approximatif suivant.

Proposition:

  1. Construisez un AttributeCache pour contenir tout le nécessaire pour styliser une seule cellule terminale. Voir #1528 pour une première version de comptage de références qui peut également contenir de vraies spécifications de couleurs. Le cache peut également être partagé entre différentes instances de terminal si nécessaire pour économiser davantage de mémoire dans plusieurs applications de terminal.
  2. Construisez un StringStorage pour contenir de courtes chaînes de données de contenu de terminal. La version en #1530 évite même de stocker des chaînes de caractères uniques en "surchargeant" la signification du pointeur. wcwidth devrait être déplacé ici.
  3. Réduisez le CharData actuel de [number, string, number, number] à [number, number] , où les nombres sont des pointeurs (numéros d'index) vers :

    • AttributeCache entrée

    • StringStorage entrée

Il est peu probable que les attributs changent beaucoup, donc un seul numéro 32 bits économisera beaucoup de mémoire au fil du temps. Le pointeur StringStorage est un véritable point de code unicode pour les caractères uniques, il peut donc être utilisé comme entrée code de CharData . La chaîne réelle est accessible par StringStorage.getString(idx) . Le quatrième champ wcwidth de CharData pouvait être accédé par StringStorage.wcwidth(idx) (pas encore implémenté). Il n'y a presque aucune pénalité d'exécution pour se débarrasser de code et wcwidth dans CharData (testé en #1529).

  1. Déplacez le CharData rétréci dans une implémentation de tampon dense basée sur Int32Array . Également testé en #1530 avec une classe stub (loin d'être entièrement fonctionnel), les avantages finaux sont susceptibles d'être :

    • Empreinte mémoire 80 % plus petite du tampon du terminal (de 5,5 Mo à 0,75 Mo)

    • légèrement plus rapide (pas encore testable, j'espère gagner 20% - 30% de vitesse)

    • Edit: beaucoup plus rapide - le temps d'exécution du script pour ls -lR /usr/lib tombé à 1,3 s (le maître est à 2,1 s) alors que l'ancien tampon est toujours actif pour la gestion du curseur, une fois supprimé, je m'attends à ce que le temps d'exécution descende en dessous de 1 s

Inconvénient - l'étape 4 demande beaucoup de travail car elle nécessitera quelques retouches sur l'interface du tampon. Mais bon - pour économiser 80% de la RAM et gagner toujours en performances d'exécution, ce n'est pas grave, n'est-ce pas ? :le sourire:

Il y a un autre problème sur lequel je suis tombé - la représentation actuelle des cellules vides. À mon humble avis, une cellule peut avoir 3 états :

  • vide : état initial de la cellule, rien n'y a encore été écrit ou le contenu a été supprimé. Il a une largeur de 1 mais aucun contenu. Actuellement utilisé dans blankLine et eraseChar , mais avec un espace comme contenu.
  • null : cellule après un caractère pleine largeur pour indiquer qu'elle n'a pas de largeur pour la représentation visuelle.
  • normal : la cellule contient du contenu et a une largeur visuelle (1 ou 2, peut-être plus grande une fois que nous prenons en charge de vrais trucs graphème/bidi, pas encore sûr de ça lol)

Le problème que je vois ici est qu'une cellule vide ne se distingue pas d'une cellule normale avec un espace inséré, les deux se ressemblent au niveau du tampon (même contenu, même largeur). Je n'ai écrit aucun code de rendu/de sortie, mais je m'attends à ce que cela conduise à des situations délicates au niveau de la sortie. Esp. la gestion de l'extrémité droite d'une ligne peut devenir lourde.
Un terminal avec 15 cols, d'abord une sortie de chaîne, qui s'est enroulée :

1: 'H', 'e', 'l', 'l', 'o', ' ', 't', 'e', 'r', 'm', 'i', 'n', 'a', 'l', ' '
2: 'w', 'o', 'r', 'l', 'd', '!', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '

versus une liste de dossiers avec ls :

1: 'R', 'e', 'a', 'd', 'm', 'e', '.', 'm', 'd', ' ', ' ', ' ', ' ', ' ', ' '
2: 'f', 'i', 'l', 'e', 'A', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '

Le premier exemple contient un espace réel après le mot 'terminal', le deuxième exemple n'a jamais touché les cellules après 'Readme.md'. La façon dont il est représenté au niveau du tampon est parfaitement logique pour le cas standard pour imprimer le contenu en tant que sortie terminale à l'écran (la pièce doit être prise de toute façon), mais pour les outils qui essaient de traiter les chaînes de contenu comme une sélection de souris ou un gestionnaire de refusion, il n'est plus clair d'où viennent les espaces.

Plus ou moins, cela conduit à la question suivante : comment déterminer la longueur réelle du contenu d'une ligne (nombre de cellules contenant quelque chose du côté gauche) ? Une approche simple compterait les cellules vides du côté droit, encore une fois le double sens d'en haut rend cela difficile à déterminer.

Proposition:
À mon humble avis, cela est facilement réparable en utilisant un autre espace réservé pour les cellules vides, par exemple un caractère de contrôle ou la chaîne vide et remplacez ceux-ci dans le processus de rendu si nécessaire. Peut-être que le rendu d'écran peut également en bénéficier, car il n'aura peut-être pas du tout à gérer ces cellules (cela dépend de la façon dont la sortie est générée).

Btw, pour la chaîne enveloppée ci-dessus, cela conduit également au problème isWrapped , qui est essentiel pour un redimensionnement redistribué ou une gestion correcte de la sélection par copier-coller. À mon humble avis, nous ne pouvons pas supprimer cela, mais nous devons l'intégrer mieux que l'atm.

@jerch travail impressionnant ! :souriant:

1 Créez un AttributeCache pour contenir tout le nécessaire pour styliser une seule cellule terminale. Voir #1528 pour une première version de comptage de références qui peut également contenir de vraies spécifications de couleurs. Le cache peut également être partagé entre différentes instances de terminal si nécessaire pour économiser davantage de mémoire dans plusieurs applications de terminal.

A fait quelques commentaires sur #1528.

2 Créez un StringStorage pour contenir de courtes chaînes de données de contenu de terminal. La version en #1530 évite même de stocker des chaînes de caractères uniques en "surchargeant" la signification du pointeur. wcwidth doit être déplacé ici.

A fait quelques commentaires sur #1530.

4 Déplacez le CharData rétréci dans une implémentation de tampon dense basée sur Int32Array. Également testé en #1530 avec une classe stub (loin d'être entièrement fonctionnel), les avantages finaux sont susceptibles d'être :

Pas encore totalement convaincu par cette idée, je pense que cela nous mordra fort lorsque nous implémenterons le reflow. Il semble que chacune de ces étapes puisse être effectuée dans l'ordre afin que nous puissions voir comment les choses se passent et voir s'il est logique de le faire une fois que nous aurons terminé 3.

Il y a un autre problème sur lequel je suis tombé - la représentation actuelle des cellules vides. Imho une cellule peut avoir 3 états

Voici un exemple de bogue qui est sorti de ce https://github.com/xtermjs/xterm.js/issues/1286 , :+1: pour différencier les cellules d'espacement et les cellules "vides"

Btw, pour la chaîne enveloppée ci-dessus, cela conduit également au problème isWrapped, qui est essentiel pour un redimensionnement redistribué ou une gestion correcte de la sélection copier-coller. À mon humble avis, nous ne pouvons pas supprimer cela, mais nous devons l'intégrer mieux que l'atm.

Je vois isWrapped disparaître lorsque nous aborderons https://github.com/xtermjs/xterm.js/issues/622 car CircularList ne contiendra que des lignes non emballées.

Pas encore totalement convaincu par cette idée, je pense que cela nous mordra fort lorsque nous implémenterons le reflow. Il semble que chacune de ces étapes puisse être effectuée dans l'ordre afin que nous puissions voir comment les choses se passent et voir s'il est logique de le faire une fois que nous aurons terminé 3.

Oui je suis avec toi (toujours amusant de jouer avec cette approche totalement différente). 1 et 2 peuvent être triés sur le volet, 3 peuvent être appliqués en fonction de 1 ou 2. 4 est facultatif, nous pourrions simplement nous en tenir à la disposition actuelle du tampon. Les économies de mémoire sont comme ceci :

  1. 1 + 2 + 3 en CircularList : économise 50% (~2.8MB de ~5.5 MB)
  2. 1 + 2 + 3 + 4 à mi-chemin - il suffit de mettre les données de la ligne dans un tableau typé mais de s'en tenir à l'accès à l'index de ligne : économise 82% (~ 0,9 Mo)
  3. Tableau 1 + 2 + 3 + 4 entièrement dense avec arithmétique de pointeur : économise 87 % (~0,7 Mo)

1. est très facile à mettre en œuvre, le comportement de la mémoire avec un scrollBack plus grand affichera toujours la mauvaise mise à l'échelle comme indiqué ici https://github.com/xtermjs/xterm.js/pull/1530#issuecomment-403542479 mais à un niveau moins toxique
2. Légèrement plus difficile à implémenter (plus d'indirections au niveau de la ligne nécessaires), mais permettra de garder intacte l'API supérieure de Buffer . Imho l'option à choisir - grosse sauvegarde de la mémoire et toujours facile à intégrer.
3. 5% d'économies de mémoire en plus que l'option 2, difficile à mettre en œuvre, modifiera toutes les API et donc littéralement toute la base de code. Imho plus d'intérêt académique ou pour les jours de pluie ennuyeux à mettre en œuvre lol.

@Tyriar J'ai effectué d'autres tests avec de la rouille pour l'utilisation de l'assemblage Web et j'ai réécrit l'analyseur. Notez que mes compétences en rouille sont un peu "rouillées" car je ne suis pas encore allée plus loin, donc ce qui suit pourrait être le résultat d'un code de rouille faible. Résultats:

  • Le traitement des données dans la partie wasm est légèrement plus rapide (5 - 10 %).
  • Les appels de JS dans wasm créent des frais généraux et consomment tous les avantages d'en haut. En fait, c'était environ 20% plus lent.
  • Le "binaire" sera plus petit que l'homologue JS (pas vraiment mesuré puisque je n'ai pas implémenté tout).
  • Pour que la transition JS <--> wasm se fasse facilement, un bloatcode est nécessaire pour gérer les types JS (uniquement la traduction de la chaîne).
  • Nous ne pouvons pas éviter la traduction de JS en wasm car le DOM du navigateur et les événements n'y sont pas accessibles. Il ne pouvait être utilisé que pour les pièces de base, qui ne sont plus critiques en termes de performances (à part la consommation de mémoire).

À moins que nous ne voulions réécrire toutes les bibliothèques principales en rouille (ou dans tout autre langage compatible wasm), nous ne pouvons rien gagner à passer à un wasm lang à mon humble avis. Un avantage des langages wasm d'aujourd'hui est le fait que la plupart prennent en charge la gestion explicite de la mémoire (pourrait nous aider à résoudre le problème de la mémoire tampon), les inconvénients sont l'introduction d'un langage totalement différent dans un projet principalement axé sur TS/JS (une barrière élevée pour les ajouts de code) et les frais de traduction entre wasm et JS land.

TL;DR
xterm.js consiste à intégrer globalement des éléments JS généraux comme le DOM et les événements pour tirer le meilleur parti de l'assemblage Web, même pour une réécriture des parties principales.

@jerch belle enquête :smiley:

Les appels de JS dans wasm créent des frais généraux et consomment tous les avantages d'en haut. En fait, c'était environ 20% plus lent.

C'était également le problème majeur pour monaco devenu ArrayBuffer dans la mesure du possible devrait nous donner le meilleur équilibre entre les performances et la simplicité (facile à mettre en œuvre, barrière à l'entrée).

@Tyriar va essayer de proposer un AttributeStorage pour contenir les données RVB. Pas encore sûr du BST, pour le cas d'utilisation typique avec seulement quelques paramètres de couleur dans une session de terminal, ce sera pire au moment de l'exécution, peut-être que cela devrait être un drop-in d'exécution une fois que les couleurs dépassent un seuil donné. De plus, la consommation de mémoire augmentera beaucoup à nouveau, même si cela économisera toujours de la mémoire car les attributs ne sont stockés qu'une seule fois et non avec chaque cellule (le pire des cas, chaque cellule contenant des attributs différents en souffrira).
Savez-vous pourquoi la fg actuelle de bg 256 couleurs est basée sur 9 bits au lieu de 8 bits ? A quoi sert le bit supplémentaire ? Ici : https://github.com/xtermjs/xterm.js/blob/6691f809069a549b4808cd2e055398d2da15db37/src/InputHandler.ts#L1596
Pourriez-vous me donner la disposition actuelle des bits de attr ? Je pense qu'une approche similaire comme le "double sens" pour le pointeur StringStorage peut encore économiser de la mémoire, mais cela nécessiterait que le MSB de attr soit réservé à la distinction du pointeur et non utilisé à d'autres fins. Cela pourrait limiter la possibilité de prendre en charge d'autres drapeaux d'attributs plus tard (parce que FLAGS utilise déjà 7 bits), manque-t-il encore des drapeaux fondamentaux qui sont susceptibles de venir ?

Un nombre attr 32 bits dans le terme tampon pourrait être compressé comme ceci :

# 256 indexed colors
32:       0 (no RGB color)
31..25:   flags (7 bits)
24..17:   fg (8 bits, see question above)
16..9:    bg
8..1:     unused

# RGB colors
32:       1 (RGB color)
31..25:   flags (7 bits)
24..1:    pointer to RGB data (address space is 2^24, which should be sufficient)

De cette façon, le stockage n'a besoin de contenir que les données RVB dans deux nombres de 32 bits tandis que les drapeaux peuvent rester dans le nombre attr .

@jerch d'ailleurs je vous ai envoyé un mail, probablement encore une fois rongé par le filtre anti-spam 😛

Savez-vous pourquoi la valeur actuelle des couleurs fg et bg 256 est basée sur 9 bits au lieu de 8 bits ? A quoi sert le bit supplémentaire ?

Je pense qu'il est utilisé pour la couleur fg/bg par défaut (qui peut être sombre ou claire), il s'agit donc en fait de 257 couleurs.

https://github.com/xtermjs/xterm.js/pull/756/files

Pourriez-vous me donner la disposition actuelle des bits d'attr?

Je pense que c'est ça :

19+:     flags (see `FLAGS` enum)
18..18:  default fg flag
17..10:  256 fg
9..9:    default bg flag
8..1:    256 bg

Vous pouvez voir ce sur quoi j'ai atterri pour truecolor dans l'ancien PR https://github.com/xtermjs/xterm.js/pull/756/files :

/**
 * Character data, the array's format is:
 * - string: The character.
 * - number: The width of the character.
 * - number: Flags that decorate the character.
 *
 *        truecolor fg
 *        |   inverse
 *        |   |   underline
 *        |   |   |
 *   0b 0 0 0 0 0 0 0
 *      |   |   |   |
 *      |   |   |   bold
 *      |   |   blink
 *      |   invisible
 *      truecolor bg
 *
 * - number: Foreground color. If default bit flag is set, color is the default
 *           (inherited from the DOM parent). If truecolor fg flag is true, this
 *           is a 24-bit color of the form 0xxRRGGBB, if not it's an xterm color
 *           code ranging from 0-255.
 *
 *        red
 *        |       blue
 *   0x 0 R R G G B B
 *      |     |
 *      |     green
 *      default color bit
 *
 * - number: Background color. The same as foreground color.
 */
export type CharData = [string, number, number, number, number];

Donc, en cela, j'avais 2 drapeaux ; un pour la couleur par défaut (si ignorer tous les bits de couleur) et un pour truecolor (si faire une couleur 256 ou 16 mil).

Cela pourrait limiter la possibilité de prendre en charge d'autres drapeaux d'attributs plus tard (parce que FLAGS utilise déjà 7 bits), manquons-nous encore des drapeaux fondamentaux qui sont susceptibles de venir ?

Oui, nous voulons de la place pour des drapeaux supplémentaires, par exemple https://github.com/xtermjs/xterm.js/issues/580, https://github.com/xtermjs/xterm.js/issues/1145, je voudrais dites au moins laisser > 3 bits si possible.

Au lieu de données de pointeur à l'intérieur de l'attr lui-même, il pourrait y avoir une autre carte contenant des références aux données rgb ? mapAttrIdxToRgb: { [idx: number]: RgbData

@Tyriar Désolé, je n'étais pas en ligne depuis quelques jours et je crains que l'e-mail n'ait été rongé par le filtre anti-spam. Pourriez-vous le renvoyer s'il vous plaît ? :rougir:

Joué un peu avec des structures de données de recherche plus intelligentes pour le stockage attrs. Les arbres et une liste de sauts comme alternative moins chère sont les plus prometteurs en ce qui concerne l'espace et le temps d'exécution de recherche/insertion. En théorie lol. En pratique, ni l'un ni l'autre ne peut surpasser ma simple recherche de tableau qui me semble très étrange (bug dans le code quelque part ?)
J'ai téléchargé un fichier de test ici https://gist.github.com/jerch/ff65f3fb4414ff8ac84a947b3a1eec58 avec un tableau par rapport à un arbre rouge-noir penché à gauche, qui teste jusqu'à 10 millions d'entrées (ce qui est presque l'espace d'adressage complet). Le réseau est toujours en avance par rapport au LLRB, je soupçonne cependant que le seuil de rentabilité est d'environ 10M. Testé sur mon ancien ordinateur portable de 7 ans, peut-être que quelqu'un peut le tester aussi bien et même mieux - indiquez-moi quelques bogues dans les impl/tests.

Voici quelques résultats (avec numéros courants) :

prefilled             time for inserting 1000 * 1000 (summed up, ms)
items                 array        LLRB
100-10000             3.5 - 5      ~13
100000                ~12          ~15
1000000               8            ~18
10000000              20-25        21-28

Ce qui me surprend vraiment, c'est le fait que la recherche de tableau linéaire ne montre aucune croissance dans les régions inférieures, c'est jusqu'à 10 000 entrées stables à ~ 4 ms (peut-être lié au cache). Le test 10M montre à la fois un temps d'exécution pire que prévu, peut-être en raison de la pagination du mem. Peut-être que JS est trop éloigné de la machine avec le JIT et tous les opts/deopts qui se produisent, je pense toujours qu'ils ne peuvent pas éliminer une étape de complexité (bien que le LLRB semble être lourd sur un seul _n_ déplaçant ainsi le seuil de rentabilité pour O ( n) vs. O(logn) vers le haut)

Btw avec des données aléatoires, la différence est encore pire.

Je pense qu'il est utilisé pour la couleur fg/bg par défaut (qui peut être sombre ou claire), il s'agit donc en fait de 257 couleurs.

C'est donc distinguer SGR 39 ou SGR 49 de l'une des 8 couleurs de la palette ?

Au lieu de données de pointeur à l'intérieur de l'attr lui-même, il pourrait y avoir une autre carte contenant des références aux données rgb ? mapAttrIdxToRgb : { [idx : nombre] : RgbData

Cela introduirait une autre indirection avec une utilisation supplémentaire de la mémoire. Avec les tests ci-dessus, j'ai également testé la différence entre toujours conserver les drapeaux dans les attributs et les enregistrer avec les données RVB dans le stockage. Étant donné que la différence est d'environ 0,5 ms pour 1 million d'entrées, je n'opterais pas pour cette configuration d'attrs compliquée, mais copiez plutôt les drapeaux sur le stockage une fois que RVB est défini. Néanmoins, j'opterais pour la distinction du 32e bit entre les attrs directs et les pointeurs, car cela évitera du tout le stockage pour les cellules non RVB.

De plus, je pense que les 8 couleurs de palette par défaut pour fg/bg ne sont pas suffisamment représentées dans le tampon actuellement. En théorie, le terminal devrait prendre en charge les modes de couleur suivants :

  1. SGR 39 + SGR 49 couleur par défaut pour fg/bg (personnalisable)
  2. SGR 30-37 + SGR 40-47 8 palette de couleurs basses pour fg/bg (personnalisable)
  3. SGR 90-97 + SGR 100-107 8 haute palette de couleurs pour fg/bg (personnalisable)
  4. SGR 38;5;n + SGR 48;5;n 256 palette indexée pour fg/bg (personnalisable)
  5. SGR 38;2;r;g;b + SGR 48;2;r;g;b RVB pour fg/bg (non personnalisable)

Les options 2.) et 3.) peuvent être fusionnées en un seul octet (en les traitant comme une seule palette fg/bg de 16 couleurs), 4.) prend 2 octets et 5.) prendra finalement 6 octets supplémentaires. Nous avons encore besoin de quelques bits pour indiquer le mode de couleur.
Pour refléter cela au niveau du tampon, nous aurions besoin des éléments suivants :

bits        for
2           fg color mode (0: default, 1: 16 palette, 2: 256, 3: RGB)
2           bg color mode (0: default, 1: 16 palette, 2: 256, 3: RGB)
8           fg color for 16 palette and 256
8           bg color for 16 palette and 256
10          flags (currently 7, 3 more reserved for future usage)
----
30

Nous avons donc besoin de 30 bits d'un nombre de 32 bits, laissant 2 bits libres à d'autres fins. Le 32ème bit pourrait contenir le pointeur par rapport à l'indicateur d'attr direct en omettant le stockage pour les cellules non RVB.

Je suggère également de regrouper l'accès attr dans une classe pratique pour ne pas exposer les détails de l'implémentation à l'extérieur (voir le fichier de test ci-dessus, il existe une première version d'une classe TextAttributes pour y parvenir).

Désolé, il y a quelques jours que je n'étais pas en ligne et je crains que l'e-mail n'ait été rongé par le filtre anti-spam. Pourriez-vous le renvoyer s'il vous plaît ?

Renvoyer

Oh d'ailleurs, ces chiffres ci-dessus pour la recherche array vs llrb sont de la merde - je pense que cela a été gâché par l'optimiseur faisant des trucs étranges dans la boucle for. Avec une configuration de test légèrement différente, il montre clairement que O(n) vs. O(log n) croît beaucoup plus tôt (avec 1000 éléments pré-remplis étant déjà plus rapides avec l'arbre).

État actuel :

Après:

Une optimisation assez simple consiste à aplatir le tableau de tableaux en un seul tableau. C'est-à-dire qu'au lieu d'un BufferLine de _N_ colonnes ayant un _data tableau de _N_ CharData cellules, où chaque CharData est un tableau de 4, ayez juste un seul tableau de _4*N_ éléments. Cela élimine la surcharge d'objet des tableaux _N_. Cela améliore également la localisation du cache, il devrait donc être plus rapide. Un inconvénient est un code légèrement plus compliqué et plus laid, mais cela semble en valoir la peine.

Pour faire suite à mon commentaire précédent, il semble intéressant d'envisager d'utiliser un nombre variable d'éléments dans le tableau _data pour chaque cellule. En d'autres termes, une représentation avec état. Des changements de position aléatoires seraient plus coûteux, mais le balayage linéaire depuis le début d'une ligne peut être assez rapide, d'autant plus qu'un simple tableau est optimisé pour la localisation du cache. Une sortie séquentielle typique serait rapide, tout comme le rendu.

En plus de réduire l'espace, l'avantage d'un nombre variable d'éléments par cellule est une flexibilité accrue : des attributs supplémentaires (comme une couleur 24 bits), des annotations pour des cellules ou des plages spécifiques, des glyphes ou
éléments DOM imbriqués.

@PerBothner Merci pour vos idées ! Oui, j'ai déjà testé la disposition du tableau dense unique avec l'arithmétique du pointeur, elle montre la meilleure utilisation de la mémoire. Des problèmes surviennent lorsqu'il s'agit de redimensionner, cela signifie essentiellement de reconstruire tout le morceau de mémoire (copier) ou de copier rapidement dans un plus gros morceau et de réaligner les parties. C'est assez exp. et à mon humble avis, non justifié par l'économie de mémoire (testée dans certains PR de terrain de jeu répertoriés ci-dessus, l'économie était d'environ 10% par rapport à la nouvelle mise en œuvre de la ligne de tampon).

À propos de votre deuxième commentaire - nous en avons déjà discuté car cela faciliterait la gestion des lignes enroulées. Pour l'instant, nous avons décidé d'utiliser l'approche row X col pour la nouvelle disposition des tampons et de le faire en premier. Je pense que nous devrions résoudre ce problème une fois que nous aurons effectué la mise en œuvre du redimensionnement de la redistribution.

À propos de l'ajout de choses supplémentaires au tampon : nous faisons actuellement ici ce que font la plupart des autres terminaux - l'avance du curseur est déterminée par wcwidth ce qui garantit de rester compatible avec l'idée de pty/termios sur la façon dont les données doivent être mises en page. Cela signifie essentiellement que nous gérons au niveau du tampon uniquement des éléments tels que des paires de substitution et des combinaisons de caractères. Toutes les autres règles de jointure de "niveau supérieur" peuvent être appliquées par le menuisier de caractères dans le moteur de rendu (actuellement utilisé par https://github.com/xtermjs/xterm-addon-ligatures pour les ligatures). J'avais un PR ouvert pour également prendre en charge les graphèmes Unicode au niveau du tampon précoce, mais je pense que nous ne pouvons pas le faire à ce stade car la plupart des backends pty n'en ont aucune idée (y en a-t-il du tout?) Et nous nous retrouverions avec des conglomérats de caractères étranges . Il en va de même pour le vrai support BIDI, je pense que les graphèmes et BIDI sont mieux faits au stade du rendu pour garder les mouvements du curseur/cellule intacts.

La prise en charge des nœuds DOM attachés aux cellules semble vraiment intéressante, j'aime cette idée. Actuellement, ce n'est pas possible dans une approche directe car nous avons différents moteurs de rendu (DOM, canvas 2D et le nouveau moteur de rendu webgl brillant), je pense que cela pourrait toujours être réalisé pour tous les moteurs de rendu en positionnant une superposition là où elle n'est pas prise en charge nativement (seul le moteur de rendu DOM serait pouvoir le faire directement). Nous aurions besoin d'une sorte d'API au niveau du tampon pour annoncer ce truc et sa taille et le moteur de rendu pourrait faire le sale boulot. Je pense que nous devrions discuter/suivre cela avec un problème séparé.

Merci pour votre réponse détaillée.

_"Des problèmes surviennent lorsqu'il s'agit de redimensionner, cela signifie essentiellement de reconstruire tout le morceau de mémoire (copier) ou de copier rapidement dans un plus gros morceau et de réaligner des parties."_

Voulez-vous dire : lors du redimensionnement, nous devrions copier _4*N_ éléments plutôt que simplement _N_ éléments ?

Il peut être judicieux que le tableau contienne toutes les cellules d'une ligne logique (déballée). Par exemple, supposons une ligne de 180 caractères et un terminal de 80 colonnes. Dans ce cas, vous pourriez avoir 3 instances BufferLine partageant toutes le même tampon _4*180_-element _data , mais chaque BufferLine contiendrait également un décalage de début.

Eh bien, j'avais tout dans un grand tableau qui a été construit par [cols] x [rows] x [needed single cell space] . Donc, cela fonctionnait toujours comme une "toile" avec une hauteur et une largeur données. C'est vraiment efficace en mémoire et rapide pour un flux d'entrée normal, mais dès que insertCell / deleteCell est invoqué (un redimensionnement ferait cela), toute la mémoire derrière la position où l'action a lieu devrait être déplacé. Pour les petits scrollbacks (<10k), ce n'est même pas un problème non plus, c'est vraiment un obstacle pour >100k lignes.
Notez que l'impl de tableau typé actuel doit toujours effectuer ces décalages, mais moins toxique car il n'a qu'à déplacer le contenu de la mémoire jusqu'à la fin de la ligne.
J'ai pensé à différentes dispositions pour contourner les décalages coûteux, le champ principal pour enregistrer des décalages de mémoire absurdes serait de séparer en fait le défilement des "lignes de terminaux chauds" (les plus récentes jusqu'à terminal.rows ) puisque seuls ceux-ci peuvent être modifié par les sauts de curseur et les insertions/suppressions.

Le partage de la mémoire sous-jacente par plusieurs objets de ligne de tampon est une idée intéressante pour résoudre le problème d'emballage. Je ne sais pas encore comment cela peut fonctionner de manière fiable sans introduire une gestion explicite des références et autres. Dans une autre version, j'ai essayé de tout faire avec une gestion explicite de la mémoire, mais le compteur de références était un véritable écueil et se sentait mal dans le pays GC. (voir #1633 pour les primitives)

Edit: Btw, la gestion explicite de la mémoire était à égalité avec l'approche actuelle de la "mémoire par ligne", j'espérais de meilleures performances en raison d'une meilleure localisation du cache, je suppose qu'elle a été mangée par un peu plus d'expérience. gestion de la mémoire dans l'abstraction JS.

La prise en charge des nœuds DOM attachés aux cellules semble vraiment intéressante, j'aime cette idée. Actuellement, ce n'est pas possible dans une approche directe car nous avons différents moteurs de rendu (DOM, canvas 2D et le nouveau moteur de rendu webgl brillant), je pense que cela pourrait toujours être réalisé pour tous les moteurs de rendu en positionnant une superposition là où elle n'est pas prise en charge nativement (seul le moteur de rendu DOM serait pouvoir le faire directement).

C'est un peu hors sujet, mais je vois que nous finirons par avoir des nœuds DOM associés à des cellules dans la fenêtre qui agiront de la même manière que les couches de rendu du canevas. De cette façon, les consommateurs pourront "décorer" les cellules en utilisant HTML et CSS et n'auront pas besoin d'entrer dans l'API canvas.

Il peut être judicieux que le tableau contienne toutes les cellules d'une ligne logique (déballée). Par exemple, supposons une ligne de 180 caractères et un terminal de 80 colonnes. Dans ce cas, vous pourriez avoir 3 instances BufferLine partageant toutes le même tampon _data de 4*180 éléments, mais chaque BufferLine contiendrait également un décalage de début.

Le plan de redistribution qui a été mentionné ci-dessus est capturé dans https://github.com/xtermjs/xterm.js/issues/622#issuecomment -375403572, fondamentalement, nous voulons avoir le tampon déballé réel, puis une vue sur le dessus qui gère les nouvelles lignes pour un accès rapide à n'importe quelle ligne donnée (optimisant également les redimensionnements horizontaux).

L'utilisation de l'approche du tableau dense peut être quelque chose que nous pourrions examiner, mais il semble que cela ne vaudrait pas la peine de gérer les sauts de ligne non emballés dans un tel tableau et le désordre qui survient lorsque les lignes sont coupées du haut de le tampon de défilement. Quoi qu'il en soit, je ne pense pas que nous devrions examiner de tels changements jusqu'à ce que le #791 soit terminé et que nous examinions le #622.

Avec PR #1796, l'analyseur prend en charge les tableaux typés, ce qui ouvre la porte à de nombreuses optimisations supplémentaires, d'autre part également à d'autres encodages d'entrée.

Pour l'instant, j'ai décidé d'utiliser Uint16Array , car il est facile à convertir avec les chaînes JS. Cela limite essentiellement le jeu à UCS2/UTF16, tandis que l'analyseur de la version actuelle peut également gérer UTF32 (UTF8 n'est pas pris en charge). Le tampon de terminal basé sur un tableau typé est actuellement configuré pour UTF32, la conversion UTF16 -> UTF32 est effectuée en InputHandler.print . A partir de là, plusieurs directions sont possibles :

  • faire tout UTF16, donc transformer le tampon de terminal en UTF16 aussi
    Oui, toujours pas déterminé quelle route prendre ici, mais j'ai testé plusieurs dispositions de tampon et est arrivé à la conclusion qu'un nombre 32 bits donne suffisamment de place pour stocker le charcode réel + wcwidth + débordement de combinaison possible (traité de manière totalement différente), alors que 16 bits ne peut pas le faire sans sacrifier les précieux bits de code de char. Notez que même avec un tampon UTF16, nous devons toujours effectuer la conversion UTF32, car wcwidth fonctionne sur les points de code Unicode. Notez également que le tampon basé sur UTF16 permettrait d'économiser plus de mémoire pour les codes de caractères inférieurs, en fait, des codes de caractères supérieurs à ceux du plan BMP se produiront rarement. Cela nécessite encore quelques investigations.
  • faire l'analyseur UTF32
    C'est assez simple, il suffit de remplacer tous les tableaux typés par la variante 32 bits. Inconvénient - la conversion UTF16 en UTF32 devrait être effectuée au préalable, ce qui signifie que toute l'entrée est convertie, même les séquences d'échappement qui ne seront jamais formées d'un code de caractère > 255.
  • rendre wcwidth compatible UTF16
    Oui, s'il s'avère que UTF16 est plus adapté au tampon de terminal, cela devrait être fait.

À propos d'UTF8 : l'analyseur ne peut actuellement pas gérer les séquences UTF8 natives, principalement en raison du fait que les caractères intermédiaires entrent en conflit avec les caractères de contrôle C1. De plus, UTF8 a besoin d'une gestion de flux appropriée avec des états intermédiaires supplémentaires, c'est désagréable et à mon humble avis, ne doit pas être ajouté à l'analyseur. UTF8 sera mieux géré au préalable, et peut-être avec un droit de conversion en UTF32 pour une gestion plus facile des points de code partout.

En ce qui concerne un éventuel encodage d'entrée UTF8 et la disposition du tampon interne, j'ai fait un test approximatif. Pour exclure l'impact beaucoup plus important du moteur de rendu canvas sur le temps d'exécution total, je l'ai fait avec le moteur de rendu webgl à venir. Avec mon ls -lR /usr/lib benchmark j'obtiens les résultats suivants :

  • maître actuel + moteur de rendu webgl :
    grafik

  • branche de terrain de jeu, applique #1796, des parties de #1811 et le moteur de rendu webgl :
    grafik

La branche de terrain de jeu effectue une conversion précoce d'UTF8 en UTF32 avant d'effectuer l'analyse et le stockage (la conversion ajoute ~ 30 ms). L'accélération est principalement obtenue par les 2 fonctions chaudes pendant le flux d'entrée, EscapeSequenceParser.parse (120 ms contre 35 ms) et InputHandler.print (350 ms contre 75 ms). Les deux bénéficient beaucoup du commutateur de tableau typé en économisant des appels .charCodeAt .
J'ai également comparé ces résultats avec un tableau de type intermédiaire UTF16 - EscapeSequenceParser.parse est légèrement plus rapide (~ 25 ms) mais InputHandler.print prend du retard en raison de l'appariement de substitution et de la recherche de point de code nécessaires dans wcwidth (120 ms).
Notez également que je suis déjà à la limite où le système peut fournir les données ls (i7 avec SSD) - l'accélération gagnée ajoute du temps d'inactivité au lieu d'accélérer l'exécution.

Sommaire:
À mon humble avis, la gestion d'entrée la plus rapide que nous puissions obtenir est un mélange de transport UTF8 + UTF32 pour la représentation du tampon. Alors que le transport UTF8 a le meilleur débit d'octets pour une entrée de terminal typique et supprime les conversions absurdes de pty à travers plusieurs couches de tampons jusqu'à Terminal.write , le tampon basé sur UTF32 peut stocker les données assez rapidement. Ce dernier est livré avec une empreinte mémoire légèrement plus élevée que l'UTF16, tandis que l'UTF16 est légèrement plus lent en raison d'une gestion des caractères plus compliquée avec plus d'indirections.

Conclusion:
Nous devrions utiliser la disposition de tampon basée sur UTF32 pour le moment. Nous devrions également envisager de passer à l'encodage d'entrée UTF8, mais cela nécessite encore de réfléchir davantage aux modifications de l'API et aux implications pour les intégrateurs (il semble que le mécanisme ipc d'électron ne puisse pas gérer les données binaires sans l'encodage BASE64 et l'encapsulation JSON, ce qui contrecarrerait les efforts de perf).

Disposition du tampon pour la prise en charge des couleurs vraies à venir :

Actuellement, la disposition de tampon basée sur un tableau typé est la suivante (une cellule):

|    uint32_t    |    uint32_t    |    uint32_t    |
|      attrs     |    codepoint   |     wcwidth    |

attrs contient tous les drapeaux nécessaires + les couleurs FG et BG basées sur 9 bits. codepoint utilise 21 bits (le maximum est de 0x10FFFF pour UTF32) + 1 bit pour indiquer la combinaison de caractères et wcwidth 2 bits (plages de 0 à 2).

L'idée est de réorganiser les bits à un meilleur taux de compression pour faire de la place aux valeurs RVB supplémentaires :

  • mettre wcwidth dans les bits de poids fort inutilisés de codepoint
  • diviser les attrs en un groupe FG et BG avec 32 bits, distribuer les drapeaux en bits inutilisés
|             uint32_t             |        uint32_t         |        uint32_t         |
|              content             |            FG           |            BG           |
| comb(1) wcwidth(2) codepoint(21) | flags(8) R(8) G(8) B(8) | flags(8) R(8) G(8) B(8) |

L'avantage de cette approche est l'accès relativement bon marché à chaque valeur par un accès à un index et max. Opérations sur 2 bits (et/ou + shift).

L'empreinte mémoire est stable par rapport à la variante actuelle, mais reste assez élevée avec 12 octets par cellule. Cela pourrait être encore optimisé en sacrifiant du temps d'exécution avec le passage à UTF16 et une indirection attr :

|        uint16_t        |              uint16_t               |
|    BMP codepoint(16)   | comb(1) wcwidth(2) attr pointer(13) |

Nous en sommes maintenant à 4 octets par cellule + un peu de place pour les attributs. Désormais, les attrs pourraient également être recyclés pour d'autres cellules. Ouai mission accomplie ! - Euh, une seconde...

En comparant l'empreinte mémoire, la deuxième approche gagne clairement. Ce n'est pas le cas pour le runtime, il y a trois facteurs principaux qui augmentent beaucoup le runtime :

  • attr indirection
    Le pointeur attr a besoin d'une recherche de mémoire supplémentaire dans un autre conteneur de données.
  • correspondance d'attr
    Pour vraiment économiser de la place avec la deuxième approche, l'attr donné devrait être comparé à des attr déjà enregistrés. Il s'agit d'une action lourde, une approche directe en parcourant simplement toutes les valeurs existantes est en O (n) pour n attrs sauvegardés, mes expériences d'arbre RB se sont terminées par un avantage de mémoire presque nul tout en étant toujours en O (log n), par rapport au accès à l'index dans l'approche 32 bits avec O(1). De plus, un arbre a un temps d'exécution pire pour quelques éléments enregistrés (c'est un peu plus rentable d'environ> 100 entrées avec mon impl d'arbre RB).
  • Couplage de substitution UTF16
    Avec un tableau typé 16 bits, nous devons dégrader en UTF16 pour les points de code, ce qui a également introduit une pénalité d'exécution (comme décrit dans le commentaire ci-dessus). Notez que des points de code supérieurs à BMP se produisent rarement, mais la seule vérification si un point de code formerait une paire de substitution ajoute ~ 50 ms.

Le côté sexy de la deuxième approche est l'économie de mémoire supplémentaire. Je l'ai donc testé avec la branche Playground (voir commentaire ci-dessus) avec une implémentation BufferLine modifiée :

grafik

Oui, nous sommes un peu revenus à notre point de départ avant de passer aux tableaux de type UTF8 + dans l'analyseur. L'utilisation de la mémoire est cependant passée de ~ 1,5 Mo à ~ 0,7 Mo (application de démonstration avec 87 cellules et 1000 lignes de défilement).

À partir de là, il s'agit d'économiser de la mémoire par rapport à la vitesse. Étant donné que nous avons déjà économisé beaucoup de mémoire en passant des tableaux js aux tableaux typés (chute de ~ 5,6 Mo à ~ 1,5 Mo pour le tas C++, coupant le comportement toxique du tas JS et GC), je pense que nous devrions aller ici avec la variante la plus rapide. Une fois que l'utilisation de la mémoire devient à nouveau un problème urgent, nous pouvons toujours passer à une disposition de tampon plus compacte, comme décrit dans la deuxième approche ici.

Je suis d'accord, optimisons la vitesse tant que la consommation de mémoire n'est pas un problème. J'aimerais également éviter autant que possible l'indirection car cela rend le code plus difficile à lire et à maintenir. Nous avons déjà pas mal de concepts et d'ajustements dans notre base de code qui rendent difficile pour les gens (y compris moi 😅) de suivre le flux de code - et en apporter plus devrait toujours être justifié par une très bonne raison. IMO sauver un autre mégaoctet de mémoire ne le justifie pas.

Néanmoins, je prends beaucoup de plaisir à lire et à apprendre de vos exercices, merci de les partager avec autant de détails !

@mofux Ouais, c'est vrai - la complexité du code est beaucoup plus élevée (lecture anticipée de substitution UTF16, calculs de points de code intermédiaires, conteneur d'arbre avec ref comptant sur les entrées attr).
Et comme la disposition 32 bits est principalement une mémoire plate (seuls les caractères de combinaison ont besoin d'indirection), il y a plus d'optimisations possibles (également partie de #1811, pas encore testé pour le moteur de rendu).

Il y a un gros avantage à s'adresser à un objet attr : c'est beaucoup plus extensible. Vous pouvez ajouter des annotations, des glyphes ou des règles de peinture personnalisées. Vous pouvez stocker les informations de lien d'une manière éventuellement plus propre et plus efficace. Peut-être définir une interface ICellPainter qui sait comment rendre sa cellule, et sur laquelle vous pouvez également accrocher des propriétés personnalisées.

Une idée est d'utiliser deux tableaux par BufferLine : un Uint32Array et un tableau ICellPainter, avec un élément chacun pour chaque cellule. L'ICellPainter actuel est une propriété de l'état de l'analyseur, et vous réutilisez donc simplement le même ICellPainter tant que l'état de la couleur/de l'attribut ne change pas. Si vous devez ajouter des propriétés spéciales à une cellule, vous devez d'abord cloner ICellPainter (s'il peut être partagé).

Vous pouvez pré-allouer ICellPainter pour les combinaisons de couleurs/attributs les plus courantes - à tout le moins avoir un objet unique correspondant aux couleurs/attributs par défaut.

Les modifications de style (telles que la modification des couleurs de premier plan/arrière-plan par défaut) peuvent être implémentées en mettant simplement à jour les instances ICellPainter correspondantes, sans avoir à mettre à jour chaque cellule.

Il existe des optimisations possibles : Par exemple, utilisez différentes instances ICellPainter pour les caractères à largeur simple et double (ou caractères à largeur nulle). (Cela économise 2 bits dans chaque élément Uint32Array.) Il y a 11 bits d'attribut disponibles dans Uint32Array (plus si nous optimisons pour les caractères BMP). Ceux-ci peuvent être utilisés pour coder les combinaisons de couleurs/attributs les plus courantes/utiles, qui peuvent être utilisées pour indexer les instances ICellPainter les plus courantes. Si c'est le cas, le tableau ICellPainter peut être alloué paresseusement - c'est-à-dire seulement si une cellule de la ligne nécessite un ICellPainter "moins commun".

On pourrait également supprimer le tableau _combined pour les caractères non BMP et les stocker dans ICellPainter. (Cela nécessite un ICellPainter unique pour chaque caractère non BMP, il y a donc un compromis ici.)

@PerBothner Ouais, une indirection est plus polyvalente et donc mieux adaptée aux extras rares. Mais comme ils sont rares, j'aimerais ne pas les optimiser en premier lieu.

Quelques notes sur ce que j'ai essayé dans plusieurs bancs d'essai :

  • contenu de la chaîne de cellules
    Venant moi-même du C++, j'ai essayé de regarder le problème comme je le ferais en C++, alors j'ai commencé avec des pointeurs pour le contenu. Il s'agissait d'un simple pointeur de chaîne, mais pointant la plupart du temps sur une seule chaîne de caractères. Quel gâchis. Ma première optimisation a donc été de me débarrasser de l'abstraction de chaîne en sauvegardant directement le point de code à la place de l'adresse (beaucoup plus facile en C/C++ qu'en JS). Cela a presque doublé l'accès en lecture/écriture tout en économisant 12 octets par cellule (8 octets pointeur + 4 octets sur chaîne, 64 bits avec 32 bits wchar_t). Note latérale - la moitié de l'augmentation de la vitesse ici est liée au cache (le cache manque en raison d'emplacements de chaîne aléatoires). Cela est devenu clair avec ma solution de contournement pour combiner le contenu des cellules - un morceau de mémoire dans lequel j'ai indexé lorsque codepoint avait le bit combiné défini (l'accès était plus rapide ici en raison d'une meilleure localisation du cache, testé avec valgrind). Transféré à JS, l'augmentation de la vitesse n'était pas si importante en raison de la conversion de chaîne en nombre nécessaire (encore plus rapide cependant), mais l'économie de mémoire était encore plus importante (devinez en raison d'un espace de gestion supplémentaire pour les types JS). Le problème était le StringStorage global pour les trucs combinés avec la gestion explicite de la mémoire, un gros anti-modèle dans JS. Une solution rapide était l'objet _combined , qui délègue le nettoyage au GC. C'est toujours sujet à changement et btw est censé stocker du contenu de chaîne arbitraire lié aux cellules (fait cela avec des graphèmes à l'esprit, mais nous ne les verrons pas de sitôt car ils ne sont pris en charge par aucun backend). C'est donc l'endroit idéal pour stocker du contenu de chaîne supplémentaire cellule par cellule.
  • atouts
    Avec les attributs, j'ai commencé à "penser grand" - avec un AttributeStorage global pour tous les attributs jamais utilisés dans toutes les instances de terminal (voir https://github.com/jerch/xterm.js/tree/AttributeStorage). Du point de vue de la mémoire, cela a plutôt bien fonctionné, principalement parce que les utilisateurs n'utilisent qu'un petit ensemble d'attributs, même avec le support des vraies couleurs. Les performances n'étaient pas si bonnes - principalement en raison du comptage des références (chaque cellule devait jeter un coup d'œil deux fois dans cette mémoire étrangère) et de la correspondance attr. Et quand j'ai essayé d'adopter la référence pour JS, cela me semblait tout simplement faux - le point où j'ai appuyé sur le bouton "STOP". Entre-temps, il s'est avéré que nous avons déjà économisé des tonnes de mémoire et d'appels GC en passant à un tableau typé, donc la disposition de mémoire plate légèrement plus coûteuse peut payer son avantage de vitesse ici.
    Ce que j'ai testé yday (dernier commentaire) était un deuxième tableau typé au niveau de la ligne pour les attrs avec l'arbre de https://github.com/jerch/xterm.js/tree/AttributeStorage pour la correspondance (un peu comme votre idée ICellPainter ). Eh bien, les résultats ne sont pas prometteurs, donc je penche pour le moment vers la mise en page plate 32 bits.

Maintenant, cette mise en page plate 32 bits s'avère être optimisée pour les choses courantes et les extras inhabituels ne sont pas possibles avec elle. Vrai. Eh bien, nous avons toujours des marqueurs (pas habitués, donc je ne peux pas dire pour le moment de quoi ils sont capables), et oui - il y a encore des bits libres dans le tampon (ce qui est une bonne chose pour les besoins futurs, par exemple nous pourrions les utiliser comme drapeaux pour traitement spécial et autres).

Tbh pour moi, c'est dommage que la disposition 16 bits avec stockage attrs fonctionne si mal, réduire de moitié l'utilisation de la mémoire est toujours un gros problème (surtout lorsque ppl commence à utiliser des lignes de défilement > 10k), mais la pénalité d'exécution et la complexité du code sont plus importantes le mem supérieur a besoin d'atm à mon humble avis.

Pouvez-vous développer l'idée ICellPainter ? Peut-être que j'ai raté une fonctionnalité cruciale jusqu'à présent.

Mon objectif pour DomTerm était de permettre et d'encourager une interaction plus riche exactement ce qui est permis par un émulateur de terminal traditionnel. L'utilisation des technologies Web permet de nombreuses choses intéressantes, il serait donc dommage de se concentrer uniquement sur un émulateur de terminal traditionnel rapide. D'autant plus que de nombreux cas d'utilisation de xterm.js (comme les REPL pour les IDE) peuvent vraiment bénéficier d'aller au-delà du simple texte. Xterm.js fonctionne bien du côté de la vitesse (quelqu'un se plaint-il de la vitesse ?), mais il ne le fait pas aussi bien du côté des fonctionnalités (les gens se plaignent de l'absence de truecolor et de graphiques intégrés, par exemple). Je pense qu'il peut être intéressant de se concentrer un peu plus sur la flexibilité et un peu moins sur les performances.

_"Pouvez-vous développer l'idée d'ICellPainter ?"_

En général, ICellPainter encapsule toutes les données par cellule, à l'

interface ICellPainter {
    drawOnCanvas(ctx: CanvasRenderingContext2D, code: number, x: number, y: number);
    // transitional - to avoid allocating IGlyphIdentifier we should replace
    //  uses by pair of ICellPainter and code.  Also, a painter may do custom rendering,
    // such that there is no 'code' or IGlyphIdentifier.
    asGlyph(code: number): IGlyphIdentifier;
    width(): number; // in pixels for flexibility?
    height(): number;
    clone(): ICellPainter;
}

Le mappage d'une cellule à ICellPainter peut être effectué de différentes manières. L'évidence est que chaque BufferLine a un tableau ICellPainter, mais cela nécessite un pointeur de 8 octets (au moins) par cellule. Une possibilité consiste à combiner le tableau _combined avec le tableau ICellPainter : Si IS_COMBINED_BIT_MASK est défini, alors ICellPainter inclut également la chaîne combinée. Une autre optimisation possible consiste à utiliser les bits disponibles dans le Uint32Array comme index dans un tableau : cela ajoute une complication et une indirection supplémentaires, mais économise de l'espace.

J'aimerais nous encourager à vérifier si nous pouvons le faire comme monaco-éditeur le fait (je pense qu'ils ont trouvé une manière vraiment intelligente et performante). Au lieu de stocker de telles informations dans le tampon, ils vous permettent de créer decorations . Vous créez une décoration pour une plage de lignes/colonnes et elle s'en tiendra à cette plage :

// decorations are buffer-dependant (we need to know which buffer to decorate)
const decoration = buffer.createDecoration({
  type: 'link',
  data: 'https://www.google.com',
  range: { startRow: 2, startColumn: 5, endRow: 2, endColumn: 25 }
});

Plus tard, un moteur de rendu pourrait récupérer ces décorations et les dessiner.

Veuillez consulter ce petit exemple qui montre à quoi ressemble l'API de monaco-editor :
https://microsoft.github.io/monaco-editor/playground.html#interacting -with-the-editor-line-and-inline-decorations

Pour des choses comme le rendu d'images à l'intérieur du terminal, Monaco utilise un concept de zones de vue qui peuvent être vues (parmi d'autres concepts) dans un exemple ici :
https://microsoft.github.io/monaco-editor/playground.html#interacting -with-the-editor-listening-to-mouse-events

@PerBothner Thx pour des éclaircissements et le sketchup. Quelques notes à ce sujet.

Nous prévoyons éventuellement de déplacer la chaîne d'entrée + le tampon dans un webworker à l'avenir. Ainsi, le tampon est censé fonctionner à un niveau abstrait et nous ne pouvons pas encore utiliser de trucs liés au rendu/représentation comme des métriques de pixels ou des nœuds DOM. Je vois vos besoins pour cela en raison du fait que DomTerm est hautement personnalisable, mais je pense que nous devrions le faire avec une API de marqueur interne améliorée et pouvons apprendre ici de monaco/vscode (merci pour les pointeurs @mofux).
J'aimerais vraiment garder le tampon central exempt de choses inhabituelles, peut-être devrions-nous discuter des stratégies de marqueur possibles avec un nouveau problème ?

Je ne suis toujours pas satisfait du résultat des résultats du test de mise en page 16 bits. Puisqu'une décision finale n'est pas encore urgente (nous ne verrons rien de tout cela avant 3.11), je vais continuer à le tester avec quelques modifications (c'est toujours la solution la plus intrigante pour moi que la variante 32 bits).

|             uint32_t             |        uint32_t         |        uint32_t         |
|              content             |            FG           |            BG           |
| comb(1) wcwidth(2) codepoint(21) | flags(8) R(8) G(8) B(8) | flags(8) R(8) G(8) B(8) |

Je pense également que nous devrions commencer par quelque chose de proche de cela, nous pourrons explorer d'autres options plus tard, mais ce sera probablement la plus facile à mettre en place et à faire fonctionner. L'indirection d'attributs est certainement prometteuse pour l'OMI, car il n'y a généralement pas autant d'attributs distincts dans une session de terminal.

J'aimerais nous encourager à vérifier si nous pouvons le faire comme monaco-éditeur le fait (je pense qu'ils ont trouvé une manière vraiment intelligente et performante). Au lieu de stocker de telles informations dans le tampon, ils vous permettent de créer des décorations. Vous créez une décoration pour une plage de lignes/colonnes et elle s'en tiendra à cette plage :

Quelque chose comme ça est l'endroit où j'aimerais voir les choses aller. Une idée que j'avais dans ce sens était de permettre aux intégrateurs d'attacher des éléments DOM à des plages pour permettre de dessiner des éléments personnalisés. Il y a 3 choses auxquelles je peux penser pour le moment que j'aimerais accomplir avec ceci:

  • Dessinez les liens soulignés de cette façon (simplifiera considérablement la façon dont ils sont dessinés)
  • Autoriser les marqueurs sur les lignes, comme un * ou quelque chose
  • Autoriser les lignes à « clignoter » pour indiquer que quelque chose s'est passé

Tout cela peut être réalisé avec une superposition et c'est un type d'API assez accessible (exposant un nœud DOM) et peut fonctionner quel que soit le type de moteur de rendu.

Je ne suis pas sûr que nous voulions nous lancer dans l'idée de permettre aux intégrateurs de modifier la façon dont les couleurs d'arrière-plan et de premier plan sont dessinées.


@jerch Je vais mettre cela sur le jalon 3.11.0 car je considère que ce problème est terminé lorsque nous supprimons l'implémentation du tableau JS qui est prévue pour ce moment-là. Il est également prévu de fusionner

En outre, une grande partie de cette discussion ultérieure serait probablement meilleure sur https://github.com/xtermjs/xterm.js/issues/484 et https://github.com/xtermjs/xterm.js/issues/1852 (créé car il n'y avait pas de problème de décorations).

@Tyriar Woot - enfin fermé :sweat_smile:

🕺 🍾

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

Questions connexes

LB-J picture LB-J  ·  3Commentaires

zhangjie2012 picture zhangjie2012  ·  3Commentaires

travisobregon picture travisobregon  ·  3Commentaires

pfitzseb picture pfitzseb  ·  3Commentaires

fabiospampinato picture fabiospampinato  ·  4Commentaires