mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
The in-memory metrics buffer was changed from 1000 to 86400 points per metric to support 30-day sparklines, but this pre-allocated ~18 MB per guest (7 slices × 86400 × 32 bytes). With 50 guests that's 920 MB — explaining why users needed to double their LXC memory after upgrading to 5.1.0. - Revert in-memory buffer to 1000 points / 24h retention - Remove eager slice pre-allocation (use append growth instead) - Add LTTB (Largest Triangle Three Buckets) downsampling algorithm - Chart endpoints now use a two-tier strategy: in-memory for ranges ≤ 2h, SQLite persistent store + LTTB for longer ranges - Reduce frontend ring buffer from 86400 to 2000 points Related to #1190
87 lines
2.2 KiB
Go
87 lines
2.2 KiB
Go
package monitoring
|
|
|
|
import (
|
|
"math"
|
|
)
|
|
|
|
// lttb performs Largest Triangle Three Buckets downsampling on a slice of
|
|
// MetricPoints. It reduces data to targetPoints while preserving the visual
|
|
// shape of the data — peaks, valleys and trends are retained.
|
|
//
|
|
// If len(data) <= targetPoints or targetPoints < 3, data is returned as-is.
|
|
func lttb(data []MetricPoint, targetPoints int) []MetricPoint {
|
|
n := len(data)
|
|
if targetPoints >= n || targetPoints < 3 {
|
|
return data
|
|
}
|
|
|
|
result := make([]MetricPoint, 0, targetPoints)
|
|
|
|
// Always keep the first point.
|
|
result = append(result, data[0])
|
|
|
|
bucketSize := float64(n-2) / float64(targetPoints-2)
|
|
prevSelected := 0
|
|
|
|
for i := 0; i < targetPoints-2; i++ {
|
|
// Current bucket range.
|
|
bucketStart := int(math.Floor(float64(i)*bucketSize)) + 1
|
|
bucketEnd := int(math.Floor(float64(i+1)*bucketSize)) + 1
|
|
if bucketEnd > n-1 {
|
|
bucketEnd = n - 1
|
|
}
|
|
|
|
// Next bucket range — used to compute the "third point" average.
|
|
nextStart := bucketEnd
|
|
nextEnd := int(math.Floor(float64(i+2)*bucketSize)) + 1
|
|
if nextEnd > n-1 {
|
|
nextEnd = n - 1
|
|
}
|
|
if nextStart >= nextEnd {
|
|
nextEnd = nextStart + 1
|
|
if nextEnd > n {
|
|
nextEnd = n
|
|
}
|
|
}
|
|
|
|
// Average of next bucket (the "C" vertex of the triangle).
|
|
avgTs := float64(0)
|
|
avgVal := float64(0)
|
|
nextCount := nextEnd - nextStart
|
|
for j := nextStart; j < nextEnd; j++ {
|
|
avgTs += float64(data[j].Timestamp.UnixMilli())
|
|
avgVal += data[j].Value
|
|
}
|
|
avgTs /= float64(nextCount)
|
|
avgVal /= float64(nextCount)
|
|
|
|
// Previously selected point (the "A" vertex).
|
|
aTs := float64(data[prevSelected].Timestamp.UnixMilli())
|
|
aVal := data[prevSelected].Value
|
|
|
|
// Find the point in the current bucket that maximises the triangle area.
|
|
maxArea := float64(-1)
|
|
bestIdx := bucketStart
|
|
|
|
for j := bucketStart; j < bucketEnd; j++ {
|
|
bTs := float64(data[j].Timestamp.UnixMilli())
|
|
bVal := data[j].Value
|
|
|
|
// Twice the triangle area (sign doesn't matter, we compare magnitudes).
|
|
area := math.Abs((aTs-avgTs)*(bVal-aVal) - (aTs-bTs)*(avgVal-aVal))
|
|
if area > maxArea {
|
|
maxArea = area
|
|
bestIdx = j
|
|
}
|
|
}
|
|
|
|
result = append(result, data[bestIdx])
|
|
prevSelected = bestIdx
|
|
}
|
|
|
|
// Always keep the last point.
|
|
result = append(result, data[n-1])
|
|
|
|
return result
|
|
}
|