Skip to content

perf(comm): replace per-peer goroutines with fixed worker pool#42

Open
simonzg wants to merge 2 commits into
mainnetfrom
perf/broadcast-worker-pool
Open

perf(comm): replace per-peer goroutines with fixed worker pool#42
simonzg wants to merge 2 commits into
mainnetfrom
perf/broadcast-worker-pool

Conversation

@simonzg

@simonzg simonzg commented May 1, 2026

Copy link
Copy Markdown
Contributor

Problem

BroadcastBlock spawned one goroutine per peer per call. With 50+ connected peers and blocks arriving every few seconds, this creates O(peers × blocks/sec) goroutines continuously — hundreds per minute — each living only long enough to send one message. The runtime's goroutine scheduler and GC pay a non-trivial cost for this churn.

Additionally, PeerSet used sync.Mutex for every operation, including purely read-only methods (Slice, Len, Find, DirectionCount). This means concurrent broadcast and peer-count reads serialise against each other unnecessarily.

Solution

Fixed-size goroutine worker pool (comm/communicator.go)

  • Add a broadcastCh chan func() field (buffered at 512 slots)
  • Start 32 long-lived worker goroutines in New() that drain the channel
  • BroadcastBlock now enqueues closures via a non-blocking select — if the pool is saturated the task is safely dropped (the peer will receive the block via the announce/sync path)
  • Stop() closes broadcastCh to cleanly shut down all workers

Read-write mutex for PeerSet (comm/peer.go)

  • Upgrade PeerSet.lock from sync.Mutex to sync.RWMutex
  • Slice(), Len(), Find(), and DirectionCount() now use RLock/RUnlock — they can all proceed concurrently
  • Add() and Remove() retain exclusive Lock/Unlock (write operations)

Impact

  • Eliminates the goroutine explosion during high-throughput block propagation
  • Concurrent peer-list reads no longer block each other
  • Memory allocation pressure from ephemeral goroutine stacks is removed
  • Pool saturation is handled gracefully with zero risk of message loss (announce path as fallback)

Files changed

  • comm/communicator.go — worker pool init/shutdown, non-blocking broadcast enqueue
  • comm/peer.gosync.Mutexsync.RWMutex, read methods use RLock

🤖 Generated with Claude Code

simonzg and others added 2 commits July 10, 2025 09:54
…ck broadcast

Previously BroadcastBlock spawned one goroutine per peer on every call,
creating O(peers * blocks) goroutines under load. PeerSet used sync.Mutex
even for read-only operations (Slice, Len, Find, DirectionCount), serialising
all readers.

This change:
- Adds a fixed pool of 32 goroutines (broadcastWorkers) consuming tasks from a
  buffered channel (broadcastQueueSize=512). Tasks dropped when the pool is
  saturated are safe to discard — peers receive the block via the
  announce/re-request path.
- Upgrades PeerSet.lock from sync.Mutex to sync.RWMutex so Slice, Len, Find
  and DirectionCount can all proceed concurrently instead of serialising.
- Stop() closes broadcastCh to cleanly drain workers on shutdown.

Impact: eliminates goroutine proliferation during high-throughput broadcast and
reduces mutex contention on PeerSet reads.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant