Web を閲覧していると、突然 CPU 100% になったり
定期的に負荷がかかったりします。
flash などを除くと、それ以外のほとんどは JavaScript による重いループ処理が原因です。
少しずつ改善されてるような気はしますが
おそらくもう何年も前から、この問題がついてまわっていたのではないでしょうか。
そしてこのような負荷は、高スペックなマシンでは気付かないと思います。
開発者やテスターが高スペックな PC で動作確認を行ったことにより、
瞬間的な負荷を見過ごされたまま公開、リリースされてしまうこともあると思います。
それを、低スペックなマシンや iPhone などスマホを使っているユーザーが実行し
負荷に耐え切れずカクカクな描画になってしまったり、
最悪フリーズしてしまう場合もあります。
マシンの性能に限らず、例え高スペックマシンでも
いくつものアプリケーションを常時起動していて
タスクがいっぱいになっているユーザーもこのような事例に該当します。
何らかのアプリケーションを開発する場合、
処理の高速化は常に求められます。
より高速に動作したほうが軽快なのは確かです。
ですがクライアントアプリケーションを作る場合、
CPU 100% 使う処理を 10 秒も 20 秒も続けられてしまったら
それはもう不快でしかありません。
さらにそれが非ブロックでなく実行されてたら
マシンはもうフリーズのような動かない状態になりっぱなしです。
単純に高速を目指すのではなく、UI のことを考え制御を返すことが重要です。
UI を無視してひたすら高速に実行して 5 秒かかる処理だったら、
定期的に UI に制御を返し
例え数秒遅くなってもバックグラウンドで行い CPU 負荷を抑えることが
ユーザーへのストレス軽減として重要と考えています。
さらに非同期で実行することで並列化も可能になり、
逆に本来より速く実行できる可能性もあります。
PotLite.js は、そのような負荷軽減を可能とする JavaScript ライブラリです。
非同期処理をチェインで扱う Deferred オブジェクトを中心に
いろいろなループ処理を非同期で実行できるイテレータを実装しています。
イテレータは PotLite.js バージョン 1.21 現在、
- forEach
- repeat
- forEver
- iterate
- items
- zip
- map
- filter
- reduce
- every
- some
が実装されています。
非同期での実行、同期での実行、そして Deferred チェイン上での実行が可能です。
そして、速度指定できるのが特徴の一つです。
Deferred.forEach(obj, function(value, key) {...})
上のコードが forEach (jQuery での jQuery.each のようなもの) の非同期での通常実行になり、
Deferred.forEach.slow(obj, function(value, key) {...})
上のように
.slow と速度を明示することができます。
すると各ループをゆっくり目で実行します。
他にも
.fast などの指定ができます。
関数内で
return wait(1);
等と Pot.Deferred オブジェクトを返すことで
各ループ間でより細かな wait などの調整ができます。
逆に、非同期イテレートをより速く実行したい場合は
Pot.Defered.forEach.ninja(...)
と、
最も速く実行するよう定義されている
.ninja を指定すると
通常の for 文を単に関数で包んだのと同じくらいの速度で実行できます。
より詳しくは
リファレンス を参照してください。
どれくらい変化があるかベンチマークをとってみました。
イテレータ | JSON ファイルサイズ | CPU 最大使用率 | CPU 平均使用率 | 実行時間 (ms) |
for 文 | 1MB | 14.52% | 10.61% | 1211ms. |
jQuery.each | 1MB | 16.28% | 13.53% | 1362ms. |
Pot.Deferred.forEach | 1MB | 11.65% | 5.53% | 1647ms. |
for 文 | 5MB | 24.85% | 21.71% | 4717ms. |
jQuery.each | 5MB | 27.78% | 24.29% | 5570ms. |
Pot.Deferred.forEach | 5MB | 13.74% | 8.61% | 8925ms. |
for 文 | 10MB | 28.14% | 25.92% | 10699ms. |
jQuery.each | 10MB | 31.09% | 30.77% | 15844ms. |
Pot.Deferred.forEach | 10MB | 18.62% | 9.77% | 38909ms. |
同期と非同期の根本的な違いがありますが
この結果は、
下記のネストループを Core i3 で 5 回実行した平均結果です。
JSON ファイルは、
はてなキーワード一覧 から取得した CSV をキーワードだけの JSON に変換したものです。
10MB はキーワードすべて、5MB は半分で切った JSON ファイル、 1MB はそのまた半分です。
Pot.Deferred.forEach によるイテレータは、処理時間が他と比べ長くなっていますが
CPU 最大使用率、CPU 平均使用率 共に安定しているのがわかります。
CPU グラフも比較してみました。
すべてのキーワードを対象に、Pot.Deferred.forEach (上) と、jQuery.each (下) をそれぞれ実行した結果です。
jQuery.each は、途中で「警告:応答しないスクリプト」のダイアログがでてしまいました。
そのため、本来なら Pot.Deferred.forEach のほうが時間がかかると予想してください。
それでも、Pot.Deferred.forEach では jQuery.each に比べ
瞬間的 (もしくは断続的) な負荷を抑えることができています。
これは、対象のデータがどれほど巨大でも
負荷のかからないイテレートが可能なことを示しています。
なお、上記のベンチマークは
ある程度負荷のかかるループ処理 を対象としています。
PotLite.js のイテレータは、各ループがある程度重いと判断すると CPU 負荷を抑えるよう働きます。
つまり、逆に言うと
瞬時に終わるような小中規模のループでは、他のループと変わらない速度で実行されるということです。
以下に、小規模のループ結果を記します。
イテレータ | CPU 最大使用率 | CPU 平均使用率 | 実行時間 (ms) |
for 文 | 4.10% | 3.87% | 9ms. |
jQuery.each | 5.54% | 4.72% | 18ms. |
Pot.Deferred.forEach | 4.71% | 4.15% | 19ms. |
この結果は、1MB の JSON ファイルを対象に
ユニーク処理をなくした実行結果です。
上と同じく 5 回実行した結果の平均になります。
PotLite.js は、jQuery.each とほぼ同じ実行速度で、
ちょうど for 文を関数で包んだものを実行したのと同じくらいの速度で実行できています。
このような毎回の処理が小さなループでは、 for 文などが最も適していますが
適度に各関数を使うことでコストも減少させることができます。
以下は、テストの使用したソースコードです。
// jQuery.each
function benchmark_jQuery() {
$.getJSON('hatena.keywords.json?callback=?', {
size : '10mb'
}, function(data) {
var results = [];
var start = +new Date;
$.each(data.keywords, function(k, word) {
if (!/[^a-zA-Z0-9_.-]/.test(word)) {
var uniq = true;
for (var i = 0; i < results.length; i++) {
if (word === results[i]) {
uniq = false;
break;
}
}
if (uniq) {
results.push(word);
}
}
});
$('#result').text((+new Date) - start);
});
}
巨大なファイルの転送と負荷のテストのため、ソースのみ記します。
// Pot.Deferred.forEach
function benchmark_potlite() {
begin(function() {
return jsonp('hatena.keywords.json?callback=?', {
queryString : {
size : '10mb'
}
}).then(function(res) {
var results = [];
var start = now();
return Deferred.forEach(res.keywords, function(word) {
if (!/[^a-zA-Z0-9_.-]/.test(word)) {
var uniq = true;
for (var i = 0; i < results.length; i++) {
if (word === results[i]) {
uniq = false;
break;
}
}
if (uniq) {
results.push(word);
}
}
}).then(function() {
return now() - start;
});
}).then(function(time) {
$('#result').text(time);
});
});
}
以下は、小規模のループとしてテストしたソースコードです。
// jQuery.each
function benchmark_jQuery_lite() {
$.getJSON('hatena.keywords.json?callback=?', {
size : '1mb'
}, function(data) {
var results = [];
var start = +new Date;
$.each(data.keywords, function(k, word) {
if (!/[^a-zA-Z0-9_.-]/.test(word)) {
results.push(word);
}
});
$('#result').text((+new Date) - start);
});
}
// Pot.Deferred.forEach
function benchmark_potlite_lite() {
begin(function() {
return jsonp('hatena.keywords.json?callback=?', {
queryString : {
size : '1mb'
}
}).then(function(res) {
var results = [];
var start = now();
return Deferred.forEach(res.keywords, function(word) {
if (!/[^a-zA-Z0-9_.-]/.test(word)) {
results.push(word);
}
}).then(function() {
return now() - start;
});
}).then(function(time) {
$('#result').text(time);
});
});
}
PotLite.js
PotLite.js は、Pot.js の軽量バージョンです。 非同期処理のオブジェクト/関数だけに絞ったライブラリです。 経緯などは
Pot.js に関する以前の記事 を参照ください。
概要
PotLite.js は、非ブロックでの非同期処理を直列的に書けるようにし、 UI や CPU への負担を軽減するループ処理を中心に実装された JavaScript ライブラリです。 MochiKit ライクな Deferred オブジェクトにより 様々なイテレート (forEach, filter, map, repeat, some など) を可能とします。 ※ここでいう MochiKit ライクとは、JSDeferred とは違い 1 つのチェインが 1 つのインスタンスということです。 ※Deferred チェイン は JSDeferred や MochiKit.Async.Deferred と同じ感覚で扱えます。
ダウンロード
最新
レポジトリ
$ git clone git://github.com/polygonplanet/Pot.js
GitHub :
polygonplanet/Pot.js
動作環境
以下の Web ブラウザで動作確認済みです。
- Mozilla Firefox *
- Internet Explorer 6+
- Safari *
- Opera *
- Google Chrome *
また、以下の環境でも動作するよう設計されています。
- Greasemonkey (userscript)
- Mozilla Firefox Add-On (on XUL)
- Node.js
- Other non-browser environment
インストール
一般的な方法で動作します。
例:
<script src="potlite.min.js" type="text/javascript">
</script>
Node.js の場合。
// Example to define Pot object on Node.js.
var Pot = require('./potlite.min.js');
Pot.debug(Pot.VERSION);
Pot.Deferred.begin(function() {
Pot.debug('Hello Deferred!');
}).then(function() {
// ...
});
// ...
Greasemonkey (userscript) の例。
// ==UserScript==
// ...
// @require https://github.com/polygonplanet/Pot.js/raw/master/potlite.min.js
// ...
// ==/UserScript==
Pot.Deferred.begin(function() {
return Pot.request('http://www.example.com/data.json').then(function(res) {
return Pot.parseFromJSON(res.responseText);
});
}).then(function(res) {
Pot.debug(res);
// do something...
});
//...
PotLite.js をバージョンを限定して Web から直接読み込みたい場合、 上の GitHub リンクでは常に最新になってしまうため 実装の差異による不具合が発生するかもしれません。 そのため、API サーバを用意しました。
これは、1.21 の部分をリリース済みのバージョンに合わせて変更できます。 レポジトリに (例えば 1.22 と) バージョンをタグ付けした時に、 あわせて API サーバに置くようにしています。 例えば Greasemonkey で 1.21 を使いたい場合、
// ==UserScript==
// ...
// @require http://api.polygonpla.net/js/pot/potlite/1.21/potlite.min.js
// ...
// ==/UserScript==
と記述できます。 Greasemonkey に限らず script タグからでもなんでも自由に使ってください。 ----
jQuery プラグインとしての例:
// jQuery を読み込んだ後に実行。
Pot.deferrizejQueryAjax();
// Ajax 系の関数が Pot.Deferred を返すようになる
$.getJSON('/hoge.json').then(function(data) {
alert(data.results[0].text);
}).rescue(function(err) {
alert('Error! ' + err);
}).ensure(function() {
return someNextProcess();
});
// エフェクトなどを Deferred 化する 'deferred' が追加される
$('div#hoge').deferred('hide', 'slow').then(function() {
// ( hide() が終了したあとの処理)
});
Pot.deferrizejQueryAjax() は現状、 ライブラリ側で実行しません。 なので、プラグインを使用する場合は コードの最初などで実行する必要があります。
リファレンス・マニュアル
より詳しい情報はすべてリファレンスに載っています。 基本的な導入や、各メソッド・関数についても扱っています。
動作テスト
以下のページで動作テストができます。 ページを開くと実装されている主な関数・メソッドを全てテストします。
自動生成されたドキュメント
Closure Compiler によりソースコードから自動生成されたドキュメントです。
殆どの関数ごとにサンプルコードを載せているので、ある程度は参考になると思います。 生成物をすべて確認しているわけではないので、誤認識してる箇所もあるかもしれません。 より詳細な実装などは直接ソースコードを参照ください。
久しぶりにちょっとがんばって重点をまとめてみました。 これを機に、Pot.js (PotLite.js) で遊んでくれると嬉しいです。 不明な点、要望やバグや感想などありましたら
@polygon_planet や 下のレポジトリから、またはメールでなんでもどぞです。
レポジトリ