Xterm.js: Verbesserungen der Pufferleistung

Erstellt am 13. Juli 2017  ·  73Kommentare  ·  Quelle: xtermjs/xterm.js

Problem

Speicher

Im Moment belegt unser Puffer zu viel Speicher, insbesondere für eine Anwendung, die mehrere Terminals mit großen Scrollbacks startet. Zum Beispiel benötigt die Demo mit einem 160x24-Terminal mit 5000 Scrollback gefüllt etwa 34 MB Speicher (siehe https://github.com/Microsoft/vscode/issues/29840#issuecomment-314539964). wahrscheinlich breitere Anschlüsse verwenden. Um Truecolor zu unterstützen (https://github.com/sourcelair/xterm.js/issues/484), muss jedes Zeichen 2 zusätzliche number Typen speichern, was den aktuellen Speicherverbrauch fast verdoppelt des Puffers.

Langsames Abrufen des Zeilentextes

Das andere Problem besteht darin, den tatsächlichen Text einer Zeile schnell abrufen zu müssen. Der Grund dafür, dass dies langsam ist, liegt an der Art und Weise, wie die Daten angeordnet sind; eine Zeile enthält ein Array von Zeichen, von denen jedes eine einzelne Zeichenfolge hat. Also konstruieren wir den String und dann wird er direkt danach zur Garbage Collection bereit sein. Bisher mussten wir dies überhaupt nicht tun, da der Text aus dem Zeilenpuffer (in der richtigen Reihenfolge) gezogen und in das DOM gerendert wird. Dies wird jedoch immer nützlicher, da wir xterm.js weiter verbessern und Funktionen wie die Auswahl und Links beide diese Daten abrufen. Wiederum mit dem 160x24/5000-Scrollback-Beispiel dauert es 30-60 ms, um den gesamten Puffer auf einem Macbook Pro Mitte 2014 zu kopieren.

Die Zukunft unterstützen

Ein weiteres potenzielles Problem in der Zukunft ist, wenn wir ein Ansichtsmodell einführen, das möglicherweise einige oder alle Daten im Puffer duplizieren muss, um Reflow zu implementieren (https://github.com/sourcelair .). /xterm.js/issues/622) richtig (https://github.com/sourcelair/xterm.js/pull/644#issuecomment-298058556) und wird möglicherweise auch benötigt, um Screenreader richtig zu unterstützen (https://github.com /sourcelair/xterm.js/issues/731). Es wäre sicherlich gut, etwas Spielraum zu haben, wenn es um das Gedächtnis geht.

Diese Diskussion begann in https://github.com/sourcelair/xterm.js/issues/484 , dies geht näher und schlägt einige zusätzliche Lösungen vor.

Ich tendiere zu Lösung 3 und zu Lösung 5, wenn es Zeit gibt und es eine deutliche Verbesserung zeigt. Würde mich über jedes Feedback freuen! /cc @jerch , @mofux , @rauchg , @parisk

1. Einfache Lösung

Dies ist im Grunde das, was wir jetzt tun, nur mit Truecolor fg und bg hinzugefügt.

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

type LineData = CharData[];

Vorteile

  • Sehr einfach

Nachteile

  • Zu viel Speicher verbraucht, würde unseren aktuellen Speicherverbrauch, der ohnehin schon zu hoch ist, fast verdoppeln.

2. Text aus CharData ziehen

Dies würde die Zeichenfolge eher an der Zeile als an der Zeile speichern, dies würde wahrscheinlich sehr große Vorteile bei der Auswahl und Verknüpfung bringen und wäre im Laufe der Zeit nützlicher, um schnellen Zugriff auf die gesamte Zeichenfolge einer Zeile zu haben.

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;

Vorteile

  • Keine Notwendigkeit, die Linie zu rekonstruieren, wann immer wir sie brauchen.
  • Weniger Arbeitsspeicher als heute aufgrund der Verwendung eines Int32Array

Nachteile

  • Um einzelne Zeichen langsam zu aktualisieren, müsste die gesamte Zeichenfolge für einzelne Zeichenänderungen neu generiert werden.

3. Speichern Sie Attribute in Bereichen

Ziehen Sie die Attribute heraus und verknüpfen Sie sie mit einem Bereich. Da es niemals überlappende Attribute geben kann, kann dies sequentiell angeordnet werden.

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

Vorteile

  • Weniger Speicher als heute, obwohl wir auch Truecolor-Daten speichern
  • Kann die Anwendung von Attributen optimieren, anstatt das Attribut jedes einzelnen Charakters zu überprüfen und von dem vorherigen zu unterscheiden
  • Kapselt die Komplexität des Speicherns der Daten in einem Array ( .flags statt [0] )

Nachteile

  • Das Ändern von Attributen eines Zeichenbereichs innerhalb eines anderen Bereichs ist komplexer

4. Attribute in einen Cache legen

Die Idee hier ist, die Tatsache zu nutzen, dass es in einer Terminalsitzung im Allgemeinen nicht so viele Stile gibt. Daher sollten wir nicht so wenige wie nötig erstellen und sie wiederverwenden.

// [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;
}

Vorteile

  • Ähnlicher Speicherverbrauch wie heute, obwohl wir auch Truecolor-Daten speichern
  • Kapselt die Komplexität des Speicherns der Daten in einem Array ( .flags statt [0] )

Nachteile

  • Weniger Speichereinsparungen als beim Range-Ansatz

5. Hybrid aus 3 & 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;
}

Vorteile

  • Voraussichtlich das schnellste und speichereffizienteste
  • Sehr speichereffizient, wenn der Puffer viele Blöcke mit Stilen enthält, aber nur von wenigen Stilen (der übliche Fall)
  • Kapselt die Komplexität des Speicherns der Daten in einem Array ( .flags statt [0] )

Nachteile

  • Komplexer als die anderen Lösungen, lohnt es sich möglicherweise nicht, den Cache einzubeziehen, wenn wir bereits ein einzelnes CharAttributes pro Block behalten?
  • Extra-Overhead in CharAttributeEntry Objekt
  • Das Ändern von Attributen eines Zeichenbereichs innerhalb eines anderen Bereichs ist komplexer

6. Hybrid aus 2 & 3

Dies nimmt die Lösung von 3, fügt aber auch eine träge ausgewertete Textzeichenfolge für einen schnellen Zugriff auf den Zeilentext hinzu. Da wir die Zeichen auch in CharData speichern, können wir sie träge auswerten.

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

Vorteile

  • Weniger Speicher als heute, obwohl wir auch Truecolor-Daten speichern
  • Kann die Anwendung von Attributen optimieren, anstatt das Attribut jedes einzelnen Charakters zu überprüfen und von dem vorherigen zu unterscheiden
  • Kapselt die Komplexität des Speicherns der Daten in einem Array ( .flags statt [0] )
  • Schnellerer Zugriff auf den aktuellen Zeilenstring

Nachteile

  • Zusätzlicher Speicher durch Hängen an Liniensträngen
  • Das Ändern von Attributen eines Zeichenbereichs innerhalb eines anderen Bereichs ist komplexer

Lösungen, die nicht funktionieren

  • Das Speichern des Strings als Int in einem Int32Array wird nicht funktionieren, da es viel zu lange dauert, das Int wieder in ein Zeichen zu konvertieren.
areperformance typplan typproposal

Hilfreichster Kommentar

Aktuellen Zustand:

Nach:

Alle 73 Kommentare

Ein anderer Ansatz, der gemischt werden könnte: Verwenden Sie indexeddb, websql oder filesystem api, um inaktive Scrollback-Einträge auf die Festplatte auszulagern 🤔

Toller Vorschlag. Ich stimme zu, dass 3. der beste Weg ist, um Speicher zu sparen und gleichzeitig True Color zu unterstützen.

Wenn wir dort ankommen und es weiterhin gut läuft, können wir dann optimieren, wie in 5. vorgeschlagen oder auf andere Weise, die uns zu diesem Zeitpunkt in den Sinn kommt und sinnvoll ist.

3. ist toll 👍.

@mofux , während es definitiv einen Anwendungsfall für die Verwendung von

Zum Thema Zukunftsförderung :
Je mehr ich darüber nachdenke, desto mehr reizt mich die Idee, ein WebWorker , das die ganze schwere Arbeit des Parsens der tty-Daten, der Pflege der Zeilenpuffer, des Abgleichens von Links, des Abgleichens von Suchtoken und dergleichen übernimmt. Grundsätzlich die schwere Arbeit in einem separaten Hintergrundthread erledigen, ohne die Benutzeroberfläche zu blockieren. Aber ich denke, dies sollte Teil einer separaten Diskussion sein, vielleicht in Richtung einer 4.0-Version 😉

+100 über WebWorker in der Zukunft, aber ich denke, wir brauchen Änderungslistenversionen von Browsern, die wir unterstützen, da nicht alle von ihnen es verwenden können ...

Wenn ich Int32Array sage, ist dies ein reguläres Array, wenn es von der Umgebung nicht unterstützt wird.

@mofux gutes Denken mit WebWorker in die Zukunft

@AndrienkoAleksandr ja, wenn wir WebWorker wollten, müssten wir auch die Alternative über die Feature-Erkennung unterstützen.

Wow schöne Liste :)

Ich tendiere auch eher zu 3., da es eine große Reduzierung des Speicherverbrauchs für über 90% der typischen Terminalnutzung verspricht. Imho-Speicheroptimierung sollte in dieser Phase das Hauptziel sein. Darüber hinaus könnten weitere Optimierungen für bestimmte Anwendungsfälle möglich sein (was mir in den Sinn kommt: "Leinwand wie Apps" wie ncurses und dergleichen werden Tonnen von Einzelzellen-Updates verwenden und die [start, end] Liste im Laufe der Zeit irgendwie verschlechtern) .

@AndrienkoAleksandr ja, ich mag die Webworker-Idee auch, da sie _etwas_ Last vom Hauptthread nehmen könnte. Das Problem hier ist (neben der Tatsache, dass es möglicherweise nicht von allen gewünschten Zielsystemen unterstützt wird) das _some_ - der JS-Anteil ist mit all den Optimierungen, die xterm.js im Laufe der Zeit gesehen hat, nicht mehr so ​​​​groß. Ein echtes Problem in Bezug auf die Leistung ist das Layout / Rendering des Browsers ...

@mofux Das Paging zu einem "fremden Speicher" ist eine gute Idee, obwohl es Teil einer höheren Abstraktion sein sollte und nicht des "gimme a interaktive Terminal-Widget-Dings", das xterm.js ist. Dies könnte durch ein Addon imho erreicht werden.

Offtopic: Habe einige Tests mit Arrays vs. typedarrays vs. asm.js durchgeführt. Alles was ich sagen kann - OMG, es ist wie 1 : 1,5 : 10 für einfache variable Lasten und Sets (auf FF noch mehr). Wenn die reine JS-Geschwindigkeit wirklich weh tut, könnte "use asm" Abhilfe schaffen. Aber ich würde dies als letzten Ausweg sehen, da es grundlegende Veränderungen bedeuten würde. Und Webassembly ist noch nicht versandfertig.

Offtopic: Habe einige Tests mit Arrays vs. typedarrays vs. asm.js durchgeführt. Alles was ich sagen kann - OMG, es ist wie 1 : 1,5 : 10 für einfache variable Lasten und Sets (auf FF noch mehr)

@jerch zur Verdeutlichung, ist das Arrays vs. typedarrays 1:1 bis 1:5?

Woops schöner Fang mit dem Komma - ich meinte 10:15:100 Geschwindigkeitstechnisch. Aber nur auf FF-typisierten Arrays waren etwas schneller als normale Arrays. asm ist auf allen Browsern mindestens 10-mal schneller als js-Arrays - getestet mit FF, Webkit (Safari), blink/V8 (Chrome, Opera).

@jerch cool, eine 50%ige Beschleunigung von typedarrays zusätzlich zu einem besseren Speicher wäre definitiv eine Investition wert.

Idee zum Speichern von Speicherplatz - vielleicht könnten wir die width für jedes Zeichen loswerden. Werde versuchen, eine weniger teure WCwidth-Version zu implementieren.

@jerch wir müssen ziemlich viel darauf zugreifen, und wir können es nicht faul laden oder so, weil wir beim Reflow die Breite jedes Zeichens im Puffer benötigen. Selbst wenn es schnell war, möchten wir es vielleicht trotzdem behalten.

Es könnte besser sein, es optional zu machen, wenn 1 angenommen wird, wenn es nicht angegeben ist:

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

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

@Tyriar Ja - nun, da ich es schon geschrieben habe, schau es dir bitte in PR #798 an
Die Beschleunigung beträgt auf meinem Computer das 10- bis 15-fache für die Kosten von 16 KB für die Nachschlagetabelle. Eventuell ist auch eine Kombination aus beidem möglich, wenn es noch benötigt wird.

Einige weitere Flags werden wir in Zukunft unterstützen: https://github.com/sourcelair/xterm.js/issues/580

Ein weiterer Gedanke: Nur der untere Teil des Terminals ( Terminal.ybase bis Terminal.ybase + Terminal.rows ) ist dynamisch. Das Scrollback, das den Großteil der Daten ausmacht, ist völlig statisch, vielleicht können wir dies nutzen. Ich wusste das bis vor kurzem nicht, aber selbst Dinge wie delete lines (DL, CSI Ps M) bringen den Scrollback nicht wieder runter, sondern fügen eine weitere Zeile ein. Auf ähnliche Weise löscht das Scrollen nach oben (SU, CSI Ps S) das Element bei Terminal.scrollTop und fügt ein Element bei Terminal.scrollBottom .

Die unabhängige Verwaltung des unteren dynamischen Teils des Terminals und das Drücken auf Scrollback, wenn die Leitung herausgeschoben wird, kann zu erheblichen Gewinnen führen. Zum Beispiel könnte der untere Teil ausführlicher sein, um das Modifizieren von Attributen, einen schnelleren Zugriff usw. zu begünstigen, während das Zurückblättern eher ein Archivierungsformat sein kann, wie oben vorgeschlagen.

Ein weiterer Gedanke: Es ist wahrscheinlich eine bessere Idee, CharAttributeEntry auf Zeilen zu beschränken, da die meisten Anwendungen so zu funktionieren scheinen. Auch wenn die Größe des Terminals geändert wird, wird rechts "leere" Auffüllung hinzugefügt, die nicht die gleichen Stile aufweist.

z.B:

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

Rechts von den Rot/Grün-Diffs befinden sich nicht gestylte "leere" Zellen.

@Tyriar
Gibt es eine Chance, dieses Thema wieder auf die Tagesordnung zu setzen? Zumindest bei ausgabeintensiven Programmen kann eine andere Art der Speicherung der Terminaldaten viel Speicher und Zeit sparen. Eine Mischung aus 2/3/4 wird eine enorme Durchsatzsteigerung bewirken, wenn wir vermeiden könnten, einzelne Zeichen des Eingabestrings aufzuteilen und zu speichern. Auch das Speichern der Attribute nur dann, wenn sie sich geändert haben, hilft, Speicher zu sparen.

Beispiel:
Mit dem neuen Parser konnten wir eine Menge Eingabezeichen einsparen, ohne mit den Attributen herumzuspielen, da wir wissen, dass sie sich in der Mitte dieses Strings nicht ändern. Die Attribute dieser Zeichenfolge könnten in einer anderen Datenstruktur oder einem anderen Attribut zusammen mit wcwidths (ja, wir brauchen diese immer noch, um den Zeilenumbruch zu finden) und Zeilenumbrüche und Stopps gespeichert werden. Dies würde im Grunde das Zellenmodell aufgeben, wenn Daten ankommen.
Problem tritt auf, wenn etwas eingreift und eine Drill-Down-Darstellung der Terminaldaten haben möchte (zB der Renderer oder eine Escape-Sequenz/Benutzer möchte den Cursor bewegen). Wir müssen in diesem Fall immer noch die Zellenberechnungen durchführen, aber es sollte ausreichen, dies nur für den Inhalt innerhalb der Terminalspalten und -zeilen zu tun. (Noch bin ich mir nicht sicher, ob es sich um herausgescrollte Inhalte handelt, die möglicherweise noch weiter cachebar und billig neu zu zeichnen sind.)

@jerch Ich treffe @mofux in ein paar Wochen eines Tages in Prag und wir wollten einige interne Verbesserungen an der Handhabung von Textattributen vornehmen/starten, die dies abdecken 😃

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

Der Algo ist ziemlich teuer, da jeder Charakter zweimal bewertet werden muss

@jerch Wenn Sie Ideen für einen schnelleren Zugriff auf Text aus dem Puffer haben, lassen Sie es uns wissen. Derzeit ist das meiste nur ein einzelnes Zeichen, wie Sie wissen, aber es könnte ein ArrayBuffer , ein String usw. sein. Ich dachte, wir sollten darüber nachdenken, dass Scrollback irgendwie unveränderlich ist.

Nun, ich habe in der Vergangenheit viel mit ArrayBuffers experimentiert:

  • sie sind in Bezug auf die Laufzeit für die typischen Methoden etwas schlechter als Array (vielleicht noch weniger optimiert von Engine-Herstellern)
  • new UintXXArray ist viel schlimmer als die literale Array-Erstellung mit []
  • Sie zahlen sich mehrmals aus, wenn Sie die Datenstruktur vorab zuordnen und wiederverwenden können (bis zu 10-mal).
  • für String-Daten frisst die Hin- und Rück-Konvertierung alle Vorteile - schade, dass JS keinen nativen String für Uint16Array-Konverter bereitstellt (teilweise jedoch mit TextEncoder machbar)

Meine Ergebnisse zu ArrayBuffer schlagen vor, sie aufgrund der Conversion-Strafe nicht für String-Daten zu verwenden. Theoretisch könnte das Terminal ArrayBuffer von node-pty bis zu den Terminaldaten verwenden (das würde mehrere Konvertierungen auf dem Weg zum Frontend sparen), bin mir nicht sicher, ob das Rendering auf diese Weise erfolgen kann, denke ich, um zu rendern Sachen braucht es immer eine letzte uint16_t string Umwandlung in

TL;DR ArrayBuffer ist überlegen, wenn Sie die Datenstruktur vorab zuordnen und wiederverwenden können. Für alles andere sind normale Arrays besser. Strings sind es nicht wert, in ArrayBuffer gequetscht zu werden.

Eine neue Idee, die ich hatte, versucht, die String-Erstellung so weit wie möglich zu reduzieren, insbesondere. versucht, die bösen Splits und Joins zu vermeiden. Es basiert in gewisser Weise auf Ihrer zweiten Idee oben mit der neuen Methode InputHandler.print , wcwidth und Zeilenstopps im Hinterkopf:

  • print bekommt jetzt ganze Strings bis zu mehreren Terminalzeilen
  • Speichern Sie diese Strings in einer einfachen Zeigerliste ohne Änderungen (kein String alloc oder gc, list alloc kann vermieden werden, wenn er mit einer prealloc-Struktur verwendet wird) zusammen mit den aktuellen Attributen
  • Cursor um wcwidth(string) % cols vorrücken
  • Sonderfall \n (harter Zeilenumbruch): Cursor um eine Zeile vorrücken, Position in Zeigerliste als harten Zeilenumbruch markieren
  • Sonderfall Zeilenüberlauf mit WrapAround: Position im String als weichen Zeilenumbruch markieren
  • Sonderfall \r : Lade den Inhalt der letzten Zeile (von der aktuellen Cursorposition bis zum letzten Zeilenumbruch) in einen Zeilenpuffer, um überschrieben zu werden
  • Datenfluss wie oben, trotz des \r Falls ist keine Zellabstraktion oder String-Aufteilung erforderlich
  • Attributänderungen sind kein Problem, solange niemand die echte cols x rows Darstellung anfordert (sie ändern nur das attr-Flag, das zusammen mit dem gesamten String gespeichert wird)

Übrigens sind die WCwidths eine Teilmenge des Graphem-Algos, daher könnte dies in Zukunft austauschbar sein.

Jetzt der gefährliche Teil 1 - jemand möchte den Cursor innerhalb der cols x rows bewegen:

  • in Zeilenumbrüchen cols rückwärts bewegen - der Anfang des aktuellen Terminalinhalts
  • jeder Zeilenumbruch bezeichnet eine echte Endlinie
  • Laden Sie Material für nur eine Seite in das Zellenmodell (noch nicht sicher, ob dies durch geschickte Stringpositionierung auch weggelassen werden kann)
  • Machen Sie die böse Arbeit: Wenn Attributänderungen angefordert werden, haben wir ein bisschen Pech und müssen entweder auf das vollständige Zellenmodell oder ein String-Split & Insert-Modell zurückgreifen (letzteres könnte zu einer schlechten Leistung führen)
  • Daten fließen wieder, jetzt mit verschlechterten Zeichenfolgen und attrs-Daten im Puffer für diese Seite

Nun der gefährliche Teil 2 - der Renderer möchte etwas zeichnen:

  • hängt irgendwie vom Renderer ab, wenn wir einen Drilldown zu einem Zellmodell durchführen müssen oder einfach die String-Offsets mit Zeilenumbrüchen und Textattributen versehen können

Vorteile:

  • sehr schneller Datenfluss
  • optimiert für die gängigste InputHandler Methode - print
  • ermöglicht umfließende Linien bei der Größenänderung des Terminals

Nachteile:

  • fast jede andere InputHandler Methode wird in dem Sinne gefährlich sein, dass dieses Flussmodell unterbrochen wird und eine Zwischenabstraktion der Zellen erforderlich ist
  • Renderer-Integration unklar (für mich zumindest atm)
  • kann die Leistung von Flüchen wie Apps beeinträchtigen (sie enthalten normalerweise "gefährlichere" Sequenzen)

Nun, dies ist ein grober Entwurf der Idee, weit davon entfernt, brauchbar zu sein, da viele Details noch nicht abgedeckt sind. Bes. die "gefährlichen" Teile könnten bei vielen Performance-Problemen unangenehm werden (wie z.

@jerch

Strings sind es nicht wert, in ArrayBuffer gequetscht zu werden.

Ich denke, Monaco speichert seinen Puffer in ArrayBuffer s und ist ziemlich leistungsstark. Ich habe mich noch nicht so intensiv mit der Umsetzung beschäftigt.

insb. versucht, die bösen Splits und Joins zu vermeiden

Welche?

Ich dachte, wir sollten darüber nachdenken, dass Scrollback irgendwie unveränderlich ist.

Eine Idee war, Scrollback vom Viewport-Bereich zu trennen. Sobald eine Zeile zum Scrollback geht, wird sie in die Scrollback-Datenstruktur verschoben. Sie können sich 2 CircularList Objekte vorstellen, eines, dessen Zeilen so optimiert sind, dass es sich nie ändert, eines für das Gegenteil.

@Tyriar Über das Scrollback - ja, da dies für den Cursor nie erreichbar ist, kann es etwas Speicher sparen, wenn Sie einfach die Zellabstraktion für herausgescrollte Zeilen löschen.

@Tyriar
Es ist sinnvoll, Strings in ArrayBuffer zu speichern, wenn wir die Konvertierung auf eine beschränken können (vielleicht die letzte für die Renderausgabe). Das ist etwas besser als das Saitenhandling überall. Dies wäre machbar, da node-pty auch Rohdaten liefern kann (und auch der Websocket kann uns Rohdaten liefern).

insb. versucht, die bösen Splits und Joins zu vermeiden

Welche?

Der ganze Ansatz besteht darin, _minimize_-Splits überhaupt zu vermeiden . Wenn niemand Cursorsprünge in die gepufferten Daten anfordert, werden Strings nie geteilt und könnten direkt zum Renderer gehen (sofern unterstützt). Keine Zellteilungen und spätere Zusammenfügungen.

@jerch gut kann es sein, wenn der Viewport erweitert wird, ich denke, wir können den Scrollback auch einziehen, wenn eine Zeile gelöscht wird? Ich bin mir nicht 100% sicher, ob es sich um das richtige Verhalten handelt.

@ Tyriar Ah richtig. Bei letzterem bin ich mir auch nicht sicher, ich denke, natives xterm erlaubt dies nur für echtes Scrollen mit der Maus oder der Bildlaufleiste. Selbst SD/SU verschiebt den Inhalt des Bildlaufpuffers nicht zurück in das "aktive" Terminal-Ansichtsfenster.

Könnten Sie mich auf die Quelle des Monaco-Editors hinweisen, in dem der ArrayBuffer verwendet wird? Anscheinend kann ich es selbst nicht finden :blush:

Hmm, lest einfach die TextEncoder/Decoder-Spezifikation neu, mit ArrayBuffers von node-pty bis zum Frontend bleiben wir im Grunde bei utf-8, es sei denn, wir übersetzen es irgendwann auf die harte Tour. xterm.js utf-8 bewusst machen? Idk, dies würde viele Zwischencodepunktberechnungen für die höheren Unicode-Zeichen beinhalten. Vorteil - es würde Speicher für ASCII-Zeichen sparen.

@rebornix könnten Sie uns einige

Hier sind einige Zahlen für typisierte Arrays und den neuen Parser (war einfacher zu übernehmen):

  • UTF-8 (Uint8Array): print Aktion springt von 190 MB/s auf 290 MB/s
  • UTF-16 (Uint16Array): print Aktion springt von 190 MB/s auf 320 MB/s

Insgesamt schneidet UTF-16 viel besser ab, aber das war zu erwarten, da der Parser dafür optimiert ist. UTF-8 leidet unter der Zwischencodepunktberechnung.

Die Konvertierung von Strings in typisierte Arrays frisst ~4% JS-Laufzeit meines Benchmarks ls -lR /usr/lib (immer weit unter 100 ms, erfolgt über eine Schleife in InputHandler.parse ). Ich habe die umgekehrte Konvertierung nicht getestet (dies wird implizit atm in InputHandller.print auf Zellenebene durchgeführt). Die Gesamtlaufzeit ist etwas schlechter als bei Strings (die eingesparte Zeit im Parser gleicht die Konvertierungszeit nicht aus). Dies kann sich ändern, wenn andere Teile ebenfalls Array-bewusst typisiert sind.

Und die dazugehörigen Screenshots (getestet mit ls -lR /usr/lib ):

mit Saiten:
grafik

mit Uint16Array:
grafik

Beachten Sie den Unterschied für EscapeSequenceParser.parse , der von einem typisierten Array profitieren kann (~30% schneller). Das InputHandler.parse führt die Konvertierung durch, daher ist es für die typisierte Array-Version schlechter. Auch GC Minor hat mehr mit typisierten Arrays zu tun (da ich das Array wegwerfe).

Edit: Ein weiterer Aspekt ist in den Screenshots zu sehen - der GC wird mit ~20% Laufzeit relevant, die lang laufenden Frames (red flagged) sind alle GC-bezogen.

Noch eine etwas radikale Idee:

  1. Erstellen Sie einen eigenen Arraybuffer-basierten virtuellen Speicher, etwas Großes (>5 MB)
    Wenn der Arraypuffer eine Länge von Vielfachen von 4 transparenten Schaltern von int8 bis int16 bis int32 , sind Typen möglich. Der Allocator gibt einen freien Index auf Uint8Array , dieser Zeiger kann durch eine einfache Bitverschiebung in eine Uint16Array oder Uint32Array Position umgewandelt werden.
  2. Schreiben Sie eingehende Strings in den Speicher als uint16_t Typ für UTF-16.
  3. Parser läuft auf den String-Zeigern und ruft Methoden in InputHandler mit Zeigern auf diesen Speicher anstelle von String-Slices auf.
  4. Erstellen Sie den Terminal-Datenpuffer im virtuellen Speicher als Ringpuffer-Array eines struct-ähnlichen Typs anstelle von nativen JS-Objekten, vielleicht so (immer noch zellbasiert):
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
}

Vorteile:

  • lässt JS-Objekte und damit GC nach Möglichkeit weg (es bleiben nur wenige lokale Objekte)
  • nur eine initiale Datenkopie in den virtuellen Speicher erforderlich
  • fast keine malloc und free Kosten (abhängig von der Geschicklichkeit des Allocators/Deallocators)
  • spart viel Speicher (vermeidet den Speicher-Overhead von JS-Objekten)

Nachteile:

  • Willkommen zur Cavascript Horror Show :schrei:
  • schwer umzusetzen, ändert irgendwie alles
  • Geschwindigkeitsvorteil ist unklar, bis er wirklich umgesetzt wird

:Lächeln:

schwer umzusetzen, ändert irgendwie alles 😉

Dies ist näher an der Funktionsweise von Monaco. Ich habe mich an diesen Blogbeitrag erinnert, der die Strategie zum Speichern von Zeichenmetadaten beschreibt https://code.visualstudio.com/blogs/2017/02/08/syntax-highlighting-optimizations

Ja, das ist im Grunde die gleiche Idee.

Ich hoffe, meine Antwort darauf, wo Monaco den Puffer speichert, ist nicht zu spät.

Alex und ich sind für Array Buffer und die meiste Zeit gibt es uns eine gute Leistung. Einige Orte, an denen wir ArrayBuffer verwenden:

Wir verwenden einfache Strings für den Textpuffer anstelle von Array Buffer, da V8-Strings einfacher zu manipulieren sind

  • Wir führen die Kodierung/Dekodierung ganz zu Beginn des Ladens einer Datei durch, sodass die Dateien in einen JS-String umgewandelt werden. V8 entscheidet, ob ein Byte oder zwei zum Speichern eines Zeichens verwendet werden.
  • Wir bearbeiten den Textpuffer sehr oft, Strings sind einfacher zu handhaben.
  • Wir verwenden das native Modul von nodej und haben bei Bedarf Zugriff auf V8-Interna.

