Integrate reviewed History read allocation improvement

Accept exact Core candidate fcd9b65b82 with retained semantic, allocation, race, monitoring-suite and actual embedded build evidence. Preserve observations and caller isolation; no native performance or release acceptance is asserted.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-04 08:48:15 +01:00
3 changed files with 303 additions and 20 deletions
@@ -17,6 +17,20 @@
## Purpose
### Lightweight detached History reads
Guest, node, disk and storage History readers allocate one exact-sized snapshot
per populated metric series, rather than repeatedly growing temporary arrays.
Empty series remain non-nil arrays for the existing JSON representation. The
copy preserves the exclusive time cutoff, original values, timestamps, order
and duplicates, including non-chronological input; it never aliases mutable
retained History. Single/batch chart and metric-window consumers retain the
same observations. Retention, sampling, source authority and SQL are unchanged.
`metrics_history_read_test.go` covers every public series, caller mutation,
cutoff/empty/unordered semantics and connected poll-to-chart/window readers.
Allocation controls and paired same-executable in-memory benchmarks measure
copy cost, not native CPU/RSS relief or attribution of an existing report.
### Used-memory history retention
Periodic in-memory History cleanup applies the existing retention window and
+27 -20
View File
@@ -457,15 +457,7 @@ func (mh *MetricsHistory) GetGuestMetrics(guestID string, metricType string, dur
return []MetricPoint{}
}
// Filter by duration
result := make([]MetricPoint, 0)
for _, point := range data {
if point.Timestamp.After(cutoffTime) {
result = append(result, point)
}
}
return result
return filterMetricsByTime(data, cutoffTime)
}
// GetNodeMetrics returns historical metrics for a node
@@ -500,23 +492,38 @@ func (mh *MetricsHistory) GetNodeMetrics(nodeID string, metricType string, durat
return []MetricPoint{}
}
// Filter by duration
result := make([]MetricPoint, 0)
for _, point := range data {
if point.Timestamp.After(cutoffTime) {
result = append(result, point)
}
}
return result
return filterMetricsByTime(data, cutoffTime)
}
// filterMetricsByTime returns only the points whose timestamp is after cutoffTime.
func filterMetricsByTime(data []MetricPoint, cutoffTime time.Time) []MetricPoint {
filtered := make([]MetricPoint, 0)
// Skip the expired prefix, then count the survivors before allocating.
// Dense chart reads otherwise repeatedly grow and copy their result under
// the History read lock. Allocate only the requested window, not all stored
// points, and keep caller-owned snapshots rather than aliasing live History.
first := 0
for first < len(data) && !data[first].Timestamp.After(cutoffTime) {
first++
}
data = data[first:]
count := 0
for _, point := range data {
if point.Timestamp.After(cutoffTime) {
filtered = append(filtered, point)
count++
}
}
filtered := make([]MetricPoint, count) // Preserve [] (not null) for empty series.
if count == len(data) {
copy(filtered, data)
return filtered
}
// Usually the retained window is a contiguous suffix. Preserve the original
// per-point filtering and order even when an input is not chronological.
next := 0
for _, point := range data {
if point.Timestamp.After(cutoffTime) {
filtered[next] = point
next++
}
}
return filtered
@@ -0,0 +1,262 @@
package monitoring
import (
"encoding/json"
"fmt"
"reflect"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
)
// Preserve the original reader as a test-only reference. Paired benchmarks use
// the same executable, inputs and cutoff; no SQL or collector is exercised.
func legacyHistoryReadFilter(data []MetricPoint, cutoff time.Time) []MetricPoint {
filtered := make([]MetricPoint, 0)
for _, point := range data {
if point.Timestamp.After(cutoff) {
filtered = append(filtered, point)
}
}
return filtered
}
func historyReadFixture(size, retained int, interleaved bool) ([]MetricPoint, time.Time) {
cutoff := time.Unix(1700000000, 0).UTC()
data := make([]MetricPoint, size)
for i := range data {
offset := i - (size - retained) + 1
if interleaved {
offset = i + 1
if i%2 == 0 {
offset = -offset
}
}
data[i] = MetricPoint{Timestamp: cutoff.Add(time.Duration(offset) * time.Second), Value: float64(i)}
}
return data, cutoff
}
func TestHistoryReadFilterPreservesSemantics(t *testing.T) {
cutoff := time.Unix(1700000000, 0).UTC()
before := MetricPoint{Timestamp: cutoff.Add(-time.Second), Value: -1}
equal := MetricPoint{Timestamp: cutoff.In(time.FixedZone("other", 3600)), Value: 0}
after := MetricPoint{Timestamp: cutoff.Add(time.Second), Value: 2}
later := MetricPoint{Timestamp: cutoff.Add(2 * time.Second), Value: 3}
for name, data := range map[string][]MetricPoint{
"nil": nil, "empty": {}, "expired": {before, equal},
"all": {after, later}, "prefix": {before, equal, after, later},
"unordered": {later, before, after, equal}, "duplicates": {before, after, after},
} {
t.Run(name, func(t *testing.T) {
original := append([]MetricPoint(nil), data...)
want := legacyHistoryReadFilter(data, cutoff)
got := filterMetricsByTime(data, cutoff)
if !reflect.DeepEqual(got, want) {
t.Fatalf("filtered points = %#v, want %#v", got, want)
}
if got == nil {
t.Fatal("empty History arrays must remain [] rather than null")
}
if len(got) > 0 {
got[0].Value = -999
got[0].Timestamp = time.Time{}
if !reflect.DeepEqual(data, original) {
t.Fatal("caller mutation changed retained History")
}
}
})
}
}
func TestHistoryReadFilterAllocationBudget(t *testing.T) {
for _, tc := range []struct {
name string
size, retained int
interleaved bool
}{
{"empty", 0, 0, false}, {"expired", 2880, 0, false},
{"one", 2880, 1, false}, {"hour", 2880, 60, false},
{"day", 2880, 1440, false}, {"all", 2880, 2880, false},
{"interleaved", 2880, 1440, true},
} {
t.Run(tc.name, func(t *testing.T) {
data, cutoff := historyReadFixture(tc.size, tc.retained, tc.interleaved)
var snapshot []MetricPoint
allocs := testing.AllocsPerRun(25, func() { snapshot = filterMetricsByTime(data, cutoff) })
budget := float64(1)
if tc.retained == 0 {
budget = 0
}
t.Logf("retained=%d input=%d allocations=%.0f capacity=%d", len(snapshot), len(data), allocs, cap(snapshot))
if allocs > budget {
t.Errorf("snapshot allocated %.0f times, want at most %.0f", allocs, budget)
}
if cap(snapshot) != len(snapshot) {
t.Errorf("snapshot capacity=%d, want exactly its %d retained points", cap(snapshot), len(snapshot))
}
})
}
}
func TestHistoryReadPublicAllocationBudget(t *testing.T) {
history := NewMetricsHistory(1024, 4*time.Hour)
now := time.Now()
for i := 0; i < 512; i++ {
stamp := now.Add(time.Duration(i-512) * time.Second)
history.AddGuestMetric("dense", "cpu", float64(i), stamp)
history.AddGuestMetric("dense", "memoryused", float64(i), stamp)
history.AddNodeMetric("dense", "cpu", float64(i), stamp)
history.AddDiskMetric("dense", "smart_temp", float64(i), stamp)
history.AddStorageMetric("dense", "usage", float64(i), stamp)
history.AddStorageMetric("dense", "used", float64(i), stamp)
}
// These entries have the same map/empty-series shape but no sample arrays.
history.AddGuestMetric("empty", "unknown", 0, now)
history.AddStorageMetric("empty", "unknown", 0, now)
for name, read := range map[string]func() []MetricPoint{
"guest": func() []MetricPoint { return history.GetGuestMetrics("dense", "cpu", time.Hour) },
"node": func() []MetricPoint { return history.GetNodeMetrics("dense", "cpu", time.Hour) },
"disk": func() []MetricPoint { return history.GetDiskMetrics("dense", "smart_temp", time.Hour) },
} {
t.Run(name, func(t *testing.T) {
var snapshot []MetricPoint
allocs := testing.AllocsPerRun(25, func() { snapshot = read() })
t.Logf("points=%d allocations=%.0f", len(snapshot), allocs)
if len(snapshot) != 512 || allocs > 1 {
t.Fatalf("dense read: points=%d allocations=%.0f, want 512/at most 1", len(snapshot), allocs)
}
})
}
for name, read := range map[string]func(string, time.Duration) map[string][]MetricPoint{
"guest-all": history.GetAllGuestMetrics, "storage-all": history.GetAllStorageMetrics,
} {
t.Run(name, func(t *testing.T) {
var snapshot map[string][]MetricPoint
emptyAllocs := testing.AllocsPerRun(25, func() { snapshot = read("empty", time.Hour) })
denseAllocs := testing.AllocsPerRun(25, func() { snapshot = read("dense", time.Hour) })
count := 0
for _, points := range snapshot {
count += len(points)
}
t.Logf("points=%d empty allocations=%.0f dense allocations=%.0f", count, emptyAllocs, denseAllocs)
if count != 1024 || denseAllocs-emptyAllocs > 2 {
t.Fatalf("two populated series allocated %.0f extra times, want at most 2; points=%d", denseAllocs-emptyAllocs, count)
}
})
}
}
func TestHistoryReadSnapshotsEverySeries(t *testing.T) {
for scope, metrics := range map[string][]string{
"guest": {"cpu", "memory", "memoryused", "disk", "gpu", "gpu_memory", "gpu_temperature", "diskread", "diskwrite", "netin", "netout", "temperature"},
"node": {"cpu", "memory", "memoryused", "disk", "netin", "netout", "temperature"},
"disk": {"smart_temp", "disk", "diskread", "diskwrite"}, "storage": {"usage", "used", "total", "avail"},
} {
for _, metric := range metrics {
t.Run(scope+"/"+metric, func(t *testing.T) {
history := NewMetricsHistory(32, 4*time.Hour)
add, read := history.AddGuestMetric, history.GetGuestMetrics
switch scope {
case "node":
add, read = history.AddNodeMetric, history.GetNodeMetrics
case "disk":
add, read = history.AddDiskMetric, history.GetDiskMetrics
case "storage":
add = history.AddStorageMetric
read = func(id, metric string, duration time.Duration) []MetricPoint {
return history.GetAllStorageMetrics(id, duration)[metric]
}
}
now := time.Now()
add("resource", metric, 10, now.Add(-2*time.Hour))
add("resource", metric, 20, now.Add(-10*time.Minute))
add("resource", metric, 30, now.Add(-time.Minute))
want := []MetricPoint{{Timestamp: now.Add(-10 * time.Minute), Value: 20}, {Timestamp: now.Add(-time.Minute), Value: 30}}
got := read("resource", metric, time.Hour)
if !reflect.DeepEqual(got, want) {
t.Fatalf("snapshot = %+v, want %+v", got, want)
}
got[0] = MetricPoint{}
got = append(got, MetricPoint{Value: -999})
if !reflect.DeepEqual(read("resource", metric, time.Hour), want) {
t.Fatal("caller mutation changed History")
}
if scope == "guest" && !reflect.DeepEqual(history.GetAllGuestMetrics("resource", time.Hour)[metric], want) {
t.Fatal("all-series guest read diverged")
}
})
}
}
}
func TestHistoryReadChartAndWindowSnapshots(t *testing.T) {
previous := mock.IsMockEnabled()
mustSetMockEnabled(t, false)
defer mustSetMockEnabled(t, previous)
now := time.Now()
history := NewMetricsHistory(32, 4*time.Hour)
monitor := &Monitor{metricsHistory: history}
const id = "instance:node:105"
for i, offset := range []time.Duration{-2 * time.Hour, -10 * time.Minute, -time.Minute} {
monitor.recordGuestMetric("vm", id, float64(10+i), 60, float64(4096+i), -1, -1, -1, -1, -1, now.Add(offset))
}
want := []MetricPoint{{Timestamp: now.Add(-10 * time.Minute), Value: 11}, {Timestamp: now.Add(-time.Minute), Value: 12}}
chart := monitor.GetGuestMetricsForChart(id, "vm", id, time.Hour)
if !reflect.DeepEqual(chart["cpu"], want) {
t.Fatalf("chart = %+v, want %+v", chart["cpu"], want)
}
emptyJSON, err := json.Marshal(chart["disk"])
if err != nil || string(emptyJSON) != "[]" {
t.Fatalf("empty chart JSON = %s, err=%v", emptyJSON, err)
}
chart["cpu"][0] = MetricPoint{}
batch := monitor.GetGuestMetricsForChartBatch("vm", []GuestChartRequest{{InMemoryKey: id, SQLResourceID: id}}, time.Hour, "cpu")
if !reflect.DeepEqual(batch[id]["cpu"], want) {
t.Fatalf("batch = %+v, want %+v", batch, want)
}
batch[id]["cpu"][0] = MetricPoint{}
points, err := monitor.metricWindowPoints(alerts.MetricWindowRequest{ResourceID: id, ResourceType: "vm", Metric: "cpu", Start: now.Add(-time.Hour), End: now})
if err != nil || len(points) != 2 || points[0].Value != 11 || points[1].Value != 12 {
t.Fatalf("metric window = %+v, err=%v", points, err)
}
if !reflect.DeepEqual(monitor.GetGuestMetrics(id, time.Hour)["cpu"], want) {
t.Fatal("chart/window mutation reached poll History")
}
}
var historyReadBenchmarkSink []MetricPoint
func BenchmarkHistoryReadFilter(b *testing.B) {
for _, tc := range []struct {
name string
size, retained int
interleaved bool
}{
{"empty", 0, 0, false}, {"expired", 2880, 0, false},
{"one", 2880, 1, false}, {"hour", 2880, 60, false},
{"day", 2880, 1440, false}, {"all", 2880, 2880, false},
{"interleaved", 2880, 1440, true},
} {
data, cutoff := historyReadFixture(tc.size, tc.retained, tc.interleaved)
for _, variant := range []struct {
name string
filter func([]MetricPoint, time.Time) []MetricPoint
}{
{"legacy", legacyHistoryReadFilter}, {"current", filterMetricsByTime},
} {
b.Run(fmt.Sprintf("%s/%s", tc.name, variant.name), func(b *testing.B) {
if !reflect.DeepEqual(variant.filter(data, cutoff), legacyHistoryReadFilter(data, cutoff)) {
b.Fatal("benchmark changed samples")
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
historyReadBenchmarkSink = variant.filter(data, cutoff)
}
})
}
}
}