GreasemonekyからFlashに処理を委譲するサンプル(途中)

Greasemonkeyスクリプトで重い処理をやっちゃうと重いので、Flashに処理を委譲しちゃおう」の途中結果です。

今回は、以下のようなシナリオの実現を目的としてました。
1. Wikipedia日本語版のページにアクセスしたら、GMスクリプト起動。
2. GMからページのテキストを抽出し、Flash側に通知。
3. Flashでテキストの2-gramを算出。
4. FlashからGMスクリプトへ2-gramを通知。
5. GMスクリプト側で2-gramをalertで表示。

Flashに処理をさせている間はブラウザは自由に動けるはずなので、重い処理をクライアント側で行う際の設計パターンの一つにならんかな?というのがこの試みの動機です。

まずは、AS3のスクリプトをば。初AS3でしたが、意外とAS3では手間取りませんでした。昨日の問題を除いてな!

//NGramer.as
package {
    import flash.display.Sprite;
    import flash.events.*;
    import flash.external.ExternalInterface;
    import flash.system.Security;
    import flash.text.TextField;

    public class NGramer extends Sprite {
        private var output:TextField;

        public function NGramer() {
            output = new TextField();
            output.y = 25;
            output.width = 450;
            output.height = 325;
            output.multiline = true;
            output.wordWrap = true;
            output.border = true;
            output.text = "Initializing...\n";
            addChild(output);

            if (ExternalInterface.available) {
                try {
	            Security.allowDomain("*");
                    output.appendText("Adding callback...\n");
                    ExternalInterface.addCallback("sendToActionScript", receivedFromJavaScript); //(1)
                } catch (error:SecurityError) {
                    output.appendText("A SecurityError occurred: " + error.message + "\n");
                } catch (error:Error) {
                    output.appendText("An Error occurred: " + error.message + "\n");
                }
            } else {
                output.appendText("External interface is not available for this container.");
            }
        }
	private function ngramize(text:String, n:int):Array {
	    var set:Object = {};
	    var ret:Array = [];
	    for(var i:int = 0; i< text.length - n + 1; ++i){
	        var gram:String = text.substring(i, i + n);
		if(!set[gram]){
		    set[gram] = true;
		    ret.push(gram);
                }
	    }
	    return ret;
	}
        private function receivedFromJavaScript(value:String):void {
	    var grams:Array = ngramize(value, 2)
	    ExternalInterface.call("sendToJavaScript", grams); //(2)
        }
    }
}

(1)の部分でJavaScriptからFlashを呼ぶ際のインタフェースとコールバックを定義し、(2)でFlashからJavaScriptの関数を呼んでいます。
FlashからJSだと、グローバルの関数しか呼べないんだよね、きっと。昨日の問題があるので、配列オブジェクトをそのまま送っています。本当はJSON化してから送ろうかと思っていたんですがね。
ファイル名や関数名がださいのは仕様です。
このファイルをmxmlc NGramer.asとしてswfを作成し、どこかのWebサーバに置いておきます(localhostでもいいが、ファイルのswfにアクセスするのはセキュリティサンドボックス侵害になるらしく駄目←ハマりポイント)。

で、問題がGM側のスクリプト。こっち側がまだヤワいので、要改善です。

// ==UserScript==
// @name           Delegation to Flash
// @namespace      http://d.hatena.ne.jp/matsuza
// @description    Delegating heavy task to Flash
// @include        http://ja.wikipedia.org/*
// ==/UserScript==	

(function(){
	var flash = document.createElement('embed')
	flash.setAttribute('src', 'http://karmatsuza.sakura.ne.jp/NGramer.swf')
	flash.setAttribute('width', '500')
	flash.setAttribute('height', '375')
	flash.setAttribute('name', 'NGramer')
	flash.setAttribute('play', 'true')
	flash.setAttribute('allowScriptAccess', 'always')
	flash.setAttribute('type', 'application/x-shockwave-flash')
	flash.setAttribute('pluginspace', 'http://www.macromedia.com/go/getflashplayer')
	document.getElementsByTagName('body')[0].appendChild(flash)

	unsafeWindow.sendToJavaScript = function(txt){alert(txt)}
	
	//document.NGramer.sendToActionScript(document.getElementById('content').innerHTML);//(1)
	//unsafeWindow.document.NGramer.sendToActionScript(document.getElementById('content').innerHTML);//(2)
	//flash.sendToActionScript(document.getElementById('content').innerHTML);//(3)
	//setTimeout("document.NGramer.sendToActionScript(document.getElementById('content').innerHTML)", 0); //(4)
	//setTimeout(function(){document.NGramer.sendToActionScript(document.getElementById('content').innerHTML)}, 1000) //(5)
	//setTimeout("document.NGramer.sendToActionScript(document.getElementById('content').innerHTML)", 1000) //(6) OK
	setTimeout(function(){unsafeWindow.document.NGramer.sendToActionScript(document.getElementById('content').innerHTML);}, 1000)

})()

最初の段落で、Flashのオブジェクトを埋め込んでいます(GM使うからIE系は無視)。allowScriptAccess属性をtrueにする必要があります(ハマりポイント)。
次の段落で、Flashから呼ばれるJSのグローバル関数を登録しています。GM環境上なので、windowではなくてunsafeWindowに対し登録する必要があります(ハマりポイント)。
で、最後の段落に試行錯誤の後が読み取れますが、Flashに対しHTMLのテキストを送信しようと試みています。
このうち、うまくいくのは(6)と一番最後の行のモノだけです(どハマりポイント)。

  • GM環境でのdocumentオブジェクトは本物のdocumentとは異なるので、色々とできないことがある模様。unsafeWindow.documentでアクセスしないと駄目?
    • (5)が失敗するのは、スコープチェーン上のdocumentがGMのdocumentだから。(6)は本物のdocumentが手に入るスコープ上でevalされて実行されるので、動いてるのだろう(たぶん)。
  • Flashオブジェクト登録直後はFlash側で準備ができていないらしく、少し待たないと駄目。

ウェイト入れるのが気持ち悪いので、もうちょっとなんとかしないと。Flash側からJS側に処理開始を通知するのが妥当かな。

それよりも重大なことがありまして……GMからFlashに処理を委譲したにもかかわらず、n-gram算出中にブラウザが非常に重くなる(というか止まる)ということ。あっっれぇぇぇ? GM(というかJavaScript)で重い処理をさせたらブラウザが止まるからFlashに処理を委譲しようよ、という案だったはずなのに、Flashに委譲しても処理が止まる? もしかしてブラウザとFlashって同じスレッドで動いてる?
マルチコアなCPU上で動かしたら異なる結果になるのかも知れませんが、うちにはマルチコアCPUが無いので検証できず。もしマルチコア環境下でもすっかりブラウザが止まってしまうようなことになってしまったら、ものすごいショボーンなんですが……。SpiderMonkey(FirefoxJavaScript実行環境)よりTamarin(FlashVM)の方が処理が速いから意味があるよ!つってもなあ。

……奥様に内緒でデスクトップPCを更新しちゃおうかな? マザボとCPUを換えるだけなら気づかれないだろうし……。