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

4
我已经通过以下代码了解了“点沿路径”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();
                        });

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


我想在点的转换发生时减少CPU使用率。 - Sarthak Saxena
4个回答

10

有几种方法可以解决这个问题,这些方法在潜在功效上差异很大。

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

如果帧率低于60,则可能意味着我们接近了CPU容量极限。我使用下面的帧率来指示CPU容量,因为它比CPU使用更易测量(而且可能不太具有侵入性)。

我对这种方法有各种各样的图表和理论,但是一旦打完字,它似乎应该是直观的,我不想纠缠于此。

本质上,目标是最大化60帧每秒可以显示多少转换 - 这样我就可以降低过渡的数量并获得CPU容量。


好的,让我们以60帧每秒运行100个以上节点沿着100个以上路径的转换。

D3v4

首先,d3v4可能在这里提供某些好处。v4同步转换,这似乎已经产生了略微改进的效果。无论如何,d3.transition非常有效且低成本,因此这不是最有用的建议 - 但升级也不是一个坏主意。

还可以通过使用不同形状的节点、通过变换或cx、cy等位置来获得一些浏览器特定的小收益。我没有实现任何这些,因为增益相对较小。

画布

其次,SVG移动速度太慢了。操作DOM需要时间,额外的元素会减慢操作并占用更多内存。我知道从编码角度来看,Canvas可能不太方便,但在这种任务中,Canvas比SVG更快。用分离的圆形元素表示每个节点(与路径相同),并对它们进行转换。

通过绘制两个画布来节省更多时间:一个画布用于绘制路径(如果需要),另一个画布在每个帧上重绘显示点。通过将每个圆的datum设置为其所在路径的长度,可以节省更多时间:不需要每次调用path.getTotalLength()。

可能像这样

画布简化线条

第三、我们仍然有一个带有SVG路径的分离节点,因此我们可以使用path.getPointAtLength() - 这实际上非常有效。但是减速这个过程的一个主要问题是使用了曲线。如果可以的话,请绘制直线(多个线段也可以)- 差异是显着的。

作为另外的奖励,请使用context.fillRect()而不是context.arc()

纯JS和Canvas

