Архитектура

Как он остаётся быстрым на обычном ноутбуке.

Торговые экраны тяжелы: длинная история, тики без остановки, много графиков на одной странице. TradeCanvas делает ровно столько работы, сколько нужно каждому кадру, в основном потоке, без стороннего кода. Эта страница показывает, как, и где работа ещё идёт.

Работа продолжается: движок достаточно быстр для продакшена, но ещё не так быстр, как будет. В последнем разделе перечислено, что осталось.

01

Маленькие пакеты, одно направление

Каждый пакет зависит только от тех, что ниже него. На вашей странице не работает ничего, кроме TradeCanvas: компоненты для фреймворков берут React, Vue или Svelte как peer-зависимость, а рендерер WebGL — отдельный чанк, который загружается, только когда его запрашивает график.

0 сторонних runtime-зависимостей

02

Путь тика

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

  1. 1Ваш потокDataAdapterWebSocket, REST, брокер: всё, у чего есть история и поток.
  2. 2Менеджер потокаStreamManagerСначала история, потом живые данные. Ответы на устаревший запрос отбрасываются; замолчавший поток замечается в пределах 30 с.
  3. 3Агрегатор тиковTickAggregatorТики попадают в кольцевой буфер на 1000 и сворачиваются в бар своего таймфрейма.
  4. 4ИндикаторыIndicatorEngineПересчитываются с последнего бара, а не с начала серии.
  5. 5Область просмотраViewportОбходятся только бары на экране; ценовая шкала строится только по ним.
  6. 6Цикл отрисовкиRenderLoopСколько бы тиков ни пришло, сцена рисуется не чаще одного раза за кадр анимации.
  7. 7Canvas 2D или WebGLRenderEngineДва canvas, а под ними GPU-canvas, когда включён WebGL.
03

Тик стоит одного бара, а не всей истории

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

Около 20 индикаторов имеют собственное инкрементальное обновление, а ещё 31 построен на основе, которая продолжает расчёт точка за точкой. Индикаторы, читающие линию другого индикатора, тоже продолжают расчёт.

0 / 64 Измерено на 100 000 баров с четырьмя индикаторами: около 0,001 мс на тик
04

При отдалении: один столбец на пиксель

Когда бары становятся уже пикселя, рисовать каждый из них — пустая работа, которую глаз не увидит. Свечи, бары, максимум-минимум и объём рисуются одним столбцом на пиксель: с первым открытием, наибольшим максимумом, наименьшим минимумом и последним закрытием баров под ним. Линии и области сохраняют около двух точек на пиксель (LTTB); линии и полосы индикаторов рисуют один отрезок на столбец пикселей.

баров в поле зрения
313
баров на пиксель
0,39
нарисовано фигур
313

Бар никогда не бывает уже четверти CSS-пикселя, поэтому график с миллионом баров при максимальном отдалении показывает не больше четырёх баров на пиксель: около 6400 на графике шириной 1600 px. Прокрутку по остальным и измеряет бенчмарк на миллион баров.

05

Два canvas, чтобы наведение почти ничего не стоило

Сцена (сетка, серия, индикаторы, рисунки, ордера) живёт на одном canvas; перекрестие, легенда, подписи осей и обратный отсчёт — на тонком canvas над ним. Движение мыши перерисовывает только верхний. Обновление данных перерисовывает сцену один раз, в следующем кадре.

Каждый полноразмерный canvas стоит браузеру одного смешивания на кадр, поэтому движок держит их число небольшим: задний canvas для чисто 2D-рисования существует, только пока он кому-то нужен.

06

WebGL через запись, а не через переписывание

График рисует вызовами Canvas 2D. С включённым WebGL эти вызовы попадают в рекордер, который превращает пути, заливки и обводки в команды GPU в физических пикселях. Всё, что GPU не может нарисовать так же, остаётся на Canvas 2D, по порядку, так что картинка одинакова в обоих случаях.

  • renderer: 'auto' просит у браузера контекст без серьёзного ограничения производительности, поэтому машина, где WebGL работал бы программно, остаётся на Canvas 2D.
  • Контекст WebGL держат не более 8 графиков на странице (setMaxWebGLCharts); остальные ждут в очереди и рисуются через Canvas 2D.
  • Если GPU теряет контекст, график продолжает работать на Canvas 2D и перестраивается на WebGL, когда контекст возвращается.
  • Свечи округляются до целых физических пикселей по одному правилу в шейдерах и на CPU, поэтому оба рендерера ставят их на одни и те же пиксели.
07

Создан для слабых машин

  • Canvas 2D по умолчаниюWebGL включается по желанию, пока его не измерили на большем числе устройств. На встроенной графике с высокой плотностью пикселей узкое место — композиция в самом браузере, а не отрисовка.
  • Одно изменение размера на кадрСобытия resize объединяются в одно на кадр, а неизменившийся размер ничего не запускает.
  • История страницамиСтарые бары подгружаются по 500 при прокрутке назад, по одному запросу за раз, с паузой после сбоя.
  • Без мигания при загрузкеВуаль загрузки ждёт 200 мс, поэтому быстрая смена символа никогда не мигает.
  • Небольшой размер загрузкиHeadless-график весит 176 KB после gzip, полный виджет — 315 KB, рендерер WebGL при использовании — ещё 18 KB.
  • Табло, обновляемые на местеТабло котировок bo-grid объединяет тики в одно обновление на кадр и перерисовывает только изменившиеся ячейки.
08

Что мы ещё улучшаем

Движок не закончен. Вот известные нам по исходному коду издержки, и именно ими мы займёмся дальше.

  1. Индикаторы обновляются при каждом обновлении бара, а не раз за кадр: поток, присылающий много тиков за кадр, пересчитывает чаще, чем рисует.
  2. Бары хранятся как массив объектов. Столбцовое хранилище из типизированных массивов занимало бы меньше памяти и обходилось бы быстрее на очень длинной истории.
  3. Всё работает в основном потоке. Воркер для расчёта индикаторов есть в коде, но к графику пока не подключён.
  4. Тепловая карта стакана хранит снимки в обычном массиве и в каждом кадре заново ищет максимум по всем уровням; профиль объёма выделяет свои корзины при каждой отрисовке.
  5. Некоторые объекты всё ещё создаются в каждом кадре (пути, небольшие массивы), что даёт работу сборщику мусора.
  6. При 2560×1400 и плотности пикселей 2 на встроенной графике одна только композиция занимает около 23 мс, поэтому кадры WebGL там в среднем длятся 29 мс.
Измерьте на своём компьютере →Аудит запускает эти сцены в вашем браузере: миллион баров на Canvas 2D и WebGL, живые тики на 100 000 баров и табло на 1000 символов.