diff --git a/controllers/admin/video.go b/controllers/admin/video.go index a6cf5a588..ee4407cc1 100644 --- a/controllers/admin/video.go +++ b/controllers/admin/video.go @@ -13,13 +13,22 @@ import ( // GetVideoPlaybackMetrics returns video playback metrics. func GetVideoPlaybackMetrics(w http.ResponseWriter, r *http.Request) { type response struct { - Errors []metrics.TimestampedValue `json:"errors"` - QualityVariantChanges []metrics.TimestampedValue `json:"qualityVariantChanges"` - Latency []metrics.TimestampedValue `json:"latency"` - SegmentDownloadDuration []metrics.TimestampedValue `json:"segmentDownloadDuration"` - SlowestDownloadRate []metrics.TimestampedValue `json:"minPlayerBitrate"` - AvailableBitrates []int `json:"availableBitrates"` - SegmentLength int `json:"segmentLength"` + Errors []metrics.TimestampedValue `json:"errors"` + QualityVariantChanges []metrics.TimestampedValue `json:"qualityVariantChanges"` + + HighestLatency []metrics.TimestampedValue `json:"highestLatency"` + MedianLatency []metrics.TimestampedValue `json:"medianLatency"` + LowestLatency []metrics.TimestampedValue `json:"lowestLatency"` + + MedianDownloadDuration []metrics.TimestampedValue `json:"medianSegmentDownloadDuration"` + MaximumDownloadDuration []metrics.TimestampedValue `json:"maximumSegmentDownloadDuration"` + MinimumDownloadDuration []metrics.TimestampedValue `json:"minimumSegmentDownloadDuration"` + + SlowestDownloadRate []metrics.TimestampedValue `json:"minPlayerBitrate"` + MedianDownloadRate []metrics.TimestampedValue `json:"medianPlayerBitrate"` + HighestDownloadRater []metrics.TimestampedValue `json:"maxPlayerBitrate"` + AvailableBitrates []int `json:"availableBitrates"` + SegmentLength int `json:"segmentLength"` } availableBitrates := []int{} @@ -37,18 +46,32 @@ func GetVideoPlaybackMetrics(w http.ResponseWriter, r *http.Request) { } errors := metrics.GetPlaybackErrorCountOverTime() - latency := metrics.GetLatencyOverTime() - durations := metrics.GetDownloadDurationsOverTime() + medianLatency := metrics.GetMedianLatencyOverTime() + minimumLatency := metrics.GetMinimumLatencyOverTime() + maximumLatency := metrics.GetMaximumLatencyOverTime() + + medianDurations := metrics.GetMedianDownloadDurationsOverTime() + maximumDurations := metrics.GetMaximumDownloadDurationsOverTime() + minimumDurations := metrics.GetMinimumDownloadDurationsOverTime() + minPlayerBitrate := metrics.GetSlowestDownloadRateOverTime() + medianPlayerBitrate := metrics.GetMedianDownloadRateOverTime() + maxPlayerBitrate := metrics.GetMaxDownloadRateOverTime() qualityVariantChanges := metrics.GetQualityVariantChangesOverTime() resp := response{ AvailableBitrates: availableBitrates, Errors: errors, - Latency: latency, + MedianLatency: medianLatency, + HighestLatency: maximumLatency, + LowestLatency: minimumLatency, SegmentLength: segmentLength, - SegmentDownloadDuration: durations, + MedianDownloadDuration: medianDurations, + MaximumDownloadDuration: maximumDurations, + MinimumDownloadDuration: minimumDurations, SlowestDownloadRate: minPlayerBitrate, + MedianDownloadRate: medianPlayerBitrate, + HighestDownloadRater: maxPlayerBitrate, QualityVariantChanges: qualityVariantChanges, } diff --git a/metrics/metrics.go b/metrics/metrics.go index 0c42b5d07..a2fb6955a 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -20,14 +20,24 @@ const ( // CollectedMetrics stores different collected + timestamped values. type CollectedMetrics struct { - CPUUtilizations []TimestampedValue `json:"cpu"` - RAMUtilizations []TimestampedValue `json:"memory"` - DiskUtilizations []TimestampedValue `json:"disk"` - errorCount []TimestampedValue `json:"-"` - lowestBitrate []TimestampedValue `json:"-"` - segmentDownloadSeconds []TimestampedValue `json:"-"` - averageLatency []TimestampedValue `json:"-"` - qualityVariantChanges []TimestampedValue `json:"-"` + CPUUtilizations []TimestampedValue `json:"cpu"` + RAMUtilizations []TimestampedValue `json:"memory"` + DiskUtilizations []TimestampedValue `json:"disk"` + + errorCount []TimestampedValue `json:"-"` + lowestBitrate []TimestampedValue `json:"-"` + medianBitrate []TimestampedValue `json:"-"` + highestBitrate []TimestampedValue `json:"-"` + + medianSegmentDownloadSeconds []TimestampedValue `json:"-"` + maximumSegmentDownloadSeconds []TimestampedValue `json:"-"` + minimumSegmentDownloadSeconds []TimestampedValue `json:"-"` + + minimumLatency []TimestampedValue `json:"-"` + maximumLatency []TimestampedValue `json:"-"` + medianLatency []TimestampedValue `json:"-"` + + qualityVariantChanges []TimestampedValue `json:"-"` } // Metrics is the shared Metrics instance. diff --git a/metrics/playback.go b/metrics/playback.go index be110c42b..e9c02a9a0 100644 --- a/metrics/playback.go +++ b/metrics/playback.go @@ -65,25 +65,57 @@ func collectPlaybackErrorCount() { } func collectSegmentDownloadDuration() { - val := 0.0 + median := 0.0 + max := 0.0 + min := 0.0 if len(windowedDownloadDurations) > 0 { - val = utils.Avg(windowedDownloadDurations) + median = utils.Median(windowedDownloadDurations) + min, max = utils.MinMax(windowedDownloadDurations) windowedDownloadDurations = []float64{} } - metrics.segmentDownloadSeconds = append(metrics.segmentDownloadSeconds, TimestampedValue{ + + metrics.medianSegmentDownloadSeconds = append(metrics.medianSegmentDownloadSeconds, TimestampedValue{ Time: time.Now(), - Value: val, + Value: median, }) - if len(metrics.segmentDownloadSeconds) > maxCollectionValues { - metrics.segmentDownloadSeconds = metrics.segmentDownloadSeconds[1:] + if len(metrics.medianSegmentDownloadSeconds) > maxCollectionValues { + metrics.medianSegmentDownloadSeconds = metrics.medianSegmentDownloadSeconds[1:] + } + + metrics.minimumSegmentDownloadSeconds = append(metrics.minimumSegmentDownloadSeconds, TimestampedValue{ + Time: time.Now(), + Value: min, + }) + + if len(metrics.minimumSegmentDownloadSeconds) > maxCollectionValues { + metrics.minimumSegmentDownloadSeconds = metrics.minimumSegmentDownloadSeconds[1:] + } + + metrics.maximumSegmentDownloadSeconds = append(metrics.maximumSegmentDownloadSeconds, TimestampedValue{ + Time: time.Now(), + Value: max, + }) + + if len(metrics.maximumSegmentDownloadSeconds) > maxCollectionValues { + metrics.maximumSegmentDownloadSeconds = metrics.maximumSegmentDownloadSeconds[1:] } } -// GetDownloadDurationsOverTime will return a window of durations errors over time. -func GetDownloadDurationsOverTime() []TimestampedValue { - return metrics.segmentDownloadSeconds +// GetMedianDownloadDurationsOverTime will return a window of durations errors over time. +func GetMedianDownloadDurationsOverTime() []TimestampedValue { + return metrics.medianSegmentDownloadSeconds +} + +// GetMaximumDownloadDurationsOverTime will return a maximum durations errors over time. +func GetMaximumDownloadDurationsOverTime() []TimestampedValue { + return metrics.maximumSegmentDownloadSeconds +} + +// GetMinimumDownloadDurationsOverTime will return a maximum durations errors over time. +func GetMinimumDownloadDurationsOverTime() []TimestampedValue { + return metrics.minimumSegmentDownloadSeconds } // GetPlaybackErrorCountOverTime will return a window of playback errors over time. @@ -92,53 +124,113 @@ func GetPlaybackErrorCountOverTime() []TimestampedValue { } func collectLatencyValues() { - val := 0.0 + median := 0.0 + min := 0.0 + max := 0.0 if len(windowedLatencies) > 0 { - val = utils.Avg(windowedLatencies) - val = math.Round(val) + median = utils.Median(windowedLatencies) + min, max = utils.MinMax(windowedLatencies) windowedLatencies = []float64{} } - metrics.averageLatency = append(metrics.averageLatency, TimestampedValue{ + metrics.medianLatency = append(metrics.medianLatency, TimestampedValue{ Time: time.Now(), - Value: val, + Value: median, }) - if len(metrics.averageLatency) > maxCollectionValues { - metrics.averageLatency = metrics.averageLatency[1:] + if len(metrics.medianLatency) > maxCollectionValues { + metrics.medianLatency = metrics.medianLatency[1:] + } + + metrics.minimumLatency = append(metrics.minimumLatency, TimestampedValue{ + Time: time.Now(), + Value: min, + }) + + if len(metrics.minimumLatency) > maxCollectionValues { + metrics.minimumLatency = metrics.minimumLatency[1:] + } + + metrics.maximumLatency = append(metrics.maximumLatency, TimestampedValue{ + Time: time.Now(), + Value: max, + }) + + if len(metrics.maximumLatency) > maxCollectionValues { + metrics.maximumLatency = metrics.maximumLatency[1:] } } -// GetLatencyOverTime will return the min, max and avg latency values over time. -func GetLatencyOverTime() []TimestampedValue { - if len(metrics.averageLatency) == 0 { +// GetMedianLatencyOverTime will return the median latency values over time. +func GetMedianLatencyOverTime() []TimestampedValue { + if len(metrics.medianLatency) == 0 { return []TimestampedValue{} } - return metrics.averageLatency + return metrics.medianLatency } -// collectLowestBandwidth will collect the lowest bandwidth currently collected +// GetMinimumLatencyOverTime will return the min latency values over time. +func GetMinimumLatencyOverTime() []TimestampedValue { + if len(metrics.minimumLatency) == 0 { + return []TimestampedValue{} + } + + return metrics.minimumLatency +} + +// GetMaximumLatencyOverTime will return the max latency values over time. +func GetMaximumLatencyOverTime() []TimestampedValue { + if len(metrics.maximumLatency) == 0 { + return []TimestampedValue{} + } + + return metrics.maximumLatency +} + +// collectLowestBandwidth will collect the bandwidth currently collected // so we can report to the streamer the worst possible streaming condition // being experienced. func collectLowestBandwidth() { - val := 0.0 + min := 0.0 + median := 0.0 + max := 0.0 if len(windowedBandwidths) > 0 { - val, _ = utils.MinMax(windowedBandwidths) - val = math.Round(val) + min, max = utils.MinMax(windowedBandwidths) + min = math.Round(min) + max = math.Round(max) + median = utils.Median(windowedBandwidths) windowedBandwidths = []float64{} } metrics.lowestBitrate = append(metrics.lowestBitrate, TimestampedValue{ Time: time.Now(), - Value: math.Round(val), + Value: math.Round(min), }) if len(metrics.lowestBitrate) > maxCollectionValues { metrics.lowestBitrate = metrics.lowestBitrate[1:] } + + metrics.medianBitrate = append(metrics.medianBitrate, TimestampedValue{ + Time: time.Now(), + Value: math.Round(median), + }) + + if len(metrics.medianBitrate) > maxCollectionValues { + metrics.medianBitrate = metrics.medianBitrate[1:] + } + + metrics.highestBitrate = append(metrics.highestBitrate, TimestampedValue{ + Time: time.Now(), + Value: math.Round(max), + }) + + if len(metrics.highestBitrate) > maxCollectionValues { + metrics.highestBitrate = metrics.highestBitrate[1:] + } } // GetSlowestDownloadRateOverTime will return the collected lowest bandwidth values @@ -151,6 +243,38 @@ func GetSlowestDownloadRateOverTime() []TimestampedValue { return metrics.lowestBitrate } +// GetMedianDownloadRateOverTime will return the collected median bandwidth values. +func GetMedianDownloadRateOverTime() []TimestampedValue { + if len(metrics.medianBitrate) == 0 { + return []TimestampedValue{} + } + return metrics.medianBitrate +} + +// GetMaximumDownloadRateOverTime will return the collected maximum bandwidth values. +func GetMaximumDownloadRateOverTime() []TimestampedValue { + if len(metrics.maximumLatency) == 0 { + return []TimestampedValue{} + } + return metrics.maximumLatency +} + +// GetMinimumDownloadRateOverTime will return the collected minimum bandwidth values. +func GetMinimumDownloadRateOverTime() []TimestampedValue { + if len(metrics.minimumLatency) == 0 { + return []TimestampedValue{} + } + return metrics.minimumLatency +} + +// GetMaxDownloadRateOverTime will return the collected highest bandwidth values. +func GetMaxDownloadRateOverTime() []TimestampedValue { + if len(metrics.highestBitrate) == 0 { + return []TimestampedValue{} + } + return metrics.highestBitrate +} + func collectQualityVariantChanges() { count := utils.Sum(windowedQualityVariantChanges) windowedQualityVariantChanges = []float64{} diff --git a/utils/performanceTimer.go b/utils/performanceTimer.go index 83f1a98e2..5d3020747 100644 --- a/utils/performanceTimer.go +++ b/utils/performanceTimer.go @@ -82,3 +82,29 @@ func MinMax(array []float64) (float64, float64) { } return min, max } + +func mean(input []float64) float64 { + sum := Sum(input) + + return sum / float64(len(input)) +} + +// Median gets the median number in a slice of numbers. +func Median(input []float64) float64 { + if len(input) == 1 { + return input[0] + } + + c := make([]float64, len(input)) + copy(c, input) + + var median float64 + l := len(c) + if l%2 == 0 { + median = mean(c[l/2-1 : l/2+1]) + } else { + median = c[l/2] + } + + return median +}