Skip to content

Commit 95df94e

Browse files
committed
feat(broadcast): replace utils/broadcast with broadcaster/broadcast
1 parent e075671 commit 95df94e

14 files changed

Lines changed: 1902 additions & 1008 deletions

broadcaster/broadcast/broadcast.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Package broadcast implements a fan-out event stream with overwrite-on-full semantics.
2+
// Callers publish with [Publisher.Send], and each [Subscription] receives every message
3+
// in order until it falls a full ring behind; it then receives a [ring.LaggedError]
4+
// saying where to resume instead of stale data. The ring is bounded, so producers never wait
5+
// on consumers. See [ring.RingBuffer] for the sequence and slot protocol.
6+
package broadcast
7+
8+
import (
9+
"github.com/NethermindEth/juno/broadcaster/broadcast/ring"
10+
)
11+
12+
// Broadcast is a fan-out event stream over a [ring.RingBuffer]. It mints [Publisher] and
13+
// [Subscribable] handles; it has no lifecycle of its own, subscriptions end via
14+
// [Subscription.Unsubscribe].
15+
type Broadcast[T any] struct {
16+
ring *ring.RingBuffer[T]
17+
}
18+
19+
// New constructs a [Broadcast] with a ring of at least the given capacity, rounded up to a power
20+
// of two.
21+
func New[T any](capacity uint64) *Broadcast[T] {
22+
return &Broadcast[T]{ring: ring.NewRingBuffer[T](capacity)}
23+
}
24+
25+
// NewPublisher returns a [Publisher], the Send handle for this [Broadcast].
26+
func (b *Broadcast[T]) NewPublisher() Publisher[T] {
27+
return Publisher[T]{ring: b.ring}
28+
}
29+
30+
// NewSubscribable returns a [Subscribable], the read-only handle for creating subscriptions.
31+
func (b *Broadcast[T]) NewSubscribable() Subscribable[T] {
32+
return Subscribable[T]{ring: b.ring}
33+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package broadcast_test
2+
3+
import (
4+
"fmt"
5+
"sync"
6+
"testing"
7+
"time"
8+
9+
"github.com/NethermindEth/juno/broadcaster/broadcast"
10+
)
11+
12+
// benchmarkThroughput measures one configuration of Broadcast end to end: nPubs publishers
13+
// into one ring, nSubs subscriptions draining channels. b.N sends are split across the
14+
// publishers. Callers vary the parameters.
15+
func benchmarkThroughput[T any](
16+
b *testing.B,
17+
payload T,
18+
label string,
19+
bufSize uint64,
20+
nPubs,
21+
nSubs int,
22+
) {
23+
name := fmt.Sprintf("%s_buffer=%d/pubs=%d/subs=%d", label, bufSize, nPubs, nSubs)
24+
b.Run(name, func(b *testing.B) {
25+
bc := broadcast.New[T](bufSize)
26+
subbable := bc.NewSubscribable()
27+
28+
type counts struct {
29+
recvd uint64
30+
lag uint64
31+
}
32+
countCh := make(chan counts, nSubs)
33+
34+
unsubs := make([]func(), nSubs)
35+
var readers sync.WaitGroup
36+
for i := range nSubs {
37+
sub := subbable.Subscribe()
38+
unsubs[i] = sub.Unsubscribe
39+
readers.Go(func() {
40+
var c counts
41+
for ev := range sub.Recv() {
42+
if _, ok := ev.AsEvent(); ok {
43+
c.recvd++
44+
} else {
45+
c.lag++
46+
}
47+
}
48+
countCh <- c
49+
})
50+
}
51+
52+
// b.Loop cannot be used here: the iteration count has to be divided among the
53+
// publishers before any of them starts.
54+
perPub := b.N / nPubs
55+
var writers sync.WaitGroup
56+
b.ResetTimer()
57+
start := time.Now()
58+
for range nPubs {
59+
pub := bc.NewPublisher()
60+
writers.Go(func() {
61+
for range perPub {
62+
pub.Send(payload)
63+
}
64+
})
65+
}
66+
writers.Wait()
67+
b.StopTimer()
68+
duration := time.Since(start).Seconds()
69+
70+
for _, unsub := range unsubs {
71+
unsub()
72+
}
73+
readers.Wait()
74+
close(countCh)
75+
76+
var totalRecvd, totalLag uint64
77+
for c := range countCh {
78+
totalRecvd += c.recvd
79+
totalLag += c.lag
80+
}
81+
82+
sent := float64(perPub * nPubs)
83+
b.ReportMetric(sent/duration, "msgs_sent_per_sec")
84+
b.ReportMetric(float64(totalRecvd)/duration, "msgs_recv_per_sec")
85+
b.ReportMetric(float64(totalRecvd)/float64(nSubs), "avg_msgs_recv_per_sub")
86+
b.ReportMetric(float64(totalLag)/float64(nSubs), "avg_lag_per_sub")
87+
b.ReportMetric(float64(totalRecvd)/(sent*float64(nSubs)), "delivered_fraction")
88+
})
89+
}
90+
91+
// BenchmarkBroadcastPublisherThroughput varies ring capacity and subscriber count for a
92+
// single publisher, across four payload shapes.
93+
func BenchmarkBroadcastPublisherThroughput(b *testing.B) {
94+
type BigStruct struct {
95+
Field1 [128]byte
96+
Field2 [256]int64
97+
Field3 string
98+
Field4 float64
99+
}
100+
payload := 10
101+
payloadBig := BigStruct{Field3: "benchmark big struct payload"}
102+
103+
for _, bufSize := range []uint64{64, 256, 512, 1024, 2048, 4096} {
104+
for _, nSubs := range []int{1, 4, 32, 128, 256, 512, 1024} {
105+
benchmarkThroughput(b, payload, "int_value", bufSize, 1, nSubs)
106+
benchmarkThroughput(b, &payload, "int_ptr", bufSize, 1, nSubs)
107+
benchmarkThroughput(b, payloadBig, "big_struct_value", bufSize, 1, nSubs)
108+
benchmarkThroughput(b, &payloadBig, "big_struct_ptr", bufSize, 1, nSubs)
109+
}
110+
}
111+
}
112+
113+
// BenchmarkBroadcastMultiPublisherThroughput holds capacity and payload fixed and varies
114+
// publisher and subscriber counts instead.
115+
func BenchmarkBroadcastMultiPublisherThroughput(b *testing.B) {
116+
payload := 10
117+
118+
for _, nPubs := range []int{1, 2, 4, 8} {
119+
for _, nSubs := range []int{1, 32, 256, 1024} {
120+
benchmarkThroughput(b, &payload, "int_ptr", 1024, nPubs, nSubs)
121+
}
122+
}
123+
}

0 commit comments

Comments
 (0)