package obs
import (
"sync"
"time"
)
// Signals are the four golden signals over the last hour, one bin a
// minute, oldest first: how many renders began, how many of them were
// over Budget, and the most store subprocesses that were running at
// once. Server errors are the fourth and live in the error journal,
// which keeps a row per occurrence and answers the same question itself.
//
// The rings behind the cards hold the last few hundred events, which on
// a quiet forge is hours and on a busy one is seconds, so a line drawn
// from them has no fixed scale and a three-minute window of them
// quantises to 0, 1 and 2. These bins are kept as they happen, bounded
// by the hour and not by traffic, so the line beside a number is an
// hour of that number whatever the forge is doing.
type Signals struct {
// Minutes is how many bins there are; the last is the current
// minute, still filling.
Minutes int `json:"minutes"`
Renders []int `json:"renders"`
Slow []int `json:"slow"`
Busy []int `json:"busy"`
}
// SignalMinutes is how far back the signals reach.
const SignalMinutes = 60
type signalRing struct {
mu sync.Mutex
at time.Time // the minute the last bin is for, truncated
rend []int
slow []int
busy []int
}
var signals signalRing
// roll advances the ring to now's minute, zero-filling the minutes that
// passed with nothing recorded in them, so a quiet hour reads as quiet
// rather than as the last busy minute stretched.
func (s *signalRing) roll(now time.Time) {
m := now.Truncate(time.Minute)
if s.at.IsZero() {
s.at = m
s.rend, s.slow, s.busy = make([]int, 1), make([]int, 1), make([]int, 1)
return
}
for s.at.Before(m) {
s.at = s.at.Add(time.Minute)
s.rend = append(s.rend, 0)
s.slow = append(s.slow, 0)
s.busy = append(s.busy, 0)
if len(s.rend) > SignalMinutes {
s.rend, s.slow, s.busy = s.rend[1:], s.slow[1:], s.busy[1:]
}
}
}
func (s *signalRing) render(now time.Time, slow bool) {
s.mu.Lock()
defer s.mu.Unlock()
s.roll(now)
s.rend[len(s.rend)-1]++
if slow {
s.slow[len(s.slow)-1]++
}
}
func (s *signalRing) running(now time.Time, live int) {
s.mu.Lock()
defer s.mu.Unlock()
s.roll(now)
if i := len(s.busy) - 1; live > s.busy[i] {
s.busy[i] = live
}
}
// snapshot is the hour as of now, the empty minutes since the last
// event filled in, so the line ends at now and not at the last thing
// that happened.
func (s *signalRing) snapshot(now time.Time) Signals {
s.mu.Lock()
defer s.mu.Unlock()
s.roll(now)
return Signals{Minutes: len(s.rend),
Renders: append([]int(nil), s.rend...),
Slow: append([]int(nil), s.slow...),
Busy: append([]int(nil), s.busy...)}
}
// SignalsNow is the last hour of signals, as of now.
func SignalsNow() Signals { return signals.snapshot(time.Now()) }
// resetSignals empties the ring, for tests.
func resetSignals() { signals = signalRing{} }