"沿路径点"d3可视化性能问题

Sar*_*ena 2 javascript performance transition d3.js

我通过以下代码浏览了"point-along-path"d3可视化:https://bl.ocks.org/mbostock/1705868.我注意到,虽然这一点正在沿着它的路径移动,但它消耗了7到11%的CPU使用率.

目前的情况,我有大约100条路径,在每条路径上,我将不得不将点(圆圈)从源移动到目的地.因此,随着更多点数同时移动,它占用了超过90%的CPU内存.

我试过:

                   function translateAlong(path) {
                      var l = path.getTotalLength();
                      return function(d, i, a) {
                          return function(t) {
                               var p = path.getPointAtLength(t * l);
                               return "translate(" + p.x + "," + p.y + ")";
                          };
                       };
                    }

                    // On each path(100 paths), we are moving circles from source to destination.
                    var movingCircle = mapNode.append('circle')
                        .attr('r', 2)
                        .attr('fill', 'white')

                    movingCircle.transition()
                        .duration(10000)
                        .ease("linear")
                        .attrTween("transform", translateAlong(path.node()))
                        .each("end", function() {
                            this.remove();
                        });
Run Code Online (Sandbox Code Playgroud)

那么什么应该是降低CPU使用率的更好方法呢?谢谢.

And*_*eid 7

有一些方法,其潜在功效差异很大.

最终,您在每个动画帧进行昂贵的操作,以计算每个点的新位置并重新渲染它.因此,应尽一切努力降低这些操作的成本.

如果帧速率低于60,则可能意味着我们正在接近CPU容量.我使用下面的帧速率来帮助指示CPU容量,因为它比CPU使用率更容易测量(并且可能更少侵入性).

我有这种方法的各种图表和理论,但一旦打字,它似乎应该是直观的,我不想详述它.

本质上,我们的目标是最大限度地提高每秒60帧的转换次数 - 这样我就可以缩减转换次数并获得CPU容量.


好的,让我们以每秒60帧的速度沿100多个路径运行超过100个节点的转换.

D3v4

首先,d3v4可能在这里提供一些好处.v4同步转换,似乎具有稍微改善的时间效果.d3.transition在任何情况下都是非常有效和低成本的,所以这不是最有用的 - 但升级并不是一个坏主意.

通过使用不同形状的节点,通过变换或通过cx,cy等进行定位,还可以获得较小的浏览器特定增益.我没有实现任何这些,因为增益相对微不足道.

帆布

其次,SVG移动速度不够快.操作DOM需要时间,其他元素会减慢操作速度并占用更多内存.从编码角度来看,我认为画布不太方便,但是对于这种任务,画布比SVG更快.使用分离的圆元素表示每个节点(与路径相同),并转换它们.

通过绘制两个画布节省更多时间:一个绘制一次并保持路径(如果需要),另一个重绘每个帧显示点.通过将每个圆的基准设置为其所在路径的长度来节省更多时间:无需每次都调用path.getTotalLength().

也许像这样

画布简化线

第三,我们仍然有一个具有SVG路径的分离节点,所以我们可以使用path.getPointAtLength()- 这实际上非常有效.减慢这一点的一个主要问题是使用曲线.如果你能做到,画直线(多段很好) - 差别很大.

作为进一步的奖励,使用context.fillRect()而不是context.arc()

纯JS和画布

