JavaScript でプロファイリング (シンプル版)

Game of Lifeアルゴリズムの問題点を探ろうと思い、以前作ったプロファイラにかけて調べることにしました。

が、このプロファイラは prototype.js (を自分用に改変したもの) を対象にした内容だったため、ライブラリを使わずに書いた Game of Life では使えません。

ということで、完全にライブラリ非依存な形に書き直してみました:

var Profiler = {
  times: {},
  scope: this,  // == window

  watch: function() {
    for (var i = 0; i < arguments.length; i++)
      this.profile(arguments[i]);
  },

  profile: function(klass) {
    this.times[klass] = {};
    var proto = eval(["this.scope",klass,"prototype"].join("."));
    for (var m in proto) {
      if (typeof(proto[m]) != "function") continue;
      (function(method, m) {
        var orig = proto[method];
        proto[method] = function() {
          var time = m.time || 0;
          var now  = new Date - 0;

          var result = orig.apply(this, arguments);

          m.time  = time + (new Date - now);
          m.count = (m.count || 0) + 1;
          return result;
        };
      })(m, this.times[klass][m]={});
    }
  },

  report: function() {
    var div = document.getElementById("_Profile_");
    if (!div) {
      div = document.createElement("div");
      div.setAttribute("id", "_Profile_");
      document.body.appendChild(div);
    }
    var table = [
      "<table><thead>", "<th>count</th>", "<th>time</th>",
      "<th>class</th>", "<th>method</th>", "</thead><tbody>"
    ];
    for (var k in this.times) {
      var t = this.times[k];
      for (var m in t) {
        if (!t[m].count) continue;
        table.push([ "<tr><td>", t[m].count, "</td>" ].join(""));
        table.push([ "<td>", t[m].time/1000, "</td>" ].join(""));
        table.push([ "<td>", k, "</td>" ].join(""));
        table.push([ "<td>", m, "</td></tr>" ].join(""));
      }
    }
    table.push("</tbody></table>");
    div.innerHTML = table.join("");
  }
};

前のバージョンではインスタンス・メソッドの動的な追加・変更を考慮したり、無駄にややこしかったんですが、今回は単純な実装にしました (アクロバティックな要素が無くなったのは寂しいですが、汎用性がある方が良いですよね)。


使い方です。

Profiler.watch("ClassName1","ClassName2");

で観察開始、

Profiler.report();

で出力となります。

どうぞご利用&お試しください


追記 [20061104]:

クラス (コンストラクタ) への参照を得るのに eval を使っていたんですが (Profiler.profile の 2 行目) 不要でした。何か勘違いしていたみたいです。


あと、プロトタイプではなくインスタンスを受け取って調べる方法もあります。以前の prototype.js の Class.create をハックするのに似た方式です。

実は最初にこちらを思いついたんですが、物事を複雑にしているかなと思って上のバージョンに書き直したのでした。折角なので載せておきますね。

watch と profile メソッドを以下のように書き換えます:

Profiler.watch = function() {
  var scope = this.scope;
  for (var i = 0; i < arguments.length; i++) {
    (function(klass) {
      klass.replace(/^(?:(.+)\.)?([^.]+)$/, function(_, ns, pkg) {
        ns = eval("scope" + (ns ? "."+ns : ""));
        var orig_const = ns[pkg];
        if (typeof(orig_const) != "function") return;

        // Rewrite constructor
        ns[pkg] = function() {
          orig_const.apply(this, arguments);  // call original constructor
          Profiler.profile(klass, this);  // send the instance to Profiler
        };
        // Restore properties
        for (var p in orig_const)
          ns[pkg][p] = orig_const[p];
        // Restore prototype (not to be enumerated by for..in)
        if (orig_const.prototype)
          ns[pkg].prototype = orig_const.prototype;
      });
    })(arguments[i]);
  }
};
Profiler.profile = function(klass, instance) {
  this.times[klass] = this.times[klass] || {};
  for (var m in instance) {
    if (typeof(instance[m]) != "function") continue;
    (function(method, m) {
      var orig = instance[method];
      instance[method] = function() {
        var time = m.time || 0;
        var now  = new Date - 0;

        var result = orig.apply(this, arguments);

        m.time  = time + (new Date - now);
        m.count = (m.count || 0) + 1;
        return result;
      };
    })(m, (this.times[klass][m] = this.times[klass][m] || {}));
  }
};

元のクラスのコンストラクタを書き換えることにより、new されたインスタンスをプロファイラが受け取れるようにしています (上で「アクロバティック」と言ったのはこれのことです)。


追記 [20061105]:

すいません、eval は要らないと思った矢先なんですが、クラスがネームスペースの中で定義されている ("." で区切られている) 場合は必要になるっぽいです。いちおう修正を試みましたので、以前のをご覧になった方は改めてご確認ください。