最后,D3和每个路径的分离节点(以便我们可以使用 path.getTotalLength() )可能开始妨碍操作。如果需要,请使用类型化数组、context.imageData和您自己的公式来定位路径上的节点。这是一个简单的骨架示例(100,000个节点, 500,000个节点, 1,000,000个节点 (Chrome是最佳选择,可能存在浏览器限制。由于路径现在基本上将整个画布涂成了纯色,所以我不会显示它们,但节点仍然沿着路径移动)。这些节点可以以10帧/秒的速度过渡70万个节点,在我的慢系统上比较那些7百万的过渡定位计算和渲染/秒,与我使用d3v3和SVG得到的大约7千个过渡定位计算和渲染/秒相比(相差三个数量级):

enter image description here

画布A是使用曲线(cardinal)和圆形标记,(链接在上面),画布B是使用直线(多段)和正方形标记。

你可以想象一下,一个可以以60帧/秒渲染1000个过渡节点的机器和脚本如果只渲染100个节点,将会有相当多的额外容量。

如果转换位置和渲染计算是主要活动并且CPU使用率已达到100%,那么减半节点数量应该大致释放出一半的CPU容量。在上面最慢的画布示例中,我的计算机以60帧/秒记录了沿着cardinal曲线移动的200个节点(然后开始下降,表明CPU容量限制了帧速率,并且因此使用率应该接近100%),而100个节点时我们有一个愉快的~50%的CPU使用率:

enter image description here

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

但是主要的节省来自于放弃复杂的cardinal曲线 - 如果可能,使用直线。其他主要节省来自于定制您的脚本以用途为目的。

与直线(多段)和正方形节点进行比较:

enter image description here

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

以上是1000个过渡节点在1000个3段路径上-比使用曲线和圆形标记好一个数量级以上。

其他选项

这些可以与上述方法结合使用。不要在每个Tick中动画化每个点

如果你不能在下一帧动画之前每个过渡Tick中定位所有节点,那么你将使用接近于CPU容量的全部资源。一个选择是不必在每个Tick中定位每个节点 - 你没有必要这样做。这是一个复杂的解决方案 - 但是每个Tick仅定位三分之一的圆形 - 每个圆仍然可以每秒定位20帧(非常顺滑),但每帧计算量是原来的1/3。对于Canvas,你仍然需要呈现每个节点 - 但你可以跳过计算2/3的节点的位置。对于SVG,这更容易一些,因为你可以修改d3-transition并包含一个every()方法,它设置多少个tick在重新计算过渡值之前经过(以便每个Tick都能过渡三分之一)。

缓存

根据情况,缓存也不是一个坏主意 - 但所有计算的前端(或数据加载)可能会导致动画延迟的开始 - 或者第一次运行变慢。这种方法确实给我带来了积极的结果,但在另一个回答中已经讨论过了,所以我不会在这里详细介绍。


很好的回答。我在Canvas+JS代码中发现了50%的速度提升(在我的机器上)。请查看我的回答编辑。 - rioV8
我在项目中使用getTotalLength()时遇到了严重的性能问题,你提供用直线绘制曲线的提示解决了这个问题。性能得到了巨大的提升!特别是在移动设备上。谢谢! - evry.one
使用类似d3的过渡效果,您只能访问[0,1]中的缓动时间。有没有办法将其转换为刻度数,以便您可以仅在某些刻度上选择性地更新元素? - BallpointBen

5

帖子编辑:

  • 这里是默认设置。(在2.7 GHz i7上100个点的CPU峰值约为99%)
  • 这里是我的版本。(在2.7 GHz i7上100个点的CPU峰值约为20%)

平均而言,我快了5倍。

我认为瓶颈在于每17ms调用一次getPointAtLength方法。如果必须进行长字符串连接,我也会避免,但在您的情况下它并不太长,所以我认为最好的方法是:

  • 预先缓存点,并仅使用给定分辨率(我在此将其分成了1000部分)计算一次
  • 减少requestAnimationFrame中对DOM方法的调用(您看到接收标准化t参数的函数)

在默认情况下,有两个调用,一个是当您调用getPointAtLength时,另一个是在您设置translate(在幕后)时。

您可以将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;
}

对于那个缓动的代码,只需进行小幅修改:

.tween("transform", translateAlong(path.node()))

设置属性并不是必要的,调用它就足够了。以下是结果: http://jsfiddle.net/ibowankenobi/8kx04y29/ 如果有改进,请告诉我,因为我不确定。

我发布后不到10秒钟就立即获得了-1。尽管从我的观察来看(iOS),这是一个可行的解决方案。 - ibrahim tanyalcin
感谢@Ibrahim提供的优化解决方案,这将我的CPU使用率降低到30%。 - Sarthak Saxena
你的解决方案在“Internet Explorer”中出现了一个问题。问题是点沿着路径不移动,因为“transformObj”变成了未定义,所以它会给出错误:“无法获取未定义或空引用的属性'setTranslate'”。否则,在其他浏览器中它可以正常工作。你能帮忙解决吗?提前感谢。 - Sarthak Saxena

1

实现这个功能的另一种方法可能是使用svg:animateMotion,你可以用它来沿着给定的路径移动一个元素。这里有一个来自docs的例子。本质上,你想要:

<svg>
  <path id="path1" d="...">
    <circle cx="" cy="" r="10" fill="red">
      <animateMotion dur="10s" repeatCount="0">
        <mpath xlink:href="#path1" />
      </animateMotion>
    </circle>
  </path>
</svg>

我没有对其进行剖析,但我认为你很难获得比使用SVG本身内置的更好的性能。

浏览器支持

请注意,经过@ibrahimtanyalcin的评论后,我开始检查浏览器兼容性。结果发现这在IE或Microsoft Edge中不受支持。


