1package phase23import (4 "slices"5 "sync"6 "time"7)89// Measures of a channel change.10const (11 // Ready is the server's: from the tune call to the stream being ready.12 Ready = "ready"13 // FirstFrame is the app's: from the key press to the first frame14 // playing.15 FirstFrame = "first frame"16)1718// Kinds are the kinds of channel the stats keep.19var Kinds = []string{"antenna", "folder", "jellyfin", "youtube", "weather"}2021// keep is how many recent tunes each kind and measure keeps.22const keep = 1002324// Stats keeps recent tune times by kind of channel and measure.25type Stats struct {26 mu sync.Mutex27 samples map[[2]string][]time.Duration // by kind and measure, oldest first28}2930// Tunes are this server's.31var Tunes = &Stats{}3233// Add records a tune's time. Kinds and measures not known, and times34// that can't be right, are left out.35func (s *Stats) Add(kind, measure string, d time.Duration) {36 if !slices.Contains(Kinds, kind) || (measure != Ready && measure != FirstFrame) || d <= 0 || d > 2*time.Minute {37 return38 }39 s.mu.Lock()40 defer s.mu.Unlock()41 if s.samples == nil {42 s.samples = map[[2]string][]time.Duration{}43 }44 k := [2]string{kind, measure}45 xs := append(s.samples[k], d)46 if len(xs) > keep {47 xs = xs[len(xs)-keep:]48 }49 s.samples[k] = xs50}5152// Summary describes one kind and measure's recent tunes, in seconds.53type Summary struct {54 Kind string `json:"kind"`55 Measure string `json:"measure"`56 Count int `json:"count"`57 P50 float64 `json:"p50"`58 P90 float64 `json:"p90"`59 Max float64 `json:"max"`60}6162// Summaries lists the kinds and measures with tunes, by kind in the order63// of Kinds, the server's measure first.64func (s *Stats) Summaries() []Summary {65 s.mu.Lock()66 defer s.mu.Unlock()67 var out []Summary68 for _, kind := range Kinds {69 for _, measure := range []string{Ready, FirstFrame} {70 xs := slices.Clone(s.samples[[2]string{kind, measure}])71 if len(xs) == 0 {72 continue73 }74 slices.Sort(xs)75 out = append(out, Summary{76 Kind: kind, Measure: measure, Count: len(xs),77 P50: quantile(xs, 0.5).Seconds(), P90: quantile(xs, 0.9).Seconds(), Max: xs[len(xs)-1].Seconds(),78 })79 }80 }81 return out82}8384// quantile is the q quantile of sorted xs, nearest rank.85func quantile(xs []time.Duration, q float64) time.Duration {86 i := int(q*float64(len(xs))+0.999999) - 187 return xs[min(max(i, 0), len(xs)-1)]88}