MessagePack + WebWorkers

追記

@os0X さんと @edvakf さんからアドバイスをいただいたので、本文とコードを修正しました。

postMessageが文字列に限定されているのは旧仕様で、Firefoxや最近のWebKit(Chrome5とか)はオブジェクトをやり取りできると思います
via @os0X http://twitter.com/edvakf/statuses/15644433920

自分でも試してみました。 http://javascript.g.hatena.ne.jp/edvakf/20100607/1275931930
via @edvakf http://twitter.com/os0x/statuses/15621293442

本文

昨日の日記の続きです。
IE9Opera で動くようになり、ちょっと速くなり、WebWorkers でデコードするモードを追加しました。

http://pigs.sourceforge.jp/blog/20100607/msgpack.htm

コード一式 http://pigs.sourceforge.jp/blog/20100607/msgpack.zip

説明

このテストは、

  1. サーバ上に置いたバイナリデータを application/octet-stream または text/plain で取得する
  2. バイナリデータを JavaScript の ByteArray に変換する
  3. ByteArray を msgpack.unpack() でデコードし、元のデータを復元する

といった一連の流れになっています。
左上でインクリメントし続けるカウンターは、UI スレッドがビジーループに突入すると一時停止します。

WebWorkers が使える環境では、2. と 3. を WebWorkers で処理します。

WebWorkers なのにカウンターが止まる理由

WebWorkers と JavaScript は基本的に文字列データのやり取りしか出来ません。
いまどきの実装(Firefox3.5+, Google Chrome4+)では、WebWorkersとJavaScript は文字列以外のデータもやり取り可能でした。

そのため、msgpack.unpack() したデータを WebWorkers 上で JSON.stringify(mix) し、JavaScript 側で JSON.parse(event.data) しオブジェクトに直しています。

// msgpack.worker.js
onmessage = function(event) {
    var mix = msgpack.unpack(event.data);

//  postMessage(JSON.stringify(mix)); // 本来は不要な処理(UI スレッドは固まらないけど、CPUを食う)
    postMessage(mix);
};
if (xhr.readyState === 4) {
    var status = xhr.status,
        rv = { ok: status >= 200 && status < 300,
               status: status, option: option, data: [] };

    if (rv.ok) {
        if (!_ie && option.worker && win.Worker) {
            var worker = new Worker(_WebWorker);

            worker.onmessage = function(event) {
//              rv.data = JSON.parse(event.data); // 本来は不要な処理(ここでUI スレッドが固まる可能性がある)
                rv.data = event.data;
                callback(rv);
            };
            worker.postMessage(xhr.responseText);
        } else {
            rv.data = msgpackunpack(_ie ? toByteArrayIE(xhr)
                                        : toByteArray(xhr.responseText));
            callback(rv);
        }
    }
}

WebWorkers を使っているにも関わらずカウンターが微妙に止まってしまうことがあるのは、JSON.parse() が挟まっているのが主な理由です。

ちょっとしたこと

WebWorkersモードでは、misc/msgpack.js が misc/msgpack.worker.js を呼び、misc/msgpack.worker.js が misc/msgpack.js を呼んで処理をしています。

misc/msgpack.js などは、ライブラリの一部/単体利用/クライアントサイド/サーバサイド/WebWorker上など多彩な環境で動作するようにデザインしてあるため、条件が整えばこのように呼び戻して使うことも可能になっています。

このおかげで、misc/msgpack.worker.js のソースコードは非常にコンパクトになっています。

importScripts("utf8.js");
utf8 || importScripts("misc/utf8.js");
utf8 || importScripts("src/misc/utf8.js");

importScripts("msgpack.js");
msgpack || importScripts("misc/msgpack.js");
msgpack || importScripts("src/misc/msgpack.js");

onmessage = function(event) {
    var mix = msgpack.unpack(event.data);

//  postMessage(JSON.stringify(mix));
    postMessage(mix);
};

反省会

  • WebWorkers のオーバーヘッドがかなり大きい。
    • ネットワークアクセスが発生するので、importScripts() を出来るだけ使わないほうがいい。
      • つまり、Worker("src.js") で読み込む src.js はあらかじめ concat + minify しておくべき。
  • ベースURLを指定できないので、importScripts() がとても使いづらい。→ WorkerGlobalScope.location から相対パスを取得可能。詳しくは 2010-06-09 の日記を参照
  • IE では WebWorkers が使えないため、Flash にデータを渡して処理する方法も考えたが未着手
    • Flash と js のブリッジ ExternalInterface が重いので、Flash 側で取得しデコードしたデータを JSON で取得する方法だと何とかなるかも? ね