谁给我点了踩——如果您能在评论中解释下踩的原因,那么我就可以改进我的回答了。 - Ian
Ian,相信我,我也有同感。我喜欢这个解决方案,因为它很简单。我会点赞来平衡一下,但是我听说谷歌计划废弃SMIL,这是真的吗? - ibrahim tanyalcin
@ibrahimtanyalcin,老实说,我认为你比我更了解这个问题。但是根据https://groups.google.com/a/chromium.org/forum/#!topic/blink-dev/5o0yiO440LM%5B126-150%5D的说法,他们似乎改变了主意。话虽如此,我不确定IE / Edge的兼容性,所以我会更新我的答案来反映这一点。感谢您的支持 - 我已经将您的代码回答评为好答案 :) - Ian
谢谢Ian。我认为对于Edge浏览器,使用带有motion-offset属性的CSS关键帧可能可以移植SMIL行为。虽然我没有尝试过,但我听说有人已经这样做了。从那时起,CPU使用率就不会成为问题。 - ibrahim tanyalcin

-1

在我的电脑上:

  • @mbostock 使用了 8% 的 CPU。
  • @ibrahimtanyalcin 使用了 11% 的 CPU。
  • @Ian 使用了 10% 的 CPU。

对于 @mbostock 和 @ibrahimtanyalcin 来说,这意味着 transitiontransform 更新使用了 CPU。

如果我在一个 SVG 中放置了 100 个这样的动画,那么会出现以下情况:

  • @mbostock 使用了 50% 的 CPU。(1 核心已满)
  • @Ian 使用了 40% 的 CPU。

所有动画看起来都很流畅。

一种可能性是在 transform 更新函数中添加一个休眠,使用 https://dev59.com/jnNA5IYBdhLWcg3wdtpd#39914235

编辑

Andrew Reid 的答案 中,我发现了一些优化。

我编写了一个只执行计算部分并计算出在4000ms内可以进行多少次迭代的Canvas + JS 100000测试版本。其中一半时间是order=false,由t控制。

我编写了自己的随机生成器,以确保每次运行修改后的代码时都得到相同的随机数。

块版本的代码:200次迭代

根据 parseInt的文档

parseInt不应作为 Math.floor()的替代品。

将数字转换为字符串,然后解析到.听起来效率不高。

Math.floor() 替换 parseInt():218次迭代

我还发现了一行没有功能且看起来不重要的代码。

let p = new Int16Array(2);

它在内部 while 循环中。

用以下行替换此行

let p;

提供300次迭代

使用这些修改后,代码可以处理更多的点,并且帧率为60 Hz。

我尝试了一些其他的方法,但它们都变得更慢了。

我很惊讶,如果我预先计算线段的长度,并将segment的计算简化为仅仅是一个数组查找,那么它比进行4个数组查找、几个Math.pow和加法以及一个Math.sqrt还要慢。


我已经反转了你的结果并在Firefox / IE中进行了测试(除了@ian,我没有测试)。首先,我设置了1000个等分辨率(数组大小为1000),并且自己处于劣势地位,尽管rAF不能超过60fps,这意味着数组大小为600甚至500也可以。 - ibrahim tanyalcin
请查看 https://imgur.com/a/Nf6suOe,对于100分来说,它们根本无法比较,一个是75%,另一个是约10-17%,您想让我上传一个测试用例吗? - ibrahim tanyalcin
很好 - 我也尝试了查找数组,对此感到惊讶(我之前没有考虑过math.floor,这是一个巨大的时间节省)- 我看到了被放弃的那一行,但是只是在发布要点后才看到的 - 有几个修订版本。 - Andrew Reid
@AndrewReid:预计算长度的结果可能是由于缓存未命中引起的吗? - rioV8
我不确定 - 这个问题一直萦绕在我的脑海中,我的测试能力在这里受到了限制,我不确定如何进行测试、代理或估计缓存丢失的时间。然而,我相对有信心,因为在非平方数上 Math.sqrt() 的速度相对较慢(它仍然非常快,但与提供一个整数时相比较不算快)。由于这一点,我相信仍然可以大幅提高该计算的性能。 - Andrew Reid
@AndrewReid:测试缓存未命中可以使用模拟器,但这对于此案例来说太多了。现在,平方根由浮点数部件处理,据我所知,它需要与尾数位数相同的周期。也许它具有检测计算中余数为零且回退至较早的整数平方的功能。 - rioV8

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接