Xterm.js: Melhorias no desempenho do buffer

Criado em 13 jul. 2017  ·  73Comentários  ·  Fonte: xtermjs/xterm.js

Problema

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

1. Solução simples

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

  • Muito simples

Contras

  • Muita memória consumida quase dobraria nosso uso de memória atual, que já é muito alto.

2. Retire o texto de CharData

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

  • Não há necessidade de reconstruir a linha sempre que precisarmos.
  • Memória mais baixa do que hoje devido ao uso de um Int32Array

Contras

  • Lento para atualizar caracteres individuais, a string inteira precisaria ser regenerada para alterações de caractere único.

3. Armazene atributos em intervalos

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

  • Memória mais baixa do que hoje, embora também estejamos armazenando dados truecolor
  • Pode otimizar a aplicação de atributos, em vez de verificar cada atributo do personagem e diferenciá-lo do anterior
  • Encapsula a complexidade de armazenar os dados dentro de uma matriz ( .flags vez de [0] )

Contras

  • Alterar os atributos de um intervalo de caracteres dentro de outro intervalo é mais complexo

4. Coloque os atributos em um cache

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

  • Uso de memória semelhante ao de hoje, embora também estejamos armazenando dados truecolor
  • Encapsula a complexidade de armazenar os dados dentro de uma matriz ( .flags vez de [0] )

Contras

  • Menos economia de memória do que a abordagem de intervalos

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

Prós

  • Protegidamente o mais rápido e mais eficiente em termos de memória
  • Muito eficiente em termos de memória quando o buffer contém muitos blocos com estilos, mas apenas de alguns estilos (o caso comum)
  • Encapsula a complexidade de armazenar os dados dentro de uma matriz ( .flags vez de [0] )

Contras

  • Mais complexo do que as outras soluções, pode não valer a pena incluir o cache se já mantivermos um único CharAttributes por bloco?
  • Sobrecarga extra no objeto CharAttributeEntry
  • Alterar os atributos de um intervalo de caracteres dentro de outro intervalo é mais complexo

6. Híbrido de 2 e 3

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

  • Memória mais baixa do que hoje, embora também estejamos armazenando dados truecolor
  • Pode otimizar a aplicação de atributos, em vez de verificar cada atributo do personagem e diferenciá-lo do anterior
  • Encapsula a complexidade de armazenar os dados dentro de uma matriz ( .flags vez de [0] )
  • Acesso mais rápido à string de linha real

Contras

  • Memória extra devido a pendurar em strings de linha
  • Alterar os atributos de um intervalo de caracteres dentro de outro intervalo é mais complexo

Soluções que não funcionam

  • Armazenar a string como um int dentro de Int32Array não funcionará, pois leva muito tempo para converter o int de volta em um caractere.
areperformance typplan typproposal

Comentários muito úteis

Estado atual:

Depois de:

Todos 73 comentários

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:

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

À 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:

  • eles são um pouco piores do que 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 []
  • eles valem a pena várias vezes se você puder pré-alocar e reutilizar a estrutura de dados (até 10 vezes), é aqui que a natureza da lista vinculada de matrizes misturadas prejudica o desempenho devido à alocação pesada e gc nos bastidores
  • para dados de string, a conversão para frente e para trás consome todos os benefícios - uma pena que JS não fornece string nativa para conversores Uint16Array (embora parcialmente factível 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 terminal
  • salve essas strings em uma lista de ponteiros simples sem qualquer alteração (sem string aloc ou gc, lista alocada pode ser evitada se usada com uma estrutura pré-alocada) junto com os atributos atuais
  • avançar o cursor em wcwidth(string) % cols
  • caso especial \n (quebra de linha): avança o cursor em uma linha, marca a posição na lista de ponteiros como quebra de linha
  • caso especial de estouro de linha com wrapAround: marca a posição na string como uma quebra de linha suave
  • caso especial \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
  • fluxos de dados como acima, apesar do caso \r , nenhuma abstração de célula ou divisão de string é necessária
  • alterações de atributo não são problema, desde que ninguém solicite a representação real cols 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 :

  • mover cols para trás nas quebras de linha - o início do conteúdo do terminal atual
  • cada quebra de linha denota uma linha terminal real
  • carregar coisas no modelo de célula por apenas uma página (ainda não tenho certeza se isso também pode ser omitido com um posicionamento inteligente de string)
  • faça o trabalho desagradável: se alterações de atributo forem solicitadas, estamos meio sem sorte e temos que voltar para o modelo de célula completa ou um modelo de divisão e inserção de string (o último pode apresentar um desempenho ruim)
  • os dados fluem novamente, agora com strings degradadas e dados de atributos no buffer dessa página

Agora a parte perigosa 2 - o renderizador quer desenhar algo:

  • meio que depende do renderizador se precisarmos detalhar até um modelo de célula ou se pudermos apenas fornecer os deslocamentos de string com quebras de linha e atributos de texto

Prós:

  • fluxo de dados muito rápido
  • otimizado para o método mais comum InputHandler - print
  • torna possível o refluxo das linhas após o redimensionamento do terminal

Contras:

  • quase todos os outros métodos InputHandler serão perigosos no sentido de interromper este modelo de fluxo e a necessidade de alguma abstração de célula intermediária
  • integração do renderizador obscura (para mim, pelo menos, atm)
  • pode degradar o desempenho de maldições como aplicativos (geralmente contêm sequências mais "perigosas")

Bem, 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):

  • UTF-8 (Uint8Array): print ação salta de 190 MB / s para 290 MB / s
  • UTF-16 (Uint16Array): print ação salta de 190 MB / s para 320 MB / s

