続・Koriのパフォーマンスをチェックして今度は納得した

↓この記事の後半のパフォーマンスチェック部分の続き。なんか違和感がある結果だなぁって思ってて、もう少し詳しくみていったら今度はわりと納得できる結果になった。

Validationの違和感

Koriはバリデーターによってパフォーマンスが大きく変わることはないのに、Honoは違っていてValibotのパフォーマンスがいいのが気になっていた。KoriとHonoでそんなに傾向に違いがでるものなのかなぁ?って。

なんでだろう?Koriのバグなのかな?と思って見てみても正しくうごいていそう。と、ふと、Valibotのエンドポイントだけにしてみたら結果が変わった。

元々はこういうふうに1つのappにすべてのエンドポイントを登録して、それぞれのエンドポイントに順番に負荷をかけて結果を見ていた。(このコードはHonoの部分)

app.get('/', (c) => c.text('Hi'))
    .get('/id/:id', (c) => {
        c.header('x-powered-by', 'benchmark')
        return c.text(`${c.req.param('id')} ${c.req.query('name')}`)
    })
    .post('/json', (c) => c.req.json().then((body) => c.json(body)))
    .post('/validate-zod', sValidator('json', zodSchema), (c) =>
        c.json(c.req.valid('json'))
    )
    .post('/validate-valibot', sValidator('json', valibotSchema), (c) =>
        c.json(c.req.valid('json'))
    )
    .post('/validate-arktype', sValidator('json', arktypeSchema), (c) =>
        c.json(c.req.valid('json'))
    )

https://github.com/bufferings/bun-http-framework-benchmark/blob/3d9a10d4cd3844f1eef221efeb9d618157ebbf11/src/bun/hono.ts#L28-L42

それを1つのエンドポイントだけにして、負荷をかけたら結果が変わったのだ。

const app = new Hono();

app.post(
  "/",
  sValidator("json", valibotSchema),
  (c) => c.json(c.req.valid("json")),
);

つまり、複数のエンドポイントを登録してたから、ルーティングがパフォーマンスに影響を与えてたってことか。ということは、バリデーターによるパフォーマンスの違いを見るなら1つのエンドポイントにしてチェックしなきゃだな。

シングルエンドポイントで再チェック

ということで、1つのエンドポイントにして負荷をかけたら、わりと納得できる結果になった。

リポジトリは前回と同じ、ここ

Results (req/s)

Runtime Framework ping query body zod valibot arktype elysia-t
bun elysia@1.4.15 262,874 68,157 66,073 50,063 47,849 49,988 45,280
deno hono@4.10.4 109,988 69,248 47,712 39,662 41,646 41,098 -
deno kori@0.3.4 95,314 81,964 55,168 40,720 41,061 41,706 -
bun hono@4.10.4 85,158 63,098 50,551 43,158 42,973 42,272 -
bun kori@0.3.4 74,613 62,301 58,756 38,383 37,140 37,936 -
node fastify@5.6.1 71,663 65,422 33,421 - - - -
node hono@4.10.4 66,110 57,674 19,748 18,060 17,658 17,937 -
node kori@0.3.4 60,881 55,401 22,714 18,324 18,591 19,173 -
node express@5.1.0 14,677 14,012 11,051 - - - -

グラフ

感想

ElysiaのPing(Hiと返すだけのエンドポイント)がめちゃ速い。最初にx64で見てたときは10万rpsを切ってたのだけど、arm64に変更したら26万rps出してきて、へー!全然違うんだなぁ!ってなった。

それ以外の項目もElysiaが速いのは速いけど、Pingほど大きな差ではないなという感想。

バリデーターによって多少の違いはあるものの、そんなに大きな違いはない。前回のHonoはZodよりValibotの方が結構いいパフォーマンスを出していたけど、今回はそこまで変わらない。

Elysiaの組み込みバリデーションの t は速いのかな?と思ってチェックしてみたけど、Elysiaの他のバリデータの方が速かった。

HonoとKoriはBunよりDenoの方がいいパフォーマンスを出してる。特にDeno + Koriのクエリパラメータだけ触るエンドポイントがElysiaを超えてるのは笑った。なんで?

ということで、今回は全体的に納得できる結果になった。

参考

結果はここに出力されている

https://github.com/bufferings/bun-http-framework-benchmark/blob/91819b029b6084d848e538f70a671f968f490792/docs/bench-single-2vm/README.md

複数エンドポイントでもチェック

複数エンドポイントを登録してたことがパフォーマンスに影響を与えてたんだよなぁ、じゃあ、複数エンドポイントを登録してる場合は、どういう傾向になるんだろう?と興味がでたので、Zodでバリデーションをかける形で1つのインスタンスに10エンドポイント登録して負荷をかけてみた。これもまぁ納得できるかな。

Endpoints

# Method Path Validation
GET /api/users/:id UUID param
GET /api/users Query params (page, limit)
POST /api/users Body
PUT /api/users/:id UUID param + body
DELETE /api/users/:id UUID param
GET /api/posts/:id Numeric ID param
GET /api/posts Query params (userId, page)
POST /api/posts Body
PUT /api/posts/:id Numeric ID param + body
POST /api/comments Body

Results (req/s)

Runtime Framework Avg
bun elysia@1.4.15 54,790 70,050 57,932 44,456 43,075 69,463 70,067 57,311 45,188 43,758 46,600
deno kori@0.3.4 47,199 46,080 59,239 40,063 30,549 60,560 62,094 60,570 40,493 31,748 40,595
deno hono@4.10.4 46,230 44,774 57,603 41,055 30,505 57,367 58,976 57,276 41,205 31,661 41,883
bun hono@4.10.4 41,618 45,340 45,374 39,937 34,483 44,890 46,449 44,152 39,989 35,572 39,991
node fastify@5.6.1 39,860 50,679 49,881 29,532 29,113 51,420 51,829 50,234 28,577 28,258 29,078
bun kori@0.3.4 39,281 43,705 43,405 35,837 32,454 43,139 44,163 42,777 36,541 33,939 36,847
node hono@4.10.4 26,003 38,991 36,905 17,803 15,780 35,951 35,000 33,150 15,684 14,774 15,988
node kori@0.3.4 21,308 27,314 25,965 17,631 16,409 22,206 26,429 25,789 17,515 16,187 17,630

グラフ

感想

やっぱり、Elysiaとめちゃくちゃ差があるって感じではないんだなぁという感想

Node.js + Fastifyが、Bun + Hono/Kori より半分のエンドポイントで速いのすごい

やっぱりHonoとKoriはDenoが速い。特にKoriはなぜかDenoと相性がいいみたい

参考

結果はここに出力されている

https://github.com/bufferings/bun-http-framework-benchmark/blob/91819b029b6084d848e538f70a671f968f490792/docs/bench-multi-2vm/README.md

まとめ

今回の結果にはわりと納得した。KoriがHonoより遅すぎないことを確認したくて始めたチェックだけど、ちょっと遅いくらいで遅すぎはしないなって思えたのでよかった。

あとは、このベンチをローカルで流しながら、フレームグラフでも眺めてちょこちょこチューニングしてみるかな。

おしまい

おまけ

負荷をかけながら、いろいろ気になったことがあって対応していったら、いろいろやりすぎてしまった。プライベートは好き勝手にできてよい。

  • ついでに、Elysiaの t という組み込みバリデーターもチェックしてみることにした
  • ついでに、SVGでグラフも生成してもらうことにした
    • ぱっと見て分かりたいなと思って「State of JSのこういうグラフみたいなので頼む!」ってお願いした
  • ついでに、Load/TargetのVMを分けようと思って、Google Cloud上にSelf Hosted Runner + 1VMを起動して負荷をかけて、終わったらシャットダウンするGitHub Actionsにした
    • だから、このリポジトリには僕以外をInviteしてはいけない(GitHub Actions経由でGoogle CloudのVMを操作できてしまうから)
  • ついでに、x64じゃなくてarm64のイメージを使うことにした
    • Bunはarm64の方がパフォーマンスがいいのかなと思って & 実際にアプリをデプロイするならarm64にするかなぁと思って。結果ElysiaのPingエンドポイントがめちゃ速くなった
  • ついでに、bombardierじゃなくてohaを使って負荷をかけることにした
    • ohaだと複数エンドポイントに負荷をかけられるから入れ替えてみたけど、結局その機能は使わなかったから、まぁbombardierのままでも良かった。でもohaはおはようだし、なんとなく好き
  • ついでに、HonoはRegExpRouterじゃなくてSmartRouterを使うことにした
    • これも、僕が使うときはRegExpRouterじゃなくてSmartRouterを使うよなと思ってそうした。遅くなるのかな?と思ったけど今回のテストの範囲では影響は感じなかった
  • Google CloudのVMでCPUをいっぱい積んだやつでやってもそんなにパフォーマンスが変わらなくて、よくよく調べてみたらNode.jsもDenoもBunも通常はシングルスレッドで動くんだった。そっか。ということで2コアにしてスッキリした。
  • あと、大阪に住んでるから大阪リージョンで構築してたんだけど、そっちにはarm64のマシンが構築できなくて、結局東京リージョンで作り直した。