架構

如何在一般筆電上保持快速。

交易畫面很吃重:漫長的歷史資料、永不停歇的 tick、同一頁面上的多張圖表。TradeCanvas 每格畫面只做必要的工作,全在主執行緒上,不含任何第三方程式碼。本頁說明它是怎麼做到的,以及哪些地方仍在改進。

持續開發中:引擎已經快到可以上線,但還沒有達到它將來的速度。最後一節列出我們已知尚待處理的部分。

01

小巧的套件,單一方向

每個套件只相依於它下層的套件。TradeCanvas 以外的程式碼都不會在你的頁面中執行:框架元件將 React、Vue 或 Svelte 作為 peer 相依套件,而 WebGL 渲染器是獨立的區塊,只在圖表需要時才載入。

0 第三方執行期相依套件

02

一筆 tick 的路徑

價格從你的行情來源抵達,在一格畫面內就呈現在螢幕上。每個階段只處理有變動的部分。

  1. 1你的行情來源DataAdapterWebSocket、REST、券商:任何提供歷史資料與串流的來源。
  2. 2串流管理器StreamManager先載入歷史資料,再接上即時資料。舊請求的回應會被捨棄;行情來源沉寂時,30 秒內就會察覺。
  3. 3Tick 彙整器TickAggregator每筆 tick 先進入容量 1,000 的環形緩衝區,再併入其週期對應的K線。
  4. 4指標IndicatorEngine從最後一根K線重新計算,而不是從序列開頭。
  5. 5可視區Viewport只走訪螢幕上的K線;價格刻度也只根據它們計算。
  6. 6繪製迴圈RenderLoop不論抵達多少筆 tick,每個動畫影格最多只繪製一次場景。
  7. 7Canvas 2D 或 WebGLRenderEngine兩層 Canvas,開啟 WebGL 時其下方還有一層 GPU Canvas。
03

一筆 tick 只花一根K線的成本,而不是整段歷史

當一筆 tick 改變最後一根K線時,每個指標都從這根K線(以及其公式需要回看的前幾根)接續計算,而不是重算整個序列。如果它記住的K線已不再相符,就會退回完整重算,因此不會漏掉任何修正。

約 20 個指標有自己的增量更新,另有 31 個建立在可逐點接續的基礎上。讀取另一個指標線條的指標也能接續計算。

0 / 64 在 100,000 根K線與四個指標上量測: 每筆 tick 約 0.001 ms
04

縮小時:每像素一欄

當K線窄於一個像素時,逐根繪製只是在做肉眼看不到的工作。蠟燭圖、美國線、高低線與成交量會以每像素一欄繪製,取其下方K線的第一個開盤價、最高的最高價、最低的最低價與最後的收盤價。折線與面積圖每像素保留約兩個點(LTTB);指標線與通道則每個像素欄繪製一段。

畫面中的K線
313
每像素的K線數
0.39
繪製的圖形
313

K線的寬度永遠不會小於四分之一個 CSS 像素,因此一張含一百萬根K線的圖表縮小到最遠時,每像素最多約顯示四根K線:在 1,600 px 寬的圖表上約 6,400 根。平移瀏覽其餘部分,正是百萬K線基準測試所量測的內容。

05

兩層 Canvas,讓懸停幾乎不花成本

場景(格線、序列、指標、繪圖、委託)位於一層 Canvas;十字游標、圖例、座標軸標籤與倒數計時位於其上方一層輕量的 Canvas。移動滑鼠只會重繪上方那一層。資料更新則在下一格畫面重繪場景一次。

每一層全尺寸 Canvas 都會讓瀏覽器每格畫面多一次合成,因此引擎盡量讓層數少:只供 2D 繪製的後層 Canvas 只在有需要時才存在。

06

以記錄實現 WebGL,而非重寫

圖表以 Canvas 2D 呼叫繪製。開啟 WebGL 時,這些呼叫會送到記錄器,由它把路徑、填色與筆畫轉成以裝置像素為單位的 GPU 指令。GPU 無法畫得一模一樣的內容會依序留在 Canvas 2D 上,因此無論哪種方式,畫面都相同。

  • renderer: 'auto' 會向瀏覽器要求沒有重大效能警示的 WebGL 內容,因此 WebGL 只能以軟體執行的電腦會維持使用 Canvas 2D。
  • 同一頁面上最多 8 張圖表持有 WebGL 內容(setMaxWebGLCharts);其餘在佇列中等待,並以 Canvas 2D 繪製。
  • 如果 GPU 丟失了內容,圖表會繼續以 Canvas 2D 繪製,並在內容恢復時重新以 WebGL 建立。
  • 著色器與 CPU 以相同規則把K線取整到完整的裝置像素,因此兩種渲染器會把K線放在相同的像素上。
07

為效能較弱的電腦而打造

  • 預設使用 Canvas 2D在更多裝置上量測過之前,WebGL 需要自行開啟。在內建顯示晶片、高像素比的環境下,瓶頸是瀏覽器本身的合成,而不是繪製。
  • 每格畫面只調整一次大小調整大小的事件會合併為每格畫面一次,尺寸沒有變化時則什麼都不做。
  • 分頁載入歷史資料往回捲動時,更早的K線每次載入 500 根,一次只發一個請求,失敗後會延後重試。
  • 不會閃現載入畫面載入遮罩會等待 200 ms,因此快速切換商品時畫面不會閃爍。
  • 下載體積小headless 圖表經 gzip 後為 176 KB,完整元件為 315 KB,使用 WebGL 渲染器時再加 18 KB。
  • 原地跳動的看板bo-grid 報價看板把 tick 合併為每格畫面一次修補,並只重繪有變動的儲存格。
08

我們仍在改進的部分

引擎尚未完成。以下是我們從原始碼中得知的成本,也是接下來要處理的項目。

  1. 指標在每次K線更新時都會更新,而不是每格畫面一次:每格畫面送出多筆 tick 的行情來源,重算的次數會比繪製還多。
  2. K線以物件陣列保存。以型別陣列組成的欄式儲存可以用更少的記憶體,並在極長的歷史資料上走訪得更快。
  3. 所有工作都在主執行緒上執行。程式碼中已有計算指標用的 worker,但尚未接入圖表。
  4. 深度熱力圖把快照存放在一般陣列中,每格畫面都重新掃描每一檔以找出最大值;成交量分佈則在每次繪製時重新配置其區間。
  5. 部分物件仍在每格畫面配置(路徑、小陣列),會增加垃圾回收器的負擔。
  6. 在 2560×1400、像素比 2 的內建顯示晶片上,光是合成就要約 23 ms,因此那裡的 WebGL 畫面平均為 29 ms。
在你的電腦上量測 →稽核頁會在你的瀏覽器中執行這些場景:Canvas 2D 與 WebGL 上的一百萬根K線、100,000 根K線上的即時 tick,以及 1,000 檔商品的看板。