Flash Advent Calendar 7日目 - 俺のWeb Workersがこんなに遅いわけがない外伝 -


今日は、らくさんのこの記事の外伝となります。

まずは、本家の記事を読んでいただければと思います。

本家「俺のWeb Workersがこんなに遅いわけがない」

目次

  • おさらい
  • worker用のJSファイルを作らない
  • buffer以外のobjectやarrayの転送で遅延するケース

おさらい

遅くなる例

const typedArray = new Uint8Array(1024 * 1024 * 256);
worker.postMessage({ "data": typedArray });

原因はpostMessageの第2引数にbufferをセットせずに送信した結果
複製されたデータを転送するのに異常な時間がかかる事です。

この回避策として、下記の実装のように第2引数の配列のbufferを設置します。
コピー無しで転送されるため大きなデータでも速度がでます。

const typedArray = new Uint8Array(1024 * 1024 * 256);
worker.postMessage({ "data": typedArray }, [typedArray.buffer]);

注意点

この時にメインスレッド側のtypedArrayの中身は空っぽになります。

const typedArray = new Uint8Array(1024 * 1024 * 256);
worker.postMessage({ "data": typedArray }, [typedArray.buffer]);

console.log(typedArray.length); // 0が出力

同じように、workerからメインスレッドに渡すとworker側のbufferは空っぽになります。

this.addEventListener("message", function (event)
{ 
    // 加工処理
    const typedArray = this.edit(event.data);   

    this.postMessage({ "data": typedArray }, [typedArray.buffer]);

    console.log(typedArray.length); // 0が出力
});

加工する前のbufferを保管したい場合は、どこかで複製が必要になるので注意が必要です。

const typedArray = new Uint8Array(1024 * 1024 * 256);
const cloneArray = typedArray.slice(0);

worker.postMessage({ "data": typedArray }, [typedArray.buffer]);

console.log(typedArray.length); // 0が出力
console.log(cloneArray.length); // 268435456が出力

worker用のJSファイルを作らない

今回はswf2js.jsだけで完結したいという目標があったので
worker用のJSファイルを設置しないで作り込みたいので以下のような対応をしました。

const worker = new Worker(URL.createObjectURL(new Blob(
    ['###JavaScriptの処理###'], 
    { "type": "text/javascript" }
));

個別にworkerディレクトリを作ります。

src/
├── worker/
│   ├── UnzipWorker.js
│   ├── UnLZMAWorker.js
│   ├── SwfParserWorker.js
etc...

その中にあるJSファイルをgulpで強制的にMinify
文字列の###JavaScriptの処理###をキーにして置換してしまいます。

gulp起動後

src/
├── worker/
│   ├── UnzipWorker.js
│   ├── UnzipWorker.min.js <= MinifyされたJSファイルが自動で上書きor追加される
│   ├── UnLZMAWorker.js
│   ├── UnLZMAWorker.min.js <= MinifyされたJSファイルが自動で上書きor追加される
│   ├── SwfParserWorker.js
│   ├── SwfParserWorker.min.js <= MinifyされたJSファイルが自動で上書きor追加される
etc...

gulp側の処理


const fs = require("fs");

...省略

replace(
    "###JavaScriptの処理###", 
    fs.readFileSync("src/worker/UnzipWorker.min.js", "utf8")
        .replace(/\\/g, "\\\\") // バックスラッシュをエスケープ
        .replace(/'/g, "\\'") // 置換元の文字列をシングルクォーテーションでくくってるので、シングルクォーテーションをエスケープ
        .replace(/\n/g, "") // 改行を削除
);

...省略

これで一個のファイルに複数のworkerを設置できます。

buffer以外のobjectやarrayの転送で遅くなるケース

大容量もしくはネストの深いobjectarrayを転送すると
送る側も受け取る側も負荷が高くなり、そこがボトルネックで速度遅延します。

回避策
1. TypedArrayに変換できないか検討
2. できるだけ小さい単位で転送する

1は上の説明の通り、一番転送速度が安定します。
ただ、文字列など使う場合はこの方法が使えません。
なので、2の「できるだけ小さい単位で転送する」事で回避を試みます。

worker側でメインスレッドにデータを送る

function 関数()
{
    const object = {};

    // objectを作り込むロジック
    ...省略

    // できるだけ小さい単位でメインスレッドに送る
    globalThis.postMessage({
        "key": "分岐しやすい文字列",
        "data": object
    });
}

メインスレッド側の個別受け取り処理


const worker = new Worker(URL.createObjectURL(new Blob(
    ['###JavaScriptの処理###'], 
    { "type": "text/javascript" }
));

worker.onmessage = function (event)
{
    switch (event.key) {

        case "分岐しやすい文字列1":
            // 受け取り処理を実装1
            return ;

        case "分岐しやすい文字列2":
            // 受け取り処理を実装2
            return ;

        case "分岐しやすい文字列3":
            // 受け取り処理を実装3
            return ;

        case "分岐しやすい文字列4":
            // 受け取り処理を実装4
            return ;

        default:
            // workerを終了する
            event.target.terminate();
            break;

    }
}

workerはメインスレッドに負荷がかからない便利な機能ですが
使い方を間違えると、サブスレッドとメインスレッドの両方に負荷がかかってしまいます。

この記事が何かの役に立てば嬉しいです。

明日からはWebGLに移行した時に色々と苦戦した事を書こうと思います。