Xterm.js: Улучшения производительности буфера

Созданный на 13 июл. 2017  ·  73Комментарии  ·  Источник: xtermjs/xterm.js

Проблема

объем памяти

Прямо сейчас наш буфер занимает слишком много памяти, особенно для приложения, которое запускает несколько терминалов с установленной большой обратной прокруткой. Например, демонстрация с использованием терминала 160x24 с заполненной прокруткой 5000 занимает около 34 МБ памяти (см. Https://github.com/Microsoft/vscode/issues/29840#issuecomment-314539964), помните, что только один терминал и мониторы 1080p будут скорее всего, используйте более широкие клеммы. Кроме того, для поддержки истинного цвета (https://github.com/sourcelair/xterm.js/issues/484) каждый символ должен будет хранить 2 дополнительных типа number что почти удвоит текущее потребление памяти. буфера.

Медленное получение текста строки

Есть еще одна проблема, связанная с необходимостью быстрого получения фактического текста строки. Причина, по которой это происходит медленно, связана со способом размещения данных; строка содержит массив символов, каждый из которых имеет строку из одного символа. Итак, мы построим строку, и сразу после этого она будет отправлена ​​на сборку мусора. Раньше нам вообще не нужно было этого делать, потому что текст извлекается из строкового буфера (по порядку) и отображается в DOM. Однако это становится все более полезным, хотя по мере дальнейшего улучшения xterm.js такие функции, как выбор и ссылки, извлекают эти данные. Опять же, используя пример обратной прокрутки 160x24 / 5000, на Macbook Pro середины 2014 года для копирования всего буфера требуется 30-60 мс.

Поддерживая будущее

Другая потенциальная проблема в будущем заключается в том, что когда мы смотрим на введение некоторой модели представления, которая может нуждаться в дублировании некоторых или всех данных в буфере, такого рода вещи потребуются для реализации перекомпоновки (https://github.com/sourcelair /xterm.js/issues/622) должным образом (https://github.com/sourcelair/xterm.js/pull/644#issuecomment-298058556) и, возможно, также необходимо для правильной поддержки программ чтения с экрана (https://github.com /sourcelair/xterm.js/issues/731). Конечно, было бы хорошо иметь место для маневра, когда дело касается памяти.

Это обсуждение началось в https://github.com/sourcelair/xterm.js/issues/484 , здесь более подробно рассматриваются и предлагаются некоторые дополнительные решения.

Я склоняюсь к решению 3 и перехожу к решению 5, если есть время, и оно показывает заметное улучшение. Буду рад любым отзывам! / cc @jerch , @mofux , @rauchg , @parisk

1. Простое решение

В основном это то, что мы делаем сейчас, только с добавлением truecolor fg и bg.

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

type LineData = CharData[];

Плюсы

  • Очень простой

Минусы

  • Слишком много потребляемой памяти почти удвоило бы текущее использование памяти, которое и так уже слишком велико.

2. Вытяните текст из CharData.

Это будет хранить строку напротив строки, а не строки, это, вероятно, приведет к очень большому выигрышу в выборе и связывании и будет более полезным со временем, имея быстрый доступ ко всей строке строки.

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;

Плюсы

  • Нет необходимости реконструировать линию всякий раз, когда она нам нужна.
  • Меньше памяти, чем сегодня, из-за использования Int32Array

Минусы

  • Медленно обновлять отдельные символы, для изменения отдельных символов потребуется регенерировать всю строку.

3. Храните атрибуты в диапазонах.

Вытягивание атрибутов и связывание их с диапазоном. Поскольку никогда не может быть перекрывающихся атрибутов, их можно расположить последовательно.

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

Плюсы

  • Меньше памяти, чем сегодня, несмотря на то, что мы также храним данные truecolor
  • Может оптимизировать применение атрибутов вместо того, чтобы проверять атрибут каждого отдельного символа и отличать его от предыдущего.
  • Инкапсулирует сложность хранения данных внутри массива ( .flags вместо [0] )

Минусы

  • Изменение атрибутов диапазона символов внутри другого диапазона более сложное

4. Поместите атрибуты в кеш.

Идея здесь состоит в том, чтобы использовать тот факт, что обычно в одном сеансе терминала не так много стилей, поэтому мы не должны создавать столько, сколько необходимо, и повторно использовать их.

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

Плюсы

  • Использование памяти аналогично сегодняшнему, хотя мы также храним данные truecolor
  • Инкапсулирует сложность хранения данных внутри массива ( .flags вместо [0] )

Минусы

  • Экономия памяти меньше, чем у диапазонов

5. Гибрид 3 и 4

type LineData = CharData[]

// [0]: The character
// [1]: The width
type CharData = [string, number];

class CharAttributes {
  private _data: Int32Array;

  // Getters pull data from _data (woo encapsulation!)
  public get flags(): number;
  public get truecolorBg(): number;
  public get truecolorFg(): number;
}

interface CharAttributeEntry {
  attributes: CharAttributes,
  start: [number, number],
  end: [number, number]
}

class Buffer extends CircularList<LineData> {
  // Sorted list since items are almost always pushed to end
  private _attributes: CharAttributeEntry[];
  private _attributeCache: ICharAttributeCache;

  public getAttributesForRows(start: number, end: number): CharAttributeEntry[] {
    // Binary search _attributes and return all visible CharAttributeEntry's to
    // be applied by the renderer
  }
}

interface ICharAttributeCache {
  // Never construct duplicate CharAttributes, figuring how the best way to
  // access both in the best and worst case is the tricky part here
  getAttributes(flags: number, fg: number, bg: number): CharAttributes;
}

Плюсы

  • Практически самый быстрый и эффективный с точки зрения памяти
  • Очень эффективен с точки зрения памяти, когда буфер содержит много блоков со стилями, но только из нескольких стилей (общий случай)
  • Инкапсулирует сложность хранения данных внутри массива ( .flags вместо [0] )

Минусы

  • Более сложные, чем другие решения, возможно, не стоит включать кеш, если мы уже храним один CharAttributes на блок?
  • Дополнительные накладные расходы в объекте CharAttributeEntry
  • Изменение атрибутов диапазона символов внутри другого диапазона более сложное

6. Гибрид 2 и 3

Это принимает решение 3, но также добавляет ленивую текстовую строку для быстрого доступа к тексту строки. Поскольку мы также сохраняем символы в CharData мы можем лениво вычислить его.

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

Плюсы

  • Меньше памяти, чем сегодня, несмотря на то, что мы также храним данные truecolor
  • Может оптимизировать применение атрибутов вместо того, чтобы проверять атрибут каждого отдельного символа и отличать его от предыдущего.
  • Инкапсулирует сложность хранения данных внутри массива ( .flags вместо [0] )
  • Более быстрый доступ к фактической строке строки

Минусы

  • Дополнительная память из-за подвешивания на линейных строках
  • Изменение атрибутов диапазона символов внутри другого диапазона более сложное

Решения, которые не работают

  • Сохранение строки как int внутри Int32Array не сработает, так как преобразование int обратно в символ занимает очень много времени.
areperformance typplan typproposal

Самый полезный комментарий

Текущее состояние:

После:

Все 73 Комментарий

Другой подход, который можно смешивать: использовать indexeddb, websql или api файловой системы для вывода неактивных записей обратной прокрутки на диск 🤔

Отличное предложение. Я согласен с тем, что 3. - это лучший вариант на данный момент, поскольку он позволяет нам экономить память, одновременно поддерживая истинный цвет.

Если мы достигнем этого уровня и все будет продолжаться хорошо, мы сможем оптимизировать, как предложено в пункте 5., или любым другим способом, который приходит нам в голову в то время и имеет смысл.

3. отлично 👍.

@mofux , хотя определенно существует вариант использования методов с использованием дискового хранилища для уменьшения объема памяти, это может ухудшить работу пользователя с библиотекой в ​​средах браузера, которые запрашивают у пользователя разрешение на использование дискового хранилища.

Что касается поддержки будущего :
Чем больше я думаю об этом, тем больше меня привлекает идея иметь WebWorker который выполняет всю тяжелую работу по синтаксическому анализу данных tty, поддержанию строковых буферов, сопоставлению ссылок, сопоставлению маркеров поиска и тому подобному. В основном выполняя тяжелую работу в отдельном фоновом потоке, не блокируя пользовательский интерфейс. Но я думаю, что это должно быть частью отдельного обсуждения, возможно, перед выпуском 4.0 😉

+100 о WebWorker в будущем, но я думаю, что нам нужно изменить список поддерживаемых нами браузеров, потому что не все из них могут его использовать ...

Когда я говорю Int32Array , это будет обычный массив, если он не поддерживается средой.

@mofux хорошее мышление с WebWorker в будущем 👍

@AndrienkoAleksandr: да, если бы мы хотели использовать WebWorker нам все равно нужно было бы поддерживать альтернативу через обнаружение функций.

Вау хороший список :)

Я также склоняюсь к 3., поскольку это обещает значительное сокращение потребления памяти более чем на 90% от типичного использования терминала. Имхо оптимизация памяти должна быть главной целью на данном этапе. Вдобавок к этому может быть применима дальнейшая оптимизация для конкретных случаев использования (что приходит мне в голову: «холст, как приложения», такие как ncurses и тому подобное, будут использовать тонны обновлений отдельных ячеек и со временем как бы ухудшат список [start, end] ) .

@AndrienkoAleksandr: да, мне тоже нравится идея веб-воркера, так как она может снять некоторую нагрузку с основного потока. Проблема здесь (помимо того факта, что он может поддерживаться не всеми желаемыми целевыми системами) - это _some_ - часть JS больше не имеет большого значения со всеми оптимизациями, которые xterm.js видел за это время. Реальная проблема с производительностью - это макет / рендеринг браузера ...

@mofux Пейджинг в какую-то «внешнюю память» - хорошая идея, хотя она должна быть частью некоторой более высокой абстракции, а не «дайте мне интерактивный виджет терминала», как xterm.js. Этого можно добиться с помощью надстройки imho.

Offtopic: проводил несколько тестов с массивами против типизированных массивов против asm.js. Все, что я могу сказать - OMG, это как 1 : 1,5 : 10 для простых загрузок и наборов переменных (на FF даже больше). Если скорость чистого JS действительно начинает ухудшаться, на помощь может прийти «use asm». Но я бы рассматривал это как крайнюю меру, поскольку это повлекло бы за собой фундаментальные изменения. Webassembly еще не готов к отправке.

Offtopic: проводил несколько тестов с массивами против типизированных массивов против asm.js. Все, что я могу сказать - OMG, это как 1: 1,5: 10 для простых переменных нагрузок и наборов (на FF даже больше)

@jerch, чтобы уточнить, это массивы по сравнению с типизированными массивами от 1: 1 до 1: 5?

Замечательный улов с запятой - я имел в виду 10:15:100 зрения скорости. Но только массивы с типом FF были немного быстрее обычных массивов. asm как минимум в 10 раз быстрее массивов js во всех браузерах - протестировано с FF, webkit (Safari), blink / V8 (Chrome, Opera).

@jerch cool, на 50% ускорение от typedarrays в дополнение к лучшей памяти определенно стоит инвестировать на данный момент.

Идея для экономии памяти - возможно, мы могли бы избавиться от width для каждого символа. Попробую реализовать менее дорогую версию wcwidth.

@jerch нам нужно немного получить к нему доступ, и мы не можем лениво загружать его или что-то еще, потому что, когда произойдет перекомпоновка, нам понадобится ширина каждого символа в буфере. Даже если бы он был быстрым, мы все равно могли бы захотеть его оставить.

Возможно, лучше сделать его необязательным, предполагая 1, если он не указан:

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

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

@Tyriar Да - ну раз уж я это уже написал, пожалуйста, взгляните на PR # 798
Ускорение на моем компьютере составляет от 10 до 15 раз за 16 Кбайт для таблицы поиска. Может быть, комбинация того и другого возможна, если она все еще необходима.

Еще несколько флагов, которые мы будем поддерживать в будущем: https://github.com/sourcelair/xterm.js/issues/580

Еще одна мысль: только нижняя часть терминала (от Terminal.ybase до Terminal.ybase + Terminal.rows ) является динамической. Прокрутка, составляющая основную часть данных, полностью статична, возможно, мы сможем это использовать. Я не знал этого до недавнего времени, но даже такие вещи, как удаление строк (DL, CSI Ps M), не возвращают прокрутку вниз, а скорее вставляют другую строку. Точно так же прокрутка вверх (SU, CSI Ps S) удаляет элемент в Terminal.scrollTop и вставляет элемент в Terminal.scrollBottom .

Независимое управление нижней динамической частью терминала и нажатие на прокрутку при выталкивании строки может привести к значительному выигрышу. Например, нижняя часть может быть более подробной, чтобы способствовать изменению атрибутов, более быстрому доступу и т. Д., Тогда как обратная прокрутка может быть в большей степени архивным форматом, как предложено выше.

Другая мысль: вероятно, лучше ограничить CharAttributeEntry строками, поскольку именно так работает большинство приложений. Также, если размер терминала изменяется, то справа добавляется «пустое» заполнение, которое не использует те же стили.

например:

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

Справа от красных / зеленых различий находятся «пустые» ячейки без стиля.

@Tyriar
Есть ли шанс вернуть этот вопрос в повестку дня? По крайней мере, для программ с интенсивным выводом другой способ хранения данных терминала может сэкономить много памяти и времени. Какой-нибудь гибрид 2/3/4 даст огромный прирост пропускной способности, если мы сможем избежать разделения и сохранения отдельных символов входной строки. Кроме того, сохранение атрибутов только после их изменения поможет сэкономить память.

Пример:
С новым парсером мы могли сохранить кучу входных символов, не вмешиваясь в атрибуты, так как мы знаем, что они не изменятся в середине этой строки. Атрибуты этой строки могут быть сохранены в какой-либо другой структуре данных или атрибуте вместе с wcwidths (да, они все еще нужны, чтобы найти разрыв строки) и разрывами и остановками строк. Это в основном отказало бы от модели ячеек при поступлении данных.
Проблема возникает, если что-то вмешивается и хочет получить детализированное представление данных терминала (например, средство визуализации или некоторая escape-последовательность / пользователь хочет переместить курсор). Если это произойдет, нам все равно придется выполнять вычисления ячеек, но этого должно быть достаточно, чтобы сделать это только для содержимого в терминальных столбцах и строках. (Еще не уверен в прокручиваемом контенте, который может быть еще более кэшируемым и дешевым для перерисовки.)

@jerch Я встречусь с @mofux однажды в Праге через пару недель, и мы собирались сделать / начать некоторые внутренние улучшения того, как обрабатываются текстовые атрибуты, которые охватывают это 😃

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

Алгоритм довольно дорогой, так как каждый символ нужно оценивать дважды.

@jerch, если у вас есть идеи по ускорению доступа к тексту из буфера, дайте нам знать. Как вы знаете, в настоящее время большая часть из них представляет собой всего лишь один символ, но это может быть ArrayBuffer , строка и т. Д. Я подумал, что нам следует подумать о том, чтобы как-то воспользоваться преимуществами неизменяемости обратной прокрутки.

Ну, я много экспериментировал с ArrayBuffers в прошлом:

  • они немного хуже, чем Array отношении времени выполнения для типичных методов (возможно, еще менее оптимизированы поставщиками движков)
  • new UintXXArray намного хуже, чем создание буквального массива с помощью []
  • они окупаются несколько раз, если вы можете предварительно выделить и повторно использовать структуру данных (до 10 раз), именно здесь природа связанного списка смешанных массивов съедает производительность из-за большого выделения и gc за кулисами
  • для строковых данных прямое и обратное преобразование съедает все преимущества - жаль, что JS не предоставляет встроенную строку для преобразователей Uint16Array (хотя частично это можно сделать с помощью TextEncoder )

Мои выводы о ArrayBuffer предлагают не использовать их для строковых данных из-за штрафа за преобразование. Теоретически терминал может использовать ArrayBuffer от node-pty до данных терминала (это сэкономит несколько преобразований на пути к интерфейсу), не уверен, что рендеринг может быть выполнен таким образом, я думаю, чтобы отобразить все это всегда требует окончательного преобразования uint16_t в string . Но даже это создание одной последней строки съест большую часть сохраненной среды выполнения - и, более того, превратит терминал изнутри в уродливого C-ish зверя. Поэтому я отказался от этого подхода.

TL; DR ArrayBuffer лучше, если вы можете предварительно выделить и повторно использовать структуру данных. Для всего остального лучше использовать обычные массивы. Строки не стоит втискивать в ArrayBuffers.

Новая идея, которую я придумал, пытается максимально уменьшить количество строк, особенно. пытается избежать неприятных разделений и объединений. Это вроде как основано на вашей второй идее, приведенной выше, с учетом нового метода InputHandler.print , wcwidth и остановки строки:

  • print теперь получает целые строки до нескольких конечных строк
  • сохранить эти строки в простом списке указателей без каких-либо изменений (без выделения строк или gc, выделения списка можно избежать, если используется со структурой prealloc'd) вместе с текущими атрибутами
  • продвинуть курсор на wcwidth(string) % cols
  • особый случай \n (жесткий разрыв строки): переместить курсор на одну строку, отметить позицию в списке указателей как жесткий разрыв
  • особый случай переполнения строки с помощью wrapAround: отметить позицию в строке как мягкий разрыв строки
  • особый случай \r : загрузить содержимое последней строки (от текущей позиции курсора до последнего разрыва строки) в некоторый строковый буфер для перезаписи
  • потоки данных, как указано выше, несмотря на случай \r не требуется ни абстракции ячеек, ни разделения строк
  • изменения атрибутов не проблема, пока никто не запрашивает реальное представление cols x rows (они просто изменяют флаг attr, который сохраняется вместе со всей строкой)

Кстати, wcwidths является подмножеством алгоритма графемы, поэтому в будущем он может быть взаимозаменяемым.

Теперь опасная часть 1 - кто-то хочет переместить курсор в пределах cols x rows :

  • переместить cols назад в разрывах строк - начало текущего содержимого терминала
  • каждый разрыв строки обозначает реальную терминальную линию
  • загружать материал в модель ячейки только для одной страницы (пока не уверен, можно ли это также пропустить с помощью умного позиционирования строки)
  • выполнить неприятную работу: если запрашиваются изменения атрибутов, нам не повезло, и мы должны вернуться либо к полной модели ячеек, либо к модели разделения и вставки строк (последняя может привести к плохой производительности)
  • потоки данных снова, теперь с ухудшенными строками и данными attrs в буфере для этой страницы

Теперь опасная часть 2 - рендерер хочет что-то нарисовать:

  • вроде как зависит от средства визуализации, если нам нужно развернуть модель ячейки или мы можем просто предоставить смещения строк с разрывами строк и текстовыми атрибутами

Плюсы:

  • очень быстрый поток данных
  • оптимизирован для наиболее распространенного метода InputHandler - print
  • делает возможным переформатирование строк при изменении размера терминала

Минусы:

  • почти любой другой метод InputHandler будет опасен в смысле прерывания этой модели потока и необходимости некоторой промежуточной абстракции ячеек
  • неясная интеграция рендерера (мне, по крайней мере, атм)
  • может снизить производительность таких проклятий, как приложения (обычно они содержат более «опасные» последовательности)

Что ж, это черновик идеи, которую нельзя использовать в банкомате, поскольку многие детали еще не раскрыты. Esp. «опасные» части могут стать неприятными из-за многих проблем с производительностью (например, деградация буфера с еще худшим поведением gc и т. д. pp)

@jerch

Строки не стоит втискивать в ArrayBuffers.

Я думаю, что Monaco хранит свой буфер в ArrayBuffer s и имеет довольно высокую производительность. Я еще не слишком глубоко разбирался в реализации.

особенно пытается избежать неприятных разделений и объединений

Какие?

Я подумал, что нам следует подумать о том, чтобы как-то использовать больше преимуществ неизменяемости прокрутки.

Одна из идей заключалась в том, чтобы отделить прокрутку от области просмотра. Как только строка переходит в режим обратной прокрутки, она помещается в структуру данных обратной прокрутки. Вы можете представить себе 2 объекта CircularList , один из которых оптимизирован для того, чтобы никогда не изменяться, а другой - для противоположного.

@Tyriar Насчет

@Tyriar
Имеет смысл хранить строки в ArrayBuffer, если мы можем ограничить преобразование одним (возможно, последним для вывода рендеринга). Это немного лучше, чем повсеместная обработка строк. Это было бы выполнимо, поскольку node-pty также может предоставлять необработанные данные (а также веб-сокет может предоставлять нам необработанные данные).

особенно пытается избежать неприятных разделений и объединений

Какие?

Весь подход , чтобы избежать _minimize_ расколов на всех. Если никто не запрашивает, что курсор переходит в буферизованные данные, строки никогда не будут разделены и могут перейти прямо в средство визуализации (если поддерживается). Никаких расщеплений и последующего соединения клеток нет.

@jerch ну, может, если область просмотра расширена, я думаю, мы можем также вытащить прокрутку при удалении строки? Не уверен на 100% в этом и даже в том, что это правильное поведение.

@Tyriar А, верно. Не уверен и в последнем, я думаю, что собственный xterm позволяет это только для реальной прокрутки мыши или полосы прокрутки. Даже SD / SU не перемещает содержимое буфера прокрутки обратно в «активное» окно просмотра терминала.

Не могли бы вы указать мне источник редактора monaco, где используется ArrayBuffer? Вроде сам не могу найти: blush:

Хм, просто перечитайте спецификацию TextEncoder / Decoder, с ArrayBuffers от node-pty до внешнего интерфейса, мы в основном застряли с utf-8, если мы не переведем его на трудный путь в какой-то момент. Осведомленность о xterm.js utf-8? Idk, это потребует многих вычислений промежуточных кодовых точек для более высоких символов Unicode. Плюс - это сэкономило бы память для символов ascii.

@rebornix не могли бы вы

Вот некоторые числа для типизированных массивов и нового синтаксического анализатора (его легче было использовать):

  • UTF-8 (Uint8Array): действие print перескакивает с 190 МБ / с на 290 МБ / с
  • UTF-16 (Uint16Array): действие print перескакивает с 190 МБ / с на 320 МБ / с

В целом UTF-16 работает намного лучше, но этого следовало ожидать, поскольку синтаксический анализатор оптимизирован для этого. UTF-8 страдает от вычисления промежуточной кодовой точки.

Преобразование строки в типизированный массив занимает ~ 4% времени выполнения JS моего теста ls -lR /usr/lib (всегда намного меньше 100 мс, выполняется с помощью цикла в InputHandler.parse ). Я не тестировал обратное преобразование (это неявно выполняется atm в InputHandller.print на ячейке на уровне ячейки). Общее время выполнения немного хуже, чем со строками (сэкономленное время в парсере не компенсирует время преобразования). Это может измениться, если другие части также поддерживают типизированный массив.

И соответствующие скриншоты (протестированы с ls -lR /usr/lib ):

со строками:
grafik

с Uint16Array:
grafik

Обратите внимание на разницу для EscapeSequenceParser.parse , который может получить прибыль от типизированного массива (примерно на 30% быстрее). InputHandler.parse выполняет преобразование, поэтому это хуже для версии с типизированным массивом. Также GC Minor имеет больше возможностей для типизированного массива (поскольку я выбрасываю массив).

Изменить: на снимках экрана можно увидеть еще один аспект - сборщик мусора становится актуальным с ~ 20% времени выполнения, длинные кадры (отмечены красным) связаны с сборщиком мусора.

Еще одна несколько радикальная идея:

  1. Создайте собственную виртуальную память на основе буфера массива, что-то большое (> 5 МБ)
    Если буфер массива имеет длину, кратную 4 прозрачным переключателям, от int8 до int16 до int32 возможны типы Uint8Array , этот указатель может быть преобразован в позицию Uint16Array или Uint32Array простым битовым сдвигом.
  2. Записывать входящие строки в память как uint16_t type для UTF-16.
  3. Парсер работает с указателями строк и вызывает методы в InputHandler с указателями на эту память вместо фрагментов строки.
  4. Создайте буфер данных терминала внутри виртуальной памяти как массив кольцевых буферов структуроподобного типа вместо собственных объектов JS, может быть, вот так (все еще на основе ячеек):
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
}

Плюсы:

  • опускает объекты JS и, следовательно, сборщик мусора, где это возможно (останется только несколько локальных объектов)
  • требуется только одна начальная копия данных в виртуальной памяти
  • почти нет затрат malloc и free (зависит от сообразительности распределителя / распределителя)
  • сэкономит много памяти (избегает накладных расходов на память объектов JS)

Минусы:

  • Добро пожаловать на Cavascript Horror Show: scream:
  • сложно реализовать, меняет все
  • выигрыш в скорости неясен, пока действительно не реализован

:улыбка:

сложно реализовать, меняет вроде все 😉

Это ближе к тому, как работает Монако, я вспомнил этот пост в блоге, в котором обсуждается стратегия хранения метаданных персонажей https://code.visualstudio.com/blogs/2017/02/08/syntax-highlighting-optimizations

Ага, это в основном та же идея.

Надеюсь, что мой ответ на вопрос, где монако хранит буфер, еще не поздно.

Мы с Алексом поддерживаем Array Buffer, и в большинстве случаев он дает нам хорошую производительность. В некоторых местах мы используем ArrayBuffer:

Мы используем простые строки для текстового буфера вместо буфера массива, поскольку строкой V8 легче манипулировать.

  • Мы делаем кодирование / декодирование в самом начале загрузки файла, поэтому файлы конвертируются в JS-строку. V8 решает, использовать ли один байт или два для хранения символа.
  • Мы очень часто редактируем текстовый буфер, строки легче обрабатывать.
  • Мы используем собственный модуль nodejs и при необходимости получаем доступ к внутренним компонентам V8.

Следующий список представляет собой краткое изложение интересных концепций, на которые я наткнулся, которые могут помочь снизить использование памяти и / или время выполнения:

  • FlatJS (https://github.com/lars-t-hansen/flatjs) - метаязык, помогающий кодировать с помощью кучи на основе буфера массива
  • http://2ality.com/2017/01/shared-array-buffer.html (объявлено как часть ES2017, будущее может быть неопределенным из-за Spectre, помимо этой очень многообещающей идеи с реальным параллелизмом и реальной атомикой)
  • webassembly / asm.js (текущее состояние? еще можно использовать? Некоторое время не следил за его разработкой, использовал emscripten для asm.js много лет назад с C lib для игрового ИИ, хотя и с впечатляющими результатами)
  • https://github.com/AssemblyScript/assemblyscript

Чтобы подвести итоги, вот небольшой совет, как мы могли бы «объединить» текстовые атрибуты.

Код в основном основан на идее экономии памяти для данных буфера (пострадает время выполнения, еще не проверено, насколько сильно). Esp. текстовые атрибуты с RGB для переднего плана и фона (когда-то поддерживаются) заставят xterm.js съесть тонны памяти на текущую ячейку по макету ячейки. Код пытается обойти это, используя изменяемый атлас подсчета ссылок для атрибутов. Это, по-моему, вариант, поскольку один терминал вряд ли будет содержать более 1 миллиона ячеек, что приведет к увеличению атласа до 1M * entry_size если все ячейки будут отличаться.

Сама ячейка просто должна содержать индекс атласа атрибутов. При изменении ячейки старый индекс должен быть разорван, а новый индексирован. Индекс атласа заменит атрибут атрибута конечного объекта и сам будет изменен в SGR.

Атлас в настоящее время обращается только к текстовым атрибутам, но при необходимости может быть расширен на все атрибуты ячеек. В то время как текущий буфер терминала содержит 2 32-битных числа для данных атрибутов (4 с RGB в текущем дизайне буфера), атлас уменьшит его до одного 32-битного числа. Записи атласа можно упаковать и дальше.

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

Редактировать:
Штраф времени выполнения почти равен нулю, для моего теста с ls -lR /usr/lib он добавляет менее 1 мс к общему времени выполнения ~ 2,3 с. Интересное примечание: команда устанавливает менее 64 слотов различных текстовых атрибутов для вывода 5 МБ данных и сэкономит более 20 МБ после полной реализации.

Сделал несколько прототипов PR для проверки некоторых изменений в буфере (общую идею изменений см. На https://github.com/xtermjs/xterm.js/pull/1528#issue-196949371):

  • PR # 1528: атлас атрибутов
  • PR # 1529: удалить wcwidth и charCode из буфера
  • PR # 1530: заменить строку в буфере кодовыми точками / значением индекса хранения ячейки

@jerch, было бы неплохо держаться подальше от слова "атлас", чтобы слово "атлас" всегда означало "атлас текстуры". Что-то вроде хранилища или кеша, наверное, было бы лучше?

ну ладно, "кеш" в порядке.

Думаю, я закончил с PR тестовых стендов. Также ознакомьтесь с комментариями по связям с общественностью, чтобы получить предысторию следующего приблизительного резюме.

Предложение:

  1. Создайте AttributeCache для хранения всего необходимого для стилизации одной терминальной ячейки. См. # 1528 для более ранней версии подсчета ссылок, которая также может содержать истинные цветовые спецификации. Кэш также может быть разделен между различными экземплярами терминала, если необходимо сохранить дополнительную память в нескольких приложениях терминала.
  2. Создайте StringStorage для хранения коротких строк данных содержимого терминала. Версия в # 1530 даже избегает хранения одиночных символьных строк, «перегружая» значение указателя. wcwidth следует переместить сюда.
  3. Уменьшите текущий CharData с [number, string, number, number] до [number, number] , где числа являются указателями (индексными числами) на:

    • AttributeCache запись

    • StringStorage запись

Атрибуты вряд ли сильно изменятся, поэтому одно 32-битное число со временем сэкономит много памяти. Указатель StringStorage - это настоящая кодовая точка Unicode для одиночных символов, поэтому может использоваться как запись code для CharData . Доступ к фактической строке можно получить с помощью StringStorage.getString(idx) . К четвертому полю wcwidth из CharData может получить доступ StringStorage.wcwidth(idx) (еще не реализовано). Практически отсутствует штраф времени выполнения за избавление от code и wcwidth в CharData (проверено в # 1529).

  1. Переместите сжатый CharData в плотную реализацию буфера на основе Int32Array . Также протестирован в # 1530 с классом-заглушкой (далеко не полностью функциональным), окончательные преимущества, вероятно, будут следующими:

    • На 80% меньше памяти, занимаемой буфером терминала (с 5,5 МБ до 0,75 МБ)

    • немного быстрее (пока не тестируемый, я ожидаю увеличения скорости на 20% - 30%)

    • Изменить: намного быстрее - время выполнения скрипта для ls -lR /usr/lib упало до 1,3 с (мастер на 2,1 с), в то время как старый буфер все еще активен для обработки курсора, после удаления я ожидаю, что время выполнения упадет ниже 1 с

Обратной стороной является то, что шаг 4 - это довольно много работы, так как он потребует некоторой доработки интерфейса буфера. Но послушайте - для экономии 80% ОЗУ и повышения производительности во время выполнения это не так уж сложно, не так ли? :улыбка:

Есть еще одна проблема, на которую я наткнулся - текущее представление пустых ячеек. Имхо у ячейки может быть 3 состояния:

  • пусто : исходное состояние ячейки, в нее еще ничего не записано или содержимое удалено. Он имеет ширину 1, но не имеет содержимого. В настоящее время используется в blankLine и eraseChar , но с пробелом в качестве содержимого.
  • null : ячейка после символа полной ширины, чтобы указать, что у нее нет ширины для визуального представления.
  • нормально : ячейка содержит некоторый контент и имеет визуальную ширину (1 или 2, может быть, больше, если мы поддерживаем настоящие графемы / биди, но пока не уверен в этом, лол)

Проблема, которую я вижу здесь, заключается в том, что пустая ячейка не отличима от нормальной ячейки с вставленным пробелом , обе выглядят одинаково на уровне буфера (одинаковое содержимое, одинаковая ширина). Я не писал код рендерера / вывода, но я ожидаю, что это приведет к неловким ситуациям на фронте вывода. Esp. обработка правого конца строки может стать громоздкой.
Терминал с 15 столбцами, сначала какой-то строковый вывод, который был обернут вокруг:

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

по сравнению со списком папок с ls :

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

Первый пример содержит реальный пробел после слова «терминал», второй пример никогда не касался ячеек после «Readme.md». То, как он представлен на уровне буфера, имеет смысл для стандартного случая вывода материала на экран в качестве вывода терминала (комнату нужно в любом случае занять), но для инструментов, которые пытаются работать со строками содержимого, такими как выделение мышью или менеджер переформатирования уже не понятно, откуда берутся пробелы.

Более или менее это приводит к следующему вопросу - как определить фактическую длину содержимого в строке (количество ячеек, которые содержат что-то с левой стороны)? Простой подход - подсчет пустых ячеек с правой стороны, опять же двойное значение сверху затрудняет определение этого.

Предложение:
Имхо, это легко исправить, используя какой-либо другой заполнитель для пустых ячеек, например, управляющий символ или пустую строку, и при необходимости замените их в процессе рендеринга. Может быть, от этого выиграет и средство визуализации экрана, поскольку ему, возможно, вообще не придется обрабатывать эти ячейки (зависит от способа создания вывода).

Кстати, для обернутой строки выше это также приводит к проблеме isWrapped , которая важна для изменения размера перекомпоновки или правильной обработки выбора копирования и вставки. Imho мы не можем удалить это, но нужно интегрировать это лучше, чем это atm.

@jerch впечатляющая работа! : смайлик:

1 Создайте AttributeCache для хранения всего необходимого для стилизации одной терминальной ячейки. См. # 1528 для более ранней версии подсчета ссылок, которая также может содержать истинные цветовые спецификации. Кэш также может быть разделен между различными экземплярами терминала, если необходимо сохранить дополнительную память в нескольких приложениях терминала.

Оставил несколько комментариев к № 1528.

2 Создайте StringStorage для хранения коротких строк данных содержимого терминала. Версия в # 1530 даже избегает хранения одиночных символьных строк, «перегружая» значение указателя. wcwidth следует переместить сюда.

Оставил несколько комментариев к № 1530.

4 Переместите сжатый CharData в реализацию плотного буфера на основе Int32Array. Также протестирован в # 1530 с классом-заглушкой (далеко не полностью функциональным), окончательные преимущества, вероятно, будут следующими:

Пока еще не полностью реализованная идея, я думаю, что она нас сильно укусит, когда мы реализуем reflow. Похоже, что каждый из этих шагов в значительной степени может быть выполнен по порядку, поэтому мы можем увидеть, как идут дела, и понять, имеет ли смысл делать это, когда мы сделаем 3.

Есть еще одна проблема, на которую я наткнулся - текущее представление пустых ячеек. Imho ячейка может иметь 3 состояния

Вот пример ошибки, возникшей из-за этого https://github.com/xtermjs/xterm.js/issues/1286,: +1: для различения ячеек с пробелами и «пустых» ячеек

Кстати, для обернутой строки выше это также приводит к проблеме isWrapped, которая важна для изменения размера перекомпоновки или правильной обработки выбора копирования и вставки. Imho мы не можем удалить это, но нужно интегрировать это лучше, чем это atm.

Я вижу, что isWrapped исчезнет, ​​когда мы займемся https://github.com/xtermjs/xterm.js/issues/622, поскольку CircularList будет содержать только развернутые строки.

Пока еще не полностью реализованная идея, я думаю, что она нас сильно укусит, когда мы реализуем reflow. Похоже, что каждый из этих шагов в значительной степени может быть выполнен по порядку, поэтому мы можем увидеть, как идут дела, и понять, имеет ли смысл делать это, когда мы сделаем 3.

Да, я с вами (все еще интересно поиграть с этим совершенно другим подходом). 1 и 2 можно выбрать по отдельности, 3 можно применить в зависимости от 1 или 2. 4 не является обязательным, мы могли бы просто придерживаться текущего макета буфера. Экономия памяти такая:

  1. 1 + 2 + 3 в CircularList : экономия 50% (~ 2,8 МБ из ~ 5,5 МБ)
  2. 1 + 2 + 3 + 4 наполовину - просто поместите данные строки в типизированный массив, но придерживайтесь доступа к индексу строки: экономия 82% (~ 0,9 МБ)
  3. 1 + 2 + 3 + 4 полностью плотный массив с арифметикой указателей: экономия 87% (~ 0,7 МБ)

1. очень легко реализовать, поведение памяти с большим scrollBack по-прежнему будет показывать плохое масштабирование, как показано здесь https://github.com/xtermjs/xterm.js/pull/1530#issuecomment -403542479, но на менее токсичном уровне
2. Немного сложнее реализовать (требуется еще несколько косвенных указаний на уровне строки), но это позволит сохранить более высокий API Buffer нетронутым. Имхо вариант - большое сохранение памяти и простота интеграции.
3. Сохранение памяти на 5% больше, чем вариант 2, который трудно реализовать, изменит все API и, следовательно, буквально всю базу кода. Имхо больше академического интереса или для скучных дождливых дней, которые нужно реализовать, лол.

@Tyriar Я провел еще несколько тестов с ржавчиной для использования веб-сборки и переписал парсер. Обратите внимание, что мои навыки ржавчины немного «ржавые», так как я еще не углубился в это, поэтому следующее может быть результатом слабого кода ржавчины. Полученные результаты:

  • Обработка данных внутри wasm-части происходит немного быстрее (5-10%).
  • Вызовы из JS в wasm создают некоторые накладные расходы и съедают все преимущества, перечисленные выше. Фактически это было примерно на 20% медленнее.
  • «Бинарный» будет меньше, чем аналог JS (на самом деле не измеряется, так как я не реализовал все).
  • Чтобы легко выполнить переход JS <--> wasm, необходим некоторый раздутый код для обработки типов JS (выполнялся только перевод строк).
  • Мы не можем избежать перевода JS в wasm, так как DOM браузера и события там недоступны. Его можно было использовать только для основных частей, которые больше не критичны к производительности (помимо потребления памяти).

Если мы не хотим переписать все основные библиотеки на rust (или на любом другом языке с поддержкой wasm), мы ничего не получим от перехода на wasm lang imho. Плюс современных языков wasm заключается в том, что большинство из них поддерживает явную обработку памяти (может помочь нам с проблемой буфера), недостатками является введение совершенно другого языка в проект, ориентированный в первую очередь на TS / JS (высокий барьер для добавления кода). и затраты на перевод между wasm и JS land.

TL; DR
xterm.js предназначен для использования в общих материалах JS, таких как DOM и события, для получения чего-либо от веб-сборки даже для переписывания основных частей.

@jerch хорошее расследование: смайлик:

Вызовы из JS в wasm создают некоторые накладные расходы и съедают все преимущества, перечисленные выше. Фактически это было примерно на 20% медленнее.

Это была основная проблема для монако, которая стала повлияло на мою позицию (хотя это было с нативным модулем узла, а не с wasm). Я считаю, что работа с ArrayBuffer s там, где это возможно, должна дать нам лучший баланс между производительностью и простотой (легкость в использовании, барьер для входа).

@Tyriar Попробую создать AttributeStorage для хранения данных RGB. Еще не уверен насчет BST, для типичного случая использования только с несколькими настройками цвета в сеансе терминала это будет хуже во время выполнения, возможно, это должно быть добавление во время выполнения, когда цвета превышают заданное значение. Кроме того, потребление памяти снова возрастет, хотя это все равно сохранит память, поскольку атрибуты сохраняются только один раз, а не вместе с каждой отдельной ячейкой (хотя в худшем случае каждая ячейка, содержащая разные атрибуты, пострадает).
Знаете ли вы, почему текущее значение цветов fg и bg 256 основано на 9 битах, а не на 8 битах? Для чего используется дополнительный бит? Здесь: https://github.com/xtermjs/xterm.js/blob/6691f809069a549b4808cd2e055398d2da15db37/src/InputHandler.ts#L1596
Не могли бы вы дать мне текущий битовый макет attr ? Я думаю, что подобный подход, такой как «двойное значение» для указателя StringStorage, может еще больше сэкономить память, но для этого потребуется, чтобы MSB attr был зарезервирован для различия указателя и не использовался для каких-либо других целей. Это может ограничить возможность поддержки дополнительных флагов атрибутов позже (поскольку FLAGS уже использует 7 битов), не хватает ли нам некоторых фундаментальных флагов, которые, вероятно, появятся?

32-битное число attr в буфере терминов можно упаковать следующим образом:

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

Таким образом, хранилище должно содержать только данные RGB в двух 32-битных числах, в то время как флаги могут оставаться в номере attr .

@jerch , кстати, я отправил вам электронное письмо, вероятно, спам-фильтр снова съел его 😛

Знаете ли вы, почему текущее значение 256 цветов fg и bg основано на 9 битах, а не на 8 битах? Для чего используется дополнительный бит?

Я думаю, он используется для цвета fg / bg по умолчанию (который может быть темным или светлым), так что на самом деле это 257 цветов.

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

Не могли бы вы дать мне текущую битовую раскладку attr?

Думаю дело в следующем:

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

Вы можете увидеть, на что я остановился для истинного цвета, в старом 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];

Итак, у меня было 2 флага; один для цвета по умолчанию (игнорировать ли все цветовые биты) и один для истинного цвета (использовать ли цвет 256 или 16 мил).

Это может ограничить возможность поддержки дополнительных флагов атрибутов позже (поскольку FLAGS уже использует 7 битов), не хватает ли нам некоторых фундаментальных флагов, которые, вероятно, появятся?

Да, нам нужно место для дополнительных флагов, например https://github.com/xtermjs/xterm.js/issues/580, https://github.com/xtermjs/xterm.js/issues/1145, я бы скажем, по крайней мере, оставьте> 3 бита, где это возможно.

Вместо данных указателя внутри самого attr может быть другая карта, которая содержит ссылки на данные rgb? mapAttrIdxToRgb: { [idx: number]: RgbData

@Tyriar Извините, несколько дней не был в сети, и, боюсь, спам-фильтр съел письмо. Не могли бы вы отправить его повторно? :краснеть:

Играл abit с более умными структурами данных поиска для хранилища attrs. Наиболее многообещающими с точки зрения пространства и времени выполнения поиска / вставки являются деревья и скиплист как более дешевая альтернатива. Теоретически лол. На практике ни один из них не может превзойти мой простой поиск по массиву, который мне кажется очень странным (ошибка где-то в коде?)
Я загрузил сюда тестовый файл https://gist.github.com/jerch/ff65f3fb4414ff8ac84a947b3a1eec58 с массивом и расположенным влево красно-черным деревом, который проверяет до 10 миллионов записей (что является почти полным адресным пространством). Тем не менее, массив далеко впереди по сравнению с LLRB, хотя я подозреваю, что безубыточность составляет около 10M. Протестировано на моем старом ноутбуке 7ys, может быть, кто-то сможет его протестировать, а даже лучше - укажите мне на некоторые ошибки в impl / tests.

Вот некоторые результаты (с текущими числами):

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

Что меня действительно удивляет, так это тот факт, что поиск по линейному массиву вообще не показывает роста в нижних областях, это до 10 тыс. Записей стабильно на ~ 4 мс (может быть связано с кешем). Тест 10M показывает худшее время выполнения, чем ожидалось, возможно, из-за подкачки памяти. Возможно, JS находится слишком далеко от машины с JIT и всеми происходящими opts / deopts, но я думаю, что они не могут устранить шаг сложности (хотя LLRB кажется тяжелым для одного _n_, таким образом перемещая точку безубыточности для O ( n) против O (logn) вверх)

Кстати, со случайными данными разница еще хуже.

Я думаю, он используется для цвета fg / bg по умолчанию (который может быть темным или светлым), так что на самом деле это 257 цветов.

Значит, это необычный SGR 39 или SGR 49 из одного из 8 цветов палитры?

Вместо данных указателя внутри самого attr может быть другая карта, которая содержит ссылки на данные rgb? mapAttrIdxToRgb: {[idx: число]: RgbData

Это приведет к еще одному косвенному обращению с дополнительным использованием памяти. С помощью приведенных выше тестов я также проверил разницу между постоянным удержанием флагов в attrs и их сохранением вместе с данными RGB в хранилище. Поскольку разница составляет ~ 0,5 мс для 1M записей, я бы не стал использовать эту сложную настройку attrs, вместо этого скопируйте флаги в хранилище после установки RGB. Тем не менее, я бы выбрал 32-битное различие между прямыми атрибутами и указателем, поскольку это позволит вообще избежать хранения для ячеек, отличных от RGB.

Также я думаю, что 8 цветов палитры по умолчанию для fg / bg в настоящее время недостаточно представлены в буфере. Теоретически терминал должен поддерживать следующие цветовые режимы:

  1. SGR 39 + SGR 49 Цвет по умолчанию для fg / bg (настраиваемый)
  2. SGR 30-37 + SGR 40-47 8 низкая цветовая палитра для fg / bg (настраиваемая)
  3. SGR 90-97 + SGR 100-107 8 высокая цветовая палитра для fg / bg (настраиваемая)
  4. SGR 38;5;n + SGR 48;5;n 256 индексированная палитра для fg / bg (настраиваемая)
  5. SGR 38;2;r;g;b + SGR 48;2;r;g;b RGB для fg / bg (не настраивается)

Варианты 2.) и 3.) могут быть объединены в один байт (рассматривая их как единую палитру fg / bg из 16 цветов), 4.) занимает 2 байта и 5.) в итоге займет еще 6 байтов. Нам все еще нужны биты для обозначения цветового режима.
Чтобы отразить это на уровне буфера, нам понадобится следующее:

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

Итак, нам нужно 30 бит 32-битного числа, оставив 2 бита свободными для других целей. 32-й бит может содержать указатель и флаг прямого атрибута без сохранения для ячеек, отличных от RGB.

Также я предлагаю обернуть доступ attr в удобный класс, чтобы не раскрывать детали реализации извне (см. Тестовый файл выше, для этого существует ранняя версия класса TextAttributes ).

Извините, несколько дней не был в сети, и, боюсь, спам-фильтр съел письмо. Не могли бы вы отправить его повторно?

Возмущаться

О, кстати, эти числа выше для поиска по массиву и llrb - дерьмо - я думаю, что оптимизатор испортил некоторые странные вещи в цикле for. С немного другой настройкой теста он ясно показывает, что O (n) по сравнению с O (log n) растет намного раньше (с предварительно заполненными 1000 элементами, которые уже быстрее с деревом).

Текущее состояние:

После:

Одна довольно простая оптимизация - объединить массив массивов в единый массив. Т.е. вместо BufferLine из _N_ столбцов, имеющих массив _data из _N_ CharData ячеек, где каждый CharData представляет собой массив из 4, просто используйте один массив из _4 * N_ элементов. Это устраняет накладные расходы на объекты _N_ массивов. Это также улучшает локальность кеша, поэтому он должен быть быстрее. Недостаток - немного более сложный и уродливый код, но, похоже, оно того стоит.

В продолжение моего предыдущего комментария, кажется, стоит подумать об использовании переменного количества элементов в массиве _data для каждой ячейки. Другими словами, представление с сохранением состояния. Случайные изменения позиции были бы более дорогими, но линейное сканирование с начала строки может быть довольно быстрым, тем более что простой массив оптимизирован для локализации кеша. Типичный последовательный вывод будет быстрым, как и рендеринг.

Помимо уменьшения пространства, преимуществом переменного количества элементов на ячейку является повышенная гибкость: дополнительные атрибуты (например, 24-битный цвет), аннотации для определенных ячеек или диапазонов, глифы или
вложенные элементы DOM.

@PerBothner Спасибо за ваши идеи! Да, я уже тестировал макет одиночного плотного массива с арифметикой указателей, он показывает лучшее использование памяти. Проблемы возникают, когда дело доходит до изменения размера, это в основном означает перестройку всего фрагмента памяти (копирование) или быстрое копирование в более крупный фрагмент и выравнивание частей. Это довольно опыт. и imho не оправдано сохранением памяти (протестировано на некоторых площадках PR, перечисленных выше, экономия составила около ~ 10% по сравнению с новой реализацией буферной строки).

По поводу вашего второго комментария - мы уже обсуждали это, так как это упростит обработку переносимых строк. На данный момент мы решили использовать подход row X col для нового макета буфера и сделать это в первую очередь. Я думаю, что мы должны решить эту проблему еще раз, как только мы выполним реализацию изменения размера reflow.

О добавлении дополнительных материалов в буфер: в настоящее время мы делаем здесь то же, что и большинство других терминалов - продвижение курсора определяется wcwidth что обеспечивает совместимость с идеей pty / termios о том, как должны быть размещены данные. Это в основном означает, что мы обрабатываем на уровне буфера только такие вещи, как суррогатные пары и комбинирование символов. Любые другие правила соединения «более высокого уровня» могут применяться объединителем символов в рендерере (в настоящее время используются https://github.com/xtermjs/xterm-addon-ligatures для лигатур). У меня был открыт PR, чтобы также поддерживать графемы Unicode на раннем уровне буфера, но я думаю, что мы не можем сделать это на этом этапе, поскольку большинство бэкэндов pty не имеют об этом понятия (есть ли вообще?), И мы получим странные конгломераты char . То же самое касается реальной поддержки BIDI, я думаю, что графемы и BIDI лучше делать на этапе рендеринга, чтобы не повредить перемещение курсора / ячейки.

Поддержка узлов DOM, прикрепленных к ячейкам, звучит очень интересно, мне нравится эта идея. В настоящее время это невозможно при прямом подходе, поскольку у нас есть разные бэкенды рендерера (DOM, canvas 2D и новый блестящий рендерер webgl), я думаю, что этого все еще можно достичь для всех рендереров, разместив наложение там, где оно не поддерживается изначально (только рендерер DOM будет уметь делать это напрямую). Нам понадобится какой-то API на уровне буфера, чтобы объявить, что материал, его размер и рендерер могут выполнить грязную работу. Думаю, нам стоит обсудить / отследить это отдельным вопросом.

Спасибо за подробный ответ.

_ «Проблемы возникают, когда дело доходит до изменения размера, это в основном означает перестройку всего фрагмента памяти (копирование) или быстрое копирование в более крупный фрагмент и выравнивание частей.» _

Вы имеете в виду: при изменении размера нам придется копировать _4 * N_ элементов, а не только _N_ элементов?

Может иметь смысл, чтобы массив содержал все ячейки для логических (развернутых) строк. Например, предположим, что это строка из 180 символов и терминал шириной 80 столбцов. В этом случае у вас может быть 3 экземпляра BufferLine использующих один и тот же буфер _4 * 180_-element _data , но каждый BufferLine также будет содержать начальное смещение.

Ну, у меня все было в одном большом массиве, который был построен [cols] x [rows] x [needed single cell space] . Таким образом, он по-прежнему работал как «холст» с заданной высотой и шириной. Это действительно эффективно с точки зрения памяти и быстро для нормального потока ввода, но как только вызывается insertCell / deleteCell (изменение размера сделает это), вся память за позицией, где происходит действие пришлось бы переместить. Для небольшой прокрутки (<10k) это тоже не проблема, это действительно остановка для> 100k строк.
Обратите внимание, что текущий типизированный массив impl все еще должен выполнять эти сдвиги, но менее токсичен, поскольку ему нужно только перемещать содержимое памяти до конца строки.
Я думал о различных макетах, чтобы обойти дорогостоящие сдвиги, основное поле для сохранения бессмысленных сдвигов памяти должно было бы фактически отделить прокрутку от «горячих строк терминала» (самые последние до terminal.rows ), поскольку только они могут быть изменяется при переходе курсора и вставке / удалении.

Совместное использование базовой памяти несколькими объектами строки буфера - интересная идея для решения проблемы упаковки. Еще не уверен, как это может работать надежно без явной обработки ссылок и тому подобного. В другой версии я пытался делать все с явной обработкой памяти, но счетчик ссылок был настоящим препятствием и чувствовал себя неправильно в мире сборщиков мусора. (см. # 1633 для примитивов)

Изменить: кстати, явная обработка памяти была на одном уровне с текущим подходом «память на строку», я надеялся на лучшую производительность из-за лучшей локализации кеша, думаю, это было съедено немного большим опытом. обработка памяти в абстракции JS.

Поддержка узлов DOM, прикрепленных к ячейкам, звучит очень интересно, мне нравится эта идея. В настоящее время это невозможно при прямом подходе, поскольку у нас есть разные бэкенды рендерера (DOM, canvas 2D и новый блестящий рендерер webgl), я думаю, что этого все еще можно достичь для всех рендереров, разместив наложение там, где оно не поддерживается изначально (только рендерер DOM будет уметь делать это напрямую).

Это немного не по теме, но я вижу, что в конечном итоге у нас будут узлы DOM, связанные с ячейками в области просмотра, которые будут действовать аналогично слоям рендеринга холста. Таким образом, потребители смогут «украшать» ячейки с помощью HTML и CSS и им не нужно будет обращаться к Canvas API.

Может иметь смысл, чтобы массив содержал все ячейки для логических (развернутых) строк. Например, предположим, что это строка из 180 символов и терминал шириной 80 столбцов. В этом случае у вас может быть 3 экземпляра BufferLine, использующие один и тот же буфер _data из 4 * 180 элементов, но каждая BufferLine также будет содержать начальное смещение.

План перекомпоновки, который был упомянут выше, зафиксирован в https://github.com/xtermjs/xterm.js/issues/622#issuecomment -375403572, в основном мы хотим иметь фактический развернутый буфер, а затем представление сверху, которое управляет новые строки для быстрого доступа к любой данной строке (также оптимизация для горизонтального изменения размера).

Использование подхода с плотным массивом может быть чем-то, на что мы могли бы обратить внимание, но похоже, что это не будет стоить дополнительных накладных расходов на управление развернутыми разрывами строк в таком массиве и беспорядок, который возникает, когда строки обрезаются сверху буфер обратной прокрутки. Тем не менее, я не думаю, что мы должны рассматривать такие изменения до тех пор, пока # 791 не будет закончен, и мы не смотрим на # 622.

С PR # 1796 синтаксический анализатор получает поддержку типизированных массивов, что открывает дверь для нескольких дополнительных оптимизаций, с другой стороны, также и для других входных кодировок.

На данный момент я решил использовать Uint16Array , так как его легко конвертировать вперед и назад с помощью строк JS. Это в основном ограничивает игру UCS2 / UTF16, в то время как синтаксический анализатор в текущей версии также может обрабатывать UTF32 (UTF8 не поддерживается). Буфер терминала на основе типизированного массива в настоящее время размещен для UTF32, преобразование UTF16 -> UTF32 выполняется в InputHandler.print . Отсюда возможны несколько направлений:

  • сделать все UTF16, таким образом превратить буфер терминала в UTF16 тоже
    Да, еще не определено, какой путь здесь выбрать, но протестировал несколько макетов буферов и пришел к выводу, что 32-битное число дает достаточно места для хранения фактического charcode + wcwidth + возможное комбинированное переполнение (обрабатывается совершенно по-другому), в то время как 16-битное не может этого сделать. не жертвуя драгоценными битами кода. Обратите внимание, что нам даже с буфером UTF16 все равно нужно выполнить преобразование UTF32, поскольку wcwidth работает с кодовыми точками Unicode. Также обратите внимание, что буфер на основе UTF16 сэкономит больше памяти для более низких кодов, на самом деле более высоких, чем кодовые коды плоскости BMP, встречаются редко. Это все еще требует некоторого расследования.
  • сделать парсер UTF32
    Это довольно просто, просто замените все типизированные массивы 32-битным вариантом. Обратной стороной является то, что преобразование UTF16 в UTF32 должно быть выполнено заранее, это означает, что весь ввод будет преобразован, даже escape-последовательности, которые никогда не будут сформированы из любого charcode> 255.
  • сделать wcwidth UTF16 совместимым
    Да, если окажется, что UTF16 больше подходит для терминального буфера, это нужно сделать.

О UTF8: синтаксический анализатор в настоящее время не может обрабатывать собственные последовательности UTF8, в основном из-за того, что промежуточные символы конфликтуют с управляющими символами C1. Также UTF8 требует правильной обработки потока с дополнительными промежуточными состояниями, это неприятно, и имхо не следует добавлять в парсер. UTF8 будет лучше обрабатываться заранее и, возможно, с преобразованием прямо в UTF32 для упрощения обработки кодовых точек повсюду.

Что касается возможной входной кодировки UTF8 и компоновки внутреннего буфера, я провел грубый тест. Чтобы исключить гораздо большее влияние средства визуализации холста на общее время выполнения, я сделал это с помощью готовящегося к выпуску средства визуализации webgl. С моим тестом ls -lR /usr/lib я получаю следующие результаты:

  • текущий мастер + рендерер webgl:
    grafik

  • ветка игровая площадка, применяется # 1796, части # 1811 и рендерер webgl:
    grafik

Ветка детской площадки выполняет раннее преобразование из UTF8 в UTF32 перед синтаксическим анализом и сохранением (преобразование добавляет ~ 30 мс). Ускорение в основном достигается за счет двух горячих функций во время входного потока: EscapeSequenceParser.parse (120 мс против 35 мс) и InputHandler.print (350 мс против 75 мс). Оба они получают большую пользу от переключателя типизированного массива, экономя вызовы .charCodeAt .
Я также сравнил эти результаты с промежуточным типизированным массивом UTF16 - EscapeSequenceParser.parse немного быстрее (~ 25 мс), но InputHandler.print отстает из-за необходимого суррогатного связывания и поиска кодовой точки в wcwidth (120 мс).
Также обратите внимание, что я уже нахожусь на пределе, когда система может предоставить данные ls (i7 с SSD) - полученное ускорение увеличивает время простоя, а не ускоряет работу.

Резюме:
Imho, самая быстрая обработка ввода, которую мы можем получить, - это смесь транспорта UTF8 + UTF32 для представления буфера. В то время как транспорт UTF8 имеет лучшую скорость упаковки байтов для типичного ввода терминала и удаляет бессмысленные преобразования из pty через несколько уровней буферов до Terminal.write , буфер на основе UTF32 может довольно быстро хранить данные. Последний имеет немного больший объем памяти, чем UTF16, в то время как UTF16 немного медленнее из-за более сложной обработки символов с большим количеством косвенных обращений.

Заключение:
На данный момент мы должны использовать макет буфера на основе UTF32. Нам также следует подумать о переходе на входную кодировку UTF8, но для этого еще нужно подумать об изменениях API и последствиях для интеграторов (похоже, механизм ipc электрона не может обрабатывать двоичные данные без кодирования BASE64 и упаковки JSON, что будет противодействовать усилиям по перфокату).

Расположение буфера для предстоящей поддержки истинного цвета:

В настоящее время макет буфера на основе типизированного массива выглядит следующим образом (одна ячейка):

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

где attrs содержит все необходимые флаги + 9-битные цвета FG и BG. codepoint использует 21 бит (макс. 0x10FFFF для UTF32) + 1 бит для обозначения комбинирования символов и wcwidth 2 бита (диапазоны от 0 до 2).

Идея состоит в том, чтобы переставить биты для лучшей упаковки, чтобы освободить место для дополнительных значений RGB:

  • поместите wcwidth в неиспользуемые старшие биты codepoint
  • разделить атрибуты на группы FG и BG с 32 битами, распределить флаги на неиспользуемые биты
|             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) |

Достоинством этого подхода является относительно дешевый доступ к каждому значению за один доступ к индексу и max. 2-битные операции (и / или + сдвиг).

Объем памяти остается стабильным по сравнению с текущим вариантом, но все еще довольно велик - 12 байтов на ячейку. Это можно дополнительно оптимизировать, пожертвовав некоторой средой выполнения, переключившись на UTF16 и косвенно attr :

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

Теперь у нас осталось 4 байта на ячейку + немного места для атрибутов. Теперь attrs можно было использовать и для других ячеек. Ура, миссия выполнена! - Эм, секундочку ...

Сравнивая объем памяти, явно выигрывает второй подход. Не так для среды выполнения, есть три основных фактора, которые значительно увеличивают время выполнения:

  • attr косвенное обращение
    Указателю attr требуется еще один поиск в памяти в другом контейнере данных.
  • attr соответствие
    Чтобы действительно сэкономить место при втором подходе, данный атрибут должен быть сопоставлен с уже сохраненными атрибутами. Это громоздкое действие, прямой подход, просто просматривая все существующие значения, находится в O (n) для n сохраненных атрибутов, мои эксперименты с деревом RB закончились почти без улучшения памяти, все еще находясь в O (log n), по сравнению с индексный доступ в 32-битном подходе с O (1). Кроме того, дерево имеет худшее время выполнения для нескольких сохраненных элементов (окупается примерно> 100 записей с моим деревом RB).
  • Суррогатное сопряжение UTF16
    С 16-битным типизированным массивом мы должны перейти на UTF16 для кодовых точек, что также привело к штрафу во время выполнения (как описано в комментарии выше). Обратите внимание, что кодовые точки, превышающие BMP, вряд ли встречаются, но только проверка того, будет ли кодовая точка образовывать суррогатную пару, добавляет ~ 50 мс.

Сексуальность второго подхода - дополнительная экономия памяти. Поэтому я протестировал его с помощью ветки игрового поля (см. Комментарий выше) с измененной реализацией BufferLine :

grafik

Да, мы как бы вернулись к тому месту, с которого начали, прежде чем перейти к типизированным массивам UTF8 + в парсере. Однако использование памяти упало с ~ 1,5 МБ до ~ 0,7 МБ (демонстрационное приложение с 87 ячейками и 1000 строками с обратной прокруткой).

Отсюда вопрос экономии памяти по сравнению со скоростью. Поскольку мы уже сэкономили много памяти, переключившись с массивов js на типизированные массивы (упало с ~ 5,6 МБ до ~ 1,5 МБ для кучи C ++, отключив токсичное поведение кучи JS и сборщик мусора), я думаю, нам следует перейти к более быстрому варианту. Как только использование памяти снова станет насущной проблемой, мы все равно можем переключиться на более компактную структуру буфера, как описано во втором подходе здесь.

Я согласен, давайте оптимизируем скорость, если потребление памяти не вызывает беспокойства. Я также хотел бы избежать косвенного обращения, насколько это возможно, потому что это затрудняет чтение и поддержку кода. У нас уже есть довольно много концепций и настроек в нашей кодовой базе, из-за которых людям (включая меня 😅) сложно следить за потоком кода - и привлечение большего количества из них всегда должно быть оправдано очень веской причиной. ИМО экономия еще одного мегабайта памяти не оправдывает этого.

Тем не менее, мне очень нравится читать и учиться на ваших упражнениях, спасибо, что поделились ими с такими подробностями!

@mofux Да, это правда - сложность кода намного выше (суррогатное чтение UTF16, вычисление промежуточных кодовых точек, контейнер дерева с подсчетом ссылок на записи attr).
А поскольку 32-битный макет - это в основном плоская память (только объединение символов требует косвенного обращения), возможны дополнительные оптимизации (также часть # 1811, еще не протестированная для средства визуализации).

У косвенного обращения к объекту attr есть одно большое преимущество: он гораздо более расширяемый. Вы можете добавлять аннотации, глифы или собственные правила рисования. Вы можете хранить информацию о ссылках, возможно, более чистым и эффективным способом. Возможно, определите интерфейс ICellPainter который знает, как отображать свою ячейку, и на который вы также можете повесить пользовательские свойства.

Одна из идей - использовать два массива на BufferLine: массив Uint32Array и массив ICellPainter, по одному элементу для каждой ячейки. Текущий ICellPainter является свойством состояния анализатора, поэтому вы просто повторно используете один и тот же ICellPainter, пока состояние цвета / атрибута не меняется. Если вам нужно добавить в ячейку специальные свойства, вы сначала клонируете ICellPainter (если он может быть общим).

Вы можете заранее выделить ICellPainter для наиболее распространенных комбинаций цвета / атрибута - по крайней мере, иметь уникальный объект, соответствующий цветам / атрибутам по умолчанию.

Изменения стиля (например, изменение цвета переднего плана / фона по умолчанию) можно реализовать, просто обновив соответствующий экземпляр (-ы) ICellPainter, без необходимости обновлять каждую ячейку.

Возможны оптимизации: например, используйте разные экземпляры ICellPainter для символов одинарной и двойной ширины (или символов нулевой ширины). (Это экономит 2 бита в каждом элементе Uint32Array.) В Uint32Array есть 11 доступных битов атрибутов (больше, если мы оптимизируем для символов BMP). Их можно использовать для кодирования наиболее распространенных / полезных комбинаций цвета / атрибутов, которые можно использовать для индексации наиболее распространенных экземпляров ICellPainter. Если это так, массив ICellPainter может быть выделен лениво - т.е. только если для какой-либо ячейки в строке требуется «менее распространенный» ICellPainter.

Можно также удалить массив _combined для символов, отличных от BMP, и сохранить их в ICellPainter. (Для этого требуется уникальный ICellPainter для каждого символа, отличного от BMP, поэтому здесь есть компромисс.)

@PerBothner Да, косвенное

Несколько заметок о том, что я пробовал на нескольких испытательных стендах:

  • содержимое строки ячейки
    Исходя из C ++, я попытался взглянуть на проблему так же, как и в C ++, поэтому начал с указателей на контент. Это был простой указатель на строку, но в большинстве случаев он указывал на одну строку char. Какая трата. Поэтому моя первая оптимизация заключалась в том, чтобы избавиться от абстракции строки путем прямого сохранения кодовой точки вместо адреса (намного проще в C / C ++, чем в JS). Это почти удвоило доступ для чтения / записи при сохранении 12 байтов на ячейку (8 байтов указателя + 4 байта в строке, 64-битный с 32-битным wchar_t). Замечание: половина прироста скорости здесь связана с кешем (пропуски кеша из-за случайного расположения строк). Это стало ясно с помощью моего обходного пути для объединения содержимого ячеек - фрагмента памяти, который я проиндексировал, когда в codepoint был установлен комбинированный бит (здесь доступ был быстрее из-за лучшей локальности кеша, проверено с помощью valgrind). Перенесенный на JS прирост скорости был не таким большим из-за необходимого преобразования строки в число (хотя и быстрее), но экономия памяти была еще больше (предположим, из-за некоторого дополнительного пространства для управления типами JS). Проблема заключалась в глобальном StringStorage для комбинированного материала с явным управлением памятью, большой антипаттерн в JS. Быстрым исправлением для этого был объект _combined , который делегирует очистку сборщику мусора. Он по-прежнему может быть изменен, и, кстати, предназначен для хранения произвольного строкового содержимого, связанного с ячейками (сделано это с учетом графем, но мы не увидим их в ближайшее время, поскольку они не поддерживаются никаким сервером). Таким образом, это место для хранения дополнительного строкового содержимого для каждой ячейки.
  • attrs
    С помощью атрибутов я начал «мыслить масштабно» - с глобального AttributeStorage для всех атрибутов, когда-либо использовавшихся во всех экземплярах терминала (см. Https://github.com/jerch/xterm.js/tree/AttributeStorage). С точки зрения памяти это сработало довольно хорошо, в основном потому, что люди используют только небольшой набор атрибутов даже с поддержкой истинного цвета. Производительность была не такой хорошей - в основном из-за подсчета ссылок (каждая ячейка должна была дважды заглядывать в эту внешнюю память) и сопоставления attr. И когда я попытался адаптировать ссылку на JS, это было неправильно - я нажал кнопку «СТОП». Между тем оказалось, что мы уже сэкономили тонны памяти и вызовов GC, переключившись на типизированный массив, поэтому немного более дорогостоящая плоская компоновка памяти может окупить свое преимущество в скорости здесь.
    То, что я тестировал yday (последний комментарий), было вторым типизированным массивом на уровне строки для атрибутов с деревом из https://github.com/jerch/xterm.js/tree/AttributeStorage для сопоставления (в значительной степени похоже на вашу идею ICellPainter ). Что ж, результаты не обнадеживают, поэтому я пока склоняюсь к плоской 32-битной раскладке.

Теперь этот плоский 32-битный макет оптимизирован для обычных вещей, а необычные дополнения с ним невозможны. Правда. Что ж, у нас все еще есть маркеры (к ним не привыкли, поэтому я не могу сейчас сказать, на что они способны), и да, в буфере все еще есть свободные биты (что хорошо для будущих нужд, например, мы могли бы использовать их как флаги для особого обращения и тому подобное).

Мне жаль, что 16-битный макет с хранилищем attrs работает так плохо, сокращение использования памяти вдвое по-прежнему имеет большое значение (особенно, когда ppl начинает использовать линии прокрутки> 10k), но штраф во время выполнения и сложность кода перевешивают более высокий мем нуждается в атм имхо.

Не могли бы вы подробнее рассказать об идее ICellPainter? Возможно, я пока упустил какую-то важную особенность.

Моя цель для DomTerm состояла в том, чтобы активировать и поощрять более широкое взаимодействие, которое обеспечивается традиционным эмулятором терминала. Использование веб-технологий позволяет делать много интересного, поэтому было бы стыдно сосредоточиться на быстром традиционном эмуляторе терминала. Тем более, что многие варианты использования xterm.js (например, REPL для IDE) могут действительно выиграть от выхода за рамки простого текста. Xterm.js делает хорошо на стороне скорости (это кто - нибудь жалуется скорости?), Но это не так хорошо на особенности (люди жалуются не хватает TrueColor и встроенных график, например). Я думаю, что, возможно, стоит сосредоточиться немного больше на гибкости и немного меньше на производительности.

_ "Не могли бы вы подробнее рассказать об идее ICellPainter?" _

Как правило, ICellPainter инкапсулирует все данные для каждой ячейки, за исключением кода / значения символа, которые поступают из Uint32Array. Это для «обычных» символьных ячеек - для встроенных изображений и других «ящиков» код / ​​значение символа может не иметь смысла.

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

Сопоставление ячейки с ICellPainter можно выполнить различными способами. Очевидно, что каждая BufferLine должна иметь массив ICellPainter, но для этого требуется 8-байтовый указатель (как минимум) на каждую ячейку. Одна из возможностей - объединить массив _combined с массивом ICellPainter: если IS_COMBINED_BIT_MASK установлен, то ICellPainter также включает объединенную строку. Другая возможная оптимизация - использовать доступные биты в Uint32Array в качестве индекса в массиве: это добавляет некоторые дополнительные сложности и косвенность, но экономит место.

Я хотел бы призвать нас проверить, можем ли мы сделать это так, как это делает monaco-editor (я думаю, они нашли действительно умный и эффективный способ). Вместо того, чтобы хранить такую ​​информацию в буфере, они позволяют создавать decorations . Вы создаете украшение для диапазона строк / столбцов, и оно будет придерживаться этого диапазона:

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

Позже средство визуализации сможет подобрать эти украшения и нарисовать их.

Пожалуйста, ознакомьтесь с этим небольшим примером, который показывает, как выглядит api монако-редактора:
https://microsoft.github.io/monaco-editor/playground.html#interacting -with-the-editor-line-and-inline-decoration.

Для таких вещей, как рендеринг изображений внутри терминала, в Монако используется концепция зон обзора, которые можно увидеть (среди других концепций) в примере здесь:
https://microsoft.github.io/monaco-editor/playground.html#interacting -with-the-editor-listen-to-mouse-events

@PerBothner Thx за разъяснения и набросок. Несколько замечаний по этому поводу.

В конечном итоге мы планируем переместить входную цепочку + буфер в веб-воркер в будущем. Таким образом, буфер предназначен для работы на абстрактном уровне, и мы не можем использовать там какие-либо вещи, связанные с рендерингом / представлением, такие как метрики пикселей или какие-либо узлы DOM. Я вижу ваши потребности в этом из-за того, что DomTerm очень настраиваемый, но я думаю, что мы должны сделать это с помощью расширенного API внутренних маркеров и можем узнать здесь из monaco / vscode (спасибо за указатели th @mofux).
Я действительно хотел бы очистить основной буфер от необычных вещей, может, нам стоит обсудить возможные стратегии маркеров в новом выпуске?

Я все еще не удовлетворен результатами теста 16-битной разметки. Поскольку окончательное решение еще не принято (мы не увидим ничего из этого до 3.11), я продолжу тестировать его с некоторыми изменениями (это все еще более интересное решение для меня, чем 32-битный вариант).

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

Я также думаю, что для начала нам следует пойти с чего-то близкого к этому, мы можем изучить другие варианты позже, но это, вероятно, будет самым простым для запуска и запуска. Косвенность атрибутов определенно обещает IMO, поскольку обычно в терминальном сеансе не так много отдельных атрибутов.

Я хотел бы призвать нас проверить, можем ли мы сделать это так, как это делает monaco-editor (я думаю, они нашли действительно умный и эффективный способ). Вместо того, чтобы хранить такую ​​информацию в буфере, они позволяют создавать украшения. Вы создаете украшение для диапазона строк / столбцов, и оно будет придерживаться этого диапазона:

Что-то вроде этого я бы хотел увидеть. Одна из идей, которые у меня возникли в этом направлении, заключалась в том, чтобы позволить встраивающим устройствам прикреплять элементы DOM к диапазонам, чтобы можно было рисовать настраиваемые объекты. На данный момент я могу придумать 3 вещи, которые я хотел бы выполнить с помощью этого:

  • Нарисуйте подчеркивания ссылок таким образом (это значительно упростит их прорисовку)
  • Разрешить маркеры в строках, например * или что-то в этом роде
  • Разрешить строкам "мигать", чтобы указать, что что-то произошло

Все это может быть достигнуто с помощью наложения, и это довольно доступный тип API (открывающий узел DOM), который может работать независимо от типа средства визуализации.

Я не уверен, что мы хотим позволить встраивающим устройствам изменять способ рисования цветов фона и переднего плана.


@jerch Я поставлю это на веху 3.11.0, поскольку считаю, что эта проблема решена, когда мы удалим реализацию массива JS, которая запланирована на то время. https://github.com/xtermjs/xterm.js/pull/1796 также планируется объединить тогда, но эта проблема всегда предназначалась для улучшения структуры памяти буфера.

Кроме того, большую часть этого более позднего обсуждения, вероятно, было бы лучше провести на https://github.com/xtermjs/xterm.js/issues/484 и https://github.com/xtermjs/xterm.js/issues/1852. (создан, поскольку не было проблем с украшениями).

@Tyriar Woot - наконец-то закрыто: sweat_smile:

🎉 🕺 🍾

Была ли эта страница полезной?
0 / 5 - 0 рейтинги

Смежные вопросы

travisobregon picture travisobregon  ·  3Комментарии

LB-J picture LB-J  ·  3Комментарии

johnpoth picture johnpoth  ·  3Комментарии

pfitzseb picture pfitzseb  ·  3Комментарии

circuitry2 picture circuitry2  ·  4Комментарии