Memória
No momento, nosso buffer está ocupando muita memória, principalmente para um aplicativo que inicia vários terminais com grandes conjuntos de scrollbacks. Por exemplo, a demonstração usando um terminal 160x24 com 5000 scrollback preenchido ocupa cerca de 34 MB de memória (consulte https://github.com/Microsoft/vscode/issues/29840#issuecomment-314539964), lembre-se de que é apenas um único terminal e monitores 1080p o faria provavelmente usará terminais mais largos. Além disso, para oferecer suporte a truecolor (https://github.com/sourcelair/xterm.js/issues/484), cada personagem precisará armazenar 2 number
tipos adicionais que quase dobrarão o consumo de memória atual do buffer.
Busca lenta do texto de uma linha
Existe o outro problema de precisar buscar o texto real de uma linha rapidamente. A razão pela qual isso é lento é devido à maneira como os dados são dispostos; uma linha contém um array de caracteres, cada um com uma única string de caracteres. Portanto, vamos construir a string e, em seguida, ela será enviada para a coleta de lixo imediatamente. Anteriormente, não precisávamos fazer isso porque o texto é extraído do buffer de linha (em ordem) e renderizado para o DOM. No entanto, isso está se tornando uma coisa cada vez mais útil à medida que melhoramos o xterm.js ainda mais, recursos como a seleção e os links extraem esses dados. Novamente usando o exemplo de scrollback de 160x24/5000, leva 30-60ms para copiar todo o buffer em um Macbook Pro de meados de 2014.
Apoiando o futuro
Outro problema potencial no futuro é quando olharmos para a introdução de algum modelo de visualização que pode precisar duplicar alguns ou todos os dados no buffer, esse tipo de coisa será necessário para implementar o reflow (https://github.com/sourcelair /xterm.js/issues/622) corretamente (https://github.com/sourcelair/xterm.js/pull/644#issuecomment-298058556) e talvez também seja necessário para oferecer suporte adequado aos leitores de tela (https://github.com /sourcelair/xterm.js/issues/731). Certamente seria bom ter algum espaço de manobra quando se trata de memória.
Esta discussão começou em https://github.com/sourcelair/xterm.js/issues/484 , entra em mais detalhes e propõe algumas soluções adicionais.
Estou inclinado para a solução 3 e avançando para a solução 5 se houver tempo e ela mostrar uma melhora acentuada. Adoraria qualquer feedback! / cc @jerch , @mofux , @rauchg , @parisk
Isso é basicamente o que estamos fazendo agora, apenas com truecolor fg e bg adicionados.
// [0]: charIndex
// [1]: width
// [2]: attributes
// [3]: truecolor bg
// [4]: truecolor fg
type CharData = [string, number, number, number, number];
type LineData = CharData[];
Prós
Contras
Isso armazenaria a string contra a linha em vez da linha, provavelmente veria ganhos muito grandes na seleção e vinculação e seria mais útil com o passar do tempo, tendo acesso rápido a toda a string de uma linha.
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;
Prós
Int32Array
Contras
Retirar os atributos e associá-los a um intervalo. Uma vez que nunca pode haver atributos sobrepostos, isso pode ser organizado sequencialmente.
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
}
}
Prós
.flags
vez de [0]
)Contras
A ideia aqui é aproveitar o fato de que geralmente não há muitos estilos em uma sessão de terminal, portanto, não devemos criar o mínimo necessário e reutilizá-los.
// [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;
}
Prós
.flags
vez de [0]
)Contras
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;
}
Prós
.flags
vez de [0]
)Contras
CharAttributes
por bloco?CharAttributeEntry
Isso leva a solução de 3, mas também adiciona uma string de texto de avaliação preguiçosa para acesso rápido ao texto da linha. Como também armazenamos os caracteres em CharData
, podemos avaliá-lo preguiçosamente.
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;
}
Prós
.flags
vez de [0]
)Contras
Int32Array
não funcionará, pois leva muito tempo para converter o int de volta em um caractere.Outra abordagem que pode ser combinada: use indexeddb, websql ou filesystem api para paginar as entradas de scrollback inativas para o disco 🤔
Ótima proposta. Eu concordo com que 3. é a melhor maneira de fazer agora, pois nos permite economizar memória ao mesmo tempo em que oferece suporte a true color.
Se chegarmos lá e as coisas continuarem indo bem, podemos otimizar conforme proposto em 5. ou de qualquer outra forma que venha à nossa mente naquele momento e faça sentido.
3. é ótimo 👍.
@mofux , embora haja definitivamente um caso de uso para o uso de técnicas de armazenamento em disco para reduzir o consumo de memória, isso pode degradar a experiência do usuário da biblioteca em ambientes de navegador que pedem permissão ao usuário para usar o armazenamento em disco.
Sobre Apoiar o futuro :
Quanto mais eu penso sobre isso, mais a ideia de ter um WebWorker
que faz todo o trabalho pesado de analisar os dados tty, manter os buffers de linha, combinar links, combinar tokens de pesquisa e coisas do gênero me atrai. Basicamente, fazer o trabalho pesado em um thread de segundo plano separado sem bloquear a interface do usuário. Mas acho que isso deveria ser parte de uma discussão separada, talvez em direção a uma versão 4.0 😉
+100 sobre o WebWorker no futuro, mas acho que precisamos alterar as versões da lista de navegadores que oferecemos suporte porque nem todos eles podem usá-lo ...
Quando digo Int32Array
, este será um array regular se não for suportado pelo ambiente.
@mofux, bom pensamento com WebWorker
no futuro 👍
@AndrienkoAleksandr sim, se quiséssemos usar WebWorker
, ainda precisaríamos oferecer suporte à alternativa por meio da detecção de recursos.
Uau, boa lista :)
Eu também tendem a me inclinar para 3., uma vez que promete uma grande redução no consumo de memória para mais de 90% do uso típico do terminal. A otimização da memória Imho deve ser o objetivo principal nesta fase. Otimização adicional para casos de uso específicos pode ser aplicável em cima disso (o que vem à minha mente: "canvas like apps" como ncurses e outros usarão toneladas de atualizações de célula única e degradarão a lista [start, end]
com o tempo) .
@AndrienkoAleksandr sim, eu gosto da ideia do webworker também, pois pode tirar _alguma_ carga do mainthread. O problema aqui é (além do fato de que ele pode não ser suportado por todos os sistemas de destino desejados) é o _alguns_ - a parte JS não é mais tão importante com todas as otimizações que o xterm.js viu ao longo do tempo. O verdadeiro problema em termos de desempenho é o layout / renderização do navegador ...
@mofux A coisa de paginação para alguma "memória estrangeira" é uma boa ideia, embora deva ser parte de alguma abstração superior e não do "me dê um widget de terminal interativo" que o xterm.js é. Isso pode ser alcançado por um addon imho.
Offtopic: Fiz alguns testes com arrays vs. typedarrays vs. asm.js. Tudo o que posso dizer - OMG, é como 1 : 1,5 : 10
para carregamentos e conjuntos de variáveis simples (no FF ainda mais). Se a velocidade JS pura realmente começar a doer, "use asm" pode ajudar. Mas eu veria isso como um último recurso, uma vez que implicaria mudanças fundamentais. E a webassembly ainda não está pronta para ser enviada.
Offtopic: Fiz alguns testes com arrays vs. typedarrays vs. asm.js. Tudo o que posso dizer - OMG, é como 1: 1,5: 10 para cargas e conjuntos variáveis simples (no FF ainda mais)
@jerch para esclarecer, arrays vs typedarrays é de 1: 1 a 1: 5?
Woops nice catch com a vírgula - eu quis dizer 10:15:100
velocidade sábia. Mas apenas em matrizes digitadas FF eram ligeiramente mais rápidas do que matrizes normais. o asm é pelo menos 10 vezes mais rápido do que os arrays js em todos os navegadores - testado com FF, webkit (Safari), blink / V8 (Chrome, Opera).
@jerch legal, uma velocidade de 50% de typedarrays além de melhor memória definitivamente valeria a pena investir por enquanto.
Ideia para economizar memória - talvez pudéssemos nos livrar dos width
de cada personagem. Vou tentar implementar uma versão wcwidth mais barata.
@jerch , precisamos acessá-lo um pouco, e não podemos carregá-lo
Pode ser melhor torná-lo opcional, assumindo 1 se não estiver especificado:
type CharData = [string, number?]; // not sure if this is valid syntax
[
// 'a'
['a'],
// '文'
['文', 2],
// after wide
['', 0],
...
]
@Tyriar Sim - bem, como eu já escrevi, por favor dê uma olhada em PR # 798
A aceleração é de 10 a 15 vezes no meu computador pelo custo de 16k bytes para a tabela de pesquisa. Talvez uma combinação de ambos seja possível, se ainda for necessária.
Mais algumas sinalizações que ofereceremos suporte no futuro: https://github.com/sourcelair/xterm.js/issues/580
Outro pensamento: apenas a parte inferior do terminal ( Terminal.ybase
a Terminal.ybase + Terminal.rows
) é dinâmica. O scrollback que constitui a maior parte dos dados é completamente estático, talvez possamos aproveitar isso. Eu não sabia disso até recentemente, mas mesmo coisas como excluir linhas (DL, CSI Ps M) não trazem o scrollback de volta para baixo, mas sim inserem outra linha. Da mesma forma, rolar para cima (SU, CSI Ps S) exclui o item em Terminal.scrollTop
e insere um item em Terminal.scrollBottom
.
Gerenciar a parte dinâmica inferior do terminal de forma independente e pressionar para rolar para trás quando a linha é removida pode levar a alguns ganhos significativos. Por exemplo, a parte inferior pode ser mais detalhada para favorecer a modificação de atributos, acesso mais rápido, etc., enquanto a rolagem para trás pode ser mais um formato de arquivo, como proposto acima.
Outro pensamento: provavelmente é uma ideia melhor restringir CharAttributeEntry
a linhas, pois é assim que a maioria dos aplicativos parece funcionar. Além disso, se o terminal for redimensionado, o preenchimento "em branco" será adicionado à direita, não compartilhando os mesmos estilos.
por exemplo:
À direita das diferenças vermelho / verde estão células "em branco" sem estilo.
@Tyriar
Alguma chance de colocar este assunto de volta na agenda? Pelo menos para programas de produção intensiva, uma maneira diferente de manter os dados do terminal pode economizar muita memória e tempo. Algum híbrido de 2/3/4 dará um grande aumento na taxa de transferência, se pudermos evitar a divisão e salvar caracteres únicos da string de entrada. Além disso, salvar os atributos apenas uma vez que eles mudaram ajudará a economizar memória.
Exemplo:
Com o novo analisador, poderíamos salvar um monte de caracteres de entrada sem mexer nos atributos, pois sabemos que eles não mudarão no meio dessa string. Os atributos dessa string podem ser salvos em alguma outra estrutura de dados ou atributo junto com wcwidths (sim, ainda precisamos deles para encontrar a quebra de linha) e quebras de linha e paradas. Isso basicamente desistiria do modelo de célula quando os dados chegassem.
O problema surge se algo intervém e deseja ter uma representação detalhada dos dados do terminal (por exemplo, o renderizador ou alguma sequência de escape / usuário deseja mover o cursor). Ainda temos que fazer os cálculos das células se isso acontecer, mas deve ser suficiente fazer isso apenas para o conteúdo dentro das colunas e linhas terminais. (Ainda não tenho certeza sobre o conteúdo rolado, que pode ser ainda mais armazenado em cache e barato para redesenhar.)
@jerch Eu vou encontrar @mofux um dia em Praga em algumas semanas e íamos fazer / começar algumas melhorias internas de como os atributos de texto são tratados, o que cobre isso 😃
De https://github.com/xtermjs/xterm.js/pull/1460#issuecomment -390500944
O algo é meio caro, pois cada caractere precisa ser avaliado duas vezes
@jerch se você tiver alguma ideia sobre o acesso mais rápido do texto do buffer, informe-nos. Atualmente, a maior parte dele é apenas um único caractere, como você sabe, mas poderia ser ArrayBuffer
, uma string, etc. Estive pensando que deveríamos pensar em tirar mais proveito do scrollback ser imutável de alguma forma.
Bem, eu experimentei muito com ArrayBuffers no passado:
Array
relação ao tempo de execução para os métodos típicos (talvez ainda menos otimizados por fornecedores de motores)new UintXXArray
é muito pior do que a criação de array literal com []
TextEncoder
)Minhas descobertas sobre ArrayBuffer
sugerem não usá-los para dados de string devido à penalidade de conversão. Em teoria, o terminal poderia usar ArrayBuffer
do node-pty até os dados do terminal (isso economizaria várias conversões no caminho para o frontend), não tenho certeza se a renderização pode ser feita dessa forma, acho que renderizar sempre precisa de uma conversão final de uint16_t
em string
. Mas mesmo aquela última criação de string consumirá a maior parte do tempo de execução salvo - e, além disso, tornaria o terminal internamente em uma besta C-ish feia. Portanto, desisti dessa abordagem.
TL; DR ArrayBuffer
é superior se você pode pré-alocar e reutilizar a estrutura de dados. Para todo o resto, matrizes normais são melhores. Strings não valem a pena serem comprimidas em ArrayBuffers.
Uma nova ideia que tive tenta diminuir a criação de cordas o máximo possível, esp. tenta evitar divisões e junções desagradáveis. É meio que baseado em sua 2ª ideia acima, com o novo método InputHandler.print
, largura de wc e paradas de linha em mente:
print
agora obtém strings inteiras até várias linhas de terminalwcwidth(string) % cols
\n
(quebra de linha): avança o cursor em uma linha, marca a posição na lista de ponteiros como quebra de linha\r
: carrega o conteúdo da última linha (da posição atual do cursor até a quebra da última linha) em algum buffer de linha para ser sobrescrito\r
, nenhuma abstração de célula ou divisão de string é necessáriacols
x rows
(eles apenas mudam o sinalizador de atributo que é salvo junto com a string inteira)A propósito, as larguras wc são um subconjunto do algoritmo do grafema, então isso pode ser intercambiável no futuro.
Agora, a parte perigosa 1 - alguém quer mover o cursor dentro de cols
x rows
:
cols
para trás nas quebras de linha - o início do conteúdo do terminal atualAgora a parte perigosa 2 - o renderizador quer desenhar algo:
Prós:
InputHandler
- print
Contras:
InputHandler
serão perigosos no sentido de interromper este modelo de fluxo e a necessidade de alguma abstração de célula intermediáriaBem, este é um rascunho da ideia, longe de ser um atm utilizável, uma vez que muitos detalhes ainda não foram cobertos. Esp. as partes "perigosas" podem se tornar desagradáveis com muitos problemas de desempenho (como degradar o buffer com comportamento gc ainda pior, etc pp)
@jerch
Strings não valem a pena serem comprimidas em ArrayBuffers.
Acho que o Monaco armazena seu buffer em ArrayBuffer
tem um desempenho muito alto. Ainda não analisei muito profundamente a implementação.
esp. tenta evitar as desagradáveis divisões e junções
Quais?
Estive pensando que deveríamos pensar em tirar mais proveito do fato de a rolagem para trás ser imutável de alguma forma.
Uma ideia era separar o scrollback da seção da janela de visualização. Uma vez que uma linha vai rolar para trás, ela é empurrada para a estrutura de dados de rolar para trás. Você poderia imaginar 2 CircularList
objetos, um cujas linhas são otimizadas para nunca mudar, um para o oposto.
@Tyriar Sobre o scrollback - sim, uma vez que isso nunca é alcançável pelo cursor, pode economizar alguma memória apenas para descartar a abstração de célula para linhas roladas.
@Tyriar
Faz sentido armazenar strings em ArrayBuffer se pudermos limitar a conversão a um (talvez o último para a saída de renderização). Isso é ligeiramente melhor do que o manuseio de cordas em todos os lugares. Isso seria possível, pois o node-pty também pode fornecer dados brutos (e também o websocket pode nos fornecer dados brutos).
esp. tenta evitar as desagradáveis divisões e junções
Quais?
Toda a abordagem é evitar _minimize_ splits em tudo. Se ninguém solicitar que o cursor salte para os dados armazenados em buffer, as strings nunca serão divididas e podem ir direto para o renderizador (se houver suporte). Nenhuma célula se divide e depois se junta.
@jerch bem, pode se a janela de visualização for expandida, acho que também podemos puxar a rolagem para trás quando uma linha é excluída? Não tenho 100% de certeza sobre isso ou mesmo se é um comportamento correto.
@Tyriar Ah certo. Não tenho certeza sobre o último também, acho que o xterm nativo permite isso apenas para a rolagem real do mouse ou da barra de rolagem. Mesmo SD / SU não move o conteúdo do scrollbuffer de volta para a janela de visualização do terminal "ativo".
Você poderia me apontar a fonte do editor monaco, onde o ArrayBuffer é usado? Parece que não consigo encontrar sozinho: blush:
Hmm, basta reler a especificação TextEncoder / Decoder, com ArrayBuffers do node-pty até o frontend, estamos basicamente presos ao utf-8, a menos que traduzamos da maneira mais difícil em algum ponto. Tornando xterm.js utf-8 ciente? Idk, isso envolveria muitos cálculos de ponto de código intermediário para os chars Unicode superiores. Proside - economizaria memória para chars ascii.
@rebornix, você poderia nos dar algumas dicas de onde o monaco armazena o buffer?
Aqui estão alguns números para matrizes digitadas e o novo analisador (foi mais fácil de adotar):
print
ação salta de 190 MB / s para 290 MB / sprint
ação salta de 190 MB / s para 320 MB / sNo geral, o UTF-16 tem um desempenho muito melhor, mas isso era esperado, pois o analisador é otimizado para isso. UTF-8 sofre com o cálculo do ponto de código intermediário.
A string para a conversão da matriz digitada consome ~ 4% do tempo de execução JS do meu benchmark ls -lR /usr/lib
(sempre muito abaixo de 100 ms, feito por meio de um loop em InputHandler.parse
). Não testei a conversão reversa (isso é feito implicitamente atm em InputHandller.print
no nível de célula por célula). O tempo de execução geral é um pouco pior do que com strings (o tempo economizado no analisador não compensa o tempo de conversão). Isso pode mudar quando outras partes também reconhecem a matriz digitada.
E as capturas de tela correspondentes (testadas com ls -lR /usr/lib
):
com cordas:
com Uint16Array:
Observe a diferença para EscapeSequenceParser.parse
, que pode lucrar com uma matriz digitada (~ 30% mais rápido). O InputHandler.parse
faz a conversão, portanto, é pior para a versão digitada do array. Além disso, GC Minor tem mais a ver com array digitado (já que eu jogo o array fora).
Editar: Outro aspecto pode ser visto nas imagens - o GC torna-se relevante com aproximadamente 20% de tempo de execução, os frames de longa execução (sinalizados em vermelho) são todos relacionados ao GC.
Apenas outra ideia um tanto radical:
int8
para int16
para int32
tipos são possíveis. O alocador retorna um índice livre em Uint8Array
, este ponteiro pode ser convertido para uma posição Uint16Array
ou Uint32Array
por uma simples mudança de bit.uint16_t
type para UTF-16.InputHandler
com ponteiros para esta memória em vez de fatias de string.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
}
Prós:
malloc
e free
(depende da inteligência do alocador / desalocador)Contras:
:sorriso:
Difícil de implementar, muda quase tudo 😉
Isso é mais próximo de como o Monaco funciona. Lembrei-me desta postagem do blog que discute a estratégia para armazenar metadados de personagens https://code.visualstudio.com/blogs/2017/02/08/syntax-highlighting-optimizations
Sim, é basicamente a mesma ideia.
Espero que minha resposta sobre onde o monaco armazena o buffer não seja tarde demais.
Alex e eu somos a favor do Array Buffer e na maioria das vezes ele nos dá um bom desempenho. Alguns lugares em que usamos ArrayBuffer:
Usamos strings simples para buffer de texto em vez de Array Buffer, pois string V8 é mais fácil de manipular
Fixou os links de
A lista a seguir é apenas um resumo rápido de conceitos interessantes que descobri e que podem ajudar a reduzir o uso de memória e / ou tempo de execução:
Para obter mais informações aqui, aqui está um truque rápido como poderíamos "mesclar" atributos de texto.
O código é impulsionado principalmente pela ideia de economizar memória para os dados do buffer (o tempo de execução sofrerá, ainda não testado quanto). Esp. os atributos de texto com RGB para primeiro e segundo plano (uma vez suportados) farão com que o xterm.js coma toneladas de memória do layout atual célula por célula. O código tenta contornar isso usando um atlas redimensionável de contagem de referências para atributos. Essa é uma opção, pois um único terminal dificilmente conterá mais de 1 milhão de células, o que aumentaria o atlas para 1M * entry_size
se todas as células fossem diferentes.
A célula em si só precisa conter o índice do atlas de atributos. Nas alterações de células, o índice antigo precisa ser refeito e o novo. O índice atlas substituiria o atributo de atributo do objeto terminal e será alterado no SGR.
O atlas atualmente trata apenas de atributos de texto, mas pode ser estendido a todos os atributos de células, se necessário. Enquanto o buffer do terminal atual contém 2 números de 32 bits para dados de atributo (4 com RGB no design do buffer atual), o atlas o reduziria a apenas um número de 32 bits. As entradas do atlas também podem ser embaladas mais adiante.
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:
A penalidade de tempo de execução é quase zero, para meu benchmark com ls -lR /usr/lib
adiciona menos de 1 ms ao tempo de execução total de ~ 2,3 s. Nota lateral interessante - o comando define menos de 64 slots de atributo de texto diferentes para a saída de 5 MB de dados e salvará mais de 20 MB depois de totalmente implementado.
Fiz alguns PRs de protótipo para testar algumas mudanças no buffer (consulte https://github.com/xtermjs/xterm.js/pull/1528#issue-196949371 para a ideia geral por trás das mudanças):
@jerch pode ser uma boa ideia ficar longe da palavra atlas para isso, de modo que "atlas" sempre signifique "atlas de textura". Algo como armazenamento ou cache provavelmente seria melhor?
ok, "cache" está bom.
Acho que terminei os PRs de teste. Por favor, dê uma olhada nos comentários de RP para obter o histórico do seguinte resumo.
Proposta:
AttributeCache
para conter tudo o que é necessário para estilizar uma única célula terminal. Veja # 1528 para uma versão inicial de contagem de referências que também pode conter especificações de true color. O cache também pode ser compartilhado entre diferentes instâncias de terminal, se necessário, para salvar mais memória em vários aplicativos de terminal.StringStorage
para conter strings de dados de conteúdo de terminal curtas. A versão em # 1530 evita até mesmo armazenar strings de caracteres únicos "sobrecarregando" o significado do ponteiro. wcwidth
deve ser movido aqui.CharData
atual de [number, string, number, number]
para [number, number]
, onde os números são ponteiros (números de índice) para:AttributeCache
entradaStringStorage
entradaÉ improvável que os atributos mudem muito, então um único número de 32 bits economizará muita memória com o tempo. O ponteiro StringStorage
é um ponto de código unicode real para caracteres únicos, portanto, pode ser usado como a entrada code
de CharData
. A string real pode ser acessada por StringStorage.getString(idx)
. O quarto campo wcwidth
de CharData
pode ser acessado por StringStorage.wcwidth(idx)
(ainda não implementado). Quase não há penalidade de tempo de execução ao se livrar de code
e wcwidth
em CharData
(testado em # 1529).
CharData
para uma implementação densa de buffer baseada em Int32Array
. Também testado em # 1530 com uma classe de stub (longe de ser totalmente funcional), os benefícios finais provavelmente serão:ls -lR /usr/lib
caiu para 1,3s (o mestre está em 2.1s) enquanto o buffer antigo ainda está ativo para manipulação do cursor, uma vez removido, espero que o tempo de execução caia para menos de 1sDesvantagem - a etapa 4 dá muito trabalho, pois precisará de algum retrabalho na interface do buffer. Mas hey - para economizar 80% da RAM e ainda ganhar desempenho de tempo de execução não é grande coisa, não é? :sorriso:
Há outro problema em que tropecei - a representação atual da célula vazia. Imho, uma célula pode ter 3 estados:
blankLine
e eraseChar
, mas com um espaço como conteúdo.O problema que vejo aqui é que uma célula vazia não é distinguível de uma célula normal com um espaço inserido, ambas têm a mesma aparência no nível do buffer (mesmo conteúdo, mesma largura). Eu não escrevi nenhum código de renderizador / saída, mas espero que isso leve a situações embaraçosas na frente de saída. Esp. o manuseio da extremidade direita de uma linha pode ser complicado.
Um terminal com 15 cols, primeiro alguma string de saída, que envolveu:
1: 'H', 'e', 'l', 'l', 'o', ' ', 't', 'e', 'r', 'm', 'i', 'n', 'a', 'l', ' '
2: 'w', 'o', 'r', 'l', 'd', '!', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '
versus uma lista de pastas com ls
:
1: 'R', 'e', 'a', 'd', 'm', 'e', '.', 'm', 'd', ' ', ' ', ' ', ' ', ' ', ' '
2: 'f', 'i', 'l', 'e', 'A', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '
O primeiro exemplo contém um espaço real após a palavra 'terminal', o segundo exemplo nunca tocou as células após 'Readme.md'. A forma como é representado no nível do buffer faz sentido para o caso padrão imprimir as coisas como saída de terminal na tela (a sala precisa ser ocupada de qualquer maneira), mas para ferramentas que tentam lidar com as strings de conteúdo como uma seleção de mouse ou um gerenciador de refluxo não está mais claro de onde vêm os espaços.
Mais ou menos isso leva à próxima questão - como determinar o comprimento real do conteúdo em uma linha (quantidade de células que contêm algo do lado esquerdo)? Uma abordagem simples contaria as células vazias do lado direito; novamente, o duplo significado visto de cima torna isso difícil de determinar.
Proposta:
Imho, isso pode ser facilmente corrigido usando algum outro espaço reservado para células vazias, por exemplo, um caractere de controle ou a string vazia e substitua aqueles no processo de renderização, se necessário. Talvez o renderizador de tela também possa se beneficiar com isso, já que pode não ter que lidar com essas células (depende da maneira como a saída é gerada).
A propósito, para a string encapsulada acima, isso também leva ao problema isWrapped
, que é essencial para um redimensionamento de refluxo ou um tratamento correto da seleção de copiar e colar. Imho, não podemos remover isso, mas precisamos integrá-lo melhor do que atm.
@jerch trabalho impressionante! :risonho:
1 Construa um AttributeCache para conter tudo o que é necessário para estilizar uma única célula terminal. Veja # 1528 para uma versão inicial de contagem de referências que também pode conter especificações de true color. O cache também pode ser compartilhado entre diferentes instâncias de terminal, se necessário, para salvar mais memória em vários aplicativos de terminal.
Fez alguns comentários sobre # 1528.
2 Crie um StringStorage para conter strings de dados de conteúdo de terminal curtas. A versão em # 1530 evita até mesmo armazenar strings de caracteres únicos "sobrecarregando" o significado do ponteiro. wcwidth deve ser movido aqui.
Fez alguns comentários sobre # 1530.
4 Mova o CharData reduzido para uma implementação densa de buffer baseada em Int32Array. Também testado em # 1530 com uma classe de stub (longe de ser totalmente funcional), os benefícios finais provavelmente serão:
Ainda não acreditei totalmente nessa ideia, mas acho que isso vai nos prejudicar quando implementarmos o reflow. Parece que cada uma dessas etapas pode ser executada em ordem, para que possamos ver como as coisas vão e ver se faz sentido fazer isso assim que terminarmos as 3.
Há outro problema em que tropecei - a representação atual da célula vazia. Imho uma célula pode ter 3 estados
Aqui está um exemplo de um bug que saiu deste https://github.com/xtermjs/xterm.js/issues/1286,: +1: para diferenciar células de espaço em branco e células "vazias"
A propósito, para a string encapsulada acima, isso também leva ao problema isWrapped, que é essencial para um redimensionamento de refluxo ou um tratamento correto da seleção de copiar e colar. Imho, não podemos remover isso, mas precisamos integrá-lo melhor do que atm.
Vejo isWrapped
indo embora quando abordamos https://github.com/xtermjs/xterm.js/issues/622, pois CircularList conterá apenas linhas não encapsuladas.
Ainda não acreditei totalmente nessa ideia, mas acho que isso vai nos prejudicar quando implementarmos o reflow. Parece que cada uma dessas etapas pode ser executada em ordem, para que possamos ver como as coisas vão e ver se faz sentido fazer isso assim que terminarmos as 3.
Sim, estou com você (ainda é divertido brincar com essa abordagem totalmente diferente). 1 e 2 podem ser selecionados, 3 pode ser aplicado dependendo de 1 ou 2. 4 é opcional, podemos apenas manter o layout do buffer atual. A economia de memória é assim:
CircularList
: economiza 50% (~ 2,8 MB de ~ 5,5 MB)1. é muito fácil de implementar, o comportamento da memória com scrollBack maior ainda mostrará o escalonamento ruim, conforme mostrado aqui https://github.com/xtermjs/xterm.js/pull/1530#issuecomment -403542479, mas em um nível menos tóxico
2. Um pouco mais difícil de implementar (são necessários mais alguns caminhos indiretos no nível da linha), mas tornará possível manter a API superior de Buffer
intacta. Imho a opção de ir para - grande mem save e ainda fácil de integrar.
3. 5% a mais de mem save do que a opção 2, difícil de implementar, irá alterar o toque em todas as APIs e, portanto, literalmente, em toda a base de código. Imho mais de interesse acadêmico ou para dias chatos de chuva a serem implementados rs.
@Tyriar Fiz mais alguns testes com ferrugem para uso de montagem web e reescrevi o analisador. Observe que minhas habilidades de ferrugem estão um pouco "enferrujadas", já que não me aprofundei ainda mais nisso, portanto, o seguinte pode ser o resultado de um código de ferrugem fraco. Resultados:
A menos que queiramos reescrever todas as bibliotecas centrais em ferrugem (ou qualquer outra linguagem capaz de wasm), não podemos ganhar nada mudando para um wasm lang imho. Uma vantagem dos wasm langs de hoje é o fato de que a maioria suporta manipulação de memória explícita (poderia nos ajudar com o problema de buffer), as desvantagens são a introdução de uma linguagem totalmente diferente em um projeto focado principalmente em TS / JS (uma grande barreira para adições de código) e os custos de tradução entre wasm e JS Land.
TL; DR
xterm.js é amplamente para coisas JS em geral, como DOM e eventos para obter qualquer coisa de webassembly, mesmo para uma reescrita das partes principais.
@jerch boa investigação: smiley:
Chamadas de JS para wasm criam alguma sobrecarga e corroem todos os benefícios acima. Na verdade, foi cerca de 20% mais lento.
Este foi o maior problema para o monaco tornar-se nativo também, o que basicamente informou minha postura (embora isso tenha ocorrido com o módulo de nó nativo, e não com o wasm). Acredito que trabalhar com ArrayBuffer
s, sempre que possível, deve nos dar o melhor equilíbrio entre desempenho e simplicidade (facilidade de implementação, barreira para entrada).
@Tyriar vai tentar criar um AttributeStorage para armazenar os dados RGB. Ainda não tenho certeza sobre o BST, para o caso de uso típico com apenas algumas configurações de cor em uma sessão de terminal, isso será pior no tempo de execução, talvez isso deva ser uma queda no tempo de execução, uma vez que as cores ultrapassam um determinado limite. Além disso, o consumo de memória aumentará muito novamente, embora ainda economize memória, uma vez que os atributos são armazenados apenas uma vez e não junto com cada célula (o pior cenário com cada célula contendo atributos diferentes sofrerá, entretanto).
Você sabe por que o fg
atual de bg
256 cores é baseado em 9 bits em vez de 8 bits? Para que é usado o bit adicional? Aqui: https://github.com/xtermjs/xterm.js/blob/6691f809069a549b4808cd2e055398d2da15db37/src/InputHandler.ts#L1596
Você poderia me dar o layout de bits atual de attr
? Acho que uma abordagem semelhante, como o "duplo significado" para o ponteiro StringStorage, pode economizar ainda mais memória, mas exigiria que o MSB de attr
fosse reservado para a distinção do ponteiro e não fosse usado para nenhuma outra finalidade. Isso pode limitar a possibilidade de suportar outros sinalizadores de atributo posteriormente (porque FLAGS
já usa 7 bits), ainda estamos perdendo alguns sinalizadores fundamentais que provavelmente virão?
Um número attr
32 bits no buffer de termo poderia ser empacotado assim:
# 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)
Desta forma, o armazenamento só precisa conter os dados RGB em dois números de 32 bits, enquanto os sinalizadores podem ficar no número attr
.
@jerch , a propósito, enviei um e-mail para você, provavelmente foi comido pelo filtro de spam de novo 😛
Você sabe por que o valor atual das cores fg e bg 256 é baseado em 9 bits em vez de 8 bits? Para que é usado o bit adicional?
Acho que é usado para a cor fg / bg padrão (que pode ser escura ou clara), então, na verdade, são 257 cores.
https://github.com/xtermjs/xterm.js/pull/756/files
Você poderia me dar o layout de bits atual de attr?
Eu acho que é isso:
19+: flags (see `FLAGS` enum)
18..18: default fg flag
17..10: 256 fg
9..9: default bg flag
8..1: 256 bg
Você pode ver o que descobri para truecolor no antigo 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];
Então, neste eu tinha 2 bandeiras; um para a cor padrão (se deve ignorar todos os bits de cor) e um para truecolor (se deve fazer 256 ou 16 mil cores).
Isso pode limitar a possibilidade de oferecer suporte a outros sinalizadores de atributo posteriormente (porque o FLAGS já usa 7 bits), ainda estamos perdendo alguns sinalizadores fundamentais que provavelmente virão?
Sim, queremos algum espaço para sinalizadores adicionais, por exemplo https://github.com/xtermjs/xterm.js/issues/580, https://github.com/xtermjs/xterm.js/issues/1145, eu diga pelo menos deixe> 3 bits onde possível.
Em vez de dados de ponteiro dentro do próprio attr, poderia haver outro mapa que contenha referências aos dados rgb? mapAttrIdxToRgb: { [idx: number]: RgbData
@Tyriar Desculpe,
Joguei um pouco com estruturas de dados de pesquisa mais inteligentes para o armazenamento de atributos. Mais promissores em relação a espaço e tempo de execução de pesquisa / inserção são árvores e um skiplist como uma alternativa mais barata. Em teoria lol. Na prática, nenhum deles pode superar minha pesquisa simples de array, o que me parece muito estranho (bug em algum lugar do código?)
Eu carreguei um arquivo de teste aqui https://gist.github.com/jerch/ff65f3fb4414ff8ac84a947b3a1eec58 com array vs. uma árvore vermelho-preto inclinada para a esquerda, que testa até 10M de entradas (que é quase o espaço de endereçamento completo). Ainda assim, o array está muito à frente em comparação com o LLRB, mas suspeito que o ponto de equilíbrio esteja em torno de 10 milhões. Testado no meu antigo laptop 7ys, talvez alguém possa testá-lo também e ainda melhor - me aponte alguns bugs no impl / tests.
Aqui estão alguns resultados (com números em execução):
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
O que realmente me surpreende é o fato de que a pesquisa de array linear não mostra nenhum crescimento nas regiões inferiores, é até 10k entradas estáveis em ~ 4ms (pode estar relacionado ao cache). O teste de 10M mostra um tempo de execução pior do que o esperado, talvez devido a mem paging de qualquer natureza. Talvez JS esteja muito longe da máquina com o JIT e todos os opts / deopts acontecendo, ainda acho que eles não podem eliminar uma etapa de complexidade (embora o LLRB pareça ser pesado em um único _n_, movendo assim o ponto de equilíbrio para O ( n) vs. O (logn) para cima)
Aliás, com dados aleatórios, a diferença é ainda pior.
Acho que é usado para a cor fg / bg padrão (que pode ser escura ou clara), então, na verdade, são 257 cores.
Então isso é diferenciar SGR 39
ou SGR 49
de uma das 8 cores da paleta?
Em vez de dados de ponteiro dentro do próprio attr, poderia haver outro mapa que contenha referências aos dados rgb? mapAttrIdxToRgb: {[idx: número]: RgbData
Isso introduziria outra indireção com uso de memória adicional. Com os testes acima, também testei a diferença entre sempre segurar os sinalizadores em attrs e salvá-los junto com os dados RGB no armazenamento. Como a diferença é de aproximadamente 0,5 ms para entradas de 1 milhão, eu não optaria por essa configuração complicada de atributos, em vez disso, copiava os sinalizadores para o armazenamento assim que RGB fosse definido. Ainda assim, eu escolheria a distinção de 32 bits entre atributos diretos e ponteiro, pois isso evitará o armazenamento de células não RGB.
Também acho que as 8 cores da paleta padrão para fg / bg não estão suficientemente representadas no buffer atualmente. Em teoria, o terminal deve suportar os seguintes modos de cores:
SGR 39
+ SGR 49
cor padrão para fg / bg (personalizável)SGR 30-37
+ SGR 40-47
8 paleta de cores baixas para fg / bg (personalizável)SGR 90-97
+ SGR 100-107
8 paleta de cores altas para fg / bg (personalizável)SGR 38;5;n
+ SGR 48;5;n
256 paleta indexada para fg / bg (personalizável)SGR 38;2;r;g;b
+ SGR 48;2;r;g;b
RGB para fg / bg (não personalizável)As opções 2.) e 3.) podem ser mescladas em um único byte (tratando-as como uma única paleta de 16 cores fg / bg), 4.) leva 2 bytes e 5.) finalmente leva mais 6 bytes. Ainda precisamos de alguns bits para indicar o modo de cor.
Para refletir isso no nível do buffer, precisaríamos do seguinte:
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
Portanto, precisamos de 30 bits de um número de 32 bits, deixando 2 bits livres para outros fins. O 32º bit pode segurar o ponteiro em comparação com o flag de atrito direto, omitindo o armazenamento para células não RGB.
Também sugiro agrupar o acesso de atrito em uma classe conveniente para não expor detalhes de implementação para o exterior (veja o arquivo de teste acima, há uma versão inicial de uma classe TextAttributes
para fazer isso).
Desculpe, fiquei alguns dias sem estar online e temo que o e-mail tenha sido comido pelo filtro de spam. Você poderia reenviar, por favor?
Reenviar
A propósito, esses números acima para a pesquisa de array vs llrb são uma porcaria - acho que foi estragado pelo otimizador fazendo algumas coisas estranhas no loop for. Com uma configuração de teste ligeiramente diferente, ele mostra claramente o O (n) vs. O (log n) crescendo muito antes (com 1000 elementos pré-preenchidos sendo já mais rápidos com a árvore).
Estado atual:
Depois de:
Uma otimização bastante simples é achatar o array-of-arrays em um único array. Ou seja, em vez de um BufferLine
de colunas _N_ possuindo um _data
matriz de _N_ CharData
células, onde cada CharData
é uma matriz de 4, só tem uma única matriz de elementos _4 * N_. Isso elimina a sobrecarga de objeto de matrizes _N_. Também melhora a localidade do cache, por isso deve ser mais rápido. Uma desvantagem é o código um pouco mais complicado e feio, mas parece que vale a pena.
Seguindo meu comentário anterior, parece valer a pena considerar o uso de um número variável de elementos no array _data
para cada célula. Em outras palavras, uma representação com estado. Mudanças aleatórias na posição seriam mais caras, mas a varredura linear do início de uma linha pode ser muito rápida, especialmente porque um array simples é otimizado para a localidade do cache. A saída sequencial típica seria rápida, assim como a renderização.
Além de reduzir o espaço, uma vantagem de um número variável de elementos por célula é o aumento da flexibilidade: atributos extras (como cor de 24 bits), anotações para células ou intervalos específicos, glifos ou
elementos DOM aninhados.
@PerBothner Thx pelas suas ideias! Sim, eu já testei o layout de matriz densa única com aritmética de ponteiro, ele mostra a melhor utilização de memória. Problemas surgem quando se trata de redimensionar, basicamente significa reconstruir todo o pedaço de memória (copiar) ou copiar rapidamente em um pedaço maior e realinhar as partes. Isso é bastante exp. e imho não é justificado pela economia de mem (testado em algum PR de playground listado acima, a economia foi algo em torno de ~ 10% em comparação com a implementação da nova linha de buffer).
Sobre seu segundo comentário - já discutimos isso, pois tornaria o manuseio das linhas quebradas mais fácil. Por enquanto, decidimos usar a abordagem coluna X linha para o novo layout do buffer e fazer isso primeiro. Acho que devemos resolver isso novamente, uma vez que fizermos a implementação de redimensionamento de reflow.
Sobre adicionar coisas adicionais ao buffer: atualmente fazemos aqui o que a maioria dos outros terminais fazem - o avanço do cursor é determinado por wcwidth
que garante a compatibilidade com a ideia de pty / termios de como os dados devem ser dispostos. Isso basicamente significa que tratamos no nível do buffer apenas coisas como pares substitutos e caracteres combinados. Qualquer outra regra de junção de "nível superior" pode ser aplicada pelo marcador de personagem no renderizador (atualmente usado por https://github.com/xtermjs/xterm-addon-ligatures para ligaduras). Eu tinha um PR aberto para suportar grafemas Unicode no nível de buffer inicial, mas acho que não podemos fazer isso nesse estágio, já que a maioria dos back-ends pty não tem noção disso (existe algum?) E acabaríamos com conglomerados char estranhos . O mesmo vale para o suporte BIDI real, acho que grafemas e BIDI são melhor feitos no estágio de renderização para manter os movimentos do cursor / célula intactos.
O suporte para nós DOM anexados às células parece muito interessante, gosto dessa ideia. Atualmente isso não é possível em uma abordagem direta, uma vez que temos diferentes back-ends de renderização (DOM, canvas 2D e o novo renderizador webgl brilhante), acho que isso ainda poderia ser alcançado para todos os renderizadores posicionando uma sobreposição onde não fosse nativamente compatível (apenas o renderizador DOM faria ser capaz de fazer isso diretamente). Precisaríamos de algum tipo de API no nível do buffer para anunciar essas coisas e seu tamanho e o renderizador poderia fazer o trabalho sujo. Acho que devemos discutir / rastrear isso com um problema separado.
Obrigado pela sua resposta detalhada.
_ "Surgem problemas quando se trata de redimensionar, basicamente significa reconstruir todo o pedaço de memória (copiar) ou copiar rapidamente em um pedaço maior e realinhar partes." _
Você quer dizer: ao redimensionar teríamos que copiar _4 * N_ elementos ao invés de apenas _N_ elementos?
Pode fazer sentido que a matriz contenha todas as células de linhas lógicas (desembrulhadas). Por exemplo, suponha uma linha de 180 caracteres e um terminal de 80 colunas. Nesse caso, você poderia ter 3 BufferLine
instâncias, todas compartilhando o mesmo _4 * 180_-element _data
buffer, mas cada BufferLine
também conteria um deslocamento inicial.
Bem, eu tinha tudo em um grande array que foi construído por [cols] x [rows] x [needed single cell space]
. Basicamente, ainda funcionava como "tela" com uma determinada altura e largura. Isso é realmente eficiente em termos de memória e rápido para o fluxo de entrada normal, mas assim que insertCell
/ deleteCell
é invocado (um redimensionamento faria isso), toda a memória atrás da posição onde a ação ocorre teria que ser mudado. Para um pequeno scrollback (<10k), isso nem mesmo é um problema, é realmente um obstáculo para> 100k linhas.
Observe que o atual array digitado impl ainda precisa fazer essas mudanças, mas menos tóxico, pois ele só precisa mover o conteúdo da memória até o final da linha.
Pensei em layouts diferentes para contornar as mudanças dispendiosas, o campo principal para economizar mudanças de memória absurdas seria realmente separar o scrollback das "linhas de terminal quentes" (as mais recentes até terminal.rows
), uma vez que apenas essas podem ser alterado por saltos do cursor e inserções / exclusões.
Compartilhar a memória subjacente por vários objetos de linha de buffer é uma ideia interessante para resolver o problema de empacotamento. Ainda não tenho certeza de como isso pode funcionar de forma confiável sem a introdução de manipulação de referência explícita e tal. Em outra versão, tentei fazer tudo com manipulação de memória explícita, mas o contador ref foi um verdadeiro obstáculo e parecia errado na terra do GC. (veja # 1633 para os primitivos)
Edit: Btw a manipulação de memória explícita estava no mesmo nível da abordagem atual de "memória por linha", eu esperava um melhor desempenho devido à melhor localidade do cache, acho que foi comido por um pouco mais de exp. manipulação de mem na abstração JS.
O suporte para nós DOM anexados às células parece muito interessante, gosto dessa ideia. Atualmente isso não é possível em uma abordagem direta, uma vez que temos diferentes back-ends de renderização (DOM, canvas 2D e o novo renderizador webgl brilhante), acho que isso ainda poderia ser alcançado para todos os renderizadores posicionando uma sobreposição onde não fosse nativamente compatível (apenas o renderizador DOM faria ser capaz de fazer isso diretamente).
É um pouco fora do assunto, mas vejo que eventualmente temos nós DOM associados a células dentro da janela de visualização, que agirão de forma semelhante às camadas de renderização da tela. Dessa forma, os consumidores poderão "decorar" células usando HTML e CSS e não precisarão entrar na API do canvas.
Pode fazer sentido que a matriz contenha todas as células de linhas lógicas (desembrulhadas). Por exemplo, suponha uma linha de 180 caracteres e um terminal de 80 colunas. Nesse caso, você poderia ter 3 instâncias de BufferLine, todas compartilhando o mesmo buffer _data de 4 * 180 elementos, mas cada BufferLine também conteria um deslocamento inicial.
O plano de refluxo que foi mencionado acima é capturado em https://github.com/xtermjs/xterm.js/issues/622#issuecomment -375403572, basicamente, queremos ter o buffer real desembrulhado e, em seguida, uma visão no topo que gerencie as novas linhas para acesso rápido a qualquer linha (otimizando também para redimensionamentos horizontais).
Usar a abordagem de matriz densa pode ser algo que poderíamos analisar, mas parece que não valeria a pena a sobrecarga extra no gerenciamento das quebras de linha não encapsuladas em tal matriz, e a confusão que surge quando as linhas são aparadas a partir do topo o buffer de rolagem. Independentemente disso, não acho que devemos examinar essas mudanças até que # 791 esteja pronto e estamos examinando # 622.
Com o PR # 1796, o analisador obtém suporte a array tipado, o que abre a porta para otimizações futuras do servidor, por outro lado, também para outras codificações de entrada.
Por enquanto eu decidi ir com Uint16Array
, já que é fácil para frente e para trás conversível com strings JS. Isso basicamente limita o jogo a UCS2 / UTF16, enquanto o analisador na versão atual também pode lidar com UTF32 (UTF8 não é compatível). O buffer de terminal baseado em matriz digitada está atualmente planejado para UTF32, a conversão UTF16 -> UTF32 é feita em InputHandler.print
. A partir daqui, existem várias direções possíveis:
wcwidth
UTF16 compatívelSobre UTF8: O analisador atualmente não pode lidar com sequências UTF8 nativas, principalmente devido ao fato de que os caracteres intermediários entram em conflito com os caracteres de controle C1. Além disso, o UTF8 precisa de tratamento de fluxo adequado com estados intermediários adicionais, isso é desagradável e imho não deve ser adicionado ao analisador. O UTF8 será melhor tratado com antecedência, e talvez com um direito de conversão para UTF32 para facilitar o manuseio de pontos de código em todo o lugar.
Em relação a uma possível codificação de entrada UTF8 e o layout do buffer interno, fiz um teste grosseiro. Para descartar o impacto muito maior do renderizador de tela no tempo de execução total, fiz isso com o próximo renderizador webgl. Com meu benchmark ls -lR /usr/lib
, obtenho os seguintes resultados:
mestre atual + renderizador webgl:
parque infantil branch, aplica-se # 1796, partes de # 1811 e renderizador webgl:
A ramificação do playground faz uma conversão inicial de UTF8 para UTF32 antes de fazer a análise e o armazenamento (a conversão adiciona cerca de 30 ms). O aumento de velocidade é obtido principalmente pelas 2 funções ativas durante o fluxo de entrada, EscapeSequenceParser.parse
(120 ms x 35 ms) e InputHandler.print
(350 ms x 75 ms). Ambos se beneficiam muito da troca de array digitada, salvando .charCodeAt
chamadas.
Eu também comparei esses resultados com uma matriz de tipo intermediário UTF16 - EscapeSequenceParser.parse
é um pouco mais rápido (~ 25 ms), mas InputHandler.print
fica para trás devido ao emparelhamento substituto necessário e procura de ponto de código em wcwidth
(120 ms).
Observe também que já estou no limite, o sistema pode fornecer os dados ls
(i7 com SSD) - a aceleração ganha adiciona tempo ocioso em vez de tornar a execução mais rápida.
Resumo:
Imho, o tratamento de entrada mais rápido que podemos obter é uma mistura de transporte UTF8 + UTF32 para representação de buffer. Enquanto o transporte UTF8 tem a melhor taxa de pacote de bytes para entrada típica de terminal e remove conversões sem sentido de pty através de várias camadas de buffers até Terminal.write
, o buffer baseado em UTF32 pode armazenar os dados muito rápido. O último vem com uma pegada de memória um pouco maior do que o UTF16, enquanto o UTF16 é um pouco mais lento devido ao tratamento de char mais complicado com mais vias indiretas.
Conclusão:
Devemos usar o layout de buffer baseado em UTF32 por enquanto. Devemos também considerar a mudança para a codificação de entrada UTF8, mas ainda é necessário pensar um pouco mais sobre as mudanças e implicações da API para integradores (parece que o mecanismo ipc do electron não consegue lidar com dados binários sem codificação BASE64 e encapsulamento JSON, o que neutralizaria os esforços de perf).
Layout de buffer para o próximo suporte true color:
Atualmente, o layout de buffer baseado em matriz digitada é o seguinte (uma célula):
| uint32_t | uint32_t | uint32_t |
| attrs | codepoint | wcwidth |
onde attrs
contém todos os sinalizadores necessários + cores FG e BG baseadas em 9 bits. codepoint
usa 21 bits (máx. É 0x10FFFF para UTF32) + 1 bit para indicar a combinação de caracteres e wcwidth
2 bits (varia de 0-2).
A ideia é reorganizar os bits para uma taxa de embalagem melhor para abrir espaço para os valores RGB adicionais:
wcwidth
em bits altos não utilizados de codepoint
| uint32_t | uint32_t | uint32_t |
| content | FG | BG |
| comb(1) wcwidth(2) codepoint(21) | flags(8) R(8) G(8) B(8) | flags(8) R(8) G(8) B(8) |
O lado positivo desta abordagem é o acesso relativamente barato a cada valor por um acesso de índice e máx. Operações de 2 bits (e / ou + shift).
A pegada de memória é estável para a variante atual, mas ainda bastante alta com 12 bytes por célula. Isso poderia ser otimizado ainda mais sacrificando algum tempo de execução com a mudança para UTF16 e uma indireção attr
:
| uint16_t | uint16_t |
| BMP codepoint(16) | comb(1) wcwidth(2) attr pointer(13) |
Agora estamos reduzidos a 4 bytes por célula + algum espaço para os atributos. Agora, os atributos também podem ser reciclados para outras células. Oba missão cumprida! - Ehm, um segundo ...
Comparando a pegada de memória, a segunda abordagem vence claramente. Não é assim para o tempo de execução, existem três fatores principais que aumentam muito o tempo de execução:
A sensualidade da segunda abordagem é a economia adicional de memória. Portanto, testei-o com o branch playground (veja o comentário acima) com uma implementação BufferLine
modificada:
Sim, estamos de volta ao ponto de partida antes de mudar para matrizes digitadas em UTF8 + no analisador. O uso de memória caiu de ~ 1,5 MB para ~ 0,7 MB (aplicativo de demonstração com 87 células e 1000 linhas de rolagem para trás).
Daqui em diante é uma questão de economizar memória vs. velocidade. Como já salvamos muita memória mudando de arrays js para arrays digitados (caiu de ~ 5,6 MB para ~ 1,5 MB para o heap C ++, cortando o comportamento tóxico do JS Heap e GC), acho que devemos ir aqui com a variante mais rápida. Uma vez que o uso de memória torna-se um problema urgente novamente, ainda podemos mudar para um layout de buffer mais compacto, conforme descrito na segunda abordagem aqui.
Eu concordo, vamos otimizar a velocidade, desde que o consumo de memória não seja uma preocupação. Eu também gostaria de evitar o engano na medida do possível porque torna o código mais difícil de ler e manter. Já temos muitos conceitos e ajustes em nossa base de código que tornam difícil para as pessoas (incluindo eu 😅) seguir o fluxo do código - e trazer mais desses sempre deve ser justificado por um bom motivo. IMO salvar outro megabyte de memória não justifica isso.
No entanto, estou gostando muito de ler e aprender com seus exercícios, obrigado por compartilhá-los com tantos detalhes!
@mofux Sim, é verdade - a complexidade do código é muito maior (UTF16 substituto para leitura antecipada, cálculos de ponto de código intermediário, contêiner de árvore com ref contando com entradas de atributo).
E uma vez que o layout de 32 bits é principalmente de memória plana (apenas a combinação de caracteres precisa de indireção), há mais otimizações possíveis (também parte do # 1811, ainda não testado para o renderizador).
Há uma grande vantagem de direcionar para um objeto de atração: é muito mais extensível. Você pode adicionar anotações, glifos ou regras de pintura personalizadas. Você pode armazenar informações de links de uma forma possivelmente mais limpa e eficiente. Talvez defina uma interface ICellPainter
que saiba como renderizar sua célula e na qual você também possa pendurar propriedades personalizadas.
Uma ideia é usar dois arrays por BufferLine: um Uint32Array e um array ICellPainter, com um elemento cada para cada célula. O ICellPainter atual é uma propriedade do estado do analisador e, portanto, você apenas reutiliza o mesmo ICellPainter, desde que o estado da cor / atributo não mude. Se você precisar adicionar propriedades especiais a uma célula, primeiro clone o ICellPainter (se ele puder ser compartilhado).
Você pode pré-alocar ICellPainter para as combinações de cores / atributos mais comuns - no mínimo, ter um objeto exclusivo correspondente às cores / atributos padrão.
As mudanças de estilo (como a mudança das cores padrão de primeiro plano / fundo) podem ser implementadas apenas atualizando a (s) instância (s) ICellPainter correspondente (s), sem ter que atualizar cada célula.
Existem otimizações possíveis: Por exemplo, use diferentes instâncias de ICellPainter para caracteres de largura simples e dupla (ou caracteres de largura zero). (Isso economiza 2 bits em cada elemento Uint32Array.) Existem 11 bits de atributo disponíveis em Uint32Array (mais se otimizarmos para caracteres BMP). Eles podem ser usados para codificar as combinações de cores / atributos mais comuns / úteis, que podem ser usadas para indexar as instâncias ICellPainter mais comuns. Nesse caso, o array ICellPainter pode ser alocado vagarosamente - ou seja, apenas se alguma célula na linha exigir um ICellPainter "menos comum".
Também é possível remover o array _combined para caracteres não BMP e armazená-los no ICellPainter. (Isso requer um ICellPainter exclusivo para cada caractere não BMP, portanto, há uma compensação aqui.)
@PerBothner Sim, uma indireção é mais versátil e, portanto, mais adequada para extras incomuns. Mas como eles são incomuns, eu gostaria de não otimizar para eles em primeiro lugar.
Algumas notas sobre o que tentei em vários testbeds:
codepoint
tinha o conjunto de bits combinado (o acesso foi mais rápido aqui devido à melhor localidade do cache, testado com valgrind). Transferido para JS, o aumento de velocidade não foi tão grande devido à string necessária para conversão de número (embora ainda mais rápida), mas a economia de mem foi ainda maior (suponha que devido a algum espaço de gerenciamento adicional para tipos JS). O problema era o StringStorage
global para o material combinado com o gerenciamento de memória explícito, um grande antipadrão em JS. Uma solução rápida para isso foi o objeto _combined
, que delega a limpeza ao GC. Ainda está sujeito a alterações e, aliás, destina-se a armazenar conteúdo de string relacionado à célula arbitrária (fizemos isso com os grafemas em mente, mas não os veremos em breve, pois não são suportados por nenhum backend). Portanto, este é o lugar para armazenar conteúdo de string adicional em uma base de célula por célula.AttributeStorage
global para todos os atributos já usados em todas as instâncias de terminal (consulte https://github.com/jerch/xterm.js/tree/AttributeStorage). Em termos de memória, isso funcionou muito bem, principalmente porque as pessoas usam apenas um pequeno conjunto de atributos, mesmo com suporte a true color. O desempenho não foi tão bom - principalmente devido à contagem de referências (cada célula teve que dar uma espiada nessa memória estrangeira duas vezes) e à correspondência de atributos. E quando tentei adotar a coisa ref para JS, parecia errado - o ponto que pressionei o botão "STOP". No meio disso, descobrimos que já salvamos toneladas de memória e chamadas de GC ao alternar para o array digitado, portanto, o layout de memória plana um pouco mais caro pode pagar sua vantagem de velocidade aqui.Agora, esse layout plano de 32 bits acabou sendo otimizado para as coisas comuns e extras incomuns não são possíveis com ele. Verdade. Bem, ainda temos marcadores (não estão acostumados com eles, então não posso dizer agora do que eles são capazes), e sim - ainda existem bits livres no buffer (o que é uma boa coisa para necessidades futuras, por exemplo, podemos usá-los como sinalizadores para tratamento especial e outros).
Tbh para mim, é uma pena que o layout de 16 bits com armazenamento Attrs tenha um desempenho tão ruim, reduzir o uso de memória pela metade ainda é um grande negócio (especialmente quando o ppl começa a usar linhas de rolagem> 10k), mas a penalidade de tempo de execução e a complexidade do código superam o membro superior precisa de atm imho.
Você pode explicar melhor a ideia do ICellPainter? Talvez eu tenha perdido algum recurso crucial até agora.
Meu objetivo com o DomTerm era habilitar e encorajar uma interação mais rica, exatamente o que é habilitado por um emulador de terminal tradicional. Usar tecnologias da web permite muitas coisas interessantes, então seria uma pena focar apenas em ser um emulador de terminal tradicional rápido. Especialmente porque muitos casos de uso para xterm.js (como REPLs para IDEs) podem realmente se beneficiar indo além do texto simples. O Xterm.js se sai bem no lado da velocidade (alguém está reclamando da velocidade?), Mas não se sai tão bem nos recursos (as pessoas estão reclamando da falta de truecolor e gráficos incorporados, por exemplo). Acho que pode valer a pena focar um pouco mais na flexibilidade e menos no desempenho.
_ "Você pode explicar melhor a ideia do ICellPainter?" _
Em geral, ICellPainter encapsula todos os dados por célula, exceto o código / valor do caractere, que vem do Uint32Array. Isso é para células de caracteres "normais" - para imagens incorporadas e outras "caixas", o código / valor do caractere pode não fazer 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;
}
O mapeamento de uma célula para ICellPainter pode ser feito de várias maneiras. O óbvio é que cada BufferLine tenha uma matriz ICellPainter, mas isso requer um ponteiro de 8 bytes (pelo menos) por célula. Uma possibilidade é combinar o array _combined com o array ICellPainter: Se o IS_COMBINED_BIT_MASK for definido, o ICellPainter também incluirá a string combinada. Outra otimização possível é usar os bits disponíveis no Uint32Array como um índice em uma matriz: Isso adiciona alguma complicação extra e indireção, mas economiza espaço.
Eu gostaria de nos encorajar a verificar se podemos fazer da maneira que o monaco-editor faz (acho que eles encontraram uma maneira realmente inteligente e de desempenho). Em vez de armazenar essas informações no buffer, eles permitem que você crie decorations
. Você cria uma decoração para um intervalo de linha / coluna e ela se manterá nesse intervalo:
// 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 }
});
Mais tarde, um renderizador pode pegar essas decorações e desenhá-las.
Por favor, verifique este pequeno exemplo que mostra como a api monaco-editor se parece:
https://microsoft.github.io/monaco-editor/playground.html#interacting -with-the-editor-line-and-inline-decorations
Para coisas como renderizar imagens dentro do terminal, o monaco usa um conceito de zonas de visualização que podem ser vistas (entre outros conceitos) em um exemplo aqui:
https://microsoft.github.io/monaco-editor/playground.html#interacting -with-the-editor-listening-to-mouse-events
@PerBothner Thx para esclarecimento e esboço. Algumas notas sobre isso.
Eventualmente, planejamos mover a cadeia de entrada + buffer para um webworker no futuro. Portanto, o buffer deve operar em um nível abstrato e não podemos usar qualquer coisa relacionada a renderização / representação lá, como métricas de pixel ou quaisquer nós DOM. Vejo suas necessidades para isso devido ao DomTerm ser altamente personalizável, mas acho que devemos fazer isso com uma API de marcador interno aprimorada e podemos aprender aqui com monaco / vscode (obrigado pelos ponteiros @mofux).
Eu realmente gostaria de manter o buffer do núcleo livre de coisas incomuns, talvez devêssemos discutir possíveis estratégias de marcador com um novo problema.
Ainda não estou satisfeito com o resultado dos resultados do teste de layout de 16 bits. Uma vez que uma decisão final ainda não é urgente (não veremos nada disso antes de 3.11), vou continuar testando-o com algumas mudanças (ainda é a solução mais intruigante para mim do que a 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) |
Eu também acho que devemos ir com algo parecido com isso para começar, podemos explorar outras opções mais tarde, mas provavelmente será a mais fácil de colocar em funcionamento. A indireção de atributos definitivamente tem uma promessa IMO, já que normalmente não há muitos atributos distintos em uma sessão de terminal.
Eu gostaria de nos encorajar a verificar se podemos fazer da maneira que o monaco-editor faz (acho que eles encontraram uma maneira realmente inteligente e de desempenho). Em vez de armazenar essas informações no buffer, eles permitem que você crie decorações. Você cria uma decoração para um intervalo de linha / coluna e ela se manterá nesse intervalo:
É algo assim que eu gostaria de ver as coisas irem. Uma ideia que tive ao longo dessas linhas era permitir que incorporadores anexassem elementos DOM a intervalos para permitir que coisas personalizadas fossem desenhadas. Existem 3 coisas em que posso pensar no momento que gostaria de realizar com isso:
Tudo isso pode ser alcançado com uma sobreposição e é um tipo de API bastante acessível (expondo um nó DOM) e pode funcionar independentemente do tipo de renderizador.
Não tenho certeza se queremos entrar no negócio de permitir que os incorporadores alterem a forma como as cores de fundo e de primeiro plano são desenhadas.
@jerch Vou colocar isso no marco 3.11.0, pois considero esse problema encerrado quando removemos a implementação do array JS que está planejada para então. https://github.com/xtermjs/xterm.js/pull/1796 também está planejado para ser mesclado, mas esse problema sempre teve como objetivo melhorar o layout da memória do buffer.
Além disso, muito dessa discussão posterior provavelmente seria melhor tratada em https://github.com/xtermjs/xterm.js/issues/1852 (criado porque não havia um problema de decoração).
@Tyriar Woot - finalmente fechado: sweat_smile:
🎉 🕺 🍾
Comentários muito úteis
Estado atual:
Depois de: