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>
7.2 KiB
Phase 2 — WakuMessage becomes a ref object (executor plan)
Self-contained implementation plan. Context: async_copy_analysis.md, async_copy_fix_plan.md. Prerequisite: Phase 1 benchmark exists and
docs/analysis/bench_baseline.mdis recorded.Objective: change
WakuMessagefrom a valueobjectto aref objectso every assignment, async-closure capture, and Result/Option bind-out of a message costs a pointer + refcount instead of deep-copying 3 seqs + a string. No proc signatures change. Expected result (verify with Phase 1 bench): ~50–60 % reduction in alloc volume per message; decode/hash counters unchanged.The danger of this phase is not the diff size — it is aliasing. Code written under value semantics may mutate "its copy" of a message; under ref semantics that mutation is now shared with every other holder. Every mutation site must be found and either cloned or proven to own a freshly constructed message.
Constraints
Same as Phase 1 (refc, nph, gitnexus impact/detect_changes, no push, verify by running).
Additionally: run gitnexus_impact({target: "WakuMessage", direction: "upstream"}) first
and report the blast radius before editing.
Step 1 — The type change (logos_delivery/waku/waku_core/message/message.nim)
Current (lines 12–29): type WakuMessage* = object with payload: seq[byte],
contentTopic: ContentTopic, meta: seq[byte], version: uint32,
timestamp: Timestamp, ephemeral: bool, proof: seq[byte].
type WakuMessage* = ref object— construction syntaxWakuMessage(payload: ...)is unchanged, so the 27 src + ~1,324 test constructions (1,174 viafakeWakuMessage,tests/testlib/wakucore.nim:74) compile as-is.- Add a structural
==(~12 LOC): both-nil → true, one-nil → false, else field-wise compare. Without this, ~107 test assertions comparing messages break on ref identity. - Add
proc clone*(msg: WakuMessage): WakuMessage— field-wise copy into a fresh ref (deep-copies the seqs; that is the point: cloning is now explicit and rare). - Fix
ensureTimestampSet(lines 31–34). Current bodyresult = message; result.timestamp = ...aliases the input under ref semantics and mutates the shared message. Rewrite non-mutating:proc ensureTimestampSet*(message: WakuMessage): WakuMessage = if message.timestamp != 0: return message var m = message.clone() m.timestamp = getNowInNanosecondTime() m - Doc comment at the type: messages are immutable by convention after construction;
mutate only freshly constructed or explicitly
clone()d instances. Also note the deliberate deviation from the AGENTS.mdRef-suffix rule (renaming ~2,100 occurrences has no semantic value).
Step 2 — Known aliasing sites (fix, don't just audit)
- Relay publish
logos_delivery/waku/waku_relay/protocol.nim:680-682:
Under ref this mutates the caller's message. Replace withvar message = wakuMessage if message.timestamp == 0: message.timestamp = getNowInNanosecondTime()let message = wakuMessage.ensureTimestampSet()(safe after Step 1.4). - RLN proof attachment — search
logos_delivery/waku/waku_rln_relay/for procs takingmsg: var WakuMessageor assigningmsg.proof =(e.g.appendRLNProof). These run on the outbound path on a message the API caller still holds. Decision rule: if the message came in through a public API,clone()before mutating (or restructure to build-then-assign locally). - Sweep for every other field mutation:
grep -rn --include='*.nim' '\.\(payload\|meta\|proof\|timestamp\|ephemeral\|version\|contentTopic\) *=' logos_delivery library appsClassify each hit: (a) freshly constructed in the same scope (e.g. the protobuf decode atwaku_core/message/codec.nim:23buildingvar msg = WakuMessage()) → fine; (b) received/parameter/stored message →clone()first or restructure. Record the classification of every hit in the phase notes (this list is the review artifact).
Step 3 — Nil-safety audit
var m: WakuMessage now defaults to nil, not an empty object; field access on it crashes.
grep -rn --include='*.nim' 'var [a-zA-Z_]*: WakuMessage\b\|: WakuMessage$' logos_delivery library apps— any declaration relying on default-init must become= WakuMessage().- Check emptiness idioms:
== WakuMessage()/== default(WakuMessage)comparisons andTable[..., WakuMessage].getOrDefaultconsumers (getOrDefault now yieldsnil). - Fields of type
WakuMessageinside other objects (e.g.MessagePush.wakuMessageinwaku_filter_v2/rpc.nim, store RPC types,queue_driver'sSortedSet[Index, WakuMessage]) — their decode paths must construct explicitly; verify each codec assigns a constructed message (rpc_codec.nimfiles for filter/lightpush/store). Option[WakuMessage]/Opt[WakuMessage](only 2 src occurrences):none/absent now also representable as nil ref — do NOT collapse the two; keep the Opt wrapper semantics as-is.
Step 4 — Tests
- Full
make test. Expected failure classes: nil-default (Step 3 misses), ref-identity assumptions (tests mutating a fixture then asserting the original unchanged — now shared), andcheck msg1 == msg2(covered by structural==). - Do NOT weaken test assertions to make them pass; fix the production/nil issue instead.
- Add new unit tests in
tests/waku_core/(~40 LOC): structural==incl. nil cases;clone()independence (mutating clone leaves original untouched);ensureTimestampSetreturns same ref when timestamp set / fresh ref when not.
Step 5 — Memory-model verification (mandatory before "done")
- refc: full suite + the Phase 1 benchmark; record the checkpoint numbers next to
bench_baseline.md(expect alloc-volume drop, identical decode/hash counters, identical message throughput or better). - ASAN run of the message-path tests (clang, refc) — this is the net that catches
a use-after-free from the new shared lifetimes: at minimum
tests/waku_relay,tests/waku_archive,tests/waku_filter_v2,tests/node. - Statement for the record (per FFI rules):
library/C bindings deep-copy message fields viacopyMeminto JSON events (library/json_message_event.nim:72-95), so no message ref crosses the FFI boundary; refc vs ORC behavior of the ref itself is standard RC — no finalizers, no cycles (WakuMessage has no back-references).
Acceptance criteria
- Full
make testgreen; ASAN run clean. - Step 2.3 mutation-site classification list produced (in PR description or phase notes).
- Phase 1 bench re-run recorded: alloc volume down ≥ 40 % (projection is 50–60 %),
decode/hash counters unchanged vs baseline. If alloc volume barely moves, something
still deep-copies — investigate before closing the phase (prime suspects: a
varparameter forcing a copy, or a leftover value-type field of WakuMessage inside a container that the compiler still copies). gitnexus_detect_changes()reviewed; commit (no push).
Estimated diff
~35 new LOC, ~160–340 changed LOC (dominated by Steps 2.3/3 fixes and test adjustments). Budget: 2–3 days, most of it audit + verification, not typing.