JavaScript性能长时间运行任务
JavaScript Performance Long Running Tasks
前几天我在这里注意到一个问题(减少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现在没有"尾部递归调用",这意味着解释器/引擎必须为每个调用保持堆栈帧,这变得沉重。
为了优化解决方案,我会尝试将其变成一个立即执行的函数,在全局作用域中调用
- Gulp Git首先运行提交任务,然后运行推送任务
- 如何在jshint任务运行且一切正常时记录消息
- 如何按顺序运行gullow任务
- 我一直收到的控制台警告是什么?推迟长时间运行的计时器任务以提高滚动的流畅性
- 如何在Grunt.js中观察多个文件,但只在更改的文件上运行任务
- 在节点.js的多个线程中运行任务
- 如何按特定顺序运行 Gulp 任务
- 如何在咕噜声插件中运行其他已注册的咕噜声任务
- Angularjs推迟了日志运行计时器任务
- 在Meteor.js中运行后台任务
- 如何使用参数按顺序运行三个任务
- 如何从Node.js运行gullow任务-gullow.start()的任何更好的替代方案
- 从gullow任务运行一个npm脚本
- 一个咕哝的任务能运行其他咕哝的任务吗
- 使用nodejs对两个任务运行异步
- 不能让Grunt ' watch '任务运行
- Gulp任务运行缓慢
- Gulp插件包括/排除行JS代码取决于任务运行
- 如何用Jasmine在Angular应用程序中模拟d3.json()调用,用Karma任务运行器进行测试
- Gradle任务运行Nashorn JavaScript