2018年11月19日

CPU負荷を抑えて重い処理を軽くするJavaScriptライブラリ「chillout.js」

「chillout.js」とは?


chillout.js は「処理時間を短くする」という物理的な高速化とは違い、CPU負荷を抑えてリソースに余裕を持たせ、重い処理でも軽く感じさせることでユーザーにとって体感的・心理的な高速化につなげる JavaScriptライブラリです。


重い処理から開放されるために


JavaScriptでfor文など繰り返し(ループ)処理をしたとき、重い処理により一瞬でもページが固まってしまうことがあります。
そんなときブラウザ画面は読み込み中のままローディングのくるくるが止まらなかったりゲーム中にマウスが効かなくてカクカクだったり、
Webページだけじゃなくnode.jsなど画面のいらない処理でも高CPU負荷が続くとマシンごと重くなって大変です。

特にスペックの低いPCやタブレットの場合、 CPU使用率100%の状態では加熱してきて冷却ファンが高回転になり、そのまま使い続けると冷却が追いつかずに熱暴走してしまう可能性もあるので、うっかり重いページを開いてファンが「ウイーン!」って言い出すとヒヤヒヤします。

最近のPCは性能がよくなってるのでそんな事態になりにくいかもしれませんが、自分のPCも真夏の気温で動かなくなって修理にだしたのはまだ新しい記憶です😭 (直りました)。

「CPUファンが回りっぱなしかと思ったらフリーズして電源が落ちた」なんてことになると作業途中だったらやり直しになるし、HDDやSSDなどの内部にもダメージが残るかもしれないのでなるべく処理を軽くしたいところです。

JavaScriptでCPU負荷を抑えるには?


重い処理のほとんどはループ処理によって発生します。ループの中でさらにループ、その中でさらにループ…。
ループ間の処理が重くなってくるとマシンのCPUは休む暇なく動き、結果として内部が熱くなるため冷却ファンをたくさん回すことになります。
単純に考えた場合、そうならないようループの途中で一定時間処理を休止させればいいんですが、それができません。
JavaScriptには一定の時間休む sleep のような機能がないからです

そこで、sleepするにはどうするか?というと「非同期」でループ処理します。

CPUを休ませるために非同期でループ処理する


JavaScriptのループは for文や while文、また Array に対して forEachなどがありそれぞれ同期で処理されますが、上述したように sleep して休ませることができないため、これらのループ処理を非同期で実現 します。

同期処理を非同期化するには、
  • setTimeoutを使う
  • process.nextTickを使う(node.js)
  • DOMイベントを使う
  • MessageChannelを使う

などの方法があり、一連の処理を Promise と組み合わせると非同期ループが実現できます

(setInterval や requestAnimationFrame を使うこともできますが、これらは精度を保とうとより正確にループしようとするため今回の用途には不向きです。)

今までも CPU負荷を抑えるためのライブラリをいくつか作ってきたのですが、当時は Promise という便利なものがJavaScriptになかったので自前で Deferred と呼ばれる Promise のようなものを定義していました。そのためにライブラリのサイズが大きくなるしAPIがガラパゴス化してたんですが、ようやくシンプルな実装にできたと思います。

処理時間を短くするんじゃなく、体感速度を向上させる心理的な高速化



処理の高速化というと、とにかく1ミリ秒でも処理時間を短くすることが手法とされますが、Webページやアプリ、ゲームといった、人が画面を見たり操作する場合 心理的に「速い」と感じればユーザーのストレスが減り結果として高速化につながります


Twitterでいい例があったので紹介。
これはエクセルマクロの話ですが 処理時間を短くするんじゃなく待ち時間を退屈させないようにして体感速度を向上させて、心理的な高速化しています。


CPU負荷を抑えてループ処理を軽くするJavaScriptライブラリ「chillout.js」



冒頭が長くなっちゃいましたが、ライブラリの紹介です。

chillout.js は、ループ処理中に適度な休憩(sleep)を入れてあげ、カクカクする重さを感じさせないライブラリ です。
また、処理が重くなるとでる「警告: 応答のないスクリプト」というブラウザ警告なしでJavaScriptを実行できます

ループ処理が重いときにはCPUが休まるくらいの休止時間、処理が速いときには休止時間なしか、わずかな休止時間をいれ本来のループを邪魔しないようにします

ベンチマーク


for文と chillout.repeat を比較します。

function heavyProcess() {
  var v;
  for (var i = 0; i < 5000; i++) {
    for (var j = 0; j < 5000; j++) {
      v = i * j;
    }
  }
  return v;
}

例として上のような重い処理(テスト用に5000*5000回繰り返す処理)に対して、

for文

JavaScriptのfor文。

var time = Date.now();
for (var i = 0; i < 1000; i++) {
  heavyProcess();
}
var processingTime = Date.now() - time;
console.log(processingTime);

CPUグラフ:
  • 処理時間: 107510ms.
  • CPU平均使用率(Nodeプロセス): 97.13%

CPUグラフは上のようになり、CPU均使用率は 100% までいってないけど 97.13% になりました。

chillout.repeat

これを「chillout.js」のメソッド chillout.repeat で実行します。

var time = Date.now();
chillout.repeat(1000, function(i) {
  heavyProcess();
}).then(function() {
  var processingTime = Date.now() - time;
  console.log(processingTime);
});

CPUグラフ:
  • 処理時間: 138432ms.
  • CPU平均使用率(Nodeプロセス): 73.88%

ベンチマーク結果


  ForStatement (for文) chillout.repeat
処理時間 107,510ms. 138,432ms.
CPU平均使用率(Nodeプロセス) 97.13% 73.88%


グラフでは少しわかりにくいかもしれませんが、CPU平均使用率は for文 97.13% に対し、 chillout.repeat は 73.88% となり、for文よりもCPU使用率が抑えられてます

そのかわりループ中にCPUを休ませてるため処理時間は 107,510ms から 138,432ms となり少しかかっています。
もっとCPU使用率を抑えるには単純に休止時間を増やせばいいんですが、そうすると処理時間が長くなるため適度なバランスにしています。

画面上ではわかりませんが、for文を実行してるときはCPUファンが「ウイーン!」と激しく回っていたのに対し、chillout.repeat のときはファンが静かめでした(環境によってそこまで変わらないかもですが)。

chillout.js は、処理速度を少し遅くするかわりに低いCPU使用率で安定してJavaScriptを実行できる特徴があります。

特にブラウザやゲームなど、人が画面を見て操作するJavaScriptのパフォーマンスにおいて最も重要なことの一つは、 数値的な速度だけでなく、安定したレスポンスによってユーザーにストレスなく動かすことと考えています。

(ベンチマークスペック: Windows8.1 タブレット / Intel(R) Atom(TM) CPU Z3740 1.33GHz)

API



for文やwhile文、Array.forEachに対応するメソッド4つがあります。そのうち2つ紹介。

chillout.repeat

for文のような動きをします。

// 5回繰り返す
chillout.repeat(5, function(i) {
  console.log(i);
}).then(function() {
  console.log('done');
});
// 0
// 1
// 2
// 3
// 4
// 'done'

chillout.forEach

Array.forEachと同じように動く。

// 配列を回す
var values = ['a', 'b', 'c'];
chillout.forEach(values, function(value) {
  console.log(value);
}).then(function() {
  console.log('done');
});

// 'a'
// 'b'
// 'c'
// 'done'

非同期処理で Promise が返されるため then で繋ぎます。
(他のAPIの詳細は chillout.jsのGitHub を参考ください。)

比較表


既存のJavaScriptループを chillout.js のAPIに置き換えると、大抵の場合 CPU使用率を抑えて実行できます。

変換例:

JavaScript chillout.jsの場合
[1, 2, 3].forEach(function(v, i) {}) chillout.forEach([1, 2, 3], function(v, i) {})
for (i = 0; i < 5; i++) {} chillout.repeat(5, function(i) {})
for (i = 10; i < 20; i += 2) {} chillout.repeat({ start: 10, step: 2, end: 20 }, function(i) {})
while (true) {} chillout.till(function() {})
while (cond()) {} chillout.till(function() {
  if (!cond()) return false;
})
for (value of [1, 2, 3]) {} chillout.forOf([1, 2, 3], function(value) {})


GitHub / chillout.js



GitHub / chillout.js

※このブログの内容は常に更新してるわけじゃないので古くなってる可能性があります。特にAPIの最新の情報は GitHub / chillout.jsを参考ください。