Skip to content
quarry Artifact [1d5d761b22]
Login

Artifact [1d5d761b22]

Artifact 1d5d761b2283189c5c4c4ffaa8b8e3c5edb14e8c50d8738fc0e0ddbcd7179f79:


package obs

import (
	"cmp"
	"slices"
	"time"
)

// ring is a fixed-size window of the most recent values. Bounded on
// purpose: this package is the snapshot, and an unbounded buffer in a
// long-lived server is a leak wearing a feature's clothes.
type ring[T any] struct {
	buf  []T
	next int
	full bool
}

func newRing[T any](n int) ring[T] { return ring[T]{buf: make([]T, n)} }

func (r *ring[T]) push(v T) {
	r.buf[r.next] = v
	r.next++
	if r.next == len(r.buf) {
		r.next, r.full = 0, true
	}
}

// all returns the window newest first.
func (r *ring[T]) all() []T {
	n := r.next
	if r.full {
		n = len(r.buf)
	}
	out := make([]T, 0, n)
	for i := 0; i < n; i++ {
		j := r.next - 1 - i
		if j < 0 {
			j += len(r.buf)
		}
		out = append(out, r.buf[j])
	}
	return out
}

// pathStat accumulates one route's durations. Kept as a sample window
// rather than a running mean: a mean hides the tail, and the tail is
// what a reader is looking for on this page.
type pathStat struct {
	n       int
	samples ring[time.Duration]
	max     time.Duration
}

func (p *pathStat) add(d time.Duration) {
	if p.samples.buf == nil {
		p.samples = newRing[time.Duration](128)
	}
	p.n++
	p.samples.push(d)
	if d > p.max {
		p.max = d
	}
}

// PathStat is one route's rolled-up timing, for the admin surface.
type PathStat struct {
	Path string
	N    int
	P50  time.Duration
	P95  time.Duration
	Max  time.Duration
}

// Snapshot is everything the ops page shows, taken under one lock so
// the panels agree with each other.
type Snapshot struct {
	Paths       []PathStat
	Renders     []Render
	Subprocess  []Subprocess
	LiveSubproc int
	PeakSubproc int
}

// Snapshot reads the rings. Slowest paths first by p95, because the
// question this page answers is "what is slow", not "what is busy".
func (r *Recorder) Snapshot() Snapshot {
	if r == nil {
		return Snapshot{}
	}
	r.mu.Lock()
	defer r.mu.Unlock()
	s := Snapshot{
		Renders: r.rend.all(), Subprocess: r.subs.all(),
		LiveSubproc: r.live, PeakSubproc: r.liveMax,
	}
	for path, st := range r.paths {
		xs := st.samples.all()
		slices.Sort(xs)
		s.Paths = append(s.Paths, PathStat{
			Path: path, N: st.n, Max: st.max,
			P50: pct(xs, 0.50), P95: pct(xs, 0.95),
		})
	}
	slices.SortFunc(s.Paths, func(a, b PathStat) int {
		if c := cmp.Compare(b.P95, a.P95); c != 0 {
			return c
		}
		return cmp.Compare(a.Path, b.Path)
	})
	return s
}

// pct is the nearest-rank percentile of a SORTED slice.
func pct(xs []time.Duration, q float64) time.Duration {
	if len(xs) == 0 {
		return 0
	}
	i := int(float64(len(xs))*q+0.5) - 1
	if i < 0 {
		i = 0
	}
	if i >= len(xs) {
		i = len(xs) - 1
	}
	return xs[i]
}