Architecture

Comment il reste rapide sur un portable ordinaire.

Les écrans de trading sont lourds : de longs historiques, des ticks qui ne s’arrêtent jamais, beaucoup de graphiques sur une page. TradeCanvas ne fait que le travail dont chaque image a besoin, sur le thread principal, sans code tiers. Cette page montre comment, et où il reste à l’améliorer.

Travail en cours : le moteur est assez rapide pour la production, mais pas encore aussi rapide qu’il le sera. La dernière section liste ce qu’il reste à faire.

01

De petits paquets, dans un seul sens

Chaque paquet ne dépend que de ceux qui sont en dessous. Rien d’extérieur à TradeCanvas ne s’exécute dans votre page : les composants de framework prennent React, Vue ou Svelte en peer, et le moteur de rendu WebGL est un chunk à part, chargé seulement quand un graphique le demande.

0 dépendance tierce à l’exécution

02

Le chemin d’un tick

Un prix arrive de votre flux et atteint l’écran en une image. Chaque étape ne traite que ce qui a changé.

  1. 1Votre fluxDataAdapterWebSocket, REST, un courtier : tout ce qui fournit un historique et un flux continu.
  2. 2Gestionnaire de fluxStreamManagerL’historique d’abord, puis le direct. Les réponses à une ancienne requête sont ignorées ; un flux muet est repéré en 30 s.
  3. 3Agrégateur de ticksTickAggregatorLes ticks entrent dans un tampon circulaire de 1 000 et sont intégrés à la barre de leur unité de temps.
  4. 4IndicateursIndicatorEngineRecalculés depuis la dernière barre, pas depuis le début de la série.
  5. 5ViewportViewportSeules les barres à l’écran sont parcourues ; l’échelle des prix vient d’elles seules.
  6. 6Boucle de renduRenderLoopQuel que soit le nombre de ticks reçus, la scène est dessinée au plus une fois par image d’animation.
  7. 7Canvas 2D ou WebGLRenderEngineDeux canvas, et un canvas GPU en dessous quand WebGL est activé.
03

Un tick coûte une barre, pas tout l’historique

Quand un tick modifie la dernière barre, chaque indicateur reprend à partir de cette barre (et des quelques barres précédentes que sa formule consulte) au lieu de recalculer la série. Si les barres qu’il a mémorisées ne correspondent plus, il repasse à un recalcul complet : une correction n’est jamais manquée.

Une vingtaine d’indicateurs ont leur propre mise à jour incrémentale, et 31 autres reposent sur une base qui reprend point par point. Les indicateurs qui lisent la ligne d’un autre indicateur reprennent aussi.

0 / 64 Mesuré sur 100 000 barres avec quatre indicateurs: environ 0,001 ms par tick
04

En vue d’ensemble : une colonne par pixel

Quand les barres deviennent plus étroites qu’un pixel, dessiner chacune gaspille un travail que l’œil ne voit pas. Bougies, barres, haut-bas et volume sont dessinés en une colonne par pixel, avec la première ouverture, le plus haut, le plus bas et la dernière clôture des barres qu’elle couvre. Les lignes et les aires gardent environ deux points par pixel (LTTB) ; les lignes et bandes d’indicateurs dessinent un trait par colonne de pixels.

barres visibles
313
barres par pixel
0,39
formes dessinées
313

Une barre n’est jamais plus étroite qu’un quart de pixel CSS : un graphique d’un million de barres en montre donc au plus environ quatre par pixel en vue d’ensemble complète, soit environ 6 400 sur un graphique de 1 600 px. Faire défiler le reste, c’est ce que mesure le benchmark du million de barres.

05

Deux canvas, pour qu’un survol ne coûte presque rien

La scène (grille, série, indicateurs, dessins, ordres) vit sur un canvas ; le réticule, la légende, les étiquettes des axes et le compte à rebours vivent sur un canvas léger au-dessus. Bouger la souris ne redessine que celui du dessus. Une mise à jour des données redessine la scène une fois, à l’image suivante.

