mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-27 05:57:27 +00:00
b240c38530
Check every timestamp before reusing selected indices within one conversion, avoiding repeated bucket work without sharing values or retaining a cross-request cache. Keep mixed cadence, unsorted observations, oldest metadata and detached wire points covered by the parent oracle. Contract-Neutral: Chart identity, scope, values, selected observations, sampling limits, oldest metadata, fallback and cache lifetimes are unchanged; only local conversion scratch and index selection are reused. Change-source: pulse-maintainer
196 lines
6.5 KiB
Go
196 lines
6.5 KiB
Go
package chartapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"math/rand"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
|
)
|
|
|
|
// Keep the pre-reuse conversion as a wire/ownership oracle. Sampling itself
|
|
// remains covered by referenceChartCap and TestChartPointSelectionMatchesOriginalCap.
|
|
func referenceConvertMetricsForChart(metrics map[string][]monitoring.MetricPoint, oldestTimestamp *int64, maxPoints int) VMChartData {
|
|
converted := make(VMChartData, len(metrics))
|
|
for metricType, metricPoints := range metrics {
|
|
if !sparklineMetrics[metricType] {
|
|
continue
|
|
}
|
|
points := make([]MetricPoint, len(metricPoints))
|
|
for i, point := range metricPoints {
|
|
ts := point.Timestamp.UnixMilli()
|
|
if ts < *oldestTimestamp {
|
|
*oldestTimestamp = ts
|
|
}
|
|
points[i] = MetricPoint{Timestamp: ts, Value: point.Value}
|
|
}
|
|
converted[metricType] = referenceChartCap(points, maxPoints)
|
|
}
|
|
return converted
|
|
}
|
|
|
|
func timelineFixture(count int, shared bool) map[string][]monitoring.MetricPoint {
|
|
result := make(map[string][]monitoring.MetricPoint)
|
|
for m, name := range []string{"cpu", "memory", "memoryused", "disk", "netin", "netout", "diskread", "diskwrite"} {
|
|
points := make([]monitoring.MetricPoint, count)
|
|
for i := range points {
|
|
offset := int64(i) * 60000
|
|
if !shared {
|
|
offset += int64((i % 7) * m)
|
|
}
|
|
points[i] = monitoring.MetricPoint{Timestamp: time.UnixMilli(1792930000000 + offset), Value: float64((i + 3*m) % 100)}
|
|
}
|
|
result[name] = points
|
|
}
|
|
return result
|
|
}
|
|
|
|
func TestConvertMetricsForChartSharedAndDivergentTimelines(t *testing.T) {
|
|
t.Parallel()
|
|
rng := rand.New(rand.NewSource(20261025))
|
|
for scenario := 0; scenario < 100; scenario++ {
|
|
n := []int{0, 1, 2, 17, 121, 240, 500, 777}[scenario%8]
|
|
source := timelineFixture(n, scenario%2 == 0)
|
|
for metric, points := range source {
|
|
// Same first/last/count with different interiors must never reuse the
|
|
// wrong representatives. Values, sub-ms timestamps and zones are not keys.
|
|
for i := range points {
|
|
switch scenario % 7 {
|
|
case 0:
|
|
points[i].Timestamp = time.UnixMilli(-1000 + int64(i%9))
|
|
case 1:
|
|
if metric == "memory" && i > 0 && i < n-1 {
|
|
points[i].Timestamp = points[i].Timestamp.Add(time.Duration(rng.Intn(120000)-60000) * time.Millisecond)
|
|
}
|
|
case 2:
|
|
if metric == "disk" {
|
|
points[i].Timestamp = time.UnixMilli(100)
|
|
}
|
|
case 3:
|
|
points[i].Timestamp = points[i].Timestamp.In(time.FixedZone("fixture", 3600)).Add(time.Duration(rng.Intn(999999)) * time.Nanosecond)
|
|
case 4:
|
|
if i > n/2 {
|
|
points[i].Timestamp = points[n/2].Timestamp.Add(time.Duration(i-n/2) * time.Millisecond)
|
|
}
|
|
case 5:
|
|
if metric == "cpu" {
|
|
points[i].Value = math.Copysign(0, -1)
|
|
}
|
|
}
|
|
}
|
|
if scenario%7 == 6 && len(points) > 2 {
|
|
points[0], points[n-1] = points[n-1], points[0]
|
|
}
|
|
}
|
|
if scenario%3 == 0 {
|
|
source["cpu"] = nil
|
|
}
|
|
source["not-a-sparkline"] = []monitoring.MetricPoint{{Timestamp: time.UnixMilli(math.MinInt64), Value: -999}}
|
|
for _, limit := range []int{-1, 0, 1, 2, 3, 30, 120, 500} {
|
|
t.Run(fmt.Sprintf("%d/cap=%d", scenario, limit), func(t *testing.T) {
|
|
wantOldest, gotOldest := int64(math.MaxInt64), int64(math.MaxInt64)
|
|
want := referenceConvertMetricsForChart(source, &wantOldest, limit)
|
|
got := convertMetricsForChart(source, &gotOldest, limit)
|
|
if !reflect.DeepEqual(got, want) || gotOldest != wantOldest {
|
|
t.Fatalf("timeline reuse changed original observations or oldest: got=%d want=%d", gotOldest, wantOldest)
|
|
}
|
|
gotJSON, err := json.Marshal(got)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantJSON, err := json.Marshal(want)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(gotJSON) != string(wantJSON) {
|
|
t.Fatal("wire bytes changed")
|
|
}
|
|
for metric, points := range got {
|
|
if len(points) > 0 {
|
|
first := source[metric][0]
|
|
points[0].Value = 99999
|
|
points[0].Timestamp = 0
|
|
if source[metric][0] != first {
|
|
t.Fatal("output aliases input")
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
var chartTimelineBenchmarkSink VMChartData
|
|
|
|
func TestConvertMetricsForChartSharedTimelineAllocations(t *testing.T) {
|
|
source := timelineFixture(240, true)
|
|
// Compare against the exact immediate-parent capped path, not the older
|
|
// full-wire oracle above, which intentionally converts before capping.
|
|
parent := func() {
|
|
oldest := int64(math.MaxInt64)
|
|
chartTimelineBenchmarkSink = parentConvertMetricsForChart(source, &oldest, 120)
|
|
}
|
|
production := func() {
|
|
oldest := int64(math.MaxInt64)
|
|
chartTimelineBenchmarkSink = convertMetricsForChart(source, &oldest, 120)
|
|
}
|
|
want := testing.AllocsPerRun(20, parent)
|
|
got := testing.AllocsPerRun(20, production)
|
|
t.Logf("shared timeline allocations: parent=%.0f production=%.0f", want, got)
|
|
if got >= want {
|
|
t.Fatalf("shared timeline must avoid repeated sampling allocations: parent=%.0f production=%.0f", want, got)
|
|
}
|
|
}
|
|
|
|
// Frozen immediate-parent path for comparable allocation measurements.
|
|
func parentConvertMetricsForChart(metrics map[string][]monitoring.MetricPoint, oldestTimestamp *int64, maxPoints int) VMChartData {
|
|
converted := make(VMChartData, len(metrics))
|
|
for metricType, metricPoints := range metrics {
|
|
if !sparklineMetrics[metricType] {
|
|
continue
|
|
}
|
|
if len(metricPoints) <= maxPoints || maxPoints <= 0 {
|
|
points := make([]MetricPoint, len(metricPoints))
|
|
for i, point := range metricPoints {
|
|
ts := point.Timestamp.UnixMilli()
|
|
if ts < *oldestTimestamp {
|
|
*oldestTimestamp = ts
|
|
}
|
|
points[i] = MetricPoint{Timestamp: ts, Value: point.Value}
|
|
}
|
|
converted[metricType] = points
|
|
continue
|
|
}
|
|
for _, point := range metricPoints {
|
|
if ts := point.Timestamp.UnixMilli(); ts < *oldestTimestamp {
|
|
*oldestTimestamp = ts
|
|
}
|
|
}
|
|
indices := chartPointIndices(len(metricPoints), maxPoints, func(i int) int64 { return metricPoints[i].Timestamp.UnixMilli() })
|
|
points := make([]MetricPoint, len(indices))
|
|
for i, index := range indices {
|
|
point := metricPoints[index]
|
|
points[i] = MetricPoint{Timestamp: point.Timestamp.UnixMilli(), Value: point.Value}
|
|
}
|
|
converted[metricType] = points
|
|
}
|
|
return converted
|
|
}
|
|
|
|
func BenchmarkConvertMetricsForChartTimelines(b *testing.B) {
|
|
for _, shared := range []bool{true, false} {
|
|
source := timelineFixture(240, shared)
|
|
b.Run(fmt.Sprintf("shared=%t", shared), func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
oldest := int64(math.MaxInt64)
|
|
chartTimelineBenchmarkSink = convertMetricsForChart(source, &oldest, 120)
|
|
}
|
|
})
|
|
}
|
|
}
|