Xterm.js: Mejoras en el rendimiento del búfer

Creado en 13 jul. 2017  ·  73Comentarios  ·  Fuente: xtermjs/xterm.js

Problema

Memoria

En este momento, nuestro búfer está ocupando demasiada memoria, particularmente para una aplicación que lanza múltiples terminales con grandes retrocesos configurados. Por ejemplo, la demostración que usa un terminal de 160x24 con 5000 scrollback lleno toma alrededor de 34 MB de memoria (consulte https://github.com/Microsoft/vscode/issues/29840#issuecomment-314539964), recuerde que es solo un terminal y los monitores de 1080p probablemente use terminales más anchos. Además, para admitir truecolor (https://github.com/sourcelair/xterm.js/issues/484), cada carácter deberá almacenar 2 tipos number adicionales que casi duplicarán el consumo de memoria actual del búfer.

Recuperación lenta del texto de una fila

Existe el otro problema de tener que buscar rápidamente el texto real de una línea. La razón por la que esto es lento se debe a la forma en que se presentan los datos; una línea contiene una matriz de caracteres, cada uno con una sola cadena de caracteres. Así que construiremos la cadena y luego estará lista para la recolección de basura inmediatamente después. Anteriormente no necesitábamos hacer esto en absoluto porque el texto se extrae del búfer de línea (en orden) y se procesa en el DOM. Sin embargo, esto se está volviendo cada vez más útil a medida que mejoramos xterm.js aún más, características como la selección y los enlaces extraen estos datos. Nuevamente, usando el ejemplo de desplazamiento hacia atrás de 160x24 / 5000, se necesitan 30-60ms para copiar todo el búfer en una Macbook Pro de mediados de 2014.

Apoyando el futuro

Otro problema potencial en el futuro es que cuando veamos la introducción de algún modelo de vista que pueda necesitar duplicar algunos o todos los datos en el búfer, este tipo de cosas será necesario para implementar el reflujo (https://github.com/sourcelair /xterm.js/issues/622) correctamente (https://github.com/sourcelair/xterm.js/pull/644#issuecomment-298058556) y tal vez también sea necesario para admitir correctamente los lectores de pantalla (https://github.com /sourcelair/xterm.js/issues/731). Sin duda, sería bueno tener algo de margen de maniobra en lo que respecta a la memoria.

Esta discusión comenzó en https://github.com/sourcelair/xterm.js/issues/484 , esto entra en más detalles y propone alguna solución adicional.

Me inclino hacia la solución 3 y avanzo hacia la solución 5 si hay tiempo y muestra una mejora notable. ¡Me encantaría recibir comentarios! / cc @jerch , @mofux , @rauchg , @parisk

1. Solución simple

Esto es básicamente lo que estamos haciendo ahora, solo que con truecolor fg y bg agregados.

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

type LineData = CharData[];

Pros

  • Muy simple

Contras

  • Demasiada memoria consumida, casi duplicaría nuestro uso de memoria actual, que ya es demasiado alto.

2. Extraiga el texto de CharData

Esto almacenaría la cadena contra la línea en lugar de la línea, esto probablemente vería ganancias muy grandes en la selección y la vinculación y sería más útil a medida que pasa el tiempo teniendo acceso rápido a la cadena completa de una línea.

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;

Pros

  • No es necesario reconstruir la línea cuando lo necesitemos.
  • Memoria más baja que la actual debido al uso de un Int32Array

Contras

  • Lento para actualizar caracteres individuales, toda la cadena debería regenerarse para cambios de caracteres individuales.

3. Almacenar atributos en rangos

Extraer los atributos y asociarlos con un rango. Dado que nunca puede haber atributos superpuestos, esto se puede organizar de forma secuencial.

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

Pros

  • Memoria más baja que la actual, aunque también estamos almacenando datos de truecolor.
  • Puede optimizar la aplicación de atributos, en lugar de verificar el atributo de cada carácter y diferenciarlo del anterior.
  • Encapsula la complejidad de almacenar los datos dentro de una matriz ( .flags lugar de [0] )

Contras

  • Cambiar los atributos de un rango de caracteres dentro de otro rango es más complejo

4. Ponga atributos en un caché

La idea aquí es aprovechar el hecho de que generalmente no hay tantos estilos en una sesión de terminal, por lo que no debemos crear tan pocos como sea necesario y reutilizarlos.

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

Pros

  • Uso de memoria similar al de hoy, aunque también estamos almacenando datos de truecolor
  • Encapsula la complejidad de almacenar los datos dentro de una matriz ( .flags lugar de [0] )

Contras

  • Menos ahorro de memoria que el enfoque de rangos

5. Híbrido de 3 y 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;
}

Pros

  • Protentially el más rápido y más eficiente en memoria
  • Muy eficiente en memoria cuando el búfer contiene muchos bloques con estilos pero solo de unos pocos estilos (el caso común)
  • Encapsula la complejidad de almacenar los datos dentro de una matriz ( .flags lugar de [0] )

Contras

  • Más complejo que las otras soluciones, puede que no valga la pena incluir el caché si ya tenemos un solo CharAttributes por bloque.
  • Gastos generales adicionales en CharAttributeEntry objeto
  • Cambiar los atributos de un rango de caracteres dentro de otro rango es más complejo

6. Híbrido de 2 y 3

Esto toma la solución de 3, pero también agrega una cadena de texto que evalúa perezosamente para un acceso rápido al texto de la línea. Dado que también estamos almacenando los caracteres en CharData , podemos evaluarlo perezosamente.

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

Pros

  • Memoria más baja que la actual, aunque también estamos almacenando datos de truecolor.
  • Puede optimizar la aplicación de atributos, en lugar de verificar el atributo de cada carácter y diferenciarlo del anterior.
  • Encapsula la complejidad de almacenar los datos dentro de una matriz ( .flags lugar de [0] )
  • Acceso más rápido a la cadena de línea real

Contras

  • Memoria adicional debido a que se cuelga de cadenas de líneas
  • Cambiar los atributos de un rango de caracteres dentro de otro rango es más complejo

Soluciones que no funcionarán

  • Almacenar la cadena como un int dentro de un Int32Array no funcionará ya que lleva mucho tiempo convertir el int de nuevo en un carácter.
areperformance typplan typproposal

Comentario más útil

Estado actual:

Después:

Todos 73 comentarios

Otro enfoque que podría combinarse: use indexeddb, websql o la api del sistema de archivos para eliminar las entradas de retroceso inactivas en el disco 🤔

Gran propuesta. Estoy de acuerdo con que 3. es la mejor manera de hacerlo por ahora, ya que nos permite ahorrar memoria al mismo tiempo que admite el color verdadero.

Si llegamos allí y las cosas siguen yendo bien, entonces podemos optimizar como se propone en 5. o de cualquier otra forma que se nos ocurra en ese momento y tenga sentido.

3. es genial 👍.

@mofux , si bien definitivamente existe un caso de uso para el uso de técnicas respaldadas por almacenamiento en disco para reducir la huella de memoria, esto puede degradar la experiencia del usuario de la biblioteca en entornos de navegador que solicitan permiso al usuario para usar el almacenamiento en disco.

Respecto a Apoyar el futuro :
Cuanto más lo pienso, más me atrae la idea de tener un WebWorker que hace todo el trabajo pesado de analizar los datos tty, mantener los búferes de línea, los enlaces coincidentes, los tokens de búsqueda coincidentes y todo eso. Básicamente, hacer el trabajo pesado en un hilo de fondo separado sin bloquear la interfaz de usuario. Pero creo que esto debería ser parte de una discusión separada, tal vez hacia una versión 4.0 😉

+100 sobre WebWorker en el futuro, pero creo que necesitamos cambiar las versiones de la lista de navegadores que apoyamos porque no todos pueden usarlo ...

Cuando digo Int32Array , esta será una matriz normal si no es compatible con el entorno.

@mofux buen pensamiento con WebWorker en el futuro 👍

@AndrienkoAleksandr sí, si quisiéramos usar WebWorker , necesitaríamos seguir admitiendo la alternativa también a través de la detección de características.

Guau bonita lista :)

También tiendo a inclinarme hacia 3. ya que promete una gran reducción en el consumo de memoria para más del 90% del uso típico del terminal. En mi opinión, la optimización de la memoria debería ser el objetivo principal en esta etapa. Además de esto, podría aplicarse una mayor optimización para casos de uso específicos (lo que me viene a la mente: "aplicaciones tipo lienzo" como ncurses y demás usarán toneladas de actualizaciones de celda única y degradarán un poco la lista [start, end] con el tiempo) .

@AndrienkoAleksandr sí, también me gusta la idea del webworker, ya que podría quitar _algo_ la carga del hilo principal. El problema aquí es (además del hecho de que podría no ser compatible con todos los sistemas de destino deseados) es el _algunos_: la parte JS ya no es tan importante con todas las optimizaciones que xterm.js ha visto a lo largo del tiempo. El problema real en cuanto al rendimiento es el diseño / representación del navegador ...

@mofux Lo de paginar a alguna "memoria externa" es una buena idea, aunque debería ser parte de una abstracción superior y no de "dame un widget de terminal interactivo" que es xterm.js. Esto podría lograrse mediante un complemento en mi humilde opinión.

Offtopic: Hice algunas pruebas con matrices frente a typedarrays frente a asm.js. Todo lo que puedo decir: Dios mío, es como 1 : 1,5 : 10 para cargas y conjuntos variables simples (en FF aún más). Si la velocidad pura de JS realmente comienza a doler, "use asm" podría estar ahí para el rescate. Pero lo vería como un último recurso, ya que implicaría cambios fundamentales. Y el montaje web aún no está listo para enviarse.

