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
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
Nachteile
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
Int32Array
Nachteile
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
.flags
statt [0]
)Nachteile
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
.flags
statt [0]
)Nachteile
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
.flags
statt [0]
)Nachteile
CharAttributes
pro Block behalten?CharAttributeEntry
ObjektDies 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
.flags
statt [0]
)Nachteile
Int32Array
wird nicht funktionieren, da es viel zu lange dauert, das Int wieder in ein Zeichen zu konvertieren.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:
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:
Array
(vielleicht noch weniger optimiert von Engine-Herstellern)new UintXXArray
ist viel schlimmer als die literale Array-Erstellung 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 Terminalzeilenwcwidth(string) % cols
vorrücken\n
(harter Zeilenumbruch): Cursor um eine Zeile vorrücken, Position in Zeigerliste als harten Zeilenumbruch markieren\r
: Lade den Inhalt der letzten Zeile (von der aktuellen Cursorposition bis zum letzten Zeilenumbruch) in einen Zeilenpuffer, um überschrieben zu werden\r
Falls ist keine Zellabstraktion oder String-Aufteilung erforderlichcols
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:
cols
rückwärts bewegen - der Anfang des aktuellen TerminalinhaltsNun der gefährliche Teil 2 - der Renderer möchte etwas zeichnen:
Vorteile:
InputHandler
Methode - print
Nachteile:
InputHandler
Methode wird in dem Sinne gefährlich sein, dass dieses Flussmodell unterbrochen wird und eine Zwischenabstraktion der Zellen erforderlich istNun, 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):
print
Aktion springt von 190 MB/s auf 290 MB/sprint
Aktion springt von 190 MB/s auf 320 MB/sInsgesamt 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:
mit Uint16Array:
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:
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.uint16_t
Typ für UTF-16.InputHandler
mit Zeigern auf diesen Speicher anstelle von String-Slices auf.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:
malloc
und free
Kosten (abhängig von der Geschicklichkeit des Allocators/Deallocators)Nachteile:
: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
Die Links von @rebornix wurden angepinnt , damit sie nicht kaputt gehen, wenn Commits hinzugefügt werden 😃
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:
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):
@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:
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.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.CharData
von [number, string, number, number]
auf [number, number]
, wobei die Zahlen Zeiger (Indexnummern) sind auf:AttributeCache
EintragStringStorage
EintragEs 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).
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: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 sinktNachteil - 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:
blankLine
und eraseChar
, jedoch mit einem Leerzeichen als Inhalt.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:
CircularList
: spart 50% (~2,8 MB von ~5,5 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:
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:
SGR 39
+ SGR 49
Standardfarbe für fg/bg (anpassbar)SGR 30-37
+ SGR 40-47
8 niedrige Farbpalette für fg/bg (anpassbar)SGR 90-97
+ SGR 100-107
8 hohe Farbpalette für fg/bg (anpassbar)SGR 38;5;n
+ SGR 48;5;n
256 indizierte Palette für fg/bg (anpassbar)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:
wcwidth
UTF16-kompatibel machenÜ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:
Spielplatz-Zweig, gilt #1796, Teile von #1811 und webgl-Renderer:
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:
wcwidth
in ungenutzte hohe Bits von 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) |
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:
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:
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:
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.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.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:
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:
🎉 🕺 🍾
Hilfreichster Kommentar
Aktuellen Zustand:
Nach: