You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
router: pre-encode broker fan-out events once per (variant, serializer)
The broker's per-event fan-out previously had every per-session send
goroutine re-encode the same EVENT message independently. With 200
subscribers on JSON that's 200x redundant codec.Encode + msgToList +
allocation work per published event.
Move encoding from N per-session goroutines into the single broker
actor goroutine. The broker pre-encodes the EVENT once for every
serializer in active use across the subscription group, stores the
bytes in a *wamp.SharedMessage wrapper, and dispatches that wrapper
to each subscriber's send channel. The peer's encodeOutbound looks up
its serializer's bytes in the cache and writes them directly to the
wire — zero encoding work in the hot per-subscriber path.
Tracking — three pieces of refcount state on each subscription, all
updated under the broker actor goroutine on subscribe / unsubscribe
(lock-free):
- serializers — set of wire-serializer formats in use across
non-local subscribers, used to know which
serializer-specific caches to populate.
- subsWithPubIdent — count of non-local subscribers that declared
the publisher_identification feature.
- subsWithoutPubIdent — count of non-local subscribers that did
NOT declare it.
At fan-out time the broker uses the pub-ident counters to decide
which event variant(s) to build:
needPlain = !disclose || subsWithoutPubIdent > 0
needDisclosed = disclose && subsWithPubIdent > 0
The disclosed variant has publisher identity stamped into Details
once (it's the same identity for every disclosed subscriber in this
fan-out — the publisher's authid/authrole). When all subscribers
have FeaturePubIdent and the publisher requested disclosure (the
overwhelmingly common case in production), only the disclosed
variant is built.
Local subscribers (in-process Go clients, meta sessions, tests) keep
the per-clone path for mutation isolation — TestEventContentSafety
pins this contract. They're excluded from the refcount.
websocketPeer.encodeOutbound and the rawsocket equivalent recognize
*wamp.SharedMessage via type assertion. Cache miss is a programmer
error (broker contract violation) and panics rather than silently
falling back to encoding — earlier work spent days chasing a silent
cache-miss regression that this contract makes loud.
Bench delta on the heavy preset (4cpu router, 4cpu harness, 4 pubs ×
200 subs × 5000/s, JSON, disclosure forced on by ironflock-router's
authorizer):
baseline opt-1 alloc-hoist this commit
events sent rate 1370/s 1457/s 2247/s (+64%)
events delivered 264k/s 277k/s 449k/s (+70%)
p50 latency 2.96s 2.81s 19.7ms (150x)
p99 latency 4.55s 4.56s 150ms (30x)
router CPU peak 369% 407% 297% (-27%)
The router is delivering 70% more events on 27% less CPU. Latency
collapsed because the broker pipeline is no longer back-pressured
by per-subscriber alloc cost.
Router pprof after the change shows JSONSerializer.Serialize gone
from the top frames (was 28-32%); 70% of remaining router CPU is
syscall.Syscall6 doing write(2) to subscriber TCP sockets — the
kernel-write boundary, not addressable in user space.
Co-resident pieces:
- wamp.SharedMessage type (new file).
- serialize.Serializer.ID() method on the interface; impls return
their constant. Plus serialize.Provider, an optional interface
that wamp.Peer impls satisfy to expose their serializer ID to
the broker without per-message type assertion.
Coverage: 63.4% -> 62.4% (more statements, same test set).
0 commit comments