Improved perfs

This commit is contained in:
Martino Ferrari
2026-05-12 10:16:48 +02:00
parent 912ecdd9ed
commit 6ff8fb5c25
14 changed files with 1357 additions and 25 deletions
+69 -11
View File
@@ -8,6 +8,7 @@ import (
"fmt"
"log/slog"
"sync"
"time"
"github.com/uopi/uopi/internal/datasource"
)
@@ -41,18 +42,38 @@ type Broker struct {
sources map[string]datasource.DataSource
subs map[SignalRef]*signalSub
log *slog.Logger
maxInterval time.Duration // 0 = unlimited
log *slog.Logger
}
// Option is a functional option for Broker configuration.
type Option func(*Broker)
// WithMaxUpdateRate limits fan-out to at most hz updates per second per signal.
// When upstream delivers faster, intermediate values are coalesced: the most
// recent value in each interval is forwarded once the interval elapses.
// A value ≤ 0 disables rate limiting (the default).
func WithMaxUpdateRate(hz float64) Option {
return func(b *Broker) {
if hz > 0 {
b.maxInterval = time.Duration(float64(time.Second) / hz)
}
}
}
// New creates a Broker whose upstream subscriptions are bound to ctx.
// Cancel ctx (or the parent context passed to main) to shut everything down.
func New(ctx context.Context, log *slog.Logger) *Broker {
return &Broker{
func New(ctx context.Context, log *slog.Logger, opts ...Option) *Broker {
b := &Broker{
ctx: ctx,
sources: make(map[string]datasource.DataSource),
subs: make(map[SignalRef]*signalSub),
log: log,
}
for _, opt := range opts {
opt(b)
}
return b
}
// Register adds a DataSource to the broker. Must be called before Subscribe.
@@ -169,7 +190,36 @@ func (b *Broker) unsubscribe(ref SignalRef, ch chan<- Update) {
// fanOut reads values from rawCh and dispatches them to all registered clients.
// It exits when sub.done is closed or rawCh is closed.
//
// When b.maxInterval > 0, updates that arrive faster than the interval are
// coalesced: only the most recent value in each interval window is forwarded,
// delivered at the end of the interval by a flush ticker. This ensures the
// latest value is always eventually seen even when the source fires rapidly.
func (b *Broker) fanOut(ref SignalRef, sub *signalSub, rawCh <-chan datasource.Value) {
maxInterval := b.maxInterval
var lastSent time.Time
var pending *Update
// Only allocate a ticker when rate-limiting is configured.
var flushC <-chan time.Time
if maxInterval > 0 {
t := time.NewTicker(maxInterval)
defer t.Stop()
flushC = t.C
}
dispatch := func(u Update) {
sub.mu.RLock()
for ch := range sub.clients {
select {
case ch <- u:
default: // slow consumer: drop rather than block
}
}
sub.mu.RUnlock()
}
for {
select {
case v, ok := <-rawCh:
@@ -177,15 +227,23 @@ func (b *Broker) fanOut(ref SignalRef, sub *signalSub, rawCh <-chan datasource.V
return
}
update := Update{Ref: ref, Value: v}
sub.mu.RLock()
for ch := range sub.clients {
select {
case ch <- update:
default:
// slow consumer: drop rather than block
}
// Rate-limit: if an interval is configured and the last delivery
// was recent, hold this update as pending (coalesce).
if maxInterval > 0 && !lastSent.IsZero() && time.Since(lastSent) < maxInterval {
pending = &update
continue
}
lastSent = time.Now()
pending = nil
dispatch(update)
case <-flushC:
// Deliver the most recent coalesced value, if any.
if pending != nil {
lastSent = time.Now()
dispatch(*pending)
pending = nil
}
sub.mu.RUnlock()
case <-sub.done:
return