JavaScriptで簡易スタック・トレース&プロファイラ

しばらく前にJavaScriptでのスタック・トレースの保存が話題となっていた。

最速インターフェース研究会 :: JavaScriptでDebugScreen、その2
http://d.hatena.ne.jp/brazil/20060117/1137427933

これらの記事を読んだとき、以前作ったプロファイラの仕組みを利用すれば、簡単なスタック・トレースならば保存できそうかなと思った。そこで、試しに作ってみた。コードは下記。

var FuncCallObserver = function(){
    var self = this;
    self.listeners = ;
    self.observing = false;
    self.observe = function(object, name){
	name = name || object.constructor;
	for(property in object){
	    if(typeof object[property] == "function"){
		object[property] = function(name_, property_, method_){
		    return function(){
			if(self.observing){
			    self.notifyBeforeCall(name_, property_);
			}
			var rv =  method_.apply(this, arguments);
			if(self.observing){
			    self.notifyAfterCall(name_, property_);
			}
			return rv;
		    }
		}(name, property, object[property]);
	    }
	}
    }
    self.notifyBeforeCall = function(name, property){
	self.observing = false;
	for(var i = 0; i < self.listeners.length; i++){
	    self.listeners[i].beforeCall(name, property);
	}
	self.observing = true;
    }
    self.notifyAfterCall = function(name, property){
	self.observing = false;
	for(var i = 0; i < self.listeners.length; i++){
	    self.listeners[i].afterCall(name, property);
	}
	self.observing = true;
    }
    self.addListener = function(listener){
	self.listeners.push(listener);
    }
    self.startObserve = function(){
	self.observing = true;
    }
    self.stopObserve = function(){
	self.observing = false;
    }
};

var StackTracer = function(){
    var self = this;
    self.stack = ;
    self.beforeCall = function(name, property){
	var key = name + "::" + property;
	self.stack.push(key);
    }
    self.afterCall = function(name, property){
	self.stack.pop();
    }
}

var Profiler = function(){
    var self = this;
    self.records = {};
    self.start = null;
    self.beforeCall = function(name, property){
	self.start = new Date;
    }
    self.afterCall = function(name, property){
	var key = name + "::" + property;
	if (typeof self.records[key] == "undefined"){
	    self.records[key] = [];
	}
	self.records[key].push( (new Date) - self.start);
    }
}

使い方はこんな感じ。

function test1(){
   test2();
}
function test2(){
   alert(s.stack.join(" -> "));
}
o = new FuncCallObserver;
s = new StackTracer;
o.addListener(s);
o.observe(window, "window");
o.startObserve();
test1(); // window::test1 -> window::test2

FuncCallObserver::observeメソッドは次の処理を実行している。引数に与えられたオブジェクト配下の関数オブジェクトを全て調べ、それらの関数オブジェクトの呼び出し前と呼び出し後に、コールバック関数を呼び出す処理を追加している。コールバック関数では、FuncCallObserverに加えられたリスナー・オブジェクトのbeforeCallメソッドとafterCallメソッドを呼び出している。また、2つのリスナー・クラスも作った。1つは、呼び出したメソッド名をスタックに格納するStackTracerクラスと、メソッドを処理するのに要した時間を記録するProfilerクラスである。

全ての関数オブジェクトをobserveするのは大変なので、ライブラリのユーザが指定する形にした。prototype.js風にクラスを記述している場合、クラスのprototypeプロパティをobserveすれば良いと思う。

なお、メソッド呼び出しの監視は、当然メソッド呼び出し時にオーバヘッドを生じさせるので、このライブラリの利用局面はデバッグ時に限られると思う。