中機能タイマー

uu.create.timer()は、window.setInterval() と window.setTimeout() の代替手段となるものです。
さほど高機能でもないので、中機能ってことで。

特徴:

  • 低負荷
    • 多数のタイマー(10本〜)を同時に使用する状況下で、CPU負荷がsetInterval(),setTimeout()よりも20〜30%程軽い。
  • サスペンド(一時停止)とレジューム(再開)が可能。
  • ループ回数を指定可能。無限ループも可能。
    • ループ回数を1にすれば、setTimeout(), 無限にすればsetInterval()互換の動作。
  • delay時間をプログラマブルに変更可能。
    • アニメーションGIFのような処理がとても簡単。
  • メモリコンパクションで使い古しのリソースをスッキリ削れる。

使いどころ:

  • もっさりなサイト(エフェクトを多用しているサイト)なら導入するだけで効果があるかも。
  • ゲームなどの多数のオブジェクトを同時多発的に動かすシーンで活躍するかも。オブジェクトが突然増えてもガクガクしづらいと思う。

弱点:

  • setInterval() や setTimeout() で事足りるならそっちがベスト。KISS大事。
  • 正確な時間を計ろうとするならこれは使えない。
  • 273785年以上は連続稼動できない。10万年に一度は再起動が必要。

仕組みとか雑記:

  • 「ワーカースレッドに一本化し、ループ処理することで、スレッドコンテキスト(オーバーヘッド)を減らす。」といった定石を、JavaScriptの世界に持ち込んでみた。
    • オブジェクトが増えても遅くなりにくい
  • 主要ループ内での、変数のムーブや演算を極力減らす。
    • 現在時刻とタイムアウト予定時間を比較する if (d.next && t >= d.next) {} がヘビーローテーションなので、ここの演算量を減らすための努力とか。
    • 関数呼出し前に次回のタイムアウト予定時間を算出しているのは、10msで処理が終わらない関数が出てきた場合(その時点でまずいのだが)でも、suspendやMemory Compactionに与える影響を減らすため。
  • ヘビーローテーション内に try / catch 書いちゃうと遅くなる。
    • 安全にsuspendとかMemory Compactionする仕組みで軽い方法はないもんか?
      • JavaScritpでスレッド間の同期(アトミックな動作)ってできたっけ?
      • 無理なら無理で安全時間を多めに取るとか、スレッド止めちゃうしかないんじゃない → 結局 Memory Compaction 作業中は、スレッドを止めることに。
  • regist()で与えられたdelay時間を、基準クロック(baseClock=10ms)で割っていないため、より自然に動作する。
  • スピードも十分。
    • ビジュアル要素を多用したベンチマークも実施したけど、結果をさらす意味が見出せなかったので今回はパス。
    • 参考までに重い順, IE6, IE7, IE8β, Firefox2.0, Opera9.2, Opera9.5β, Firefox3.0β5, Safari3.1
  • this.tick は現在時間を保持する変数ではなく、何回呼ばれたかをぼんやり覚えておく変数なので、実時間と大幅にずれても問題はなし。
    • 正確に現在時間がほしければ、適時 (new Date().getTime()) すべき。

uupaa.jsに組み込まれた状態のソース。今回は単体で動作するHTMLソースは無し(uupaa.jsが必要)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>JavaScript::timer test</title>
<!--[if IE]><script type="text/javascript" src="../../lib/xpath.js"></script><![endif]-->
<script type="text/javascript" src="../../uupaa.js"></script>
<style type="text/css">body { background-color: black; color: white; }</style>
</head>
<body>
<script type="text/javascript">
window.onload = function() {
  // 一定時間(0ms, 100ms, 200ms, 300ms)で、ステータスバーに、"Hello"を追加
  var fn1 = function() { window.status += "Hello"; };
  var delay = function(n) { return n * 100; }; // create delay value
  uu.tm10.regist(fn1, delay, 4);

  // 10ms毎にステータスバーに"."を追加
  var fn2 = function() { window.status += "."; };
  uu.tm10.regist(fn2, 10, 100); // 100回ドットを打ったら停止
}
</script>
</body>
</html>
---- uupaa.js から抜粋 ----
// --- timer extend ---
uu.create.timer = uu.create.basicClass();
uu.create.timer.prototype = {
  construct: function(baseClock /* = 10ms */) {
    this.baseClock = baseClock || 10;
    this.tick = 0;
    this.itID = 0; // interval timer id (window.setInterval result)
    this.data = [];
    this.lock = false; // true: under memory-compaction execution
  },
  destruct: function() {
    var me = this;
    uu.registWindowEvent("onunload", function() {
      me.lock = true;
      me.suspend();
      me.data = null;
    });
  },
  /** regist delay function
   *
   * 遅延評価関数を登録します。
   * suspend状態ならresumeします。
   *
   * @param function        fn          遅延評価関数を指定します。文字列も指定可能です。
   * @param number/function delay       遅延時間をms単位で指定します。
   *                                    遅延時間を返す関数を指定することもできます。
   *                                    呼び出し回数を引数に関数を呼び出します。呼び出し回数は0以上の整数です。
   * @param number          loop        評価回数を指定します。0なら無限, 1なら1回, 2なら2回だけ評価します。
   * @return number                     unregist()で使用するIDを返します。
   */
  regist: function(fn, delay, loop /* = 0 */) {
    var dfn = uu.isF(delay) ? delay : undefined;
    var id = this.data.length;
    this.resume();
    this.data[id] = { id: id, next: this.tick + (dfn ? dfn(0) : delay), count: 0, dfn: dfn,
                      fn: (uu.isF(fn)) ? fn : new Function(fn), delay: delay, loop: loop || 0 };
    return id;
  },
  /** unregist delay function
   *
   * 登録済みの遅延評価関数を抹消します。
   *
   * @param number    id    regist()が返すIDを指定します。
   */
  unregist: function(id) {
    this.data.some(function(v, i) {
      return (v.id === id) ? (v.next = 0, true) : false;
    });
  },
  /** 一時停止 */
  suspend: function() {
    this.itID && uuw.clearInterval(this.itID);
    this.itID = 0;
  },
  /** 再開
   *
   * @return boolean  再開成功でtrueを返します。失敗でfalseを返します。
   */
  resume: function() {
    if (!this.itID && !this.lock) {
      this.tick = (new Date()).getTime();
      this.itID = this.run();
    }
    return !!this.itID;
  },
  /** Memory Compaction */
  mc: function() {
    var me = this;
    me.lock = true;
    me.suspend();
    uuw.setTimeout(function() {
      var i = 0, sz = me.data.length, tmp = [];
      for (; i < sz; ++i) {
        me.data[i].next && tmp.push(me.data[i]);
      }
      me.data = tmp; // swap
      me.lock = false;
      if (me.data.length) { me.resume(); }
    }, Math.min(me.baseClock * 4, 100)); // 100ms wait(根拠なし)
  },
  /** loop
   *
   * @private
   */
  run: function() {
    var me = this;
    return uuw.setInterval(function() {
      var i = 0, sz = me.data.length, t = me.tick += me.baseClock, d;
      for (; i < sz && !me.lock; ++i) {
        d = me.data[i];
        if (d.next && t >= d.next) {
          d.next = (d.loop && !(--d.loop)) ? 0 : t + (d.dfn ? d.dfn(++d.count) : d.delay);
          d.fn();
        }
      }
    }, me.baseClock);
  }
};
uu.tm10 = new uu.create.timer(10);

使い方

  • 登録
    • var id = uu.tm10.regist(遅延評価したい関数または文字列, 遅延時間または遅延時間生成関数, ループ回数) で遅延評価関数を指定します。返されるidはunregistで使います。必要なら取っといてください。
      • uu.tm10.regist(function() { alert("hoge"); }, 1000, 1); とすると、1000ms(1秒)後に "hoge"が表示されます。
  • 抹消
    • uu.tm10.unregist(id); で登録してある遅延評価関数を抹消します。
  • 一時停止と再開
    • uu.tm10.suspend(); で一時停止。 uu.tm10.resume()で再開します。
  • スッキリ
    • uu.tm10.mc(); でMemory Compactionします。mc中はタイマーを止めます(suspend→resume)。

そのうちに、http://pigs.sourceforge.jp/wiki/index.php?uupaa.js でリリースする予定です。
Google Code Archive - Long-term storage for Google Code Project Hosting.で、より改善された最新版をリリースしています。