Offtopic: Hice algunas pruebas con matrices frente a typedarrays frente a asm.js. Todo lo que puedo decir: Dios mío, es como 1: 1,5: 10 para cargas y conjuntos variables simples (en FF aún más)

@jerch para aclarar, ¿es que las matrices vs typedarrays son 1: 1 a 1: 5?

Woops, buena captura con la coma: quise decir 10:15:100 cuanto a velocidad. Pero solo en las matrices de tipo FF eran ligeramente más rápidas que las matrices normales. asm es al menos 10 veces más rápido que las matrices js en todos los navegadores: probado con FF, webkit (Safari), blink / V8 (Chrome, Opera).

@jerch cool, una aceleración del 50% de los typedarrays además de una mejor memoria definitivamente valdría la pena invertir por ahora.

Idea para ahorrar memoria: tal vez podamos deshacernos del width de cada carácter. Intentaré implementar una versión de wcwidth menos costosa.

@jerch , necesitamos acceder a él bastante, y no podemos cargarlo con pereza ni nada porque cuando llegue el reflujo necesitaremos el ancho de cada carácter en el búfer. Incluso si fuera rápido, podríamos querer mantenerlo alrededor.

Podría ser mejor hacerlo opcional, asumiendo 1 si no se especifica:

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

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

@Tyriar Sí, bueno, como ya lo escribí, échale un vistazo en PR # 798
La aceleración es de 10 a 15 veces en mi computadora por el costo de 16k bytes para la tabla de búsqueda. Tal vez sea posible una combinación de ambos si aún es necesario.

Algunas banderas más que admitiremos en el futuro: https://github.com/sourcelair/xterm.js/issues/580

Otro pensamiento: solo la parte inferior de la terminal ( Terminal.ybase a Terminal.ybase + Terminal.rows ) es dinámica. El retroceso que constituye la mayor parte de los datos es completamente estático, tal vez podamos aprovechar esto. No sabía esto hasta hace poco, pero incluso cosas como eliminar líneas (DL, CSI Ps M) no traen el desplazamiento hacia abajo, sino que insertan otra línea. De manera similar, desplazarse hacia arriba (SU, CSI Ps S) elimina el elemento en Terminal.scrollTop e inserta un elemento en Terminal.scrollBottom .

Administrar la parte dinámica inferior de la terminal de forma independiente y presionar para desplazarse hacia atrás cuando la línea se empuja hacia afuera podría generar algunas ganancias significativas. Por ejemplo, la parte inferior podría ser más detallada para favorecer la modificación de atributos, un acceso más rápido, etc., mientras que el scrollback puede ser más un formato de archivo como se propone en lo anterior.

Otro pensamiento: probablemente sea una mejor idea restringir CharAttributeEntry a filas, ya que así es como parecen funcionar la mayoría de las aplicaciones. Además, si se cambia el tamaño de la terminal, se agrega un relleno "en blanco" a la derecha, que no comparte los mismos estilos.

p.ej:

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

A la derecha de las diferencias rojo / verde hay celdas "en blanco" sin estilo.

@Tyriar
¿Alguna posibilidad de volver a incluir este tema en la agenda? Al menos para los programas de salida intensiva, una forma diferente de almacenar los datos del terminal puede ahorrar mucha memoria y tiempo. Algún híbrido de 2/3/4 dará un gran aumento de rendimiento, si pudiéramos evitar dividir y guardar caracteres individuales de la cadena de entrada. Además, guardar los atributos solo una vez que hayan cambiado ayudará a ahorrar memoria.

Ejemplo:
Con el nuevo analizador podríamos guardar un montón de caracteres de entrada sin jugar con los atributos, ya que sabemos que no cambiarán en el medio de esa cadena. Los atributos de esa cadena podrían guardarse en alguna otra estructura de datos o atributo junto con wcwidths (sí, todavía los necesitamos para encontrar los saltos de línea) y los saltos de línea y las paradas. Básicamente, esto abandonaría el modelo de celda cuando se reciban datos.
El problema surge si algo interviene y quiere tener una representación detallada de los datos del terminal (por ejemplo, el renderizador o alguna secuencia de escape / el usuario quiere mover el cursor). Aún tenemos que hacer los cálculos de celda si eso sucede, pero debería ser suficiente hacer esto solo para el contenido dentro de las columnas y filas terminales. (Todavía no estoy seguro sobre el contenido desplazado, que podría almacenarse en caché aún más y ser barato volver a dibujar).

@jerch Me reuniré con @mofux un día en Praga en un par de semanas e íbamos a hacer / comenzar algunas mejoras internas de cómo se manejan los atributos de texto que cubren esto 😃

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

El algoritmo es un poco caro ya que cada carácter debe evaluarse dos veces

@jerch, si tiene alguna idea sobre un acceso más rápido al texto del búfer, háganoslo saber. Actualmente, la mayor parte es solo un carácter como saben, pero podría ser un ArrayBuffer , una cadena, etc. He estado pensando que deberíamos pensar en aprovechar más el hecho de que el scrollback sea inmutable de alguna manera.

Bueno, he experimentado mucho con ArrayBuffers en el pasado:

  • son un poco peores que Array respecto al tiempo de ejecución de los métodos típicos (quizás aún menos optimizados por los proveedores de motores)
  • new UintXXArray es mucho peor que la creación de una matriz literal con []
  • pagan varias veces si puede preasignar y reutilizar la estructura de datos (hasta 10 veces), aquí es donde la naturaleza de la lista enlazada de las matrices mixtas se come el rendimiento debido a la gran cantidad de alloc y gc detrás de escena
  • para los datos de cadena, la conversión de ida y vuelta consume todos los beneficios; una lástima que JS no proporcione una cadena nativa a los convertidores Uint16Array (aunque en parte es factible con TextEncoder )

Mis hallazgos sobre ArrayBuffer sugieren no usarlos para datos de cadena debido a la penalización de conversión. En teoría, la terminal podría usar ArrayBuffer desde node-pty hasta los datos de la terminal (esto ahorraría varias conversiones en el camino hacia la interfaz), no estoy seguro de si el renderizado se puede hacer de esa manera, creo que renderizar cosas que siempre necesita una conversión final de uint16_t a string . Pero incluso esa última creación de cadena consumirá la mayor parte del tiempo de ejecución guardado y, además, convertiría internamente el terminal en una fea bestia C-ish. Por lo tanto, abandoné este enfoque.

TL; DR ArrayBuffer es superior si puede preasignar y reutilizar la estructura de datos. Para todo lo demás, las matrices normales son mejores. No vale la pena meter cadenas en ArrayBuffers.

Una nueva idea que se me ocurrió intenta reducir la creación de cadenas tanto como sea posible, especialmente. trata de evitar las desagradables divisiones y uniones. Se basa un poco en su segunda idea anterior con el nuevo método InputHandler.print , wcwidth y paradas de línea en mente:

  • print ahora obtiene cadenas completas hasta varias líneas terminales
  • guarde esas cadenas en una lista de punteros simple sin ninguna alteración (sin cadena alloc o gc, list alloc puede evitarse si se usa con una estructura preasignada) junto con los atributos actuales
  • avanzar el cursor por wcwidth(string) % cols
  • caso especial \n (salto de línea duro): avanzar el cursor una línea, marcar la posición en la lista de punteros como salto fijo
  • desbordamiento de línea de caso especial con wrapAround: marcar la posición en la cadena como un salto de línea suave
  • caso especial \r : carga el contenido de la última línea (desde la posición actual del cursor hasta el último salto de línea) en algún búfer de línea para sobrescribirlo
  • los datos fluyen como el anterior, a pesar del caso de \r , no se necesita abstracción de celdas ni división de cadenas
  • los cambios de atributo no son un problema, siempre y cuando nadie solicite la representación cols x rows real (solo cambian el indicador de atributo que se guarda junto con toda la cadena)

Por cierto, los wcwidths son un subconjunto del algoritmo de grafema, por lo que esto podría ser intercambiable en el futuro.

Ahora la parte peligrosa 1 : alguien quiere mover el cursor dentro de cols x rows :

  • mover cols hacia atrás en los saltos de línea: el comienzo del contenido actual del terminal
  • cada salto de línea denota una línea terminal real
  • cargue cosas en el modelo de celda para solo una página (aún no estoy seguro si esto también se puede omitir con un posicionamiento inteligente de la cadena)
  • Haga el trabajo desagradable: si se solicitan cambios de atributo, no tenemos suerte y tenemos que recurrir al modelo de celda completa o al modelo de división e inserción de cadenas (este último podría presentar un mal rendimiento)
  • los datos fluyen de nuevo, ahora con cadenas degradadas y datos de atributos en el búfer de esa página

Ahora la parte peligrosa 2 : el renderizador quiere dibujar algo:

  • depende un poco del renderizador si necesitamos profundizar en un modelo de celda o simplemente podemos proporcionar las compensaciones de cadena con saltos de línea y atributos de texto

Pros:

  • flujo de datos muy rápido
  • optimizado para el método InputHandler más común - print
  • Hace posible el reflujo de líneas al cambiar el tamaño de la terminal

Contras:

  • casi todos los demás métodos InputHandler serán peligrosos en el sentido de interrumpir este modelo de flujo y la necesidad de una abstracción de celda intermedia
  • integración del renderizador poco clara (para mí al menos atm)
  • podría degradar el rendimiento de maldiciones como aplicaciones (normalmente contienen secuencias más "peligrosas")

Bueno, este es un borrador de la idea, lejos de ser un cajero automático utilizable, ya que muchos detalles aún no están cubiertos. Esp. las partes "peligrosas" pueden volverse desagradables con muchos problemas de rendimiento (como la degradación del búfer con un comportamiento de gc aún peor, etc. pp)

