Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/kelindar/event
Browse files Browse the repository at this point in the history
  • Loading branch information
kelindar committed Oct 7, 2023
2 parents d91e1f2 + d167a27 commit e3929b8
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 56 deletions.
11 changes: 7 additions & 4 deletions README.md
Expand Up @@ -79,8 +79,11 @@ It should output something along these lines, where order is not guaranteed give
## Benchmarks

```
cpu: Intel(R) Core(TM) i7-9700K CPU @ 3.60GHz
BenchmarkEvent/1-consumers-8 10021444 119.1 ns/op 10021301 msg 0 B/op 0 allocs/op
BenchmarkEvent/10-consumers-8 799999 1595 ns/op 7999915 msg 0 B/op 0 allocs/op
BenchmarkEvent/100-consumers-8 99048 14308 ns/op 9904769 msg 0 B/op 0 allocs/op
cpu: 13th Gen Intel(R) Core(TM) i7-13700K
BenchmarkEvent/1x1-24 38709926 31.94 ns/op 30.89 million/s 1 B/op 0 allocs/op
BenchmarkEvent/1x10-24 8107938 133.7 ns/op 74.76 million/s 45 B/op 0 allocs/op
BenchmarkEvent/1x100-24 774168 1341 ns/op 72.65 million/s 373 B/op 0 allocs/op
BenchmarkEvent/10x1-24 5755402 301.1 ns/op 32.98 million/s 7 B/op 0 allocs/op
BenchmarkEvent/10x10-24 750022 1503 ns/op 64.47 million/s 438 B/op 0 allocs/op
BenchmarkEvent/10x100-24 69363 14878 ns/op 67.11 million/s 3543 B/op 0 allocs/op
```
52 changes: 33 additions & 19 deletions default_test.go
Expand Up @@ -8,33 +8,47 @@ import (
"sync"
"sync/atomic"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

/*
cpu: 13th Gen Intel(R) Core(TM) i7-13700K
BenchmarkEmit/1-subs-24 13407880 87.10 ns/op 13.41 million 0 B/op 0 allocs/op
BenchmarkEmit/10-subs-24 1000000 1012 ns/op 10.00 million 0 B/op 0 allocs/op
BenchmarkEmit/100-subs-24 103896 11714 ns/op 10.39 million 0 B/op 0 allocs/op
BenchmarkEvent/1x1-24 38709926 31.94 ns/op 30.89 million/s 1 B/op 0 allocs/op
BenchmarkEvent/1x10-24 8107938 133.7 ns/op 74.76 million/s 45 B/op 0 allocs/op
BenchmarkEvent/1x100-24 774168 1341 ns/op 72.65 million/s 373 B/op 0 allocs/op
BenchmarkEvent/10x1-24 5755402 301.1 ns/op 32.98 million/s 7 B/op 0 allocs/op
BenchmarkEvent/10x10-24 750022 1503 ns/op 64.47 million/s 438 B/op 0 allocs/op
BenchmarkEvent/10x100-24 69363 14878 ns/op 67.11 million/s 3543 B/op 0 allocs/op
*/
func BenchmarkEmit(b *testing.B) {
for _, subs := range []int{1, 10, 100} {
b.Run(fmt.Sprintf("%d-subs", subs), func(b *testing.B) {
var count uint64
for i := 0; i < subs; i++ {
defer On(func(ev MyEvent1) {
atomic.AddUint64(&count, 1)
})()
}
func BenchmarkEvent(b *testing.B) {
for _, topics := range []int{1, 10} {
for _, subs := range []int{1, 10, 100} {
b.Run(fmt.Sprintf("%dx%d", topics, subs), func(b *testing.B) {
var count atomic.Int64
for i := 0; i < subs; i++ {
for id := 10; id < 10+topics; id++ {
defer OnType(uint32(id), func(ev MyEvent3) {
count.Add(1)
})()
}
}

b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
Emit(MyEvent1{})
}
b.ReportMetric(float64(count)/1e6, "million")
})
start := time.Now()
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
for id := 10; id < 10+topics; id++ {
Emit(MyEvent3{ID: id})
}
}

elapsed := time.Since(start)
rate := float64(count.Load()) / 1e6 / elapsed.Seconds()
b.ReportMetric(rate, "million/s")
})
}
}
}

Expand Down
122 changes: 95 additions & 27 deletions event.go
Expand Up @@ -9,6 +9,7 @@ import (
"reflect"
"strings"
"sync"
"time"
)

// Event represents an event contract
Expand All @@ -21,11 +22,32 @@ type Event interface {
// Dispatcher represents an event dispatcher.
type Dispatcher struct {
subs sync.Map
done chan struct{} // Cancellation
df time.Duration // Flush interval
}

// NewDispatcher creates a new dispatcher of events.
func NewDispatcher() *Dispatcher {
return &Dispatcher{}
return &Dispatcher{
df: 500 * time.Microsecond,
done: make(chan struct{}),
}
}

// Close closes the dispatcher
func (d *Dispatcher) Close() error {
close(d.done)
return nil
}

// isClosed returns whether the dispatcher is closed or not
func (d *Dispatcher) isClosed() bool {
select {
case <-d.done:
return true
default:
return false
}
}

// Subscribe subscribes to an event, the type of the event will be automatically
Expand All @@ -37,21 +59,25 @@ func Subscribe[T Event](broker *Dispatcher, handler func(T)) context.CancelFunc

// SubscribeTo subscribes to an event with the specified event type.
func SubscribeTo[T Event](broker *Dispatcher, eventType uint32, handler func(T)) context.CancelFunc {
ctx, cancel := context.WithCancel(context.Background())
sub := &consumer[T]{
queue: make(chan T, 1024),
exec: handler,
if broker.isClosed() {
panic(errClosed)
}

// Add to consumer group, if it doesn't exist it will create one
s, _ := broker.subs.LoadOrStore(eventType, new(group[T]))
s, loaded := broker.subs.LoadOrStore(eventType, &group[T]{
cond: sync.NewCond(new(sync.Mutex)),
})
group := groupOf[T](eventType, s)
group.Add(ctx, sub)
sub := group.Add(handler)

// Start flushing asynchronously if we just created a new group
if !loaded {
go group.Process(broker.df, broker.done)
}

// Return unsubscribe function
return func() {
group.Del(sub)
cancel() // Stop async processing
}
}

Expand Down Expand Up @@ -80,57 +106,97 @@ func groupOf[T Event](eventType uint32, subs any) *group[T] {
panic(errConflict[T](eventType, subs))
}

// ------------------------------------- Subscriber List -------------------------------------
// ------------------------------------- Subscriber -------------------------------------

// consumer represents a consumer with a message queue
type consumer[T Event] struct {
queue chan T // Message buffer
exec func(T) // Process callback
queue []T // Current work queue
stop bool // Stop signal
}

// Listen listens to the event queue and processes events
func (s *consumer[T]) Listen(ctx context.Context) {
func (s *consumer[T]) Listen(c *sync.Cond, fn func(T)) {
pending := make([]T, 0, 128)

for {
select {
case ev := <-s.queue:
s.exec(ev)
case <-ctx.Done():
return
c.L.Lock()
for len(s.queue) == 0 {
switch {
case s.stop:
c.L.Unlock()
return
default:
c.Wait()
}
}

// Swap buffers and reset the current queue
temp := s.queue
s.queue = pending
pending = temp
s.queue = s.queue[:0]
c.L.Unlock()

// Outside of the critical section, process the work
for i := 0; i < len(pending); i++ {
fn(pending[i])
}
}
}

// ------------------------------------- Subscriber Group -------------------------------------

// group represents a consumer group
type group[T Event] struct {
sync.RWMutex
cond *sync.Cond
subs []*consumer[T]
}

// Process periodically broadcasts events
func (s *group[T]) Process(interval time.Duration, done chan struct{}) {
ticker := time.NewTicker(interval)
for {
select {
case <-done:
return
case <-ticker.C:
s.cond.Broadcast()
}
}
}

// Broadcast sends an event to all consumers
func (s *group[T]) Broadcast(ev T) {
s.RLock()
defer s.RUnlock()
s.cond.L.Lock()
for _, sub := range s.subs {
sub.queue <- ev
sub.queue = append(sub.queue, ev)
}
s.cond.L.Unlock()
}

// Add adds a subscriber to the list
func (s *group[T]) Add(ctx context.Context, sub *consumer[T]) {
go sub.Listen(ctx)
func (s *group[T]) Add(handler func(T)) *consumer[T] {
sub := &consumer[T]{
queue: make([]T, 0, 128),
}

// Add the consumer to the list of active consumers
s.Lock()
s.cond.L.Lock()
s.subs = append(s.subs, sub)
s.Unlock()
s.cond.L.Unlock()

// Start listening
go sub.Listen(s.cond, handler)
return sub
}

// Del removes a subscriber from the list
func (s *group[T]) Del(sub *consumer[T]) {
s.Lock()
defer s.Unlock()
s.cond.L.Lock()
defer s.cond.L.Unlock()

// Search and remove the subscriber
sub.stop = true
subs := make([]*consumer[T], 0, len(s.subs))
for _, v := range s.subs {
if v != sub {
Expand All @@ -142,6 +208,8 @@ func (s *group[T]) Del(sub *consumer[T]) {

// ------------------------------------- Debugging -------------------------------------

var errClosed = fmt.Errorf("event dispatcher is closed")

// Count returns the number of subscribers in this group
func (s *group[T]) Count() int {
return len(s.subs)
Expand Down
63 changes: 57 additions & 6 deletions event_test.go
Expand Up @@ -4,6 +4,7 @@
package event

import (
"fmt"
"sync"
"sync/atomic"
"testing"
Expand All @@ -15,21 +16,22 @@ func TestPublish(t *testing.T) {
d := NewDispatcher()
var wg sync.WaitGroup

// Subscribe
// Subscribe, must be received in order
var count int64
defer Subscribe(d, func(ev MyEvent1) {
atomic.AddInt64(&count, 1)
assert.Equal(t, int(atomic.AddInt64(&count, 1)), ev.Number)
wg.Done()
})()

// Publish
wg.Add(2)
Publish(d, MyEvent1{})
Publish(d, MyEvent1{})
wg.Add(3)
Publish(d, MyEvent1{Number: 1})
Publish(d, MyEvent1{Number: 2})
Publish(d, MyEvent1{Number: 3})

// Wait and check
wg.Wait()
assert.Equal(t, int64(2), count)
assert.Equal(t, int64(3), count)
}

func TestUnsubscribe(t *testing.T) {
Expand Down Expand Up @@ -88,6 +90,49 @@ func TestPublishDifferentType(t *testing.T) {
})
}

func TestCloseDispatcher(t *testing.T) {
d := NewDispatcher()
defer SubscribeTo(d, TypeEvent1, func(ev MyEvent2) {})()

assert.NoError(t, d.Close())
assert.Panics(t, func() {
SubscribeTo(d, TypeEvent1, func(ev MyEvent2) {})
})
}

func TestMatrix(t *testing.T) {
const amount = 1000
for _, subs := range []int{1, 10, 100} {
for _, topics := range []int{1, 10} {
expected := subs * topics * amount
t.Run(fmt.Sprintf("%dx%d", topics, subs), func(t *testing.T) {
var count atomic.Int64
var wg sync.WaitGroup
wg.Add(expected)

d := NewDispatcher()
for i := 0; i < subs; i++ {
for id := 0; id < topics; id++ {
defer SubscribeTo(d, uint32(id), func(ev MyEvent3) {
count.Add(1)
wg.Done()
})()
}
}

for n := 0; n < amount; n++ {
for id := 0; id < topics; id++ {
go Publish(d, MyEvent3{ID: id})
}
}

wg.Wait()
assert.Equal(t, expected, int(count.Load()))
})
}
}
}

// ------------------------------------- Test Events -------------------------------------

const (
Expand All @@ -106,3 +151,9 @@ type MyEvent2 struct {
}

func (t MyEvent2) Type() uint32 { return TypeEvent2 }

type MyEvent3 struct {
ID int
}

func (t MyEvent3) Type() uint32 { return uint32(t.ID) }

0 comments on commit e3929b8

Please sign in to comment.