Skip to content
quarry Artifact [cc0875ae92]
Login

Artifact [cc0875ae92]

Artifact cc0875ae9282fe28134a75ff4456f4989b0f7372c6529bda84276636dbbb1c72:


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{} }