perf(metrics): keep downsampled series ordered

This commit is contained in:
rcourtman
2026-03-27 12:16:31 +00:00
parent ff8b64d3ef
commit 27507a47ad
4 changed files with 91 additions and 4 deletions
@@ -204,6 +204,12 @@ and `frontend-modern/src/components/Dashboard/useDashboardState.ts` must keep
filters, stats, and table visibility driven by the workload route's own
REST-backed health so websocket churn does not hide already-fetched workloads
or swap the protected hot path into a false disconnected shell.
The same performance ownership now applies to the downsampled
`pkg/metrics/store.go` batched query path. `QueryAllBatch` may drop the global
SQLite `ORDER BY resource_id, metric_type, bucket_ts` sort from the grouped
query only if the runtime preserves ascending timestamps within every returned
resource/metric series and the hot-path proof keeps both latency and ordering
guarded together in the explicit metrics SLO surface.
That runtime is now intentionally split by concern:
`frontend-modern/src/components/Dashboard/useDashboardState.ts` owns
top-level dashboard orchestration, workload loading, and composition across
+45 -4
View File
@@ -8,6 +8,7 @@ import (
"net/url"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
@@ -890,7 +891,6 @@ func (s *Store) queryAllBatchWithTier(resourceType string, resourceIDs []string,
WHERE resource_type = ? AND resource_id IN (%s) AND tier = ?
AND timestamp >= ? AND timestamp <= ?
GROUP BY resource_id, metric_type, bucket_ts
ORDER BY resource_id, metric_type, bucket_ts ASC
`, inClause)
} else {
params = make([]interface{}, 0, len(resourceIDs)+4)
@@ -925,7 +925,8 @@ func (s *Store) queryAllBatchWithTier(resourceType string, resourceIDs []string,
}
defer rows.Close()
result := make(map[string]map[string][]MetricPoint)
result := make(map[string]map[string][]MetricPoint, len(resourceIDs))
seriesCapacity := estimateQueryAllBatchSeriesCapacity(start, end, stepSecs)
for rows.Next() {
var resourceID, metricType string
var ts int64
@@ -937,12 +938,52 @@ func (s *Store) queryAllBatchWithTier(resourceType string, resourceIDs []string,
p.Timestamp = time.Unix(ts, 0)
if _, exists := result[resourceID]; !exists {
result[resourceID] = make(map[string][]MetricPoint)
result[resourceID] = make(map[string][]MetricPoint, 8)
}
if _, exists := result[resourceID][metricType]; !exists {
result[resourceID][metricType] = make([]MetricPoint, 0, seriesCapacity)
}
result[resourceID][metricType] = append(result[resourceID][metricType], p)
}
return result, rows.Err()
if err := rows.Err(); err != nil {
return nil, err
}
if stepSecs > 1 {
sortMetricSeriesPoints(result)
}
return result, nil
}
func estimateQueryAllBatchSeriesCapacity(start, end time.Time, stepSecs int64) int {
if stepSecs <= 1 {
return 0
}
if !end.After(start) {
return 1
}
stepDuration := time.Duration(stepSecs) * time.Second
buckets := int(end.Sub(start)/stepDuration) + 2
if buckets < 1 {
return 1
}
return buckets
}
func sortMetricSeriesPoints(result map[string]map[string][]MetricPoint) {
for _, metricMap := range result {
for metricType, points := range metricMap {
if len(points) < 2 {
continue
}
sort.Slice(points, func(i, j int) bool {
return points[i].Timestamp.Before(points[j].Timestamp)
})
metricMap[metricType] = points
}
}
}
// selectTier chooses the appropriate data tier based on time range
+7
View File
@@ -385,6 +385,13 @@ func TestSLO_QueryAllBatch(t *testing.T) {
if len(result[id]) != len(metricTypes) {
t.Fatalf("sanity: expected %d metric types for %s, got %d", len(metricTypes), id, len(result[id]))
}
for metricType, points := range result[id] {
for i := 1; i < len(points); i++ {
if points[i].Timestamp.Before(points[i-1].Timestamp) {
t.Fatalf("sanity: expected ascending timestamps for %s/%s, got %v before %v", id, metricType, points[i], points[i-1])
}
}
}
}
latencies := measureLatencies(t, func() {
+33
View File
@@ -565,6 +565,39 @@ func TestQueryAllBatch(t *testing.T) {
}
})
t.Run("downsampled results stay ordered per metric series", func(t *testing.T) {
downsampledStoreDir := t.TempDir()
downsampledCfg := DefaultConfig(downsampledStoreDir)
downsampledCfg.DBPath = filepath.Join(downsampledStoreDir, "metrics-batch-downsampled.db")
downsampledCfg.FlushInterval = time.Hour
downsampledStore, err := NewStore(downsampledCfg)
if err != nil {
t.Fatalf("NewStore(downsampled): %v", err)
}
defer downsampledStore.Close()
base := time.Unix(10_000, 0)
downsampledStore.writeBatch([]bufferedMetric{
{resourceType: "disk", resourceID: "disk-ordered", metricType: "smart_temp", value: 33, timestamp: base.Add(130 * time.Second), tier: TierRaw},
{resourceType: "disk", resourceID: "disk-ordered", metricType: "smart_temp", value: 31, timestamp: base.Add(10 * time.Second), tier: TierRaw},
{resourceType: "disk", resourceID: "disk-ordered", metricType: "smart_temp", value: 32, timestamp: base.Add(70 * time.Second), tier: TierRaw},
})
batch, err := downsampledStore.QueryAllBatch("disk", []string{"disk-ordered"}, base, base.Add(3*time.Minute), 60)
if err != nil {
t.Fatalf("QueryAllBatch(downsampled): %v", err)
}
points := batch["disk-ordered"]["smart_temp"]
if len(points) != 3 {
t.Fatalf("expected 3 bucketed points, got %d", len(points))
}
if !points[0].Timestamp.Before(points[1].Timestamp) || !points[1].Timestamp.Before(points[2].Timestamp) {
t.Fatalf("expected ascending timestamps, got %+v", points)
}
})
t.Run("chunked resource lists return complete results beyond sqlite parameter threshold", func(t *testing.T) {
chunkedStoreDir := t.TempDir()
chunkedCfg := DefaultConfig(chunkedStoreDir)