logos-delivery/docs/analysis/plan_phase1_bench.md
NagyZoltanPeter 05ac82d454
docs: async copy analysis and 3-phase elimination plan
Analysis of chronos/refc deep-copy behavior on the WakuMessage path
(~19+F payload copies, 4-6 SHA-256 passes per inbound relayed message)
plus executor plans for: perf harness, WakuMessage ref object, WakuEnvelope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 04:07:59 +02:00

126 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 1 — Message-path performance benchmark (executor plan)
> Self-contained implementation plan. Context: [async_copy_analysis.md](async_copy_analysis.md)
> and [async_copy_fix_plan.md](async_copy_fix_plan.md). Read both before starting.
>
> **Objective**: a reproducible before/after benchmark of the WakuMessage inbound/outbound
> pipeline, plus decode/hash counters that verify the analysis claims (≥4 decodes and 46
> SHA-256 passes per inbound relayed message on the current code). This phase changes **no
> production behavior** — counters must compile to nothing without the define.
## Constraints (apply to all phases)
- Nim 2.2.4, `--mm:refc` (enforced by nimble tasks). Build via `make` / nimble tasks only.
- Format all touched files with nph (`make nph/<file>` or the pre-commit hook).
- Run `gitnexus_impact` before editing cross-module symbols and `gitnexus_detect_changes()`
before committing (project rule in AGENTS.md). Commit at the end of the phase; **never push**.
- Verify claims by running things, not by reasoning ("should work" is not done).
## Existing infrastructure to reuse
- `apps/benchmarks/benchmarks.nim` — existing RLN benchmark; nimble task at
`logos_delivery.nimble:391-393` (`task benchmarks``buildBinary name, "apps/benchmarks/"`).
Mirror this pattern; note it already imports from `tests/` (`tests/waku_rln_relay/utils_onchain`),
so importing `tests/testlib/*` from `apps/benchmarks/` is accepted practice.
- `tests/testlib/wakucore.nim``fakeWakuMessage*` (line ~74) for deterministic messages.
- `tests/testlib/wakunode.nim` (verify exact name) — node construction helpers used by all
node tests; use these for the macro scenario.
## Step 1 — Instrumentation counters (`-d:msgPathCounters`)
New file `logos_delivery/waku/waku_core/message/path_counters.nim` (~35 LOC):
```nim
when defined(msgPathCounters):
type MsgPathCounters* = object
decodeCalls*, hashCalls*: int64
decodedBytes*, hashedBytes*: int64
var msgPathCounters* {.global.}: MsgPathCounters
template countDecode*(bytes: int) = ... # inc calls, add bytes
template countHash*(bytes: int) = ...
proc resetMsgPathCounters*() = ...
else:
template countDecode*(bytes: int) = discard
template countHash*(bytes: int) = discard
```
Hook points (one line each, guarded template — zero cost when undefined):
- `logos_delivery/waku/waku_core/message/codec.nim:23` — inside
`proc decode*(T: type WakuMessage, buffer: seq[byte])`: `countDecode(buffer.len)`.
- `logos_delivery/waku/waku_core/message/digest.nim:53` — inside `computeMessageHash`:
`countHash(msg.payload.len + msg.meta.len)`.
Export via `waku_core` module as needed. Single-threaded chronos — plain int64, no atomics.
## Step 2 — Micro benchmark (in-process pipeline, no network)
New file `apps/benchmarks/message_path_bench.nim` (~250320 LOC total with Step 3).
Setup: one WakuNode with relay mounted + archive (SQLite **in-memory** driver) + store-sync
(reconciliation) mounted; subscribe one shard so `registerRelayHandler`
(`logos_delivery/waku/node/subscription_manager.nim:29`) installs the full
`uniqueTopicHandler` chain (trace/filter/archive/sync/internal handlers, :45-82).
Injection: per message, replicate the gossipsub inbound sequence using WakuRelay's own
registered artifacts (both are stored in public tables on `WakuRelay`,
`waku_relay/protocol.nim:624,632`):
1. `let data = msg.encode().buffer` (prepared up front, excluded from timing)
2. `discard await relay.topicValidator[shard](shard, Message(topic: shard, data: data))`
— exercises the ordered-validator decode (`protocol.nim:543`)
3. `await relay.topicHandlers[shard](shard, data)` — exercises topicHandler decode
(`protocol.nim:603`) + full dispatch chain
Note in the output that observer decodes (`protocol.nim:271,326,341`) are NOT exercised by
the micro bench (they fire only in real gossipsub dispatch) — the macro scenario covers them.
If `topicValidator`/`topicHandlers` turn out not to be exported, add a
`when defined(msgPathCounters)` accessor to `waku_relay/protocol.nim` rather than exporting
the fields unconditionally.
Workload: fixed-seed generator (xorshift or `std/random` with `randomize(42)` equivalent —
NO time-based seed), payload mix **10 kB / 50 kB / 150 kB at 25 % / 50 % / 25 %**, unique
timestamps/nonce per message (avoid dedup), N = 1,000 messages + 100 warmup (excluded).
## Step 3 — Macro benchmark (two nodes, loopback gossipsub)
Same file, second scenario: two in-process nodes (testlib helpers), connected, both
subscribed to the shard; node B additionally has archive + sync mounted. Node A publishes
the same workload; node B counts arrivals via an app handler; stop timing when all N
received (with a sane timeout). This path includes libp2p rpcHandler + observers, so
counters here should show the full ≥4 decodes/message.
## Step 4 — Metrics & output
Per scenario emit one human block + one machine-readable CSV line to stdout:
```
scenario,msgs,payload_profile,wall_ms,msg_per_s,ns_per_msg_p50,ns_per_msg_p99,
decodes_per_msg,hashes_per_msg,hashed_MB,decoded_MB,occupied_mem_delta_MB,gc_collections
```
Sources: `getMonoTime` deltas per message for p50/p99; `getOccupiedMem()` before/after;
refc `GC_getStatistics()` (print raw string); counters from Step 1 (reset between scenarios).
## Step 5 — Build task + baseline capture
- Add nimble task `benchMessagePath` to `logos_delivery.nimble` mirroring the existing
`benchmarks` task (:391-393), with `-d:msgPathCounters` added to its flags.
- Run it on the **current branch before Phases 2/3**; save both scenario outputs verbatim
into `docs/analysis/bench_baseline.md` with the commit hash and machine info
(`sysctl -n machdep.cpu.brand_string`, macOS version).
## Acceptance criteria
1. `make test` still fully green (production code untouched except no-op templates).
2. A production build **without** `-d:msgPathCounters` compiles and the counter templates
vanish (verify: `nim c` of wakunode2 target still succeeds).
3. Two consecutive micro runs differ by < 5 % on msg/s (determinism check).
4. Macro scenario counters confirm the analysis: `decodes_per_msg ≥ 4`,
`hashes_per_msg` in 46 on the receiving node. **If not, STOP and report the measured
numbers before proceeding to Phase 2 the analysis (and the expected gains) must be
recalibrated first.**
5. `docs/analysis/bench_baseline.md` committed with the numbers.
## Estimated diff
~370480 new LOC, ~10 changed LOC, no behavior change. Budget: 11.5 days.