clone

a desktop tv and program guide for over-the-air broadcasts: what's on air, what your tuner gets, live tv, recordings

main
airwaves / internal / phase / phase.go
phase.go 2.8K
1// Package phase times the steps of starting a stream, so a slow channel2// change shows where its time went: "tune 9.1 (antenna): first data 1.1s,3// first frame 1.9s, first segment 3.8s, ready 5.6s". It also keeps recent4// tune times by kind of channel, as the server and the app measured them,5// for the admin page.6package phase78import (9 "context"10 "fmt"11 "strings"12 "sync"13 "time"14)1516// Timer records when each step of one stream's start happened.17type Timer struct {18 // Label names the stream in logs: "9.1 (antenna)".19 Label string20 // Kind is the kind of channel for the stats: "antenna", "folder",21 // "jellyfin", "youtube" or "weather"; empty for none.22 Kind string23 start time.Time2425 mu sync.Mutex26 marks []Mark27}2829// Mark is one step, at a time since the start.30type Mark struct {31 Name string `json:"name"`32 At time.Duration `json:"-"`33 MS int64 `json:"ms"`34}3536// New starts timing a stream.37func New(label, kind string) *Timer {38 return &Timer{Label: label, Kind: kind, start: time.Now()}39}4041// Mark records that a step happened now. Only a step's first time counts.42// A nil Timer records nothing.43func (t *Timer) Mark(name string) {44 if t == nil {45 return46 }47 at := time.Since(t.start)48 t.mu.Lock()49 defer t.mu.Unlock()50 for _, m := range t.marks {51 if m.Name == name {52 return53 }54 }55 t.marks = append(t.marks, Mark{Name: name, At: at, MS: at.Milliseconds()})56}5758// Has reports whether a step was marked.59func (t *Timer) Has(name string) bool {60 if t == nil {61 return false62 }63 t.mu.Lock()64 defer t.mu.Unlock()65 for _, m := range t.marks {66 if m.Name == name {67 return true68 }69 }70 return false71}7273// Since is how long ago the timer started.74func (t *Timer) Since() time.Duration {75 if t == nil {76 return 077 }78 return time.Since(t.start)79}8081// Marks lists the steps in the order they happened.82func (t *Timer) Marks() []Mark {83 if t == nil {84 return nil85 }86 t.mu.Lock()87 defer t.mu.Unlock()88 out := make([]Mark, len(t.marks))89 copy(out, t.marks)90 return out91}9293// String lists the steps: "first data 1.1s, ready 5.6s".94func (t *Timer) String() string {95 var parts []string96 for _, m := range t.Marks() {97 parts = append(parts, fmt.Sprintf("%s %.1fs", m.Name, m.At.Seconds()))98 }99 if len(parts) == 0 {100 return "no steps"101 }102 return strings.Join(parts, ", ")103}104105type key struct{}106107// NewContext returns ctx carrying t, for the code that starts the stream108// to mark its steps on.109func NewContext(ctx context.Context, t *Timer) context.Context {110 return context.WithValue(ctx, key{}, t)111}112113// FromContext returns the Timer ctx carries, or nil.114func FromContext(ctx context.Context) *Timer {115 t, _ := ctx.Value(key{}).(*Timer)116 return t117}118119// Step marks a step on ctx's Timer, if it has one.120func Step(ctx context.Context, name string) { FromContext(ctx).Mark(name) }