最后,D3和每个路径的分离节点(所以我们可以使用path.getTotalLength())可以开始阻碍.如果需要使用类型数组,context.imageData和您自己的路径上的节点定位公式.这是一个快速简单的例子(100 000个节点,500 000个节点,1 000 000个节点(Chrome最适合这个,可能的浏览器限制.由于路径现在基本上为整个画布着色为纯色我不会显示它们但是在我的慢速系统上,它们可以以每秒10帧的速度转换70万个节点.将这700万个转换定位计算和渲染/秒与大约7千个过渡定位计算和渲染/秒进行比较我得到的是d3v3和SVG (三个数量级的差异):

在此输入图像描述

画布A是曲线(基数)和圆形标记(上面的链接),画布B是直线(多段)线和方形标记.

正如您可能想象的那样,如果只渲染100个节点,那么可以以每秒60帧的速度渲染1000个转换节点的机器和脚本将具有相当大的额外容量.

如果转换位置和渲染计算是主要活动且CPU使用率为100%,则一半节点应释放大约一半的CPU容量.在上面最慢的画布示例中,我的机器记录了200个节点沿着基本曲线以每秒60帧的速度转换(然后它开始下降,表明CPU容量限制了帧速率,因此使用率应该接近100%),有100个节点我们有一个令人满意的~50%CPU使用率:

在此输入图像描述

水平中心线占CPU使用率的50%,转换重复6次

但是,通过丢弃复杂的基数曲线可以找到关键的节省 - 如果可能的话,使用直线.另一个重要的节省是通过自定义脚本来实现目的.

将上面的内容与直线(多段静止)和方形节点进行比较:

在此输入图像描述

同样,水平中心线占CPU使用率的50%,转换重复6次

以上是1000个3段路径上的1000个过渡节点 - 比曲线和圆形标记更好一个数量级.

其他选择

这些可以与上述方法结合使用.

不要为每个滴答点设置动画

如果您无法在下一个动画帧之前定位所有节点的每个转换节拍,那么您将使用接近所有CPU容量.一种选择是不要每个节点定位每个节点 - 你不必.这是一个复杂的解决方案 - 但每个圆圈定位三分之一的圆圈 - 每个圆圈仍然可以定位每秒20帧(相当平滑),但每帧的计算量是其他情况下的1/3.对于画布,您仍然需要渲染每个节点 - 但您可以跳过计算三分之二节点的位置.对于SVG,这有点容易,因为您可以修改d3-transition以包含一个every()方法,该方法设置在重新计算转换值之前经过的滴答数(以便每个滴答转换三分之一).

高速缓存

根据具体情况,缓存也不是一个坏主意 - 但所有计算(或数据加载)的前端可能会导致动画开始时出现不必要的延迟 - 或首次运行缓慢.这种方法确实为我带来了积极的结果,但在另一个答案中讨论,所以我不会在这里讨论.


ibr*_*cin 5

帖子编辑:

  • 是默认值。(在 2.7Ghz i7 时 100 点的峰值 CPU 约为 %99)
  • 是我的版本。(2.7Ghz i7 时 100 点的峰值 CPU 约为 20%)

平均而言,我快了 5 倍。

我认为这里的瓶颈是getPointAtLength每 17 毫秒调用一次方法。如果必须的话,我也会避免冗长的字符串连接,但在你的情况下它并不长,所以我认为最好的方法是:

  • 预先缓存点并且只用给定的分辨率计算一次(我在这里分为 1000 个部分)
  • 减少对 requestAnimationFrame 中的 DOM 方法的调用(您看到该函数接收规范化的 t 参数)

在默认情况下,有 2 个调用,1 个调用时调用getPointAtLength,然后在设置翻译时调用另一个(在引擎盖下)。

你可以用translateAlong下面的替换:

 function translateAlong(path){
    var points  = path.__points || collectPoints(path);
    return function (d,i){
        var transformObj = this.transform.baseVal[0];
        return function(t){
            var index = t * 1000 | 0,
                point = points[index];
            transformObj.setTranslate(point[0],point[1]);
        }
    }
}
function collectPoints(path) {
    var l = path.getTotalLength(),
        step = l*1e-3,
        points = [];
    for(var i = 0,p;i<=1000;++i){
        p = path.getPointAtLength(i*step);
        points.push([p.x,p.y]);
    }
    return path.__points = points;
}
Run Code Online (Sandbox Code Playgroud)

以及对该补间线的小修改:

.tween("transform", translateAlong(path.node()))
Run Code Online (Sandbox Code Playgroud)

不需要设置attr,调用它就足够了。结果如下:

http://jsfiddle.net/ibowankenobi/8kx04y29/

告诉我它是否确实有所改善,因为我不是 100% 确定。