@jerch

No vale la pena meter cadenas en ArrayBuffers.

Creo que Monaco almacena su búfer en ArrayBuffer sy tiene un rendimiento bastante alto. Todavía no he analizado demasiado a fondo la implementación.

esp. trata de evitar las desagradables divisiones y uniones

¿Cuáles?

He estado pensando que deberíamos pensar en aprovechar más el hecho de que el scrollback sea inmutable de alguna manera.

Una idea era separar el scrollback de la sección de la ventana gráfica. Una vez que una línea se desplaza hacia atrás, se inserta en la estructura de datos de desplazamiento hacia atrás. Podrías imaginar 2 CircularList objetos, uno cuyas líneas están optimizadas para no cambiar nunca y otro para lo contrario.

@Tyriar Acerca del desplazamiento hacia acceder a él, podría ahorrar algo de memoria para simplemente eliminar la abstracción de la celda para las líneas desplazadas.

@Tyriar
Tiene sentido almacenar cadenas en ArrayBuffer si podemos limitar la conversión a una (tal vez la última para la salida de render). Eso es un poco mejor que el manejo de cuerdas por todas partes. Esto sería factible ya que node-pty también puede proporcionar datos sin procesar (y también el websocket puede brindarnos datos sin procesar).

esp. trata de evitar las desagradables divisiones y uniones

¿Cuáles?

Todo el enfoque es evitar _minimize_ se divide en absoluto. Si nadie solicita que el cursor salte a los datos almacenados en búfer, las cadenas nunca se dividirán y podrían ir directamente al renderizador (si es compatible). Ninguna celda se divide y luego se une en absoluto.

@jerch bueno, si se expande la ventana

@Tyriar Ah, cierto. Tampoco estoy seguro de esto último, creo que el xterm nativo permite esto solo para el mouse real o el desplazamiento de la barra de desplazamiento. Incluso SD / SU no mueve el contenido del búfer de desplazamiento de nuevo a la ventana gráfica del terminal "activa".

¿Podría indicarme la fuente del editor de Mónaco, donde se usa ArrayBuffer? Parece que no puedo encontrarlo yo mismo: rubor:

Hmm, simplemente vuelva a leer la especificación TextEncoder / Decoder, con ArrayBuffers desde node-pty hasta la interfaz, básicamente estamos atrapados con utf-8, a menos que lo traduzcamos de la manera difícil en algún momento. ¿Hacer que xterm.js sea compatible con utf-8? Idk, esto implicaría muchos cálculos de puntos de código intermedios para los caracteres Unicode superiores. Proside: ahorraría memoria para caracteres ascii.

@rebornix, ¿ podrías darnos algunos consejos sobre dónde almacena Mónaco el búfer?

Aquí hay algunos números para matrices escritas y el nuevo analizador (fue más fácil de adoptar):

  • UTF-8 (Uint8Array): la acción print salta de 190 MB / sa 290 MB / s
  • UTF-16 (Uint16Array): la acción print salta de 190 MB / sa 320 MB / s

En general, UTF-16 funciona mucho mejor, pero eso se esperaba ya que el analizador está optimizado para eso. UTF-8 sufre el cálculo del punto de código intermedio.

La cadena para la conversión de matriz escrita consume ~ 4% del tiempo de ejecución de JS de mi punto ls -lR /usr/lib referencia InputHandler.parse ). No probé la conversión inversa (esto se hace implícitamente en un cajero automático en InputHandller.print en la celda por nivel de celda). El tiempo de ejecución general es ligeramente peor que con cadenas (el tiempo ahorrado en el analizador no compensa el tiempo de conversión). Esto puede cambiar cuando otras partes también se escriben con arreglo a matrices.

Y las capturas de pantalla correspondientes (probadas con ls -lR /usr/lib ):

con cuerdas:
grafik

con Uint16Array:
grafik

Tenga en cuenta la diferencia de EscapeSequenceParser.parse , que puede beneficiarse de una matriz escrita (~ 30% más rápido). El InputHandler.parse realiza la conversión, por lo que es peor para la versión de matriz escrita. Además, GC Minor tiene más que hacer con la matriz con tipo (ya que tiro la matriz).

Editar: Se puede ver otro aspecto en las capturas de pantalla: el GC se vuelve relevante con un tiempo de ejecución de ~ 20%, los cuadros de larga ejecución (marcados con bandera roja) están todos relacionados con el GC.

Solo otra idea algo radical:

  1. Cree su propia memoria virtual basada en arraybuffer, algo grande (> 5 MB)
    Si el búfer de matriz tiene una longitud de múltiplos de 4 cambios transparentes de int8 a int16 a int32 son posibles los tipos. El asignador devuelve un índice libre en la Uint8Array , este puntero se puede convertir a una Uint16Array o Uint32Array mediante un simple cambio de bit.
  2. Escriba las cadenas entrantes en la memoria como tipo uint16_t para UTF-16.
  3. El analizador se ejecuta en los punteros de cadena y llama a métodos en InputHandler con punteros a esta memoria en lugar de segmentos de cadena.
  4. Cree el búfer de datos del terminal dentro de la memoria virtual como una matriz de búfer en anillo de un tipo de estructura en lugar de objetos JS nativos, tal vez así (todavía basado en celdas):
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
}

Pros:

  • omite los objetos JS y, por lo tanto, GC siempre que sea posible (solo quedarán unos pocos objetos locales)
  • solo se necesita una copia de datos inicial en la memoria virtual
  • casi sin costos de malloc y free (depende de la inteligencia del asignador / desasignador)
  • ahorrará mucha memoria (evita la sobrecarga de memoria de los objetos JS)

Contras:

  • Bienvenido a Cavascript Horror Show: grito:
  • difícil de implementar, cambia un poco todo
  • El beneficio de velocidad no está claro hasta que se implementa realmente

:sonrisa:

diversión difícil de implementar, cambia un poco todo lo 😉

Esto está más cerca de cómo funciona Mónaco, recordé esta publicación de blog que analiza la estrategia para almacenar metadatos de caracteres https://code.visualstudio.com/blogs/2017/02/08/syntax-highlighting-optimizations

Sí, esa es básicamente la misma idea.

Espero que mi respuesta a dónde almacena Mónaco el búfer no sea demasiado tarde.

Alex y yo estamos a favor de Array Buffer y la mayoría de las veces nos da un buen rendimiento. Algunos lugares en los que usamos ArrayBuffer:

Usamos cadenas simples para el búfer de texto en lugar de Array Buffer, ya que la cadena V8 es más fácil de manipular

  • Hacemos la codificación / decodificación al comienzo de la carga de un archivo, por lo que los archivos se convierten en una cadena JS. V8 decide si utilizar uno o dos bytes para almacenar un carácter.
  • Realizamos ediciones en el búfer de texto con mucha frecuencia, las cadenas son más fáciles de manejar.
  • Estamos utilizando el módulo nativo de nodejs y tenemos acceso a los componentes internos de V8 cuando es necesario.

La siguiente lista es solo un resumen rápido de conceptos interesantes con los que tropecé y que podrían ayudar a reducir el uso de memoria y / o el tiempo de ejecución:

Para empezar a rodar todo aquí, aquí hay un truco rápido sobre cómo podemos "fusionar" atributos de texto.

El código está impulsado principalmente por la idea de ahorrar memoria para los datos del búfer (el tiempo de ejecución se verá afectado, aún no se ha probado cuánto). Esp. los atributos de texto con RGB para el primer plano y el fondo (una vez admitidos) harán que xterm.js consuma toneladas de memoria por el diseño actual celda por celda. El código intenta evitar esto mediante el uso de un atlas de recuento de referencias de tamaño variable para los atributos. En mi humilde opinión, esta es una opción, ya que una sola terminal difícilmente contendrá más de 1 millón de celdas, lo que haría crecer el atlas a 1M * entry_size si todas las celdas difieren.

La celda en sí solo necesita contener el índice del atlas de atributos. En los cambios de celda, el índice anterior debe eliminarse y el nuevo debe ser ref. El índice del atlas reemplazaría el atributo de atributo del objeto terminal y se cambiará él mismo en SGR.

Actualmente, el atlas solo aborda los atributos de texto, pero podría extenderse a todos los atributos de celda si fuera necesario. Mientras que el búfer de terminal actual contiene 2 números de 32 bits para datos de atributos (4 con RGB en el diseño de búfer actual), el atlas lo reduciría a solo un número de 32 bits. Las entradas del atlas también se pueden empaquetar más.

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

Editar:
La penalización por tiempo de ejecución es casi cero, para mi punto de referencia con ls -lR /usr/lib agrega menos de 1 mseg al tiempo de ejecución total de ~ 2.3 s. Nota al margen interesante: el comando establece menos de 64 ranuras de atributos de texto diferentes para la salida de 5 MB de datos y ahorrará más de 20 MB una vez implementado por completo.