Die folgende Liste ist nur eine kurze Zusammenfassung interessanter Konzepte, über die ich gestolpert bin und die helfen könnten, die Speichernutzung und/oder die Laufzeit zu senken:

  • FlatJS (https://github.com/lars-t-hansen/flatjs) - Metasprache zur Unterstützung der Codierung mit Arraybuffer-basierten Heaps
  • http://2ality.com/2017/01/shared-array-buffer.html (angekündigt als Teil von ES2017, die Zukunft könnte aufgrund von Spectre ungewiss sein, abgesehen von dieser vielversprechenden Idee mit echter Nebenläufigkeit und echter Atomik)
  • webassembly/asm.js (aktueller Stand? noch nutzbar? Habe die Entwicklung einige Zeit nicht verfolgt, habe vor Jahren mit einer C-Lib für eine Spiele-KI emscripten für asm.js verwendet, aber mit beeindruckenden Ergebnissen)
  • https://github.com/AssemblyScript/assemblyscript

Um hier alles ins Rollen zu bringen, hier ein kurzer Hack, wie wir Textattribute "zusammenführen" können.

Der Code wird hauptsächlich von der Idee angetrieben, Speicher für die Pufferdaten zu sparen (Laufzeit wird darunter leiden, wie viel noch nicht getestet). Bes. die Textattribute mit RGB für Vorder- und Hintergrund (sobald unterstützt) führen dazu, dass xterm.js beim aktuellen Zellen-Layout tonnenweise Speicher verbraucht. Der Code versucht dies zu umgehen, indem er einen skalierbaren Ref-Zählatlas für Attribute verwendet. Dies ist imho eine Option, da ein einzelnes Terminal kaum mehr als 1 Million Zellen aufnehmen kann, was den Atlas auf 1M * entry_size erhöhen würde, wenn sich alle Zellen unterscheiden.

Die Zelle selbst muss nur den Index des Attributatlas enthalten. Bei Zellenänderungen muss der alte Index unref'd und der neue ref'd sein. Der Atlasindex würde das Attributattribut des Terminalobjekts ersetzen und wird selbst in SGR geändert.

Der Atlas adressiert derzeit nur Textattribute, könnte aber bei Bedarf auf alle Zellattribute erweitert werden. Während der aktuelle Terminalpuffer 2 32-Bit-Zahlen für Attributdaten enthält (4 mit RGB im aktuellen Pufferdesign), würde der Atlas ihn auf nur eine 32-Bit-Zahl reduzieren. Auch die Atlaseinträge können weiter gepackt werden.

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

Bearbeiten:
Die Laufzeitstrafe ist fast null, für meinen Benchmark mit ls -lR /usr/lib addiert sich die Gesamtlaufzeit von ~2,3 s um weniger als 1 ms. Interessante Randnotiz - der Befehl setzt weniger als 64 verschiedene Textattribut-Slots für die Ausgabe von 5 MB Daten und spart nach vollständiger Implementierung mehr als 20 MB.

Einige Prototyp-PRs erstellt, um einige Änderungen am Puffer zu testen (siehe https://github.com/xtermjs/xterm.js/pull/1528#issue-196949371 für die allgemeine Idee hinter den Änderungen):

  • PR #1528 : Attributatlas
  • PR #1529: wcwidth und charCode aus dem Puffer entfernen
  • PR #1530: String im Puffer durch Codepunkte / Zellenspeicherindexwert ersetzen

@jerch es könnte eine gute Idee sein, sich dafür vom Wort Atlas fernzuhalten, damit "atlas" immer "Texturatlas" bedeutet. Sowas wie Store oder Cache wäre wohl besser?

oh ok, "cache" ist in Ordnung.

Ich schätze, ich bin mit den Testbed-PRs fertig. Bitte werfen Sie auch einen Blick auf die PR-Kommentare, um die Hintergründe der folgenden groben Zusammenfassung zu erfahren.

Vorschlag:

  1. Erstellen Sie ein AttributeCache für alles, was zum Stylen einer einzelnen Terminalzelle benötigt wird. Siehe #1528 für eine frühe Version zum Zählen von Refs, die auch echte Farbspezifikationen enthalten kann. Der Cache kann bei Bedarf auch zwischen verschiedenen Terminalinstanzen geteilt werden, um weiteren Speicher in mehreren Terminal-Apps zu sparen.
  2. Erstellen Sie ein StringStorage , um kurze Terminalinhaltsdatenstrings zu speichern. Die Version in #1530 vermeidet sogar das Speichern einzelner Zeichenfolgen durch "Überladen" der Zeigerbedeutung. wcwidth sollte hierher verschoben werden.
  3. Verkleinern Sie das aktuelle CharData von [number, string, number, number] auf [number, number] , wobei die Zahlen Zeiger (Indexnummern) sind auf:

    • AttributeCache Eintrag

    • StringStorage Eintrag

Es ist unwahrscheinlich, dass sich die Attribute stark ändern, daher spart eine einzige 32-Bit-Zahl im Laufe der Zeit viel Speicher. Der StringStorage Zeiger ist ein echter Unicode-Codepoint für einzelne Zeichen und kann daher als code Eintrag von CharData . Auf den eigentlichen String kann über StringStorage.getString(idx) zugegriffen werden. Auf das vierte Feld wcwidth von CharData könnte von StringStorage.wcwidth(idx) zugegriffen werden (noch nicht implementiert). Es gibt fast keine Laufzeiteinbußen, wenn Sie code und wcwidth in CharData loswerden (getestet in #1529).

  1. Verschieben Sie die geschrumpften CharData in eine dichte Int32Array basierte Pufferimplementierung. Auch getestet in Nr. 1530 mit einer Stub-Klasse (bei weitem nicht voll funktionsfähig), die endgültigen Vorteile sind wahrscheinlich:

    • 80 % geringerer Speicherbedarf des Terminalpuffers (von 5,5 MB auf 0,75 MB)

    • etwas schneller (noch nicht testbar, ich erwarte einen Geschwindigkeitsgewinn von 20% - 30%)

    • Bearbeiten: viel schneller - Skriptlaufzeit für ls -lR /usr/lib auf 1,3s gesunken (Master ist bei 2,1s), während der alte Puffer noch für die Cursorbehandlung aktiv ist. Nach dem Entfernen erwarte ich, dass die Laufzeit unter 1s sinkt

Nachteil - Schritt 4 ist ziemlich viel Arbeit, da es einige Überarbeitungen an der Pufferschnittstelle erfordert. Aber hey - um 80% des Arbeitsspeichers zu sparen und trotzdem die Laufzeitleistung zu steigern, ist es kein Problem, oder? :Lächeln:

Es gibt ein weiteres Problem, über das ich gestolpert bin - die aktuelle leere Zellendarstellung. Imho kann eine Zelle 3 Zustände haben:

  • empty : Ausgangszustand der Zelle, es wurde noch nichts geschrieben oder der Inhalt wurde gelöscht. Es hat eine Breite von 1, aber keinen Inhalt. Wird derzeit in blankLine und eraseChar , jedoch mit einem Leerzeichen als Inhalt.
  • null : Zelle nach einem Zeichen voller Breite, um anzuzeigen, dass es keine Breite für die visuelle Darstellung hat.
  • normal : Zelle enthält etwas Inhalt und hat eine visuelle Breite (1 oder 2, vielleicht größer, wenn wir echtes Graphem/Bidi-Zeug unterstützen, da bin ich mir noch nicht sicher lol)

Das Problem, das ich hier sehe, ist, dass eine leere Zelle nicht von einer normalen Zelle mit eingefügtem Leerzeichen zu unterscheiden ist , beide sehen auf Pufferebene gleich aus (gleicher Inhalt, gleiche Breite). Ich habe keinen Renderer-/Ausgabecode geschrieben, aber ich erwarte, dass dies zu unangenehmen Situationen an der Ausgabefront führt. Bes. die Handhabung des rechten Endes einer Leitung kann umständlich werden.
Ein Terminal mit 15 Spalten, zuerst eine String-Ausgabe, die umgebrochen wurde:

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

im Vergleich zu einer Ordnerauflistung mit ls :

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

Das erste Beispiel enthält ein reelles Leerzeichen nach dem Wort 'terminal', das zweite Beispiel hat die Zellen nach 'Readme.md' nie berührt. Die Darstellung auf Pufferebene macht durchaus Sinn für den Standardfall, das Zeug als Terminalausgabe auf den Bildschirm zu drucken (der Raum muss sowieso genommen werden), aber für Tools, die versuchen, mit den Inhaltsstrings umzugehen wie eine Mausauswahl oder einem Reflow-Manager ist nicht mehr klar, woher die Leerzeichen kommen.

Dies führt mehr oder weniger zur nächsten Frage - wie bestimmt man die tatsächliche Inhaltslänge in einer Zeile (Anzahl der Zellen, die etwas von der linken Seite enthalten)? Ein einfacher Ansatz würde die leeren Zellen von der rechten Seite aus zählen, auch hier macht die doppelte Bedeutung von oben dies schwer zu bestimmen.

Vorschlag:
Imho ist dies leicht zu beheben, indem man einen anderen Platzhalter für leere Zellen verwendet, zB ein Kontrollzeichen oder den leeren String und diese bei Bedarf im Renderprozess ersetzt. Vielleicht kann auch der Bildschirmrenderer davon profitieren, da er diese Zellen möglicherweise überhaupt nicht verarbeiten muss (abhängig von der Art und Weise, wie die Ausgabe generiert wird).

Übrigens, für die obige umschlossene Zeichenfolge führt dies auch zu dem isWrapped Problem, das für eine umfließende Größenänderung oder ein korrektes Copy&Paste-Selektionshandling unerlässlich ist. Imho können wir das nicht entfernen, müssen es aber besser integrieren, als es atm ist.

@jerch beeindruckende Arbeit! :smiley:

1 Erstellen Sie einen AttributeCache, der alles enthält, was zum Stylen einer einzelnen Terminalzelle benötigt wird. Siehe #1528 für eine frühe Version zum Zählen von Refs, die auch echte Farbspezifikationen enthalten kann. Der Cache kann bei Bedarf auch zwischen verschiedenen Terminalinstanzen geteilt werden, um weiteren Speicher in mehreren Terminal-Apps zu sparen.

Habe einige Kommentare zu #1528 abgegeben.

2 Erstellen Sie einen StringStorage, um kurze Datenstrings für den Terminalinhalt zu speichern. Die Version in #1530 vermeidet sogar das Speichern einzelner Zeichenfolgen durch "Überladen" der Zeigerbedeutung. wcwidth sollte hierher verschoben werden.

Habe einige Kommentare zu #1530 abgegeben.

4 Verschieben Sie die geschrumpften CharData in eine dichte Int32Array-basierte Pufferimplementierung. Auch getestet in Nr. 1530 mit einer Stub-Klasse (bei weitem nicht voll funktionsfähig), die endgültigen Vorteile sind wahrscheinlich:

Diese Idee ist noch nicht ganz überzeugt, ich denke, es wird uns hart treffen, wenn wir Reflow implementieren. Es sieht so aus, als ob jeder dieser Schritte so ziemlich in der richtigen Reihenfolge durchgeführt werden kann, damit wir sehen können, wie die Dinge laufen und ob es Sinn macht, dies zu tun, sobald wir 3 erledigt haben.

Es gibt ein weiteres Problem, über das ich gestolpert bin - die aktuelle leere Zellendarstellung. Imho kann eine Zelle 3 Zustände haben

Hier ist ein Beispiel für einen Fehler, der aus diesem https://github.com/xtermjs/xterm.js/issues/1286 , :+1: hervorgegangen ist, um Leerraumzellen und "leere" Zellen zu unterscheiden

Übrigens, für die obige umschlossene Zeichenfolge führt dies auch zum isWrapped-Problem, das für eine umfließende Größenänderung oder ein korrektes Copy&Paste-Selektionshandling unerlässlich ist. Imho können wir das nicht entfernen, müssen es aber besser integrieren, als es atm ist.

Ich sehe, dass isWrapped verschwindet, wenn wir https://github.com/xtermjs/xterm.js/issues/622 in Angriff nehmen, da CircularList nur unverpackte Zeilen enthält.

Diese Idee ist noch nicht ganz überzeugt, ich denke, es wird uns hart treffen, wenn wir Reflow implementieren. Es sieht so aus, als ob jeder dieser Schritte so ziemlich in der richtigen Reihenfolge durchgeführt werden kann, damit wir sehen können, wie die Dinge laufen und ob es Sinn macht, dies zu tun, sobald wir 3 erledigt haben.

Ja, ich bin bei dir (es macht immer noch Spaß, mit diesem völlig anderen Ansatz herumzuspielen). 1 und 2 könnten ausgewählt werden, 3 kann je nach 1 oder 2 angewendet werden. 4 ist optional, wir könnten uns einfach an das aktuelle Pufferlayout halten. Die Speichereinsparungen sind wie folgt:

  1. 1 + 2 + 3 in CircularList : spart 50% (~2,8 MB von ~5,5 MB)
  2. 1 + 2 + 3 + 4 auf halbem Weg - legen Sie einfach die Zeilendaten in ein typisiertes Array, aber bleiben Sie beim Zeilenindexzugriff: spart 82% (~ 0,9 MB)
  3. 1 + 2 + 3 + 4 voll dichtes Array mit Pointer-Arithmetik: spart 87% (~0,7 MB)

1. ist sehr einfach zu implementieren, das Speicherverhalten mit größerem scrollBack zeigt immer noch die schlechte Skalierung wie hier gezeigt https://github.com/xtermjs/xterm.js/pull/1530#issuecomment -403542479 aber auf einer weniger toxischen Ebene
2. Etwas schwieriger zu implementieren (einige weitere Umleitungen auf Zeilenebene erforderlich), aber ermöglicht es, die höhere API von Buffer intakt zu halten. Imho die Option zu gehen - Big Mem sparen und trotzdem einfach zu integrieren.
3. 5% mehr Speichereinsparung als Option 2, schwer zu implementieren, wird die gesamte API und damit buchstäblich die gesamte Codebasis ändern. Imho eher von akademischem Interesse oder für langweilige Regentage umsetzbar lol.

@Tyriar Ich habe einige weitere Tests mit Rost für die Webassembly-Nutzung durchgeführt und den Parser neu geschrieben. Beachten Sie, dass meine Rostfähigkeiten etwas "rostig" sind, da ich mich noch nicht tiefer damit befasst habe, daher könnte das Folgende das Ergebnis eines schwachen Rostcodes sein. Ergebnisse:

  • Das Datenhandling innerhalb des wasm-Teils ist etwas schneller (5 - 10 %).
  • Anrufe von JS in wasm verursachen einen gewissen Overhead und verbrauchen alle Vorteile von oben. Tatsächlich war es ~20% langsamer.
  • Die "Binärdatei" wird kleiner sein als das JS-Gegenstück (nicht wirklich gemessen, da ich nicht alles implementiert habe).
  • Um die JS <--> wasm Transition einfach zu machen, wird etwas Bloatcode benötigt, um die JS-Typen zu handhaben (nur die String-Übersetzung).
  • Wir können die JS-zu-Wasm-Übersetzung nicht vermeiden, da das Browser-DOM und die Ereignisse dort nicht zugänglich sind. Es konnte nur für Kernteile verwendet werden, die nicht mehr so ​​leistungskritisch sind (abgesehen vom Speicherverbrauch).

Wenn wir nicht die gesamten Kernbibliotheken in Rust (oder einer anderen wasm-fähigen Sprache) umschreiben möchten, können wir nichts davon gewinnen, zu einem wasm lang imho zu wechseln. Ein Plus von wasm langs ist heutzutage die Tatsache, dass die meisten explizite Speicherverwaltung unterstützen (könnte uns beim Pufferproblem helfen), Nachteile sind die Einführung einer völlig anderen Sprache in ein hauptsächlich TS/JS-fokussiertes Projekt (eine hohe Barriere für Code-Ergänzungen) und die Übersetzungskosten zwischen Wasm und JS Land.

TL;DR
xterm.js ist im Großen und Ganzen in allgemeine JS-Sachen wie DOM und Ereignisse einzuordnen, um alles aus der Webassembly zu gewinnen, sogar für eine Neufassung der Kernteile.

@jerch nette Untersuchung :

Anrufe von JS in wasm verursachen einen gewissen Overhead und verbrauchen alle Vorteile von oben. Tatsächlich war es ~20% langsamer.

Dies war auch das Hauptproblem für Monaco, das nativ wurde, was in erster Linie meine Haltung beeinflusst hat (obwohl das mit dem nativen Node-Modul und nicht mit wasm war). Ich glaube, dass die Arbeit mit ArrayBuffer s, wo immer es möglich ist, uns die beste Balance zwischen Perfektion und Einfachheit geben sollte (einfache Implementierung, Einstiegsbarriere).

@Tyriar Ich werde versuchen, einen AttributeStorage zu entwickeln, um die RGB-Daten zu speichern. Bei BST bin ich mir noch nicht sicher, für den typischen Anwendungsfall mit nur wenigen Farbeinstellungen in einer Terminalsitzung wird dies in der Laufzeit schlimmer, vielleicht sollte dies ein Laufzeit-Drop-In sein, sobald die Farben einen bestimmten Schwellenwert überschreiten. Auch der Speicherverbrauch wird wieder stark ansteigen, obwohl es immer noch Speicher spart, da die Attribute nur einmal und nicht zusammen mit jeder einzelnen Zelle gespeichert werden (das schlimmste Szenario, bei dem jede Zelle andere Attribute enthält, wird jedoch darunter leiden).
Wissen Sie, warum der aktuelle fg und bg 256 Farben auf 9 Bit statt auf 8 Bit basiert? Wofür wird das zusätzliche Bit verwendet? Hier: https://github.com/xtermjs/xterm.js/blob/6691f809069a549b4808cd2e055398d2da15db37/src/InputHandler.ts#L1596
Könnten Sie mir das aktuelle Bit-Layout von attr ? Ich denke, ein ähnlicher Ansatz wie die "doppelte Bedeutung" für den StringStorage-Zeiger kann weiter Speicher sparen, aber das würde erfordern, dass das MSB von attr für die Zeigerunterscheidung reserviert und nicht für andere Zwecke verwendet wird. Dies könnte die Möglichkeit einschränken, später weitere Attribut-Flags zu unterstützen (da FLAGS bereits 7 Bit verwendet), fehlen uns noch einige grundlegende Flags, die wahrscheinlich kommen werden?

Eine 32-Bit- attr Zahl im Termpuffer könnte wie folgt gepackt werden:

# 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)

Auf diese Weise muss der Speicher nur die RGB-Daten in zwei 32-Bit-Zahlen enthalten, während die Flags in der Zahl attr bleiben können.

@jerch ich habe dir übrigens eine Mail geschickt, wurde wohl wieder vom Spamfilter gefressen 😛

Wissen Sie, warum der aktuelle fg- und bg-256-Farbenwert auf 9 Bit statt auf 8 Bit basiert? Wofür wird das zusätzliche Bit verwendet?

Ich denke, es wird für die Standardfarbe fg / bg verwendet (die dunkel oder hell sein kann), also sind es tatsächlich 257 Farben.

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

Könnten Sie mir das aktuelle Bit-Layout von attr geben?

Ich denke, es ist dies:

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

Sie können sehen, worauf ich für Truecolor in der alten PR https://github.com/xtermjs/xterm.js/pull/756/files gelandet bin:

/**
 * 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];

In diesem hatte ich also 2 Flaggen; eine für die Standardfarbe (ob alle Farbbits ignoriert werden sollen) und eine für Truecolor (ob 256 oder 16 mil Farbe).

Dies könnte die Möglichkeit einschränken, später weitere Attribut-Flags zu unterstützen (da FLAGS bereits 7 Bit verwendet), fehlen uns noch einige grundlegende Flags, die wahrscheinlich kommen werden?

Ja, wir wollen etwas Platz für zusätzliche Flags, zum Beispiel https://github.com/xtermjs/xterm.js/issues/580, https://github.com/xtermjs/xterm.js/issues/1145, I'd sagen Sie, lassen Sie mindestens > 3 Bits, wenn möglich.

Anstelle von Zeigerdaten innerhalb des attr selbst könnte es eine andere Karte geben, die Verweise auf die rgb-Daten enthält? mapAttrIdxToRgb: { [idx: number]: RgbData

@Tyriar Sorry, war ein paar Tage nicht online und ich befürchte, dass die E-Mail vom Spamfilter gefressen wurde. Könnten Sie es bitte erneut senden? :erröten:

Habe ein bisschen mit clevereren Lookup-Datenstrukturen für den attrs-Speicher gespielt. Am vielversprechendsten in Bezug auf Platz und Such-/Einfügelaufzeit sind Bäume und eine Skiplist als günstigere Alternative. Theoretisch lol. In der Praxis kann keiner meine einfache Array-Suche übertreffen, die mir sehr seltsam erscheint (Fehler im Code irgendwo?)
Ich habe hier eine Testdatei hochgeladen https://gist.github.com/jerch/ff65f3fb4414ff8ac84a947b3a1eec58 mit Array vs. einem linksgerichteten rot-schwarzen Baum, der bis zu 10 Millionen Einträge testet (was fast der komplette Adressierungsraum ist). Trotzdem ist das Array im Vergleich zum LLRB weit vorne, ich vermute jedoch, dass der Break-Even bei etwa 10 Millionen liegt. Getestet auf meinem 7 Jahre alten Laptop, vielleicht kann es jemand auch und noch besser testen - weisen Sie mich auf einige Fehler in den Impl/Tests hin.

Hier einige Ergebnisse (mit laufenden Nummern):

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

Was mich wirklich überrascht, ist die Tatsache, dass die lineare Array-Suche in den unteren Regionen überhaupt kein Wachstum zeigt, sie ist bis zu 10k Einträge stabil bei ~4ms (könnte Cache-bezogen sein). Der 10M-Test zeigt für beide eine schlechtere Laufzeit als erwartet, möglicherweise aufgrund von Mem-Paging. Vielleicht ist JS mit dem JIT und all den Opts/Deopts zu weit von der Maschine entfernt, dennoch denke ich, dass sie einen Komplexitätsschritt nicht eliminieren können (obwohl der LLRB auf einem einzigen _n_ schwer zu sein scheint, wodurch der Break-Even-Punkt für O( n) vs. O(logn) aufwärts)

Übrigens mit Zufallsdaten ist der Unterschied noch schlimmer.

Ich denke, es wird für die Standardfarbe fg / bg verwendet (die dunkel oder hell sein kann), also sind es tatsächlich 257 Farben.

Das ist also SGR 39 oder SGR 49 von einer der 8 Palettenfarben zu unterscheiden?

Anstelle von Zeigerdaten innerhalb des attr selbst könnte es eine andere Karte geben, die Verweise auf die rgb-Daten enthält? mapAttrIdxToRgb: { [idx: Zahl]: RgbData

Dies würde eine weitere Umleitung mit zusätzlicher Speichernutzung einführen. Mit den obigen Tests habe ich auch den Unterschied getestet, ob die Flags immer in attrs gehalten werden oder ob sie zusammen mit RGB-Daten im Speicher gespeichert werden. Da der Unterschied bei 1 Mio. Einträgen ~ 0,5 ms beträgt, würde ich dieses komplizierte Attrs-Setup nicht verwenden, sondern Flags in den Speicher kopieren, sobald RGB gesetzt ist. Trotzdem würde ich mich für die 32-Bit-Unterscheidung zwischen direkten Attributen und Zeigern entscheiden, da dies die Speicherung für Nicht-RGB-Zellen überhaupt vermeidet.

Außerdem denke ich, dass die standardmäßigen 8 Palettenfarben für fg/bg derzeit nicht ausreichend im Puffer dargestellt werden. Theoretisch sollte das Terminal folgende Farbmodi unterstützen:

  1. SGR 39 + SGR 49 Standardfarbe für fg/bg (anpassbar)
  2. SGR 30-37 + SGR 40-47 8 niedrige Farbpalette für fg/bg (anpassbar)
  3. SGR 90-97 + SGR 100-107 8 hohe Farbpalette für fg/bg (anpassbar)
  4. SGR 38;5;n + SGR 48;5;n 256 indizierte Palette für fg/bg (anpassbar)
  5. SGR 38;2;r;g;b + SGR 48;2;r;g;b RGB für fg/bg (nicht anpassbar)

Option 2.) und 3.) können zu einem einzigen Byte zusammengeführt werden (behandeln sie als eine einzelne 16-Farben-fg/bg-Palette), 4.) benötigt 2 Bytes und 5.) benötigt schließlich 6 weitere Bytes. Wir brauchen noch einige Bits, um den Farbmodus anzuzeigen.
Um dies auf der Pufferebene widerzuspiegeln, benötigen wir Folgendes:

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

Wir brauchen also 30 Bit einer 32-Bit-Zahl, wobei 2 Bit für andere Zwecke frei bleiben. Das 32. Bit könnte den Zeiger gegen das direkte Attr-Flag halten, wobei die Speicherung für Nicht-RGB-Zellen weggelassen wird.

Außerdem schlage ich vor, den attr-Zugriff in eine praktische Klasse zu packen, um Implementierungsdetails nicht nach außen offenzulegen (siehe die Testdatei oben, es gibt eine frühe Version einer TextAttributes Klasse, um dies zu erreichen).

Sorry, war ein paar Tage nicht online und ich befürchte die E-Mail wurde vom Spamfilter gefressen. Könnten Sie es bitte erneut senden?

Übelnehmen

Ach übrigens, diese Zahlen oben für die Array- vs. llrb-Suche sind Mist - ich denke, es wurde durch den Optimierer verdorben, der einige seltsame Dinge in der for-Schleife macht. Mit einem etwas anderen Testaufbau zeigt es deutlich, dass O(n) vs. O(log n) viel früher wächst (wobei 1000 vorgefüllte Elemente mit dem Baum bereits schneller sind).

Aktuellen Zustand:

Nach:

Eine ziemlich einfache Optimierung besteht darin, das Array-von-Arrays auf ein einzelnes Array zu reduzieren. Dh statt BufferLine von _N_ Spalten mit einem _data Array von _N_ CharData Zellen, wobei jedes CharData ein Array von 4 ist, nur ein einzelnes Array von _4*N_ Elementen. Dies eliminiert den Objekt-Overhead von _N_ Arrays. Es verbessert auch die Cache-Lokalität, daher sollte es schneller sein. Ein Nachteil ist etwas komplizierterer und hässlicherer Code, aber es scheint sich zu lohnen.

Als Fortsetzung meines vorherigen Kommentars scheint es eine Überlegung wert zu sein, eine variable Anzahl von Elementen im Array _data für jede Zelle zu verwenden. Mit anderen Worten eine zustandsbehaftete Darstellung. Zufällige Positionsänderungen wären teurer, aber das lineare Scannen vom Anfang einer Zeile kann ziemlich schnell sein, zumal ein einfaches Array für die Cache-Lokalität optimiert ist. Eine typische sequentielle Ausgabe wäre schnell, ebenso wie das Rendern.

Ein Vorteil einer variablen Anzahl von Elementen pro Zelle ist neben der Platzersparnis auch die erhöhte Flexibilität: Zusätzliche Attribute (wie 24-Bit-Farbe), Anmerkungen für bestimmte Zellen oder Bereiche, Glyphen oder
verschachtelte DOM-Elemente.

@PerBothner Thx für deine Ideen! Ja, ich habe das Single-Dense-Array-Layout bereits mit Pointer-Arithmetik getestet, es zeigt die beste Speicherauslastung. Probleme treten bei der Größenänderung auf, das heißt im Grunde genommen den gesamten Speicherblock neu aufzubauen (überkopieren) oder schnell in einen größeren Block zu kopieren und Teile neu auszurichten. Das ist ziemlich exp. und imho nicht durch die Mem-Einsparung gerechtfertigt (getestet in einigen oben aufgeführten Playground-PRs, die Einsparungen betrugen etwa 10 % im Vergleich zur neuen Pufferlinienimplementierung).

Zu Ihrem zweiten Kommentar - wir haben das bereits besprochen, da es die Handhabung der umbrochenen Zeilen erleichtern würde. Für den Moment haben wir uns entschieden, den Zeilen-X-Spalten-Ansatz für das neue Puffer-Layout zu verwenden und das zuerst zu erledigen. Ich denke, wir sollten dies noch einmal ansprechen, sobald wir die Reflow-Resize-Implementierung durchgeführt haben.

Über das Hinzufügen von zusätzlichem Material zum Puffer: Wir machen hier derzeit das, was die meisten anderen Terminals tun - der Cursor-Vorschub wird durch wcwidth was sicherstellt, dass es mit der Idee von pty/termios kompatibel bleibt, wie die Daten angeordnet werden sollen. Dies bedeutet im Grunde, dass wir auf Pufferebene nur Dinge wie Ersatzpaare und das Kombinieren von Zeichen behandeln. Alle anderen "höheren" Join-Regeln können vom Character Joiner im Renderer angewendet werden (derzeit von https://github.com/xtermjs/xterm-addon-ligatures für Ligaturen verwendet). Ich hatte eine PR offen, um auch Unicode-Graphen auf früher Pufferebene zu unterstützen, aber ich denke, wir können dies zu diesem Zeitpunkt nicht tun, da die meisten Pty-Backends keine Ahnung davon haben (gibt es überhaupt welche?) . Das Gleiche gilt für echte BIDI-Unterstützung, ich denke, Grapheme und BIDI werden besser in der Renderer-Phase durchgeführt, um die Cursor- / Zellenbewegungen intakt zu halten.

Die Unterstützung von DOM-Knoten, die an Zellen angehängt sind, klingt wirklich interessant, diese Idee gefällt mir. Derzeit ist dies im direkten Ansatz nicht möglich, da wir verschiedene Renderer-Backends haben (DOM, Canvas 2D und der neue glänzende Webgl-Renderer). direkt machen können). Wir bräuchten eine Art API auf Pufferebene, um dieses Zeug und seine Größe bekannt zu geben und der Renderer könnte die Drecksarbeit machen. Ich denke, wir sollten dies mit einem separaten Thema besprechen/verfolgen.

Danke für deine ausführliche Antwort.

_"Probleme gibt es bei der Größenänderung, das heißt im Grunde genommen den gesamten Speicherblock neu aufzubauen (überkopieren) oder schnell in einen größeren Block zu kopieren und Teile neu auszurichten."_

Meinst du: Bei der Größenänderung müssten wir _4*N_ Elemente kopieren und nicht nur _N_ Elemente?

Es kann sinnvoll sein, dass das Array alle Zellen für eine logische (unverpackte) Zeile enthält. Nehmen Sie zB eine 180-stellige Zeile und ein 80-spaltiges Terminal an. In diesem Fall könnten Sie 3 BufferLine Instanzen haben, die alle denselben _4*180_-Element- _data Puffer teilen, aber jede BufferLine würde auch einen Start-Offset enthalten.

Nun, ich hatte alles in einem großen Array, das von [cols] x [rows] x [needed single cell space] . Es funktionierte also im Grunde noch als "Leinwand" mit einer gegebenen Höhe und Breite. Dies ist wirklich speichereffizient und schnell für den normalen Eingabefluss, aber sobald insertCell / deleteCell aufgerufen wird (eine Größenänderung würde das tun), der gesamte Speicher hinter der Position, an der die Aktion stattfindet müsste verschoben werden. Für kleine Scrollbacks (<10k) ist dies auch kein Problem, bei >100k Zeilen ist es wirklich ein Showstopper.
Beachten Sie, dass das aktuelle typisierte Array impl diese Verschiebungen immer noch ausführen muss, aber weniger toxisch, da es nur den Speicherinhalt bis zum Zeilenende verschieben muss.
Ich dachte über verschiedene Layouts die teueren Schichten, Hauptfeld zu speichern Unsinn Speicherverschiebungen wären zu umgehen , um tatsächlich die Rückholung aus den „heißen Anschlussreihen“ zu trennen (die letzten bis zu terminal.rows ) , da diese nur sein können durch Cursorsprünge und Einfügen/Löschen verändert.

Die gemeinsame Nutzung des zugrunde liegenden Speichers durch mehrere Pufferzeilenobjekte ist eine interessante Idee, um das Wrapping-Problem zu lösen. Ich bin mir noch nicht sicher, wie dies zuverlässig funktionieren kann, ohne explizites Ref-Handling und dergleichen einzuführen. In einer anderen Version habe ich versucht, alles mit explizitem Memory-Handling zu machen, aber der Ref-Zähler war ein echter Showstopper und fühlte sich im GC-Land falsch an. (siehe #1633 für die Primitiven)

Bearbeiten: Übrigens war die explizite Speicherbehandlung auf Augenhöhe mit dem aktuellen "Speicher pro Zeile"-Ansatz. mem-Handling in der JS-Abstraktion.

Die Unterstützung von DOM-Knoten, die an Zellen angehängt sind, klingt wirklich interessant, diese Idee gefällt mir. Derzeit ist dies im direkten Ansatz nicht möglich, da wir verschiedene Renderer-Backends haben (DOM, Canvas 2D und der neue glänzende Webgl-Renderer). direkt machen können).

Es ist ein bisschen vom Thema abgekommen, aber ich sehe, dass wir irgendwann DOM-Knoten haben, die mit Zellen innerhalb des Ansichtsfensters verbunden sind, die sich ähnlich wie die Renderebenen der Leinwand verhalten. Auf diese Weise können Verbraucher Zellen mit HTML und CSS "dekorieren" und müssen nicht in die Canvas-API einsteigen.

Es kann sinnvoll sein, dass das Array alle Zellen für eine logische (unverpackte) Zeile enthält. Nehmen Sie zB eine 180-stellige Zeile und ein 80-spaltiges Terminal an. In diesem Fall könnten Sie 3 BufferLine-Instanzen haben, die alle denselben 4*180-Element-_data-Puffer teilen, aber jede BufferLine würde auch einen Start-Offset enthalten.

Der oben erwähnte Plan für den Reflow ist in https://github.com/xtermjs/xterm.js/issues/622#issuecomment -375403572 erfasst die neuen Zeilen für den schnellen Zugriff auf jede gegebene Zeile (auch Optimierung für horizontale Größenänderungen).

Die Verwendung des dichten Array-Ansatzes könnte etwas sein, das wir uns ansehen könnten, aber es scheint, dass es den zusätzlichen Aufwand nicht wert wäre, die nicht umbrochenen Zeilenumbrüche in einem solchen Array zu verwalten, und das Durcheinander, das entsteht, wenn Zeilen von oben abgeschnitten werden der Scrollback-Puffer. Unabhängig davon denke ich, dass wir uns nicht mit solchen Änderungen beschäftigen sollten, bis #791 fertig ist und wir uns #622 ansehen.

Mit PR #1796 erhält der Parser typisierte Array-Unterstützung, die die Tür für diverse weitere Optimierungen öffnet, andererseits aber auch zu anderen Eingabekodierungen.

Für den Moment habe ich mich für Uint16Array , da es einfach mit JS-Strings hin- und herkonvertierbar ist. Dies beschränkt das Spiel grundsätzlich auf UCS2/UTF16, während der Parser in der aktuellen Version auch mit UTF32 umgehen kann (UTF8 wird nicht unterstützt). Der typisierte Array-basierte Terminalpuffer ist derzeit für UTF32 ausgelegt, die UTF16 --> UTF32-Konvertierung erfolgt in InputHandler.print . Von hier aus sind mehrere Richtungen möglich:

  • alle UTF16 machen, also auch den Terminalpuffer in UTF16 umwandeln
    Ja, immer noch nicht entschieden, welchen Weg man hier einschlagen soll, habe aber mehrere Pufferlayouts getestet und bin zu dem Schluss gekommen, dass eine 32-Bit-Zahl genug Platz bietet, um den tatsächlichen Zeichencode + WC-Breite + möglichen Kombinationsüberlauf zu speichern (völlig anders gehandhabt), während 16bit das nicht kann ohne wertvolle Zeichencode-Bits zu opfern. Beachten Sie, dass wir auch mit einem UTF16-Puffer noch die UTF32-Konvertierung durchführen müssen, da wcwidth mit Unicode-Codepunkten arbeitet. Beachten Sie auch, dass UTF16-basierte Puffer mehr Speicher für niedrigere Zeichencodes sparen würden, tatsächlich werden höhere Zeichencodes als BMP-Ebenen-Zeichencodes selten vorkommen. Dies bedarf noch einiger Untersuchung.
  • machen Parser UTF32
    Das ist ganz einfach, ersetzen Sie einfach alle typisierten Arrays durch die 32-Bit-Variante. Nachteil - die Konvertierung von UTF16 in UTF32 müsste vorher durchgeführt werden, was bedeutet, dass die gesamte Eingabe konvertiert wird, sogar Escape-Sequenzen, die niemals aus einem Zeichencode > 255 gebildet werden.
  • wcwidth UTF16-kompatibel machen
    Ja, wenn sich herausstellt, dass UTF16 besser für den Terminalpuffer geeignet ist, sollte dies getan werden.

Über UTF8: Der Parser kann derzeit keine nativen UTF8-Sequenzen verarbeiten, hauptsächlich aufgrund der Tatsache, dass die Zwischenzeichen mit C1-Steuerzeichen kollidieren. Auch UTF8 benötigt eine korrekte Stream-Behandlung mit zusätzlichen Zwischenzuständen, das ist unangenehm und sollte imho nicht zum Parser hinzugefügt werden. UTF8 wird vorher besser gehandhabt, und vielleicht mit einem Konvertierungsrecht auf UTF32 für einfachere Codepoint-Handhabung überall.

Bezüglich einer möglichen UTF8-Eingabekodierung und des internen Pufferlayouts habe ich einen groben Test gemacht. Um den viel größeren Einfluss des Canvas-Renderers auf die Gesamtlaufzeit auszuschließen, habe ich es mit dem kommenden webgl-Renderer gemacht. Mit meinem ls -lR /usr/lib Benchmark erhalte ich folgende Ergebnisse:

  • aktueller Master + Webgl-Renderer:
    grafik

  • Spielplatz-Zweig, gilt #1796, Teile von #1811 und webgl-Renderer:
    grafik

Der Playground-Zweig führt eine frühe Konvertierung von UTF8 zu UTF32 durch, bevor er das Parsen und Speichern durchführt (die Konvertierung fügt ~ 30 ms hinzu). Die Beschleunigung wird hauptsächlich durch die 2 Hot-Funktionen während des Eingabeflusses gewonnen, EscapeSequenceParser.parse (120 ms vs. 35 ms) und InputHandler.print (350 ms vs. 75 ms). Beide profitieren sehr vom typisierten Array-Switch, indem sie .charCodeAt Aufrufe sparen.
Ich habe diese Ergebnisse auch mit einem UTF16-Zwischentyp-Array verglichen - EscapeSequenceParser.parse ist etwas schneller (~ 25 ms), aber InputHandler.print fällt aufgrund der erforderlichen Ersatzpaarung und Codepunktsuche in wcwidth (120 ms).
Beachten Sie auch, dass ich bereits am Limit bin, das das System die ls Daten bereitstellen kann (i7 mit SSD) - die gewonnene Geschwindigkeit erhöht die Leerlaufzeit, anstatt den Lauf zu beschleunigen.

Zusammenfassung:
Imho ist die schnellste Eingabebehandlung, die wir bekommen können, eine Mischung aus UTF8-Transport + UTF32 für die Pufferdarstellung. Während der UTF8-Transport die beste Byte-Packrate für typische Terminaleingaben hat und unsinnige Konvertierungen von pty durch mehrere Pufferschichten bis zu Terminal.write , kann der UTF32-basierte Puffer die Daten ziemlich schnell speichern. Letzteres hat einen etwas höheren Speicherbedarf als UTF16, während UTF16 aufgrund der komplizierteren Zeichenbehandlung mit mehr Indirektionen etwas langsamer ist.

Abschluss:
Wir sollten vorerst das UTF32-basierte Pufferlayout verwenden. Wir sollten auch in Betracht ziehen, auf die UTF8-Eingabecodierung umzusteigen, aber dies erfordert noch einige Überlegungen zu den API-Änderungen und den Auswirkungen für Integratoren (anscheinend kann der IPC-Mechanismus von Electron ohne BASE64-Codierung und JSON-Wrapping nicht mit Binärdaten umgehen, was den Perf-Bemühungen entgegenwirken würde).

Pufferlayout für die kommende True-Color-Unterstützung:

Derzeit ist das typisierte Array-basierte Pufferlayout wie folgt (eine Zelle):

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

wobei attrs alle benötigten Flags + 9-Bit-basierte FG- und BG-Farben enthält. codepoint verwendet 21 Bits (max. 0x10FFFF für UTF32) + 1 Bit, um das Kombinieren von Zeichen anzuzeigen, und wcwidth 2 Bits (Bereich 0-2).

Die Idee ist, die Bits zu einer besseren Packrate neu anzuordnen, um Platz für die zusätzlichen RGB-Werte zu schaffen:

  • setze wcwidth in ungenutzte hohe Bits von codepoint
  • Attrs in eine FG- und BG-Gruppe mit 32 Bit aufteilen, Flags in ungenutzte Bits verteilen
|             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) |

Nachteil dieses Ansatzes ist der relativ günstige Zugriff auf jeden Wert durch einen Indexzugriff und max. 2-Bit-Operationen (und/oder + Shift).

Der Speicherbedarf ist stabil zur aktuellen Variante, aber mit 12 Byte pro Zelle immer noch recht hoch. Dies könnte weiter optimiert werden, indem man durch den Wechsel zu UTF16 und einer attr Indirektion etwas Laufzeit opfert:

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

Jetzt sind wir bei 4 Bytes pro Zelle + etwas Platz für die Attribute. Jetzt könnten die Attrs auch für andere Zellen recycelt werden. Yay-Mission erfüllt! - Ähm, eine Sekunde...

Beim Vergleich des Speicherbedarfs gewinnt der zweite Ansatz eindeutig. Nicht so bei der Laufzeit, es gibt drei Hauptfaktoren, die die Laufzeit stark erhöhen:

  • attr-Umleitung
    Der attr-Zeiger benötigt eine zusätzliche Speichersuche in einem anderen Datencontainer.
  • attr-Matching
    Um beim zweiten Ansatz wirklich Platz zu sparen, müsste die angegebene Attr mit bereits gespeicherten Attrs abgeglichen werden. Dies ist eine umständliche Aktion, ein direkter Ansatz durch einfaches Durchsuchen aller vorhandenen Werte ist in O(n) für n gespeicherte Attrs Indexzugriff im 32-Bit-Ansatz mit O(1). Außerdem hat ein Baum eine schlechtere Laufzeit für wenige gespeicherte Elemente (lohnt sich etwas um >100 Einträge mit meinem RB-Baum-Impl).
  • UTF16-Ersatzpaarung
    Bei einem 16-Bit-typisierten Array müssen wir für die Codepunkte auf UTF16 degradieren, was auch Laufzeiteinbußen mit sich brachte (wie im obigen Kommentar beschrieben). Beachten Sie, dass Codepunkte, die höher als BMP sind, kaum vorkommen, dennoch fügt die Prüfung allein, ob ein Codepunkt ein Ersatzpaar bilden würde, ~ 50 ms hinzu.

Der Reiz des zweiten Ansatzes liegt in der zusätzlichen Speicherersparnis. Daher habe ich es mit dem Playground-Zweig (siehe Kommentar oben) mit einer modifizierten BufferLine Implementierung getestet:

grafik

Ja, wir sind irgendwie wieder da, wo wir angefangen haben, bevor wir im Parser auf UTF8 + typisierte Arrays umgestiegen sind. Der Speicherverbrauch sank jedoch von ~ 1,5 MB auf ~ 0,7 MB (Demo-App mit 87 Zellen und 1000 Zeilen Scrollback).

Ab hier gilt es, Speicher vs. Geschwindigkeit zu sparen. Da wir durch den Wechsel von js-Arrays zu typisierten Arrays bereits viel Speicher gespart haben (von ~ 5,6 MB auf ~ 1,5 MB für den C++-Heap gesenkt, das giftige JS-Heap-Verhalten und GC abgeschnitten), sollten wir hier mit der schnelleren Variante vorgehen. Sobald die Speicherauslastung wieder ein dringendes Problem wird, können wir immer noch zu einem kompakteren Pufferlayout wechseln, wie im zweiten Ansatz hier beschrieben.

Ich stimme zu, lassen Sie uns auf Geschwindigkeit optimieren, solange der Speicherverbrauch kein Problem darstellt. Ich möchte auch die Umleitung so weit wie möglich vermeiden, da der Code dadurch schwerer zu lesen und zu warten ist. Wir haben bereits ziemlich viele Konzepte und Optimierungen in unserer Codebasis, die es Leuten (einschließlich mir 😅) schwer machen, dem Codefluss zu folgen - und weitere davon sollten immer mit einem sehr guten Grund gerechtfertigt sein. IMO, ein weiteres Megabyte Speicher zu sparen, rechtfertigt dies nicht.

Trotzdem macht es mir großen Spaß, deine Übungen zu lesen und zu lernen, danke, dass du sie so ausführlich teilst!

@mofux Ja, das stimmt - die Codekomplexität ist viel höher (
Und da das 32-Bit-Layout meistens aus flachem Speicher besteht (nur das Kombinieren von Zeichen erfordert Indirektion), sind weitere Optimierungen möglich (auch Teil von #1811, noch nicht für den Renderer getestet).

Die Umleitung auf ein attr-Objekt hat einen großen Vorteil: Es ist viel erweiterbarer. Sie können Anmerkungen, Glyphen oder benutzerdefinierte Malregeln hinzufügen. Sie können Linkinformationen möglicherweise sauberer und effizienter speichern. Definieren Sie vielleicht eine ICellPainter Schnittstelle, die weiß, wie ihre Zelle gerendert wird, und an die Sie auch benutzerdefinierte Eigenschaften hängen können.

Eine Idee ist, zwei Arrays pro BufferLine zu verwenden: ein Uint32Array und ein ICellPainter-Array mit jeweils einem Element für jede Zelle. Der aktuelle ICellPainter ist eine Eigenschaft des Parser-Status, und Sie verwenden denselben ICellPainter einfach wieder, solange sich der Farb-/Attributstatus nicht ändert. Wenn Sie einer Zelle spezielle Eigenschaften hinzufügen müssen, klonen Sie zuerst den ICellPainter (sofern dieser freigegeben wird).

Sie können ICellPainter für die gängigsten Farb-/Attributkombinationen vorab zuweisen – zumindest haben Sie ein eindeutiges Objekt, das den Standardfarben/Attributen entspricht.

Stiländerungen (z. B. das Ändern der Standardvorder-/Hintergrundfarben) können durch einfaches Aktualisieren der entsprechenden ICellPainter-Instanz(en) implementiert werden, ohne dass jede Zelle aktualisiert werden muss.

Es gibt mögliche Optimierungen: Verwenden Sie beispielsweise verschiedene ICellPainter-Instanzen für Zeichen mit einfacher und doppelter Breite (oder Zeichen mit Nullbreite). (Das spart 2 Bits in jedem Uint32Array-Element.) Es gibt 11 verfügbare Attributbits in Uint32Array (mehr, wenn wir für BMP-Zeichen optimieren). Diese können verwendet werden, um die gebräuchlichsten/nützlichsten Farb-/Attributkombinationen zu codieren, die verwendet werden können, um die gebräuchlichsten ICellPainter-Instanzen zu indizieren. Wenn dies der Fall ist, kann das ICellPainter-Array träge zugewiesen werden - dh nur, wenn eine Zelle in der Zeile einen "weniger verbreiteten" ICellPainter erfordert.

Man könnte auch das Array _combined für Nicht-BMP-Zeichen entfernen und diese im ICellPainter speichern. (Dafür ist für jedes Nicht-BMP-Zeichen ein eindeutiger ICellPainter erforderlich, daher gibt es hier einen Kompromiss.)

@PerBothner Ja, eine

Einige Anmerkungen zu dem, was ich in mehreren Testbeds ausprobiert habe:

  • Inhalt der Zellzeichenfolge
    Da ich selbst von C++ komme, habe ich versucht, das Problem so zu betrachten, wie ich es in C++ tun würde, also begann ich mit Zeigern für den Inhalt. Dies war ein einfacher Zeichenfolgenzeiger, der jedoch die meiste Zeit auf eine einzelne Zeichenfolge zeigte. Was für eine Verschwendung. Meine erste Optimierung bestand daher darin, die String-Abstraktion loszuwerden, indem direkt der Codepunkt anstelle der Adresse gespeichert wurde (in C/C++ viel einfacher als in JS). Dadurch wurde der Lese-/Schreibzugriff fast verdoppelt und gleichzeitig 12 Bytes pro Zelle eingespart (8 Bytes Pointer + 4 Bytes auf String, 64bit mit 32bit wchar_t). Randbemerkung - die Hälfte des Geschwindigkeitsschubs hier ist Cache-bezogen (Cache-Fehlschläge aufgrund zufälliger String-Positionen). Dies wurde bei meiner Problemumgehung zum Kombinieren von Zelleninhalten deutlich - ein Speicherblock, den ich indiziert habe, als codepoint das kombinierte Bit gesetzt hatte (der Zugriff war hier schneller aufgrund der besseren Cache-Lokalität, getestet mit Valgrind). Auf JS übertragen war der Geschwindigkeitsschub aufgrund der erforderlichen String-zu-Zahl-Konvertierung nicht so groß (obwohl immer noch schneller), aber die Speichereinsparung war noch größer (vermutlich aufgrund einiger zusätzlicher Verwaltungsräume für JS-Typen). Problem war das globale StringStorage für das kombinierte Zeug mit der expliziten Speicherverwaltung, ein großes Antimuster in JS. Ein Quickfix dafür war das _combined Objekt, das die Bereinigung an den GC delegiert. Es kann sich immer noch ändern und btw ist dazu gedacht, beliebige zellbezogene String-Inhalte zu speichern (habe dies mit Graphemen im Hinterkopf, aber wir werden sie nicht so bald sehen, da sie von keinem Backend unterstützt werden). Dies ist also der Ort, um zusätzliche Zeichenfolgeninhalte Zelle für Zelle zu speichern.
  • attrs
    Mit den Attrs habe ich "think big" angefangen - mit einem globalen AttributeStorage für alle Attrs, die jemals in allen Terminalinstanzen verwendet wurden (siehe https://github.com/jerch/xterm.js/tree/AttributeStorage). In Bezug auf den Speicher hat dies ziemlich gut geklappt, hauptsächlich weil ppl selbst mit True Color-Unterstützung nur einen kleinen Satz von Attrs verwenden. Die Leistung war nicht so gut - hauptsächlich aufgrund des Ref-Zählens (jede Zelle musste zweimal in diesen fremden Speicher schauen) und des Attr-Matchings. Und als ich versuchte, das Ref-Ding an JS zu übertragen, fühlte es sich einfach falsch an - der Punkt, an dem ich die "STOP"-Taste gedrückt habe. Zwischendurch hat sich herausgestellt, dass wir durch die Umstellung auf getipptes Array bereits tonnenweise Speicher und GC-Aufrufe gespart haben, sodass sich das etwas teurere flache Speicherlayout hier seinen Geschwindigkeitsvorteil auszahlen kann.
    Was ich yday (letzter Kommentar) getestet habe, war ein zweites typisiertes Array auf Zeilenebene für die attrs mit dem Baum von https://github.com/jerch/xterm.js/tree/AttributeStorage für das Matching (ziemlich ähnlich wie Ihre ICellPainter-Idee ). Nun, die Ergebnisse sind nicht vielversprechend, daher tendiere ich vorerst zum flachen 32-Bit-Layout.

Nun stellt sich heraus, dass dieses flache 32-Bit-Layout für den üblichen Kram optimiert ist und ungewöhnliche Extras damit nicht möglich sind. Wahr. Nun, wir haben immer noch Marker (nicht daran gewöhnt, daher kann ich im Moment nicht sagen, wozu sie fähig sind) und yepp - es gibt noch freie Bits im Puffer (was für zukünftige Bedürfnisse gut ist, z Flaggen für besondere Behandlung und dergleichen).

Tbh für mich ist es schade, dass das 16-Bit-Layout mit attrs-Speicher so schlecht abschneidet, die Halbierung des Speicherverbrauchs ist immer noch eine große Sache (insbesondere wenn ppl beginnen, Scroll-Linien >10k zu verwenden), aber die Laufzeiteinbußen und die Codekomplexität überwiegen das höhere mem braucht atm imho.

Können Sie die Idee von ICellPainter näher erläutern? Vielleicht habe ich bisher ein entscheidendes Feature übersehen.

Mein Ziel für DomTerm war es, eine umfassendere Interaktion zu ermöglichen und zu fördern, genau das, was ein herkömmlicher Terminalemulator ermöglicht. Die Verwendung von Webtechnologien ermöglicht viele interessante Dinge, daher wäre es schade, sich nur darauf zu konzentrieren, ein schneller traditioneller Terminalemulator zu sein. Zumal viele Anwendungsfälle für xterm.js (wie REPLs für IDEs) wirklich davon profitieren können, über einfachen Text hinauszugehen. Xterm.js tut gut auf der Geschwindigkeitsseite (beschwert jemand über die Geschwindigkeit?), Aber es funktioniert nicht so gut auf Features (Leute beschweren sich über fehlende True Color und eingebettete Grafiken, zum Beispiel). Ich denke, es kann sich lohnen, sich etwas mehr auf Flexibilität und etwas weniger auf Leistung zu konzentrieren.

_"Können Sie die Idee von ICellPainter näher erläutern?"_

Im Allgemeinen kapselt ICellPainter alle Daten pro Zelle mit Ausnahme des Zeichencodes/-werts, der aus dem Uint32Array stammt. Das ist für "normale" Zeichenzellen - für eingebettete Bilder und andere "Boxen" ist der Zeichencode/-wert möglicherweise nicht sinnvoll.

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

Das Zuordnen einer Zelle zu ICellPainter kann auf verschiedene Weise erfolgen. Das Offensichtliche ist, dass jede BufferLine ein ICellPainter-Array hat, aber das erfordert (mindestens) einen 8-Byte-Zeiger pro Zelle. Eine Möglichkeit besteht darin, das Array _combined mit dem Array ICellPainter zu kombinieren: Wenn IS_COMBINED_BIT_MASK gesetzt ist, dann enthält der ICellPainter auch den kombinierten String. Eine andere mögliche Optimierung besteht darin, die verfügbaren Bits im Uint32Array als Index für ein Array zu verwenden: Das fügt zusätzliche Komplikationen und Umwege hinzu, spart aber Platz.

Ich möchte uns ermutigen, zu prüfen, ob wir es so machen können, wie es monaco-editor macht (ich denke, sie haben einen wirklich intelligenten und performanten Weg gefunden). Anstatt solche Informationen im Puffer zu speichern, können Sie decorations erstellen. Sie erstellen eine Dekoration für einen Zeilen- / Spaltenbereich und sie bleibt bei diesem Bereich:

// 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 }
});

Später könnte ein Renderer diese Dekorationen aufnehmen und zeichnen.

Bitte sehen Sie sich dieses kleine Beispiel an, das zeigt, wie die monaco-Editor-API aussieht:
https://microsoft.github.io/monaco-editor/playground.html#interacting -with-the-editor-line-and-inline-decorations

Für Dinge wie das Rendern von Bildern im Terminal verwendet Monaco ein Konzept von Sichtzonen, das (neben anderen Konzepten) in einem Beispiel hier zu sehen ist:
https://microsoft.github.io/monaco-editor/playground.html#interacting -with-the-editor-listening-to-mouse-events

@PerBothner Thx für die Klarstellung und die Skizze. Dazu ein paar Anmerkungen.

Wir planen schließlich, die Eingabekette + Puffer in Zukunft in einen Webworker zu verschieben. Daher soll der Puffer auf einer abstrakten Ebene arbeiten und wir können dort noch keine Render- / Darstellungsbezogenen Dinge wie Pixelmetriken oder DOM-Knoten verwenden. Ich sehe Ihren Bedarf dafür, weil DomTerm hochgradig anpassbar ist, aber ich denke, wir sollten dies mit einer verbesserten internen Marker-API tun und können hier von monaco/vscode lernen (thx für die Zeiger @mofux).
Ich würde den Kernpuffer wirklich gerne von ungewöhnlichen Dingen freihalten, vielleicht sollten wir mit einer neuen Ausgabe mögliche Markerstrategien diskutieren?

Mit dem Ergebnis der 16-Bit-Layout-Testergebnisse bin ich immer noch nicht zufrieden. Da eine endgültige Entscheidung noch nicht dringlich ist (vor 3.11 werden wir davon nichts sehen), werde ich es mit ein paar Änderungen weiter testen (es ist für mich immer noch die faszinierendere Lösung als die 32-Bit-Variante).

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

Ich denke auch, dass wir zu Beginn etwas in der Nähe davon machen sollten, wir können später andere Optionen erkunden, aber dies wird wahrscheinlich am einfachsten sein, um loszulegen. Die Attributindirektion ist definitiv vielversprechend, da es in einer Terminalsitzung normalerweise nicht so viele verschiedene Attribute gibt.

Ich möchte uns ermutigen, zu prüfen, ob wir es so machen können, wie es monaco-editor macht (ich denke, sie haben einen wirklich intelligenten und performanten Weg gefunden). Anstatt solche Informationen im Puffer zu speichern, können Sie Dekorationen erstellen. Sie erstellen eine Dekoration für einen Zeilen- / Spaltenbereich und sie bleibt bei diesem Bereich:

So etwas würde ich gerne sehen. Eine Idee, die ich in diese Richtung hatte, war, es Einbettern zu ermöglichen, DOM-Elemente an Bereiche anzuhängen, damit benutzerdefinierte Dinge gezeichnet werden können. Mir fallen im Moment 3 Dinge ein, die ich damit erreichen möchte:

  • Link-Unterstreichungen auf diese Weise zeichnen (wird das Zeichnen erheblich vereinfachen)
  • Erlaube Markierungen in Zeilen, wie ein * oder ähnliches
  • Erlaube, dass Zeilen "blinken", um anzuzeigen, dass etwas passiert ist

All dies könnte mit einem Overlay erreicht werden, und es ist eine ziemlich zugängliche Art von API (die einen DOM-Knoten verfügbar macht) und kann unabhängig vom Renderer-Typ funktionieren.

Ich bin mir nicht sicher, ob wir in das Geschäft einsteigen wollen, Einbetter zu erlauben, die Darstellung von Hintergrund- und Vordergrundfarben zu ändern.


@jerch Ich werde dies auf den Meilenstein 3.11.0 setzen, da ich dieses Problem als erledigt betrachte, wenn wir die für dann geplante JS-Array-Implementierung entfernen. https://github.com/xtermjs/xterm.js/pull/1796 soll dann auch zusammengeführt werden, aber bei diesem Problem ging es immer darum, das Speicherlayout des Puffers zu verbessern.

Außerdem wäre ein Großteil dieser späteren Diskussion wahrscheinlich besser unter https://github.com/xtermjs/xterm.js/issues/484 und https://github.com/xtermjs/xterm.js/issues/1852 (erstellt, da es kein Dekorationsproblem gab).

@Tyriar Woot - endlich geschlossen :sweat_smile:

🎉 🕺 🍾

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen