2009年 01月 25日

JSDeferred を高速化する (試し中)

現状の JSDeferred で何が遅いかっていうと、Deferred.next の setTimeout が遅いのです。setTimeout は interval を 0 に設定しても最低でも 10msec はかかってしまうため、next() や、それを使っている call(), loop() を何度も呼びまくるケースでは ブラウザが全くアイドルであっても 10msec * next() の回数分はかかってしまいます。

実際のところ、setTimeout を使っている理由は「現在の処理が終わったあとに、指定した処理を実行する」という用途なので、それと同じように使える location.href = "javascript:" を試してみたら、とりあえず Fx3.1 ではかなり高速になりました。

コメントをいろいろ頂きまして、最終的に (new Image).onerror (Gecko, WebKit, Opera) による非同期化と ランダム script.onreadystatechange (IE) によるものになりました。

location.href = "javascript:"; は Fx 以外だと同期するっぽくてダメでした (そもそも最初思いついたときはテスト用のページを作るのが面倒だったので GM のコンテキストでやっていて、他のブラウザのことをシカトってました)

「現在の処理が終わったあとに、指定した処理を実行する」と書いたのですが、正確には「「現在の処理が終わったあとに、指定した処理を実行する (ただし UI をブロックしない)」みたいな感じです。

Deferred.next = function (fun) {
	var d = new Deferred();
	var me = Deferred.next;

	switch (true) {
		case (me._enable_faster_way && me._enable_faster_way_Image) : {
			var img = new Image();
			var handler = function () {
				d.canceller();
				d.call();
			};
			img.addEventListener("load", handler, false);
			img.addEventListener("error", handler, false);
			d.canceller = function () {
				img.removeEventListener("load", handler, false);
				img.removeEventListener("error", handler, false);
			};
			img.src = "data:,/ _ / X";
			break;
		}
		case (me._enable_faster_way && me._enable_faster_way_readystatechange && (Math.random() < 0.875)) : {
			var cancel = false;
			var script = document.createElement("script");
			script.type = "text/javascript";
			script.src  = "javascript:";
			script.onreadystatechange = function () {
				if (!cancel) {
					d.canceller();
					d.call();
				}
			};
			d.canceller = function () {
				if (!cancel) {
					cancel = true;
					script.onreadystatechange = null;
					document.body.removeChild(script);
				}
			};
			document.body.appendChild(script);
			break;
		}
		default : {
			var id = setTimeout(function () { clearTimeout(id); d.call() }, 0);
			d.canceller = function () { try { clearTimeout(id) } catch (e) {} };
		}
	}

	if (fun) d.callback.ok = fun;

	return d;
};
Deferred.next._enable_faster_way = true;
Deferred.next._enable_faster_way_Image = (/\b(?:Gecko\/|AppleWebKit\/|Opera\/)/.test(navigator.userAgent));
Deferred.next._enable_faster_way_readystatechange = (/\bMSIE\b/.test(navigator.userAgent));

これで主要ブラウザは数倍早く動くように。

Opera は異様に早くコールバックがきます。

参考までに同じスクリプトをそれぞれのブラウザで動かしたときのどのぐらいかかるか表にしときます。結構ばらつくのと、環境が違うのであんまりアテにならないです。

環境 setTimeout faster_way %
Opera 9.5 (Mac) 80sec 1.7sec 4706%
Internet Explorer (Win), calc time 120sec 6sec 2000%
Safari 3.1 (Mac) 80sec 6sec 1300%
Google Chrome (Win) 31sec 2.4sec 1200%
Firefox 3.1b2 (Mac) 83sec 10sec 830%
Internet Explorer (Win) 120sec 17sec 700%

ちなみに setTimeout のコールバック最小時間は、IE は 15-16msec で、Chrome は 4-5msec 他のブラウザは 10-12msec です。( http://gist.github.com/51564 ) setTimeout 版では思いっきりそれの差が出てます。

http://subtech.g.hatena.ne.jp/cho45/20090124/1232782523 のスクリプト (1471 step かかる) を普通に Fx3.1 で実行すると、100sec ほどかかるのが、以下のような Deferred.next にすると 50sec ぐらいまで縮みました。(ちなみに Gauche で同じことすると一瞬)

Deferred.next = function (fun) {
	var d = new Deferred();
	var Global = typeof(unsafeWindow) == "undefined" ? (function () { return this })() : unsafeWindow;
	var cbname;
	do {
			cbname = "callback" + String(Math.random()).slice(2);
	} while (typeof(Global[cbname]) != "undefined");

	Global[cbname] = function () {
			delete Global[cbname];
			d.call();
	};

	d.canceller = function () {
			Global[cbname] = function () {
					delete Global[cbname];
			};
	};

	if (fun) d.callback.ok = fun;

	location.href = "javascript:" + cbname + "()";

	return d;
};

が、IE だととりあえずこのままだと動かない\(^o^)/

[あとで続き書く]

とりあえず delete Global[cbname]; が IE だとエラーなので try catch すればうごくはず。もうねる

残念ながらこの方法は Firefox 以外だと動かないみたいだ。Firefox 以外では location.href に代入した瞬間に実行されてしまう。うんこですね。ぬか喜びですね。

http://coderepos.org/share/changeset/29001

Image オブジェクトの error イベントを掴まえてくるようにしてみた。Fx, Safari, Opera ではテスト通ってくれるので判定いれてみてる。他になんかいい方法ないかなぁ

なぜか location.href 代入よりさらに早くなって 21sec ぐらいで完了するようになった。