Hizo algunos PR prototipos para probar algunos cambios en el búfer (consulte https://github.com/xtermjs/xterm.js/pull/1528#issue-196949371 para obtener la idea general detrás de los cambios):

  • PR # 1528: atlas de atributos
  • PR # 1529: elimine wcwidth y charCode del búfer
  • PR # 1530: reemplace la cadena en el búfer por puntos de código / valor de índice de almacenamiento de celda

@jerch , podría ser una buena idea mantenerse alejado de la palabra atlas para que "atlas" siempre signifique "atlas de texturas". ¿Algo como almacenar o caché probablemente sería mejor?

oh ok, "caché" está bien.

Supongo que he terminado con los PR del banco de pruebas. Por favor, también eche un vistazo a los comentarios de relaciones públicas para obtener los antecedentes del siguiente resumen aproximado.

Propuesta:

  1. Construya un AttributeCache para contener todo lo necesario para diseñar una sola celda terminal. Consulte el n. ° 1528 para obtener una versión temprana de conteo de referencias que también puede contener especificaciones de color verdadero. La caché también se puede compartir entre diferentes instancias de terminal si es necesario para guardar más memoria en múltiples aplicaciones de terminal.
  2. Cree un StringStorage para contener cadenas de datos de contenido de terminal cortas. La versión en # 1530 incluso evita almacenar cadenas de caracteres individuales "sobrecargando" el significado del puntero. wcwidth debe moverse aquí.
  3. Reduzca el CharData actual de [number, string, number, number] a [number, number] , donde los números son punteros (números de índice) a:

    • AttributeCache entrada

    • StringStorage entrada

Es poco probable que los atributos cambien mucho, por lo que un solo número de 32 bits ahorrará mucha memoria con el tiempo. El puntero StringStorage es un punto de código Unicode real para caracteres individuales, por lo tanto, puede usarse como la entrada code de CharData . Se puede acceder a la cadena real mediante StringStorage.getString(idx) . Se puede acceder al cuarto campo wcwidth de CharData mediante StringStorage.wcwidth(idx) (aún no implementado). Casi no hay penalización de tiempo de ejecución por deshacerse de code y wcwidth en CharData (probado en # 1529).

  1. Mueva el CharData reducido a una implementación de búfer basada en Int32Array densa. También probado en # 1530 con una clase stub (lejos de ser completamente funcional), es probable que los beneficios finales sean:

    • 80% menos de espacio de memoria del búfer del terminal (de 5,5 MB a 0,75 MB)

    • un poco más rápido (aún no comprobable, espero ganar entre un 20% y un 30% de velocidad)

    • Editar: mucho más rápido: el tiempo de ejecución del script para ls -lR /usr/lib redujo a 1.3s (el maestro está en 2.1s) mientras que el búfer anterior todavía está activo para el manejo del cursor, una vez eliminado, espero que el tiempo de ejecución caiga por debajo de 1s

Desventaja: el paso 4 es bastante trabajo, ya que necesitará algunas modificaciones en la interfaz del búfer. Pero bueno, para ahorrar el 80% de la RAM y seguir ganando rendimiento en tiempo de ejecución, no es gran cosa, ¿verdad? :sonrisa:

Hay otro problema con el que me tropecé: la representación actual de la celda vacía. En mi opinión, una celda puede tener 3 estados:

  • vacío : estado inicial de la celda, aún no se ha escrito nada o se eliminó el contenido. Tiene un ancho de 1 pero no tiene contenido. Actualmente se usa en blankLine y eraseChar , pero con un espacio como contenido.
  • nulo : celda después de un carácter de ancho completo para indicar que no tiene ancho para la representación visual.
  • normal : la celda contiene algo de contenido y tiene un ancho visual (1 o 2, tal vez más grande una vez que admitamos cosas reales de grafema / bidi, aún no estoy seguro de eso jajaja)

El problema que veo aquí es que una celda vacía no se distingue de una celda normal con un espacio insertado, ambas tienen el mismo aspecto a nivel de búfer (mismo contenido, mismo ancho). No escribí ninguno de los códigos de renderizado / salida, pero espero que esto conduzca a situaciones incómodas en el frente de salida. Esp. el manejo del extremo derecho de una línea puede resultar engorroso.
Una terminal con 15 columnas, primero una salida de cadena, que se envolvió:

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

versus una lista de carpetas con ls :

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

El primer ejemplo contiene un espacio real después de la palabra 'terminal', el segundo ejemplo nunca tocó las celdas después de 'Readme.md'. La forma en que se representa a nivel de búfer tiene mucho sentido para el caso estándar para imprimir el material como salida de terminal a la pantalla (la sala debe tomarse de todos modos), pero para herramientas que intentan lidiar con las cadenas de contenido como una selección del mouse o un gestor de reflujo ya no está claro de dónde vienen los espacios.

Más o menos, esto lleva a la siguiente pregunta: ¿cómo determinar la longitud real del contenido en una línea (cantidad de celdas que contienen algo del lado izquierdo)? Un enfoque simple contaría las celdas vacías desde el lado derecho, nuevamente, el doble significado de arriba hace que esto sea difícil de determinar.

Propuesta:
En mi humilde opinión, esto se puede solucionar fácilmente utilizando algún otro marcador de posición para las celdas vacías, por ejemplo, un carácter de control o la cadena vacía y reemplácelos en el proceso de renderizado si es necesario. Tal vez el renderizador de pantalla también pueda beneficiarse de esto, ya que es posible que no tenga que manejar esas celdas en absoluto (depende de la forma en que se genere la salida).

Por cierto, para la cadena envuelta arriba, esto también conduce al problema isWrapped , que es esencial para un cambio de tamaño de reflujo o un manejo correcto de la selección de copiar y pegar. En mi humilde opinión, no podemos eliminar eso, pero necesitamos integrarlo mejor de lo que es atm.

@jerch trabajo impresionante! : smiley:

1 Cree un AttributeCache para almacenar todo lo necesario para diseñar una sola celda terminal. Consulte el n. ° 1528 para obtener una versión temprana de conteo de referencias que también puede contener especificaciones de color verdadero. La caché también se puede compartir entre diferentes instancias de terminal si es necesario para guardar más memoria en múltiples aplicaciones de terminal.

Hizo algunos comentarios sobre el n. ° 1528.

2 Cree un StringStorage para contener cadenas de datos de contenido de terminal cortas. La versión en # 1530 incluso evita almacenar cadenas de caracteres individuales "sobrecargando" el significado del puntero. wcwidth debe moverse aquí.

Hice algunos comentarios sobre el n. ° 1530.

4 Mueva los CharData reducidos a una implementación de búfer densa basada en Int32Array. También probado en # 1530 con una clase stub (lejos de ser completamente funcional), es probable que los beneficios finales sean:

No estoy totalmente convencido de esta idea todavía, creo que nos morderá duro cuando implementemos el reflujo. Parece que cada uno de estos pasos se puede hacer prácticamente en orden para que podamos ver cómo van las cosas y ver si tiene sentido hacer esto una vez que terminemos los 3.

Hay otro problema con el que me tropecé: la representación actual de la celda vacía. En mi humilde opinión, una celda puede tener 3 estados

Aquí hay un ejemplo de un error que surgió de este https://github.com/xtermjs/xterm.js/issues/1286,: +1: para diferenciar celdas de espacio en blanco y celdas "vacías"

Por cierto, para la cadena envuelta arriba, esto también conduce al problema isWrapped, que es esencial para un cambio de tamaño de reflujo o un manejo correcto de la selección de copiar y pegar. En mi humilde opinión, no podemos eliminar eso, pero necesitamos integrarlo mejor de lo que es atm.

Veo que isWrapped desaparecerá cuando abordemos https://github.com/xtermjs/xterm.js/issues/622 ya que CircularList solo contendrá filas sin envolver.

No estoy totalmente convencido de esta idea todavía, creo que nos morderá duro cuando implementemos el reflujo. Parece que cada uno de estos pasos se puede hacer prácticamente en orden para que podamos ver cómo van las cosas y ver si tiene sentido hacer esto una vez que terminemos los 3.

Sí, estoy contigo (sigue siendo divertido jugar con este enfoque totalmente diferente). 1 y 2 se pueden seleccionar, 3 se puede aplicar dependiendo de 1 o 2. 4 es opcional, simplemente podríamos ceñirnos al diseño de búfer actual. Los ahorros de memoria son así:

  1. 1 + 2 + 3 en CircularList : ahorra 50% (~ 2,8 MB de ~ 5,5 MB)
  2. 1 + 2 + 3 + 4 a la mitad: simplemente coloque los datos de la línea en una matriz escrita, pero manténgase en el acceso al índice de la fila: ahorra 82% (~ 0.9MB)
  3. 1 + 2 + 3 + 4 matriz completamente densa con aritmética de puntero: ahorra un 87% (~ 0,7 MB)

1. es muy fácil de implementar, el comportamiento de la memoria con scrollBack más grande seguirá mostrando el escalado incorrecto como se muestra aquí https://github.com/xtermjs/xterm.js/pull/1530#issuecomment -403542479 pero en un nivel menos tóxico
2. Ligeramente más difícil de implementar (se necesitan algunas más indirecciones a nivel de línea), pero hará posible mantener intacta la API más alta de Buffer . En mi humilde opinión, la opción para elegir: gran memoria de almacenamiento y aún fácil de integrar.
3. Un 5% más de memoria guardada que la opción 2, difícil de implementar, cambiará todas las API y, por lo tanto, literalmente, toda la base del código. En mi opinión más de interés académico o para los aburridos días de lluvia que se implementarán jajaja.

@Tyriar Hice algunas pruebas adicionales con rust para el uso del

  • El manejo de datos dentro de la parte wasm es un poco más rápido (5 - 10%).
  • Las llamadas de JS a wasm crean algunos gastos generales y se comen todos los beneficios de arriba. De hecho, fue un 20% más lento.
  • El "binario" será más pequeño que la contraparte de JS (no realmente medido ya que no implementé todas las cosas).
  • Para que la transición de JS <--> wasm se realice fácilmente, se necesita un código de relleno para manejar los tipos de JS (solo se hizo la traducción de cadenas).
  • No podemos evitar la traducción de JS a wasm ya que el DOM del navegador y los eventos no son accesibles allí. Solo podría usarse para partes centrales, que ya no son tan críticas para el rendimiento (además del consumo de mem).

A menos que queramos reescribir todas las librerías centrales en rust (o cualquier otro lenguaje capaz de wasm), no podemos ganar nada con movernos a un wasm lang en mi humilde opinión. Una ventaja de los wasm langs de hoy en día es el hecho de que la mayoría admite el manejo explícito de la memoria (podría ayudarnos con el problema del búfer), las desventajas son la introducción de un lenguaje totalmente diferente en un proyecto enfocado principalmente en TS / JS (una gran barrera para las adiciones de código) y los costos de traducción entre wasm y JS land.

TL; DR
xterm.js es, en términos generales, incluir cosas JS generales como DOM y eventos para obtener algo del ensamblaje web, incluso para una reescritura de las partes principales.

@jerch bonita investigación: smiley:

Las llamadas de JS a wasm crean algunos gastos generales y se comen todos los beneficios de arriba. De hecho, fue un 20% más lento.

Este también fue el principal problema para que Mónaco se volviera nativo , lo que ha informado principalmente mi postura sobre (aunque eso fue con el módulo de nodo nativo, no con wasm). Creo que trabajar con ArrayBuffer s siempre que sea posible debería darnos el mejor equilibrio entre rendimiento y simplicidad (fácil de implícita, barrera de entrada).

@Tyriar Voy a intentar crear un AttributeStorage para contener los datos RGB. Aún no estoy seguro sobre el BST, para el caso de uso típico con solo unas pocas configuraciones de color en una sesión de terminal, esto será peor en tiempo de ejecución, tal vez esto debería ser un tiempo de ejecución directo una vez que los colores superen un límite determinado. Además, el consumo de memoria aumentará mucho de nuevo, aunque seguirá ahorrando memoria, ya que los atributos se almacenan solo una vez y no junto con cada celda (en el peor de los casos, cada celda con atributos diferentes sufrirá).
¿Sabe por qué el fg actual de bg 256 colores se basa en 9 bits en lugar de 8 bits? ¿Para qué se utiliza la broca adicional? Aquí: https://github.com/xtermjs/xterm.js/blob/6691f809069a549b4808cd2e055398d2da15db37/src/InputHandler.ts#L1596
¿Podría darme el diseño de bits actual de attr ? Creo que un enfoque similar como el "doble significado" para el puntero StringStorage puede ahorrar más memoria, pero eso requeriría que el MSB de attr se reserve para la distinción del puntero y no se use para ningún otro propósito. Esto podría limitar la posibilidad de admitir más banderas de atributos más adelante (porque FLAGS ya usa 7 bits), ¿todavía nos faltan algunas banderas fundamentales que es probable que aparezcan?

Un attr 32 bits en el búfer de término podría empaquetarse así:

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

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

De esta manera, el almacenamiento solo necesita contener los datos RGB en dos números de 32 bits, mientras que los indicadores pueden permanecer en el attr .

@jerch, por cierto, te envié un correo electrónico, probablemente el filtro de spam se lo comió de nuevo 😛

¿Sabe por qué el valor actual de 256 colores fg y bg se basa en 9 bits en lugar de 8 bits? ¿Para qué se utiliza la broca adicional?

Creo que se usa para el color fg / bg predeterminado (que podría ser oscuro o claro), por lo que en realidad tiene 257 colores.

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

¿Podría darme el diseño de bits actual de attr?

Creo que es esto:

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

Puede ver lo que obtuve para truecolor en el antiguo PR https://github.com/xtermjs/xterm.js/pull/756/files :

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

Entonces en esto tenía 2 banderas; uno para el color predeterminado (ya sea para ignorar todos los bits de color) y uno para color verdadero (ya sea para colores de 256 o 16 mil).

Esto podría limitar la posibilidad de admitir más banderas de atributos más adelante (porque FLAGS ya usa 7 bits), ¿todavía nos faltan algunas banderas fundamentales que probablemente vendrán?

Sí, queremos espacio para banderas adicionales, por ejemplo https://github.com/xtermjs/xterm.js/issues/580, https://github.com/xtermjs/xterm.js/issues/1145, yo digamos que al menos deje> 3 bits cuando sea posible.

En lugar de datos de puntero dentro del propio atributo, ¿podría haber otro mapa que contenga referencias a los datos rgb? mapAttrIdxToRgb: { [idx: number]: RgbData

@Tyriar Lo siento,

Jugó abit con estructuras de datos de búsqueda más inteligentes para el almacenamiento de attrs. Lo más prometedor con respecto al espacio y el tiempo de ejecución de búsqueda / inserción son los árboles y una lista de omisión como alternativa más barata. En teoría jajaja. En la práctica, ninguno de los dos puede superar mi búsqueda de matriz simple, lo que me parece muy extraño (¿error en el código en alguna parte?)
Subí un archivo de prueba aquí https://gist.github.com/jerch/ff65f3fb4414ff8ac84a947b3a1eec58 con matriz frente a un árbol rojo-negro inclinado hacia la izquierda, que prueba hasta 10 millones de entradas (que es casi el espacio de direccionamiento completo). Aún así, la matriz está muy por delante en comparación con el LLRB, aunque sospecho que el punto de equilibrio es de alrededor de 10 millones. Probado en mi vieja computadora portátil de 7 años, tal vez alguien pueda probarlo también e incluso mejor: apúnteme algunos errores en las pruebas impl /.

Aquí hay algunos resultados (con números continuos):

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

Lo que realmente me sorprende es el hecho de que la búsqueda de matriz lineal no muestra ningún crecimiento en las regiones inferiores en absoluto, es hasta 10k entradas estables a ~ 4ms (podría estar relacionado con la caché). La prueba de 10M muestra un tiempo de ejecución peor de lo esperado, tal vez debido a la paginación de mem. Tal vez JS está demasiado lejos de la máquina con el JIT y todas las opciones / deoptizaciones sucediendo, aún así creo que no pueden eliminar un paso de complejidad (aunque el LLRB parece ser pesado en un solo _n_ moviendo así el punto de equilibrio para O ( n) vs.O (logn) hacia arriba)

Por cierto, con datos aleatorios, la diferencia es aún peor.

Creo que se usa para el color fg / bg predeterminado (que podría ser oscuro o claro), por lo que en realidad tiene 257 colores.

Entonces, ¿esto es diferente SGR 39 o SGR 49 de uno de los 8 colores de la paleta?

En lugar de datos de puntero dentro del propio atributo, ¿podría haber otro mapa que contenga referencias a los datos rgb? mapAttrIdxToRgb: {[idx: number]: RgbData

Esto introduciría otra indirecta con uso de memoria adicional. Con las pruebas anteriores, también probé la diferencia entre mantener siempre las banderas en attrs y guardarlas junto con los datos RGB en el almacenamiento. Dado que la diferencia es de ~ 0.5ms para entradas de 1M, no optaría por esta complicada configuración de attrs, sino que copie las banderas en el almacenamiento una vez que se configure RGB. Aún así, optaría por la distinción de 32 bits entre attrs directos frente a puntero, ya que esto evitará el almacenamiento en absoluto para las celdas que no son RGB.

También creo que los 8 colores de paleta predeterminados para fg / bg no están suficientemente representados en el búfer actualmente. En teoría, el terminal debería admitir los siguientes modos de color:

  1. SGR 39 + SGR 49 color predeterminado para fg / bg (personalizable)
  2. SGR 30-37 + SGR 40-47 8 paleta de colores bajos para fg / bg (personalizable)
  3. SGR 90-97 + SGR 100-107 8 paleta de colores altos para fg / bg (personalizable)
  4. SGR 38;5;n + SGR 48;5;n 256 paleta indexada para fg / bg (personalizable)
  5. SGR 38;2;r;g;b + SGR 48;2;r;g;b RGB para fg / bg (no personalizable)

Las opciones 2.) y 3.) se pueden fusionar juntas en un solo byte (tratándolas como una sola paleta fg / bg de 16 colores), 4.) toma 2 bytes y 5.) finalmente tomará 6 bytes más. Todavía necesitamos algunos bits para indicar el modo de color.
Para reflejar esto en el nivel del búfer, necesitaríamos lo siguiente:

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