Chaque canvas pleine taille coûte au navigateur une composition par image : le moteur en garde donc peu. Un canvas d’arrière-plan pour le dessin en 2D seul n’existe que tant que quelque chose en a besoin.

06

WebGL par enregistrement, pas par réécriture

Le graphique dessine avec des appels Canvas 2D. Avec WebGL activé, ces appels vont à un enregistreur qui transforme tracés, remplissages et contours en commandes GPU, en pixels de l’appareil. Ce que le GPU ne sait pas dessiner à l’identique reste en Canvas 2D, dans l’ordre : l’image est la même dans les deux cas.

  • renderer: 'auto' demande au navigateur un contexte sans réserve majeure de performance : une machine dont le WebGL tournerait en logiciel garde Canvas 2D.
  • Au plus 8 graphiques d’une page détiennent un contexte WebGL (setMaxWebGLCharts) ; les autres attendent dans une file et dessinent en Canvas 2D.
  • Si le GPU perd le contexte, le graphique continue en Canvas 2D et se reconstruit en WebGL quand le contexte revient.
  • Les bougies sont arrondies aux pixels entiers de l’appareil avec la même règle dans les shaders et sur le CPU : les deux moteurs de rendu les placent sur les mêmes pixels.
07

Conçu pour les machines modestes

  • Canvas 2D par défautWebGL reste optionnel tant qu’il n’a pas été mesuré sur davantage d’appareils. Sur une puce graphique intégrée à ratio de pixels élevé, le goulot d’étranglement est la composition du navigateur lui-même, pas le dessin.
  • Un redimensionnement par imageLes événements de redimensionnement sont regroupés en un seul par image, et une taille qui n’a pas changé ne déclenche rien.
  • L’historique par pagesLes barres plus anciennes se chargent par 500 quand vous remontez dans le temps, une requête à la fois, avec un délai croissant après un échec.
  • Pas de clignotement au chargementLe voile de chargement attend 200 ms : un changement de symbole rapide ne clignote jamais.
  • Léger à téléchargerLe graphique headless pèse 176 KB compressé en gzip, le widget complet 315 KB, et le moteur de rendu WebGL 18 KB de plus quand il sert.
  • Des tableaux qui se mettent à jour sur placeLe tableau des cours bo-grid regroupe les ticks en un correctif par image et ne redessine que les cellules qui ont changé.
08

Ce que nous améliorons encore

Le moteur n’est pas terminé. Voici les coûts que nous connaissons, relevés dans le code source ; ce sont les prochains chantiers.

  1. Les indicateurs se mettent à jour à chaque mise à jour de barre, pas une fois par image : un flux qui envoie beaucoup de ticks par image recalcule plus souvent qu’il ne dessine.
  2. Les barres sont stockées dans un tableau d’objets. Un stockage en colonnes de tableaux typés utiliserait moins de mémoire et se parcourrait plus vite sur de très longs historiques.
  3. Tout s’exécute sur le thread principal. Un worker pour le calcul des indicateurs existe dans le code, mais n’est pas encore branché au graphique.
  4. La heatmap de profondeur garde ses instantanés dans un tableau simple et parcourt tous les niveaux pour trouver le maximum à chaque image ; le profil de volume alloue ses tranches à chaque dessin.
  5. Certains objets sont encore alloués à chaque image (tracés, petits tableaux), ce qui donne du travail au ramasse-miettes.
  6. En 2560×1400 à un ratio de pixels de 2 sur une puce graphique intégrée, la composition prend à elle seule environ 23 ms : les images WebGL y durent donc 29 ms en moyenne.
Mesurez-le sur votre machine →L’audit lance ces scènes dans votre navigateur : un million de barres en Canvas 2D et en WebGL, des ticks en direct sur 100 000 barres et un tableau de 1 000 symboles.