Files
DrSmoothl f3f5d27c65 perf(bench): bench harness for receive/send chains (RoadMap #5 baseline)
vitest-driven benches at `packages/bridge/bench/recv.bench.ts` and
`packages/onebot/bench/send.bench.ts`. Picks up proton's vite plugin
so call sites get the inlined codecs (running via tsx + proton
runtime-fallback would throw without a pre-built runtime map).

Run via `pnpm -F @snowluma/bridge bench` and
`pnpm -F @snowluma/onebot bench`.

What's measured
---------------
Receive chain:
  - parseMsgPush: pure decode + dispatch (no SQLite, no event emit)
    * group_message text          ~ 0.29 μs/op  (3.4M ops/sec)
    * group_message 5 mixed       ~ 0.50 μs/op  (2.0M ops/sec)
    * group_member_join           ~ 0.25 μs/op  (4.1M ops/sec)
  - IncomingPacketPipeline.process: full pipeline (with SQLite via
    rememberGroupMemberIdentity, BridgeEventBus emit)
    * group_message text          ~ 0.42 μs/op  (2.4M ops/sec)
    * group_message 5 mixed       ~ 0.67 μs/op  (1.5M ops/sec)
    * group_member_join           ~17.5  μs/op  ( 57K ops/sec) ⚠

Send chain:
  - parseMessage (OneBot input → MessageElement[])
    * CQ string text              ~ 0.31 μs/op
    * CQ string mixed             ~ 1.22 μs/op
    * segment array text          ~ 0.32 μs/op
    * segment array 5             ~ 1.07 μs/op
  - messageStore SQLite writes:
    * storeMeta                   ~17.0  μs/op  ( 59K ops/sec) ⚠
    * storeEvent (~250B JSON)     ~23.2  μs/op  ( 43K ops/sec) ⚠
  - messageStore reads:
    * findMeta hit                ~ 5.4 μs/op  (186K ops/sec)
    * findMeta miss               ~ 3.7 μs/op  (272K ops/sec)
    * findEvent hit               ~ 3.8 μs/op  (262K ops/sec)

Headline finding
----------------
Pure decode/encode is already cheap (sub-μs). The real cost per
sent message is the SQLite write pair (storeMeta + storeEvent =
~40 μs). On receive, group_message itself does NOT trigger
identity writes; only join/leave/admin events do — but those go
through 4-6 db.prepare(sql).run/get(...) calls each, all re-parsing
SQL on every invocation (the canonical SQLite anti-pattern).

The follow-up commits in this series target:
  1. Replace `node:sqlite` (experimental, API may shift) with
     `better-sqlite3` (stable, sync, ~30-50% faster wire) + cache
     prepared statements as readonly fields. Single commit, both
     wins together — better-sqlite3's API encourages stmt caching
     by design. Expected: storeMeta / storeEvent drop to ~3-5 μs,
     group_member_join drops to ~3-5 μs.
  2. Skip rememberGroupMemberIdentity DB write when the in-memory
     uidByUin / uinByUid cache already has the mapping unchanged.
  3. Drop the `Buffer.from(pkt.body)` allocation in msg-push
     context (pkt.body is already Uint8Array; proton accepts it).
  4. Make `elementsToJson` sync-fast-path when no async resolvers
     are wired (saves per-element microtask overhead).

Each commit re-runs these benches and the message should quote the
before/after numbers so the deltas are auditable.

Note: tsx is NOT involved — vitest's bench mode owns the run, and
the proton vite plugin is configured in each `vitest.config.ts`.
Absolute numbers reflect the production code path.
2026-05-21 10:26:29 +08:00
..