JavaScript性能长时间运行任务

JavaScript Performance Long Running Tasks

本文关键字:任务 运行 长时间 性能 JavaScript      更新时间:2023-09-26

前几天我在这里注意到一个问题(减少Javascript CPU使用),我很感兴趣。

本质上,这家伙想逐个字符地加密一些文件。显然,一次性完成所有这些操作会锁定浏览器。

他的第一个想法是每次处理大约1kb的字符串,然后暂停X毫秒,这样用户就可以在处理之间继续与页面交互。他也考虑过使用webWorkers(这是最好的主意),但它显然不是跨浏览器的。

现在我真的不想讨论为什么这在javascript中可能不是一个好主意。但是我想看看我是否能想出一个解决办法。

我记得在js conf上看过Douglas Crockford的一个视频,视频是关于node.js和事件循环的。但是我记得他说过要把长时间运行的函数分解成单独的块,这样新调用的函数就会被放到事件循环的末尾。而不是用长时间运行的任务阻塞事件循环,阻止其他任何事情的发生。

我知道这是一个值得我研究的解决方案。作为一个前端开发人员,我从来没有真正经历过在JS中运行非常长时间的任务,我很想知道如何分解它们以及它们是如何执行的。

我决定尝试一个递归函数,它从0毫秒的setTimeout内部调用自己。我认为这将在事件循环中为运行时想要发生的任何其他事情提供中断。但我也认为,当没有其他事情发生时,你会得到最大的计算量。

这是我想到的。

我要为代码道歉。我是在控制台进行实验,所以这是快速和肮脏的。
function test(i, ar, callback, start){
    if ( ar === undefined ){
        var ar = [],
        start = new Date;
    };
    if ( ar.length < i ){
        ar.push( i - ( i - ar.length )  );
        setTimeout(function(){
            test( i, ar, callback, start);
        },0);
    }
    else {
        callback(ar, start);
    };
}

(您可以将此代码粘贴到控制台,它将工作)

本质上,函数所做的是获取一个数字,创建一个数组,并在array.length < number将计数压入数组时调用自己。它将在第一次调用中创建的数组传递给所有后续调用。

我测试了一下,它似乎完全按照预期工作。只是它的性能相当差。我用……

(同样,这不是一个性感的代码)

test(5000, undefined, function(ar, start ){ 
    var finish = new Date; 
    console.log(
        ar.length,
        'timeTaken: ', finish - start 
    ); 
});

现在我显然想知道它花了多长时间来完成,上面的代码花了大约20秒。现在在我看来,JS不应该花20秒数到5000。加上它正在执行一些计算和处理以将项压入数组的事实。但20美元还是有点贵。

所以我决定同时生成几个,看看这是如何影响浏览器性能和计算速度的。

(代码没有变得更性感)

function foo(){ 
test(5000, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 1'  ) });
test(5000, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 2'  ) });
test(5000, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 3'  ) });
test(5000, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 4'  ) });
test(5000, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 5'  ) });
};

所以总共有5个,同时运行并且不会导致浏览器挂起。

在进程结束后,几乎完全同时返回所有结果。所有这些都花了21.5秒才完成。这只比一个单独的慢1.5秒。但是,我在具有:hover效果的元素的窗口周围移动鼠标只是为了确保浏览器仍在响应,所以这可能是1.5s开销的一部分。

因此,由于这些函数显然是并行运行的,因此浏览器中剩下的计算空间更多。

有谁能解释一下这里的性能方面发生了什么,并详细说明如何改进这样的功能?

我这样做只是为了疯狂。

function foo(){
    var count = 100000000000000000000000000000000000000;  
    test(count, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 1'  ) });
    test(count, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 2'  ) });
    test(count, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 3'  ) });
    test(count, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 4'  ) });
    test(count, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 5'  ) });
};

在我写这篇文章的时候它一直在运行,而且还在继续。浏览器没有抱怨或挂起。一旦结束,我会添加完成时间。

setTimeout没有最小延迟值0ms。最小延迟在5ms-20ms之间,具体取决于浏览器。

我自己的个人测试表明,setTimeout不会立即将您的背放在事件堆栈上

生活例子

它在再次被调用之前有一个任意最小的时间延迟

var s = new Date(),
    count = 10000,
    cb = after(count, function() {
        console.log(new Date() - s);    
    });
doo(count, function() {
    test(10, undefined, cb);
});
  • 并行运行10000个,计数到10需要500ms。
  • 运行100到10需要60ms。
  • 运行1计数到10需要40ms。
  • 从1计数到100需要400ms。

显然,似乎每个单独的setTimeout必须等待至少4ms才能再次调用。但这就是瓶颈所在。setTimeout的个体延迟

如果你并行安排100个或更多的这些,那么它就会工作。

我们如何优化这个?

var s = new Date(),
    count = 100,
    cb = after(count, function() {
        console.log(new Date() - s);    
    }),
    array = [];
doo(count, function() {
    test(10, array, cb);
});

设置100在同一个数组上并行运行。这将避免主要的瓶颈,即setTimeout延迟。

以上操作在2ms内完成。

var s = new Date(),
    count = 1000,
    cb = after(count, function() {
        console.log(new Date() - s);    
    }),
    array = [];
doo(count, function() {
    test(1000, array, cb);
});

在7毫秒内完成

var s = new Date(),
    count = 1000,
    cb = after(1, function() {
        console.log(new Date() - s);    
    }),
    array = [];
doo(count, function() {
    test(1000000, array, cb);
});

并行运行1000个作业大致是最优的。但你会开始遇到瓶颈。计数到100万仍然需要4500ms。

您的问题是开销vs工作单元的问题。您的setTimeout开销非常高,而您的工作单元ar.push非常低。解决方案是一种古老的优化技术,称为块处理。不是每次调用处理一个UoW,而是需要处理一个UoW块。"块"有多大取决于每个UoW占用的时间以及每次setTimeout/call/迭代(在UI变得无响应之前)可以花费的最大时间。

function test(i, ar, callback, start){
if ( ar === undefined ){
    var ar = [],
    start = new Date;
};
if ( ar.length < i ){
    // **** process a block **** //
    for(var x=0; x<50 && ar.length<i; x++){
        ar.push( i - ( i - ar.length )  );
    }
    setTimeout(function(){
        test( i, ar, callback, start);
    },0);
}
else {
    callback(ar, start);
};
}

你必须在不给用户造成UI/性能问题的情况下处理最大的块。前面的运行速度要快50倍(块大小)。

这和我们使用缓冲区读取文件而不是一次读取一个字节的原因是一样的。

只是一个假说…代码之所以这么慢,是因为你正在构建一个有5000个递归实例的递归堆栈吗?你的调用不是真正的递归,因为它是通过settimeout函数发生的,但是你传递给它的函数是一个闭包,所以它必须存储所有的闭包上下文…

性能问题可能与管理内存的成本有关,这也可以解释为什么您的最后一个测试似乎使事情变得更糟…

我没有尝试过解释器的任何东西,但是看看计算时间是否与递归的数量呈线性关系可能会很有趣,或者不是……例如:100、500、1000、5000次递归…

我要做的第一件事就是不使用闭包:
setTimeout(test, 0, i, ar, callback, start);

他实际上谈到了这一点,你使用的是递归函数,而JavaScript现在没有"尾部递归调用",这意味着解释器/引擎必须为每个调用保持堆栈帧,这变得沉重。

为了优化解决方案,我会尝试将其变成一个立即执行的函数,在全局作用域中调用