🧊

Cloudflare WorkersをRust(WASM)で書くと速いのか

なんとなく察しはついてるけど、いちおう確かめておこうかと。

GitHub - leader22/workers-benchmark

詳細はこのリポジトリに。

Rustで書くには

ドキュメントなどあらゆる情報は、Cloudflare公式のこのリポジトリにある内容がすべて。

GitHub - cloudflare/workers-rs: Write Cloudflare Workers in 100% Rust via WebAssembly

Workerグローバルのコードがそれ用のcrateになってて、それを使ってRustでコードを書く。RequestやらKVやらだけでなく、いわゆるRouterやちょっとした便利関数まで実装されてた。

READMEにあるコード例をそのまま貼るとこんな雰囲気で。

use worker::*;

#[event(fetch)]
pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result<Response> {

    // Create an instance of the Router, which can use paramaters (/user/:name) or wildcard values
    // (/file/*pathname). Alternatively, use `Router::with_data(D)` and pass in arbitrary data for
    // routes to access and share using the `ctx.data()` method.
    let router = Router::new();

    // useful for JSON APIs
    #[derive(Deserialize, Serialize)]
    struct Account {
        id: u64,
        // ...
    }
    router
        .get_async("/account/:id", |_req, ctx| async move {
            if let Some(id) = ctx.param("id") {
                let accounts = ctx.kv("ACCOUNTS")?;
                return match accounts.get(id).json::<Account>().await? {
                    Some(account) => Response::from_json(&account),
                    None => Response::error("Not found", 404),
                };
            }

            Response::error("Bad Request", 400)
        })
        // handle files and fields from multipart/form-data requests
        .post_async("/upload", |mut req, _ctx| async move {
            let form = req.form_data().await?;
            if let Some(entry) = form.get("file") {
                match entry {
                    FormEntry::File(file) => {
                        let bytes = file.bytes().await?;
                    }
                    FormEntry::Field(_) => return Response::error("Bad Request", 400),
                }
                // ...

                if let Some(permissions) = form.get("permissions") {
                    // permissions == "a,b,c,d"
                }
                // or call `form.get_all("permissions")` if using multiple entries per field
            }

            Response::error("Bad Request", 400)
        })
        // read/write binary data
        .post_async("/echo-bytes", |mut req, _ctx| async move {
            let data = req.bytes().await?;
            if data.len() < 1024 {
                return Response::error("Bad Request", 400);
            }

            Response::from_bytes(data)
        })
        .run(req, env)
        .await
}

だいたいイメージどおりだった。非同期なコードを書くけど、`tokio`とか`async-std`とかのそれではない。

実行時は`wasm32-unknown-unknown`でコンパイルするので、そこで動かないものは動かないとのこと。

気になる比較結果

  • リクエストにとりあえずレスポンスするだけのコードを書いて
  • JavaScriptとRust(WASM)でそれぞれデプロイして
  • そのビルドされたサイズを見比べて
  • 速度を`autocannon`で計測した

のが冒頭のリポジトリであるコレ。

GitHub - leader22/workers-benchmark

結果としてはまぁ大方の予想通り。

  • 速度: 大差ないけど、Rust(WASM)のほうが微妙に遅い
  • サイズ: Rust(WASM)のほうが明らかにデカい

もっと遅くなるかと思ってたけど、意外にこんなもんで済むのか〜ってなったのは収穫だった。

サイズに関してはもうどうしようもなさそう・・・?このガイドに従ってもう少し頑張れるって書いてあった。

Shrinking .wasm Size - Rust and WebAssembly

`opt-level`を`s`から`z`にしたら、16KBくらい減ったけど誤差っぽい。`gzip`するとか`brotli`するとかはやってない。

というわけで

もう少し踏み込んだ処理をする内容だったらば、パフォーマンスに差が出てきたりするんか・・・?って思いつつ、それはでも結局JSでやるべきか vs WASMでやるべきかの話でしかなく、WorkerのコードとしてRustを選ぶ理由にはならんかな・・?

そもそも、Rustで書いたプロジェクトもビルドするとこういう構成になる。

build
├── README.md
├── index.js
├── package.json
└── worker
    ├── index_bg.mjs
    ├── index_bg.wasm
    └── shim.mjs

つまり実行環境としては処理の大部分がWASMに寄るってだけで、実質はJavaScriptのそれなのでは・・。

わざわざ全部Rustで書きたい強い気持ちがないなら、JavaScript(TypeScript)でいいし、WASMのモジュールが必要ならそのときに`import`すればいいはず。

あとRustで書くとき、ファイル開いてからCOC経由で`rust-analyzer`が仕事するようになるまで30秒くらい待つのが地味に不便で、在りし日のTSCの遅さを思い出した。