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
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
Les inconvénients
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
Int32Array
Les inconvénients
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
.flags
au lieu de [0]
)Les inconvénients
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
.flags
au lieu de [0]
)Les inconvénients
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
.flags
au lieu de [0]
)Les inconvénients
CharAttributes
par bloc ?CharAttributeEntry
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
.flags
au lieu de [0]
)Les inconvénients
Int32Array
ne fonctionnera pas car il faut beaucoup trop de temps pour reconvertir l'entier en caractère.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:
À 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é :
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 []
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 terminaleswcwidth(string) % cols
\n
(saut de ligne dur) : avance le curseur d'une ligne, marque la position dans la liste des pointeurs comme un saut de ligne\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é\r
aucune abstraction de cellule ni division de chaîne n'est nécessairecols
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
:
cols
arrière dans les sauts de ligne - le début du contenu actuel du terminalMaintenant, la partie dangereuse 2 - le moteur de rendu veut dessiner quelque chose :
Avantages:
InputHandler
la plus courante - print
Les inconvénients:
InputHandler
seront dangereuses dans le sens d'interrompre ce modèle de flux et de nécessiter une abstraction de cellule intermédiaireEh 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) :
print
passe de 190 Mo/s à 290 Mo/sprint
passe de 190 Mo/s à 320 Mo/sDans 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 :
avec Uint16Array :
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 :
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.uint16_t
pour UTF-16.InputHandler
avec des pointeurs vers cette mémoire au lieu de tranches de chaîne.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:
malloc
frais de free
(dépend de l'intelligence de l'allocateur/désallocateur)Les inconvénients:
: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
Épinglé les liens de @rebornix afin qu'ils ne se cassent pas lorsque des commits sont ajoutés 😃
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 :
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) :
@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:
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.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.CharData
actuel de [number, string, number, number]
à [number, number]
, où les nombres sont des pointeurs (numéros d'index) vers :AttributeCache
entréeStringStorage
entréeIl 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).
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 :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 sInconvé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 :
blankLine
et eraseChar
, mais avec un espace comme contenu.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 :
CircularList
: économise 50% (~2.8MB de ~5.5 MB)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:
À 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 :
SGR 39
+ SGR 49
couleur par défaut pour fg/bg (personnalisable)SGR 30-37
+ SGR 40-47
8 palette de couleurs basses pour fg/bg (personnalisable)SGR 90-97
+ SGR 100-107
8 haute palette de couleurs pour fg/bg (personnalisable)SGR 38;5;n
+ SGR 48;5;n
256 palette indexée pour fg/bg (personnalisable)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 :
wcwidth
compatible UTF16À 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 :
branche de terrain de jeu, applique #1796, des parties de #1811 et le moteur de rendu webgl :
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 |
où 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 :
wcwidth
dans les bits de poids fort inutilisés de codepoint
| 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 :
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 :
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 :
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.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.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:
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:
🕺 🍾
Commentaire le plus utile
État actuel :
Après: