logos-delivery/docs/analysis/async_copy_fix_plan.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

181 lines
8.8 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.

# Async copy elimination — three-phase implementation plan
Companion to [async_copy_analysis.md](async_copy_analysis.md). Baseline: ~19 + F
full-payload copies and 46 SHA-256 passes per inbound relayed message (refc).
Executor-grade plans (self-contained, one per phase):
[Phase 1 — benchmark](plan_phase1_bench.md) ·
[Phase 2 — WakuMessage ref object](plan_phase2_wakumessage_ref.md) ·
[Phase 3 — WakuEnvelope](plan_phase3_wakuenvelope.md)
Measured blast radius (grep, 2026-07-14):
| Pattern | src (logos_delivery + library + apps) | tests |
|---|---|---|
| `WakuMessage` occurrences | ~420 in 88 files | 1,714 in 64 files |
| `WakuMessage(...)` constructions | 27 | 1,324 (**1,174 via `fakeWakuMessage`** in `tests/testlib/wakucore.nim:74`) |
| `message:/msg: WakuMessage` params | 88 | 100 |
| `computeMessageHash(` call sites | 28 | 208 |
| `WakuRelayHandler` references | 19 | 5 |
| structural `==` on messages | 5 | ~107 |
---
## Phase 1 — End-to-end performance harness (before/after proof)
Goal: reproducible numbers on master *before* any change; re-run after each phase.
Deliverables:
1. `tests/benchmarks/bench_message_path.nim` — two scenarios:
- **micro**: inject N deterministic messages (fixed seed; payload mix 10/50/150 kB)
directly into the relay topicHandler/`uniqueTopicHandler` with archive + filter
(F = 1, 5, 20 subscribed peers) + sync mounted; measures the full internal pipeline
without network noise.
- **macro**: two in-process nodes over loopback gossipsub, publish → receive, same
payload mix; captures libp2p-side copies too.
2. Instrumentation counters under `-d:msgPathCounters` in `waku_core`:
`computeMessageHash` invocations, `WakuMessage.decode` invocations, bytes hashed,
bytes materialized by decode. (~3040 LOC, zero cost when not defined.)
3. Metrics emitted per run (CSV/JSON to stdout): msg/s, ns/msg (p50/p99),
`getOccupiedMem()` delta, refc `GC_getStatistics()`, counter totals.
4. `make benchmark` target + baseline results committed to `docs/analysis/bench_baseline.md`.
| Item | New LOC | Changed LOC |
|---|---|---|
| bench harness (micro + macro) | ~280380 | — |
| counters in waku_core | ~3040 | ~10 |
| Makefile / nimble task | ~15 | — |
| baseline results doc | ~40 | — |
| **Total** | **~370480** | **~10** |
Verify: harness runs deterministically twice with <5 % variance; counters match the
analysis predictions (≥4 decodes, 46 hashes per message) this *validates the analysis*
before we change anything.
**Time: 11.5 days.**
---
## Phase 2 — `WakuMessage` becomes a `ref object`
Goal: every assignment/capture/bind-out of a message becomes a pointer + RC op instead of
a 3-seq deep copy no signature changes anywhere.
What actually changes (`logos_delivery/waku/waku_core/message/message.nim`):
- `WakuMessage* = ref object` (construction syntax `WakuMessage(payload: ...)` is
unchanged, so the 27 src + 1,324 test constructions compile as-is).
- **Structural `==`** proc (~12 LOC) otherwise ref-identity comparison breaks the ~107
test assertions.
- `clone*(msg: WakuMessage): WakuMessage` helper (~10 LOC) for deliberate-mutation sites.
- Known aliasing fix: relay publish timestamp patch
([waku_relay/protocol.nim:680-682](../../logos_delivery/waku/waku_relay/protocol.nim))
currently relies on value-copy semantics must become `clone()` (or set the timestamp
before entering publish).
- Nil-safety audit: `var msg: WakuMessage` now defaults to `nil`, not an empty object.
88 src param sites reviewed; expected actual fixes small (decode already does
`var msg = WakuMessage()`).
- Aliasing audit: any post-construction mutation of a received message (queue_driver
retention becomes a *shared* ref that's the intended win, but document
immutable-by-convention in message.nim).
- Style-guide deviation note: AGENTS.md prescribes `Ref` suffix for ref object types;
renaming to `WakuMessageRef` would touch ~2,100 occurrences for zero semantic value
keep the name, document the deviation at the type.
| Item | New LOC | Changed LOC |
|---|---|---|
| message.nim (ref + `==` + clone + doc comment) | ~35 | ~10 |
| relay publish mutation fix | | ~10 |
| nil/aliasing audit fixes across src (est.) | | ~60120 |
| test fixes (nil defaults, identity assumptions; `==` covers the rest) | | ~80200 |
| **Total** | **~35** | **~160340** |
Effect on copy budget (per message, from 19 + F): decode bind-outs 4, dispatch env
captures 7, archive/filter wrap 2 **~6 + F remaining** (the 4 redundant decode
*materializations* and all hashing remain). Expected from Phase 1 harness: **~5060 %
reduction in alloc volume**, hashes unchanged.
Memory-model note (refc/ORC): under refc, ref copy = pointer + RC increment (cheap,
non-atomic); no cycles are introduced (WakuMessage has no back-references), so refc's
cycle collector is not engaged. Under a future ORC build, same story with ORC RC. FFI
(`library/`) already deep-copies fields via `copyMem` into JSON events unaffected;
ref gives a stable pointer, which is FFI-friendly.
Verify: full `make test` green, benchmark re-run recorded, ASAN/refc build of the
message-path tests clean (lifetimes now shared this is the run that would catch a
premature-free or unexpected-mutation bug).
**Time: 23 days** (LOC is small; the cost is the aliasing audit + full-suite verification).
---
## Phase 3 — API break: `WakuEnvelope` (WakuMessage, PubsubTopic, msgHash)
Goal: decode once, hash once, carry both through the whole internal dispatch as one ref.
Design (`logos_delivery/waku/waku_core/message/envelope.nim`, new):
```nim
type WakuEnvelope* = ref object
msg*: WakuMessage
pubsubTopic*: PubsubTopic
hash*: WakuMessageHash
proc init*(T: type WakuEnvelope, pubsubTopic: PubsubTopic, msg: WakuMessage): T =
WakuEnvelope(msg: msg, pubsubTopic: pubsubTopic,
hash: computeMessageHash(pubsubTopic, msg))
```
Changes:
1. **Relay decode-once** (`waku_relay/protocol.nim`): ordered validator decodes and builds
the envelope; `onRecv`/`onValidated`/`onSend` observers and the subscribe topicHandler
consume the envelope instead of re-decoding (removes 3 decodes + validator-side hash).
Mechanism: msgId-keyed handoff (small TimedCache) or restructured observer chain.
2. **`WakuRelayHandler`** becomes `proc(envelope: WakuEnvelope): Future[void]` 19 src +
5 test references.
3. **Dispatch** (`node/subscription_manager.nim`): `uniqueTopicHandler` + 6 sub-handlers
(trace/filter/archive/sync/internal/legacyApp) + `MessageSeenEvent` take the envelope.
4. **Consumers**: `archive.handleMessage`, `filter_v2.handleMessage`, sync ingress,
lightpushrelay callbacks, REST/API + `library/` event emission use `envelope.hash`,
deleting ~1014 of the 28 src `computeMessageHash` call sites.
5. **Filter per-peer buffer share** (folded in here): encode `MessagePush` once, pass
`ref seq[byte]` (or raw-async `pushToPeer`) removes the F per-peer buffer copies.
| Item | New LOC | Changed LOC |
|---|---|---|
| envelope.nim + export | ~50 | |
| relay decode-once + observer restructure | ~60 | ~120180 |
| WakuRelayHandler + subscription_manager | | ~90130 |
| archive / filter / sync / lightpush / events / library | | ~150250 |
| filter shared push buffer | | ~25 |
| tests (relay, node, archive, filter, sync handler tests) | ~40 | ~250400 |
| **Total** | **~150** | **~640990** |
Effect on copy budget: **~6 + F ~3 copies, 46 1 hash pass** (plus 1 encode + hash on
the outbound leg). Combined with Phase 2 this is the full projected gain: ~7580 % less
copy volume, ~7080 % less hashing ~4× single-core throughput headroom at the
10/50/150 kB profile.
Verify: full suite + benchmark re-run vs Phase-1 baseline and Phase-2 checkpoint; counter
assertion in the micro bench: exactly 1 decode + 1 hash per inbound message.
**Time: 46 days.**
---
## Summary
| Phase | New LOC | Changed LOC | Time | Copies (of 19 + F) | Hashes (of 46) |
|---|---|---|---|---|---|
| 1 perf harness | ~370480 | ~10 | 11.5 d | (measures baseline) | |
| 2 WakuMessage ref | ~35 | ~160340 | 23 d | ~6 + F | 46 (unchanged) |
| 3 WakuEnvelope | ~150 | ~640990 | 46 d | ~3 | 1 |
| **Total** | **~555665** | **~8101,340** | **710.5 d (~2 wks)** | **84 %** | **80 %** |
Risks to watch:
- Phase 2 aliasing: any code that mutated its by-value copy now mutates a shared message
the relay timestamp patch is the one known site; the ASAN run + full suite is the net.
- Phase 3 relay observer restructure is the only genuinely fiddly part (gossipsub gives no
validatorhandler side channel); the msgId cache must be bounded (TimedCache) and
eviction-safe.
- Test LOC ranges are dominated by handler-signature churn in Phase 3; the 1,174
`fakeWakuMessage` call sites are untouched in all phases.