No 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:
grafik

com Uint16Array:
grafik

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:

  1. Crie sua própria memória virtual baseada em arraybuffer, algo grande (> 5 MB)
    Se o arraybuffer tem um comprimento de múltiplos de 4 switches transparentes de 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.
  2. Escreva strings de entrada na memória como uint16_t type para UTF-16.
  3. O analisador é executado nos ponteiros de string e chama métodos em InputHandler com ponteiros para esta memória em vez de fatias de string.
  4. Crie o buffer de dados do terminal dentro da memória virtual como uma matriz de buffer em anel de um tipo struct em vez de objetos JS nativos, talvez assim (ainda baseado em células):
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:

  • omite objetos JS e, portanto, GC onde possível (apenas alguns objetos locais permanecerão)
  • apenas uma cópia de dados inicial na memória virtual necessária
  • quase nenhum custo de malloc e free (depende da inteligência do alocador / desalocador)
  • vai economizar muita memória (evita a sobrecarga de memória de objetos JS)

Contras:

  • Bem-vindo ao Cavascript Horror Show: scream:
  • difícil de implementar, muda meio de tudo
  • o benefício da velocidade não está claro até que seja realmente implementado

: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

  • Fazemos a codificação / decodificação no início do carregamento de um arquivo, então os arquivos são convertidos em string JS. O V8 decide se deve usar um ou dois bytes para armazenar um caractere.
  • Fazemos edições no buffer de texto com frequência, as strings são mais fáceis de manusear.
  • Estamos usando o módulo nativo nodejs e temos acesso aos componentes internos do V8 quando necessário.

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:

  • FlatJS (https://github.com/lars-t-hansen/flatjs) - meta linguagem para ajudar na codificação com heaps baseados em arraybuffer
  • http://2ality.com/2017/01/shared-array-buffer.html (anunciado como parte do ES2017, o futuro pode ser incerto devido a Spectre, além dessa ideia muito promissora com simultaneidade real e atômica real)
  • webassembly / asm.js (estado atual? utilizável ainda? Não acompanhou seu desenvolvimento por algum tempo, usou emscripten para asm.js anos atrás com um C lib para um jogo de IA com resultados impressionantes)
  • https://github.com/AssemblyScript/assemblyscript

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

  • PR # 1528: atlas de atributos
  • PR # 1529: remover wcwidth e charCode do buffer
  • PR # 1530: substitui a string no buffer por pontos de código / valor de índice de armazenamento de célula

@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:

  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.
  2. Construa 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.
  3. Reduza o CharData atual de [number, string, number, number] para [number, number] , onde os números são ponteiros (números de índice) para:

    • AttributeCache entrada

    • StringStorage 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).

  1. Mova o encolhido 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:

    • 80% menos espaço de memória do buffer do terminal (de 5,5 MB a 0,75 MB)

    • um pouco mais rápido (ainda não testável, espero ganhar de 20% a 30% da velocidade)

    • Editar: muito mais rápido - o tempo de execução do script para 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 1s

Desvantagem - 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:

  • vazio : estado inicial da célula, nada foi gravado ainda ou o conteúdo foi excluído. Tem uma largura de 1, mas nenhum conteúdo. Atualmente usado em blankLine e eraseChar , mas com um espaço como conteúdo.
  • null : célula após um caractere de largura total para indicar que não tem largura para representação visual.
  • normal : a célula contém algum conteúdo e tem uma largura visual (1 ou 2, talvez maior, uma vez que suportamos grafemas / bidis reais, não tenho certeza sobre isso ainda lol)

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:

  1. 1 + 2 + 3 em CircularList : economiza 50% (~ 2,8 MB de ~ 5,5 MB)
  2. 1 + 2 + 3 + 4 no meio do caminho - basta colocar os dados da linha em uma matriz digitada, mas manter o acesso ao índice de linha: economiza 82% (~ 0,9 MB)
  3. 1 + 2 + 3 + 4 matriz totalmente densa com aritmética de ponteiro: economiza 87% (~ 0,7 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:

  • O tratamento de dados dentro da parte wasm é ligeiramente mais rápido (5 - 10%).
  • Chamadas de JS para wasm criam alguma sobrecarga e corroem todos os benefícios acima. Na verdade, foi cerca de 20% mais lento.
  • O "binário" será menor do que a contraparte JS (não realmente medido, pois não implementei todas as coisas).
  • Para obter a transição JS <--> wasm feita facilmente, algum bloatcode é necessário para lidar com os tipos JS (apenas fez a tradução da string).
  • Não podemos evitar a tradução de JS para wasm, pois o DOM do navegador e os eventos não podem ser acessados ​​lá. Ele só poderia ser usado para peças centrais, que não são mais críticas para o desempenho (além do consumo de memória).

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:

  1. SGR 39 + SGR 49 cor padrão para fg / bg (personalizável)
  2. SGR 30-37 + SGR 40-47 8 paleta de cores baixas para fg / bg (personalizável)
  3. SGR 90-97 + SGR 100-107 8 paleta de cores altas para fg / bg (personalizável)
  4. SGR 38;5;n + SGR 48;5;n 256 paleta indexada para fg / bg (personalizável)
  5. 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:

  • faça todos UTF16, portanto, transforme o buffer do terminal em UTF16 também
    Sim, ainda não decidi qual caminho seguir aqui, mas testei vários layouts de buffer e cheguei à conclusão, que um número de 32 bits dá espaço suficiente para armazenar o charcode real + wcwidth + possível combinação de estouro (tratado totalmente diferente), enquanto 16 bits não pode fazer isso sem sacrificar bits preciosos do charcode. Observe que mesmo com um buffer UTF16 ainda temos que fazer a conversão UTF32, já que wcwidth funciona em pontos de código Unicode. Observe também que o buffer baseado em UTF16 economizaria mais memória para charcodes mais baixos, de fato, códigos de charcodes maiores do que BMP plane raramente ocorrerão. Isso ainda precisa de alguma investigação.
  • make parser UTF32
    Isso é muito simples, basta substituir todas as matrizes digitadas pela variante de 32 bits. Desvantagem - a conversão de UTF16 em UTF32 teria que ser feita de antemão, significa que toda a entrada é convertida, mesmo as sequências de escape que nunca serão formadas por qualquer charcode> 255.
  • tornar wcwidth UTF16 compatível
    Sim, se o UTF16 for mais adequado para o buffer do terminal, isso deve ser feito.

Sobre 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:
    grafik

  • parque infantil branch, aplica-se # 1796, partes de # 1811 e renderizador webgl:
    grafik

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:

  • coloque wcwidth em bits altos não utilizados de codepoint
  • dividir os atributos em um grupo FG e BG com 32 bits, distribuir sinalizadores em bits não 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) |

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:

  • atrito indireto
    O ponteiro de atrito precisa de uma pesquisa de memória adicional em outro contêiner de dados.
  • Attr Matching
    Para realmente economizar espaço com a segunda abordagem, os atributos dados teriam que ser comparados aos atributos já salvos. Esta é uma ação complicada, uma abordagem direta, simplesmente olhando através de todos os valores existentes em O (n) para n atributos salvos, meus experimentos de árvore RB terminaram em quase nenhum benefício de memória enquanto ainda estavam em O (log n), em comparação com o índice de acesso na abordagem de 32 bits com O (1). Além disso, uma árvore tem um tempo de execução pior para poucos elementos salvos (compensa um pouco em torno de> 100 entradas com minha árvore RB impl).
  • UTF16 surrogate pairing
    Com uma matriz digitada de 16 bits, temos que degradar para UTF16 para os pontos de código, o que também introduziu penalidade de tempo de execução (conforme descrito no comentário acima). Observe que pontos de código mais altos do que BMP dificilmente ocorrem, mas a verificação isolada se um ponto de código formaria um par substituto adiciona ~ 50 ms.

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:

grafik

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:

  • conteúdo de string de célula
    Vindo do C ++, tentei olhar para o problema como faria no C ++, então comecei com dicas para o conteúdo. Este era um ponteiro de string simples, mas apontando na maioria das vezes para uma única string char. Que desperdício. Minha primeira otimização, portanto, foi me livrar da abstração da string salvando diretamente o ponto de código em vez do endereço (muito mais fácil em C / C ++ do que em JS). Isso quase dobrou o acesso de leitura / gravação ao salvar 12 bytes por célula (ponteiro de 8 bytes + 4 bytes em string, 64 bits com wchar_t de 32 bits). Sidenote - metade do aumento de velocidade aqui está relacionado ao cache (falhas de cache devido a localizações de strings aleatórias). Isso ficou claro com minha solução alternativa para combinar o conteúdo da célula - um pedaço de memória no qual indexei quando 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.
  • atrs
    Com os atributos, comecei a "pensar grande" - com um 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.
    O que testei yday (último comentário) foi uma segunda matriz digitada em nível de linha para os atributos com a árvore de https://github.com/jerch/xterm.js/tree/AttributeStorage para a correspondência (muito semelhante à sua ideia ICellPainter ) Bem, os resultados não são promissores, portanto, me inclino para o layout plano de 32 bits por enquanto.

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:

  • Desenhe links sublinhados desta forma (simplificará significativamente a forma como são desenhados)
  • Permitir marcadores em linhas, como um * ou algo assim
  • Permitir que as linhas "pisquem" para indicar que algo aconteceu

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:

🎉 🕺 🍾

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

Tyriar picture Tyriar  ·  4Comentários

chris-tse picture chris-tse  ·  4Comentários

johnpoth picture johnpoth  ·  3Comentários

albinekb picture albinekb  ·  4Comentários

jestapinski picture jestapinski  ·  3Comentários