Entonces necesitamos 30 bits de un número de 32 bits, dejando 2 bits libres para otros propósitos. El bit 32 podría contener el indicador de puntero frente a atributo directo omitiendo el almacenamiento para celdas no RGB.

También sugiero resumir el acceso attr en una clase conveniente para no exponer los detalles de implementación al exterior (vea el archivo de prueba anterior, hay una versión anterior de una clase TextAttributes para lograr esto).

Lo siento, pasaron unos días sin estar en línea y me temo que el filtro de correo no deseado se comió el correo electrónico. ¿Podrías reenviarlo por favor?

Resentirse de

Oh, por cierto, esos números anteriores para la búsqueda de matriz vs llrb son una mierda; creo que el optimizador lo echó a perder al hacer algunas cosas extrañas en el bucle for. Con una configuración de prueba ligeramente diferente, muestra claramente que O (n) frente a O (log n) crece mucho antes (con 1000 elementos precargados que ya son más rápidos con el árbol).

Estado actual:

Después:

Una optimización bastante simple es aplanar la matriz de matrices en una sola matriz. Es decir, en lugar de BufferLine de _N_ columnas que tienen una matriz _data de celdas _N_ CharData , donde cada CharData es una matriz de 4, solo tenga una matriz de _4 * N_ elementos. Esto elimina la sobrecarga de objetos de _N_ matrices. También mejora la ubicación de la caché, por lo que debería ser más rápido. Una desventaja es el código un poco más complicado y feo, pero parece que vale la pena.

Como seguimiento de mi comentario anterior, parece que vale la pena considerar el uso de un número variable de elementos en la matriz _data para cada celda. En otras palabras, una representación con estado. Los cambios aleatorios de posición serían más costosos, pero el escaneo lineal desde el comienzo de una línea puede ser bastante rápido, especialmente porque una matriz simple está optimizada para la localidad de caché. La salida secuencial típica sería rápida, al igual que la renderización.

Además de reducir el espacio, una ventaja de un número variable de elementos por celda es una mayor flexibilidad: atributos adicionales (como color de 24 bits), anotaciones para celdas o rangos específicos, glifos o
elementos DOM anidados.

@PerBothner ¡ Gracias por tus ideas! Sí, ya probé el diseño de matriz densa única con aritmética de puntero, muestra la mejor utilización de la memoria. Los problemas surgen cuando se trata de cambiar el tamaño, básicamente significa reconstruir todo el fragmento de memoria (copiar) o copiar rápidamente en un fragmento más grande y realinear partes. Esto es bastante exp. y en mi humilde opinión no está justificado por el ahorro de mem (probado en algunas relaciones públicas del patio de recreo enumeradas anteriormente, el ahorro fue de alrededor de ~ 10% en comparación con la implementación de la nueva línea de amortiguación).

Acerca de su segundo comentario, ya lo discutimos, ya que facilitaría el manejo de las líneas ajustadas. Por ahora, decidimos ir con el enfoque de fila X col para el nuevo diseño de búfer y hacerlo primero. Creo que deberíamos abordar esto nuevamente una vez que hagamos la implementación del cambio de tamaño del reflujo.

Acerca de agregar cosas adicionales al búfer: actualmente hacemos aquí lo que hacen la mayoría de las otras terminales: el avance del cursor está determinado por wcwidth que garantiza que se mantenga compatible con la idea de pty / termios de cómo se deben distribuir los datos. Esto básicamente significa que manejamos a nivel de búfer solo cosas como pares sustitutos y combinación de caracteres. El ensamblador de caracteres puede aplicar cualquier otra regla de unión de "nivel superior" en el renderizador (actualmente utilizado por https://github.com/xtermjs/xterm-addon-ligatures para ligaduras). Tenía un PR abierto para admitir también grafemas Unicode en el nivel de búfer inicial, pero creo que no podemos hacer esto en esa etapa, ya que la mayoría de los backends pty no tienen noción de esto (¿hay alguno?) Y terminaríamos con conglomerados de caracteres extraños. . Lo mismo ocurre con el soporte real de BIDI, creo que los grafemas y BIDI se hacen mejor en la etapa de renderizado para mantener intactos los movimientos del cursor / celda.

El soporte para los nodos DOM adjuntos a las celdas suena realmente interesante, me gusta esa idea. Actualmente, eso no es posible en un enfoque directo, ya que tenemos diferentes backends de renderizado (DOM, lienzo 2D y el nuevo renderizador webgl brillante), creo que esto aún podría lograrse para todos los renderizadores colocando una superposición donde no se admite de forma nativa (solo el renderizador DOM lo haría poder hacerlo directamente). Necesitaríamos algún tipo de API a nivel de búfer para anunciar esas cosas y su tamaño y el renderizador podría hacer el trabajo sucio. Creo que deberíamos discutir / rastrear esto con un tema separado.

Gracias por tu respuesta detallada.

_ "Los problemas surgen cuando se trata de cambiar el tamaño, básicamente significa reconstruir todo el bloque de memoria (copiar) o copiar rápidamente en un bloque más grande y realinear las partes". _

¿Quiere decir: al cambiar el tamaño tendríamos que copiar _4 * N_ elementos en lugar de solo _N_ elementos?

Puede que tenga sentido que la matriz contenga todas las celdas de una línea lógica (desenvuelta). Por ejemplo, suponga una línea de 180 caracteres y una terminal de 80 columnas de ancho. En ese caso, podría tener 3 BufferLine instancias, todas compartiendo el mismo elemento _4 * 180_ _data búfer, pero cada BufferLine también contendría un desplazamiento inicial.

Bueno, tenía todo en una gran matriz que fue construida por [cols] x [rows] x [needed single cell space] . Así que básicamente todavía funcionaba como "lienzo" con una altura y un ancho determinados. Esto es realmente eficiente en memoria y rápido para el flujo de entrada normal, pero tan pronto como se invoca insertCell / deleteCell (un cambio de tamaño haría eso), toda la memoria detrás de la posición donde tiene lugar la acción tendría que ser cambiado. Para scrollback pequeño (<10k), esto tampoco es un problema, realmente es sorprendente para> 100k líneas.
Tenga en cuenta que la matriz implícita actual todavía tiene que hacer esos cambios, pero es menos tóxica, ya que solo tiene que mover el contenido de la memoria hasta el final de la línea.
Pensé en diferentes diseños para eludir los costosos cambios, el campo principal para ahorrar cambios de memoria sin sentido sería separar el desplazamiento hacia atrás de las "filas de terminales calientes" (las más recientes hasta terminal.rows ) ya que solo esas pueden ser alterado por los saltos del cursor e inserta / elimina.

Compartir la memoria subyacente por varios objetos de línea de búfer es una idea interesante para resolver el problema de envoltura. Todavía no estoy seguro de cómo esto puede funcionar de manera confiable sin introducir un manejo explícito de referencias y tal. En otra versión, traté de hacer todo con manejo de memoria explícito, pero el contador de referencias fue un verdadero espectáculo y se sintió mal en la tierra de GC. (ver # 1633 para las primitivas)

Editar: Por cierto, el manejo explícito de la memoria estaba a la par con el enfoque actual de "memoria por línea", esperaba un mejor rendimiento debido a una mejor ubicación de caché, supongo que fue devorado por un poco más de experiencia. manejo de mem en la abstracción JS.

El soporte para los nodos DOM adjuntos a las celdas suena realmente interesante, me gusta esa idea. Actualmente, eso no es posible en un enfoque directo, ya que tenemos diferentes backends de renderizado (DOM, lienzo 2D y el nuevo renderizador webgl brillante), creo que esto aún podría lograrse para todos los renderizadores colocando una superposición donde no se admite de forma nativa (solo el renderizador DOM lo haría poder hacerlo directamente).

Está un poco fuera de tema, pero veo que eventualmente tendremos nodos DOM asociados con celdas dentro de la ventana gráfica que actuarán de manera similar a las capas de renderizado del lienzo. De esa manera, los consumidores podrán "decorar" celdas usando HTML y CSS y no necesitarán ingresar a la API de lienzo.

Puede que tenga sentido que la matriz contenga todas las celdas de una línea lógica (desenvuelta). Por ejemplo, suponga una línea de 180 caracteres y una terminal de 80 columnas de ancho. En ese caso, podría tener 3 instancias de BufferLine, todas compartiendo el mismo búfer _data de 4 * 180 elementos, pero cada BufferLine también contendría un desplazamiento de inicio.

El plan de reflujo que se mencionó anteriormente se captura en https://github.com/xtermjs/xterm.js/issues/622#issuecomment -375403572, básicamente queremos tener el búfer sin empaquetar real y luego una vista en la parte superior que administra las nuevas líneas para un acceso rápido a cualquier línea dada (también optimizando para cambios de tamaño horizontales).

El uso del enfoque de matriz densa puede ser algo que podríamos considerar, pero parece que no valdría la pena la sobrecarga adicional para administrar los saltos de línea sin envolver en una matriz de este tipo, y el desorden que se produce cuando las filas se recortan desde la parte superior de el búfer de retroceso. Independientemente, no creo que debamos considerar tales cambios hasta que el # 791 esté terminado y estemos viendo el # 622.

Con PR # 1796, el analizador obtiene soporte de matriz tipada, lo que abre la puerta a varias optimizaciones adicionales y, por otro lado, también a otras codificaciones de entrada.

Por ahora, decidí ir con Uint16Array , ya que es fácil de convertir con cadenas JS. Esto básicamente limita el juego a UCS2 / UTF16, mientras que el analizador en la versión actual también puede manejar UTF32 (UTF8 no es compatible). El búfer de terminal basado en matriz con tipo está actualmente diseñado para UTF32, la conversión UTF16 -> UTF32 se realiza en InputHandler.print . A partir de aquí hay varias direcciones posibles:

  • haga todo UTF16, por lo tanto, convierta el búfer de terminal en UTF16 también
    Sí, todavía no he decidido qué camino tomar aquí, pero probé varios diseños de búfer y llegué a la conclusión de que un número de 32 bits da suficiente espacio para almacenar el código de caracteres real + wcwidth + posible desbordamiento de combinación (manejado de manera totalmente diferente), mientras que 16 bits no pueden hacer eso sin sacrificar preciosos bits de código de caracteres. Tenga en cuenta que incluso con un búfer UTF16 todavía tenemos que hacer la conversión UTF32, ya que wcwidth funciona en puntos de código Unicode. También tenga en cuenta que el búfer basado en UTF16 ahorraría más memoria para códigos de caracteres más bajos, de hecho, los códigos de caracteres más altos que los del plano BMP rara vez ocurrirán. Esto todavía necesita algo de investigación.
  • hacer analizador UTF32
    Eso es bastante simple, simplemente reemplace todas las matrices escritas con la variante de 32 bits. Desventaja: la conversión de UTF16 a UTF32 debería realizarse de antemano, lo que significa que toda la entrada se convierte, incluso las secuencias de escape que nunca se formarán con ningún código de caracteres> 255.
  • hacer que wcwidth UTF16 sea compatible
    Sí, si resulta que UTF16 es más adecuado para el búfer del terminal, esto debería hacerse.

Acerca de UTF8: el analizador actualmente no puede manejar secuencias UTF8 nativas, principalmente debido al hecho de que los caracteres intermedios chocan con los caracteres de control C1. Además, UTF8 necesita un manejo de flujo adecuado con estados intermedios adicionales, eso es desagradable y, en mi humilde opinión, no debería agregarse al analizador. UTF8 se manejará mejor de antemano, y tal vez con un derecho de conversión a UTF32 para un manejo más fácil de los puntos de código en todo el lugar.

Con respecto a una posible codificación de entrada UTF8 y el diseño del búfer interno, hice una prueba aproximada. Para descartar el impacto mucho mayor del renderizador de lienzo en el tiempo de ejecución total, lo hice con el próximo renderizador webgl. Con mi punto ls -lR /usr/lib referencia

  • maestro actual + renderizador webgl:
    grafik

  • rama del patio de recreo, aplica el n. ° 1796, partes del n. ° 1811 y el renderizador webgl:
    grafik

La rama del patio de recreo realiza una conversión temprana de UTF8 a UTF32 antes de realizar el análisis y el almacenamiento (la conversión agrega ~ 30 ms). La aceleración se obtiene principalmente mediante las 2 funciones activas durante el flujo de entrada, EscapeSequenceParser.parse (120 ms frente a 35 ms) y InputHandler.print (350 ms frente a 75 ms). Ambos se benefician mucho del cambio de matriz escrita al ahorrar llamadas .charCodeAt .
También comparé estos resultados con una matriz de tipo intermedio UTF16: EscapeSequenceParser.parse es un poco más rápido (~ 25 ms) pero InputHandler.print queda atrás debido al emparejamiento sustituto necesario y la búsqueda de puntos de código en wcwidth (120 ms).
También tenga en cuenta que ya estoy en el límite que el sistema puede proporcionar los datos ls (i7 con SSD): la aceleración obtenida agrega tiempo de inactividad en lugar de acelerar la ejecución.

Resumen:
En mi humilde opinión, el manejo de entrada más rápido que podemos obtener es una mezcla de transporte UTF8 + UTF32 para la representación del búfer. Si bien el transporte UTF8 tiene el mejor paquete de bytes para la entrada de terminal típica y elimina las conversiones sin sentido de pty a través de varias capas de búferes hasta Terminal.write , el búfer basado en UTF32 puede almacenar los datos bastante rápido. Este último viene con una huella de memoria ligeramente mayor que UTF16, mientras que UTF16 es un poco más lento debido a un manejo de caracteres más complicado con más indirecciones.

Conclusión:
Deberíamos ir con el diseño de búfer basado en UTF32 por ahora. También deberíamos considerar cambiar a la codificación de entrada UTF8, pero esto aún necesita pensar un poco más en los cambios de la API y las implicaciones para los integradores (parece que el mecanismo ipc de Electron no puede manejar datos binarios sin la codificación BASE64 y el ajuste JSON, lo que contrarrestaría los esfuerzos de rendimiento).

Diseño de búfer para el próximo soporte de color verdadero:

Actualmente, el diseño del búfer basado en matrices escritas es el siguiente (una celda):

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

donde attrs contiene todos los indicadores necesarios + colores FG y BG basados ​​en 9 bits. codepoint usa 21 bits (el máximo es 0x10FFFF para UTF32) + 1 bit para indicar la combinación de caracteres y wcwidth 2 bits (rangos de 0-2).

La idea es reorganizar los bits a una mejor tasa de paquete para dejar espacio para los valores RGB adicionales:

  • poner wcwidth en bits altos no utilizados de codepoint
  • dividir attrs en un grupo FG y BG con 32 bits, distribuir banderas en bits no utilizados
|             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) |

La ventaja de este enfoque es el acceso relativamente barato a todos los valores mediante un acceso de índice y un máximo. Operaciones de 2 bits (y / o + desplazamiento).

La huella de memoria es estable con respecto a la variante actual, pero sigue siendo bastante alta con 12 bytes por celda. Esto podría optimizarse aún más sacrificando algo de tiempo de ejecución con el cambio a UTF16 y una dirección indirecta attr :

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

Ahora tenemos 4 bytes por celda + algo de espacio para los atributos. Ahora los attrs también podrían reciclarse para otras células. ¡Yay misión cumplida! - Ehm, un segundo ...

Comparando la huella de memoria, el segundo enfoque gana claramente. No es así para el tiempo de ejecución, hay tres factores principales que aumentan mucho el tiempo de ejecución:

  • attr indirección
    El puntero attr necesita una búsqueda de memoria adicional en otro contenedor de datos.
  • coincidencia de atributos
    Para realmente ahorrar espacio con el segundo enfoque, el atributo dado tendría que coincidir con los atributos ya guardados. Esta es una acción engorrosa, un enfoque directo simplemente mirando a través de todos los valores existentes está en O (n) para n attrs guardados, mis experimentos de árbol RB terminaron en casi ningún beneficio de memoria mientras aún estaban en O (log n), en comparación con el acceso de índice en el enfoque de 32 bits con O (1). Además, un árbol tiene un peor tiempo de ejecución para algunos elementos guardados (paga algo alrededor de> 100 entradas con mi implícita de árbol RB).
  • Emparejamiento sustituto UTF16
    Con una matriz tipada de 16 bits, tenemos que degradar a UTF16 para los puntos de código, lo que también introdujo una penalización en tiempo de ejecución (como se describe en el comentario anterior). Tenga en cuenta que los puntos de código más altos que BMP casi no ocurren, aún así, la sola verificación de si un punto de código formaría un par sustituto agrega ~ 50 ms.

La sensualidad del segundo enfoque es el ahorro de memoria adicional. Por lo tanto, lo probé con la rama del patio de recreo (ver el comentario anterior) con una implementación BufferLine modificada:

grafik

Sí, estamos un poco de regreso a donde comenzamos antes de cambiar a matrices de tipo UTF8 + en el analizador. Sin embargo, el uso de memoria se redujo de ~ 1,5 MB a ~ 0,7 MB (aplicación de demostración con 87 celdas y 1000 líneas de desplazamiento hacia atrás).

A partir de aquí, se trata de ahorrar memoria frente a velocidad. Dado que ya ahorramos mucha memoria al cambiar de matrices js a matrices escritas (se redujo de ~ 5.6 MB a ~ 1.5 MB para el montón de C ++, cortando el comportamiento tóxico de JS Heap y GC), creo que deberíamos ir aquí con la variante más rápida. Una vez que el uso de la memoria vuelve a ser un problema urgente, aún podemos cambiar a un diseño de búfer más compacto como se describe en el segundo enfoque aquí.

Estoy de acuerdo, optimicemos la velocidad siempre que el consumo de memoria no sea una preocupación. También me gustaría evitar la indirección en la medida de lo posible porque hace que el código sea más difícil de leer y mantener. Ya tenemos bastantes conceptos y ajustes en nuestra base de código que dificultan que las personas (incluyéndome a mí) sigan el flujo del código, y traer más de estos siempre debe estar justificado por una muy buena razón. En mi opinión, ahorrar otro megabyte de memoria no lo justifica.

Sin embargo, disfruto mucho leyendo y aprendiendo de tus ejercicios, ¡gracias por compartirlos con tanto detalle!

@mofux Sí, eso es cierto: la complejidad del código es mucho mayor (lectura anticipada sustituta de UTF16, cálculo de puntos de código intermedios, contenedor de árbol con ref contando en entradas de atributo).
Y dado que el diseño de 32 bits es principalmente memoria plana (solo la combinación de caracteres necesita direccionamiento indirecto), hay más optimizaciones posibles (también parte de # 1811, aún no probado para el renderizador).

Hay una gran ventaja de direccionar indirectamente a un objeto attr: es mucho más extensible. Puede agregar anotaciones, glifos o reglas de pintura personalizadas. Puede almacenar la información de los enlaces de una manera posiblemente más limpia y más eficiente. Quizás defina una interfaz ICellPainter que sepa cómo representar su celda, y en la que también pueda colgar propiedades personalizadas.

Una idea es utilizar dos matrices por BufferLine: una matriz Uint32Array e ICellPainter, con un elemento para cada celda. El ICellPainter actual es una propiedad del estado del analizador, por lo que simplemente reutiliza el mismo ICellPainter siempre que el estado de color / atributo no cambie. Si necesita agregar propiedades especiales a una celda, primero clone ICellPainter (si pudiera ser compartido).

Puede preasignar ICellPainter para las combinaciones de colores / atributos más comunes; como mínimo, tenga un objeto único que corresponda a los colores / atributos predeterminados.

Los cambios de estilo (como cambiar los colores de primer plano / fondo predeterminados) se pueden implementar simplemente actualizando las instancias de ICellPainter correspondientes, sin tener que actualizar cada celda.

Hay posibles optimizaciones: por ejemplo, use diferentes instancias de ICellPainter para caracteres de ancho simple y doble (o caracteres de ancho cero). (Eso ahorra 2 bits en cada elemento Uint32Array.) Hay 11 bits de atributo disponibles en Uint32Array (más si optimizamos para caracteres BMP). Estos se pueden usar para codificar las combinaciones de colores / atributos más comunes / útiles, que se pueden usar para indexar las instancias de ICellPainter más comunes. Si es así, la matriz ICellPainter se puede asignar de forma perezosa, es decir, solo si alguna celda de la línea requiere un ICellPainter "menos común".

También se podría eliminar la matriz _combined para caracteres que no sean BMP y almacenarlos en ICellPainter. (Eso requiere un ICellPainter único para cada personaje que no sea BMP, por lo que aquí hay una compensación).

@PerBothner Sí, una indirección es más versátil y, por lo tanto, más adecuada para extras poco comunes. Pero como son poco comunes, me gustaría no optimizarlos en primer lugar.

Algunas notas sobre lo que he probado en varios bancos de pruebas:

  • contenido de la cadena de celda
    Viniendo yo mismo de C ++, traté de ver el problema como lo haría en C ++, así que comencé con punteros para el contenido. Este era un puntero de cadena simple, pero apuntaba la mayor parte del tiempo a una sola cadena de caracteres. Que desperdicio. Por lo tanto, mi primera optimización fue deshacerme de la abstracción de cadenas guardando directamente el punto de código en lugar de la dirección (mucho más fácil en C / C ++ que en JS). Esto casi duplicó el acceso de lectura / escritura y ahorró 12 bytes por celda (puntero de 8 bytes + 4 bytes en la cadena, 64 bits con wchar_t de 32 bits). Nota al margen: la mitad del aumento de velocidad aquí está relacionado con la caché (la caché pierde debido a ubicaciones de cadenas aleatorias). Esto quedó claro con mi solución para combinar el contenido de la celda: un trozo de memoria en el que indexé cuando codepoint tenía el bit combinado establecido (el acceso fue más rápido aquí debido a una mejor localidad de caché, probado con valgrind). Transferido a JS, el aumento de velocidad no fue tan bueno debido a la conversión de cadena a número necesaria (aunque aún más rápido), pero el ahorro de memoria fue aún mayor (supongo que debido a un espacio de administración adicional para los tipos de JS). El problema era el StringStorage global para las cosas combinadas con la gestión de memoria explícita, un gran antipatrón en JS. Una solución rápida para eso fue el objeto _combined , que delega la limpieza al GC. Todavía está sujeto a cambios y, por cierto, está destinado a almacenar contenido de cadena arbitrario relacionado con la celda (lo hice teniendo en cuenta los grafemas, pero no los veremos pronto, ya que no son compatibles con ningún backend). Así que este es el lugar para almacenar contenido de cadena adicional celda por celda.
  • attrs
    Con los atributos comencé a "pensar en grande", con un AttributeStorage global para todos los atributos utilizados en todas las instancias de terminal (consulte https://github.com/jerch/xterm.js/tree/AttributeStorage). En cuanto a la memoria, esto funcionó bastante bien, principalmente porque las personas usan solo un pequeño conjunto de atributos incluso con soporte de color verdadero. El rendimiento no fue tan bueno, principalmente debido al recuento de referencias (cada celda tuvo que mirar dos veces en esta memoria externa) y la coincidencia de atributos. Y cuando traté de adoptar la referencia a JS, me sentí simplemente mal: el punto en el que presioné el botón "DETENER". En el medio, resultó que ya ahorramos toneladas de memoria y llamadas GC al cambiar a una matriz con tipo, por lo que el diseño de memoria plana un poco más costoso puede compensar su ventaja de velocidad aquí.
    Lo que probé yday (último comentario) fue una segunda matriz escrita en el nivel de línea para los atributos con el árbol de https://github.com/jerch/xterm.js/tree/AttributeStorage para la coincidencia (casi como su idea de ICellPainter ). Bueno, los resultados no son prometedores, por lo que me inclino por el diseño plano de 32 bits por ahora.

Ahora, este diseño plano de 32 bits resulta estar optimizado para las cosas comunes y los extras poco comunes no son posibles con él. Verdadero. Bueno, todavía tenemos marcadores (no estamos acostumbrados a ellos, así que no puedo decir ahora de qué son capaces), y sí, todavía hay bits libres en el búfer (lo cual es bueno para necesidades futuras, por ejemplo, podríamos usarlos como banderas para tratamiento especial y tal).

Tbh para mí es una lástima que el diseño de 16 bits con almacenamiento de attrs funcione tan mal, reducir a la mitad el uso de la memoria sigue siendo un gran problema (especialmente cuando las personas comienzan a usar líneas de desplazamiento> 10k), pero la penalización del tiempo de ejecución y la complejidad del código superan el peso la mem superior necesita atm en mi humilde opinión.

¿Puede ampliar la idea de ICellPainter? Tal vez me perdí alguna característica crucial hasta ahora.

Mi objetivo para DomTerm era permitir y fomentar una interacción más rica, justo lo que permite un emulador de terminal tradicional. El uso de tecnologías web permite muchas cosas interesantes, por lo que sería una pena centrarse en ser un emulador de terminal tradicional rápido. Especialmente porque muchos casos de uso de xterm.js (como REPL para IDE) pueden beneficiarse realmente de ir más allá del simple texto. A Xterm.js le va bien en el lado de la velocidad (¿alguien se queja de la velocidad?), Pero no le va tan bien en las funciones (la gente se queja de la falta de colores verdaderos y gráficos incrustados, por ejemplo). Creo que puede valer la pena centrarse un poco más en la flexibilidad y un poco menos en el rendimiento.

_ "¿Puedes dar más detalles sobre la idea de ICellPainter?" _

En general, ICellPainter encapsula todos los datos por celda excepto el código / valor de carácter, que proviene de Uint32Array. Eso es para celdas de caracteres "normales"; para imágenes incrustadas y otras "cajas", el código / valor de carácter puede no tener sentido.

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

La asignación de una celda a ICellPainter se puede realizar de varias formas. Lo obvio es que cada BufferLine tenga una matriz ICellPainter, pero eso requiere un puntero de 8 bytes (al menos) por celda. Una posibilidad es combinar la matriz _combined con la matriz ICellPainter: si se establece IS_COMBINED_BIT_MASK, ICellPainter también incluye la cadena combinada. Otra posible optimización es usar los bits disponibles en Uint32Array como un índice en una matriz: eso agrega alguna complicación adicional e indirecta, pero ahorra espacio.

Me gustaría animarnos a comprobar si podemos hacerlo de la forma en que lo hace monaco-editor (creo que encontraron una forma realmente inteligente y eficaz). En lugar de almacenar dicha información en el búfer, le permiten crear decorations . Creas una decoración para un rango de filas / columnas y se mantendrá en ese rango:

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

Más tarde, un renderizador podría recoger esas decoraciones y dibujarlas.

Consulte este pequeño ejemplo que muestra cómo se ve la API de monaco-editor:
https://microsoft.github.io/monaco-editor/playground.html#interacting -with-the-editor-line-and-inline-decorations

Para cosas como renderizar imágenes dentro de la terminal, mónaco usa un concepto de zonas de vista que se pueden ver (entre otros conceptos) en un ejemplo aquí:
https://microsoft.github.io/monaco-editor/playground.html#interacting -with-the-editor-listening-to-mouse-events

@PerBothner Thx por la aclaración y el boceto. Algunas notas sobre eso.

Eventualmente planeamos mover la cadena de entrada + búfer a un webworker en el futuro. Por lo tanto, el búfer está destinado a operar en un nivel abstracto y no podemos usar ningún elemento relacionado con el renderizado / representación allí todavía, como métricas de píxeles o cualquier nodo DOM. Veo sus necesidades para esto debido a que DomTerm es altamente personalizable, pero creo que deberíamos hacerlo con una API de marcador interno mejorada y podemos aprender aquí de monaco / vscode (gracias por los punteros @mofux).
Realmente me gustaría mantener limpio el búfer del núcleo de cosas poco comunes, ¿tal vez deberíamos discutir posibles estrategias de marcadores con un nuevo problema?

Todavía no estoy satisfecho con el resultado de la prueba de diseño de 16 bits. Dado que una decisión final aún no es urgente (no veremos nada de esto antes de 3.11), seguiré probándolo con algunos cambios (sigue siendo la solución más intrigante para mí que la variante de 32 bits).

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

También creo que deberíamos ir con algo parecido a esto para empezar, podemos explorar otras opciones más adelante, pero probablemente esta sea la más fácil de poner en marcha. La indirección de atributos definitivamente tiene una promesa en mi opinión, ya que generalmente no hay tantos atributos distintos en una sesión de terminal.

Me gustaría animarnos a comprobar si podemos hacerlo de la forma en que lo hace monaco-editor (creo que encontraron una forma realmente inteligente y eficaz). En lugar de almacenar dicha información en el búfer, le permiten crear decoraciones. Creas una decoración para un rango de filas / columnas y se mantendrá en ese rango:

Algo como esto es donde me gustaría que las cosas fueran. Una idea que tuve en este sentido fue permitir que los incrustadores adjunten elementos DOM a rangos para permitir que se dibujen cosas personalizadas. Hay 3 cosas en las que puedo pensar en este momento que me gustaría lograr con esto:

  • Dibujar subrayados de enlace de esta manera (simplificará significativamente la forma en que se dibujan)
  • Permitir marcadores en filas, como un * o algo
  • Permitir que las filas "parpadeen" para indicar que sucedió algo

Todo esto se puede lograr con una superposición y es un tipo de API bastante accesible (que expone un nodo DOM) y puede funcionar independientemente del tipo de renderizador.

No estoy seguro de que queramos entrar en el negocio de permitir que los incrustadores cambien la forma en que se dibujan los colores de fondo y primer plano.


@jerch Pondré esto en el hito 3.11.0 ya que considero que este problema terminó cuando eliminemos la implementación de la matriz JS que está planeada para entonces. También se planea fusionar https://github.com/xtermjs/xterm.js/pull/1796 en ese momento, pero este problema siempre tuvo como objetivo mejorar el diseño de la memoria del búfer.

Además, gran parte de esta discusión posterior probablemente sería mejor en https://github.com/xtermjs/xterm.js/issues/484 y https://github.com/xtermjs/xterm.js/issues/1852 (creado porque no había un problema de decoración).

@Tyriar Woot - finalmente cerrado: sweat_smile:

🎉 🕺 🍾

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

Tyriar picture Tyriar  ·  4Comentarios

fabiospampinato picture fabiospampinato  ·  4Comentarios

parisk picture parisk  ·  3Comentarios

Mlocik97-issues picture Mlocik97-issues  ·  3Comentarios

johnpoth picture johnpoth  ·  3Comentarios