架构

如何在一台普通笔记本上保持流畅。

交易界面很重:漫长的历史、永不停歇的行情、一个页面上的多张图表。TradeCanvas 每一帧只做必要的工作,在主线程上完成,不含任何第三方代码。本页说明它是怎么做到的,以及还在哪些地方改进。

仍在进行中:引擎已经快到可以投入使用,但还没有达到它将来的速度。最后一节列出了我们已知尚待完成的部分。

01

小巧的包,单向依赖

每个包只依赖它下面的包。你的页面里不会运行 TradeCanvas 之外的任何代码:框架组件把 React、Vue 或 Svelte 作为 peer 依赖,WebGL 渲染器是一个独立的代码块,只在图表需要时才加载。

0 个第三方运行时依赖

02

一笔行情的路径

价格从你的数据源到达,在一帧之内呈现在屏幕上。每个阶段只处理发生变化的部分。

  1. 1你的数据源DataAdapterWebSocket、REST、经纪商:任何提供历史数据和数据流的来源。
  2. 2流管理器StreamManager先加载历史,再接入实时。过时请求的响应会被丢弃;数据源沉默超过 30 秒即可察觉。
  3. 3行情聚合器TickAggregator每笔行情进入容量为 1,000 的环形缓冲区,并合并到所属周期的K线中。
  4. 4指标IndicatorEngine从最后一根K线重新计算,而不是从序列开头。
  5. 5视口Viewport只遍历屏幕上的K线;价格刻度也只由它们决定。
  6. 6渲染循环RenderLoop无论到达多少笔行情,每个动画帧最多绘制一次场景。
  7. 7Canvas 2D 或 WebGLRenderEngine两层画布;开启 WebGL 时,下面还有一层 GPU 画布。
03

一笔行情只花一根K线的代价,而不是整段历史

当一笔行情改变了最后一根K线时,每个指标都会从这根K线(以及其公式需要回看的前几根)继续计算,而不是重算整个序列。如果它记住的K线已经对不上,就退回到完整重算,因此不会漏掉任何修正。

约 20 个指标有各自的增量更新,另有 31 个建立在逐点续算的基础之上。读取另一个指标线条的指标同样可以续算。

0 / 64 在 100,000 根K线、四个指标上测得: 每笔行情约 0.001 ms
04

缩小时:每像素一列

当K线窄于一个像素时,逐根绘制只是在做肉眼看不见的工作。蜡烛图、美国线、高低线和成交量按每像素一列绘制,取该列下所有K线的第一个开盘价、最高的最高价、最低的最低价和最后一个收盘价。折线和面积图每像素保留约两个点(LTTB);指标线和通道每个像素列绘制一段。

视图中的K线
313
每像素K线数
0.39
绘制的图形
313

一根K线永远不会窄于四分之一个 CSS 像素,因此一张容纳百万根K线的图表在完全缩小时每像素最多显示约四根K线:在 1,600 px 宽的图表上约为 6,400 根。百万K线基准测试衡量的正是平移浏览其余部分的表现。

05

两层画布,悬停几乎没有代价

场景(网格、序列、指标、画线、订单)位于一层画布上;十字光标、图例、坐标轴标签和倒计时位于其上方一层轻薄的画布上。移动鼠标只重绘顶层。数据更新则在下一帧重绘一次场景。

每一层全尺寸画布每帧都要让浏览器做一次合成,因此引擎尽量少用:仅用于 2D 绘制的后台画布只在有需要时才存在。

06

WebGL 靠记录,而不是重写

图表通过 Canvas 2D 调用来绘制。开启 WebGL 后,这些调用会交给一个记录器,由它把路径、填充和描边转换为以设备像素为单位的 GPU 命令。GPU 无法画得一模一样的内容会按顺序留在 Canvas 2D 上,因此两种方式画出的结果相同。

  • renderer: 'auto' 会向浏览器请求一个没有重大性能缺陷的上下文,因此在 WebGL 只能以软件方式运行的机器上会保留 Canvas 2D。
  • 一个页面上最多 8 张图表持有 WebGL 上下文(setMaxWebGLCharts);其余图表排队等待,并用 Canvas 2D 绘制。
  • 如果 GPU 丢失了上下文,图表会改用 Canvas 2D 继续运行,并在上下文恢复后重新用 WebGL 构建。
  • 蜡烛在着色器和 CPU 上按同一规则对齐到整数设备像素,因此两种渲染器会把它们画在相同的像素上。
07

为低配机器而设计

  • 默认使用 Canvas 2D在更多设备上完成测量之前,WebGL 需要手动开启。在高像素比的集成显卡上,瓶颈是浏览器自身的合成,而不是绘制。
  • 每帧只调整一次尺寸resize 事件会合并为每帧一次,尺寸没有变化时什么也不做。
  • 分页加载历史向回滚动时,较早的K线每次加载 500 根,一次只发一个请求,失败后会退避。
  • 没有加载闪烁加载遮罩会等待 200 ms,因此快速切换品种时不会闪烁。
  • 下载体积小无界面图表 gzip 后为 176 KB,完整组件为 315 KB,使用 WebGL 渲染器时再加 18 KB。
  • 原地刷新的报价板bo-grid 行情报价板把行情合并为每帧一次补丁,只重绘发生变化的单元格。
08

我们仍在改进的地方

引擎还没有完成。以下是我们从源码中了解到的开销,也是接下来要着手的工作。

  1. 指标在每次K线更新时都会更新,而不是每帧一次:每帧发送多笔行情的数据源,重算的次数会多于绘制的次数。
  2. K线以对象数组的形式保存。采用类型化数组的列式存储会占用更少内存,在超长历史上遍历也更快。
  3. 所有工作都在主线程上运行。代码中已有一个用于指标计算的 worker,但尚未接入图表。
  4. 深度热力图把快照保存在普通数组中,并在每一帧重新扫描所有档位以求最大值;成交量分布在每次绘制时都会分配其分桶。
  5. 有些对象仍在每一帧分配(路径、小数组),给垃圾回收器带来工作量。
  6. 在 2560×1400、像素比为 2 的集成显卡上,仅合成就需要约 23 ms,因此那里的 WebGL 帧平均为 29 ms。
在你的机器上测一测 →审计页会在你的浏览器中运行这些场景:Canvas 2D 与 WebGL 上的百万根K线、100,000 根K线上的实时行情,以及 1,000 个品种的报价板。