From cf12c37e6302883e66039078d6e504b1014b26cc Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 07:33:33 +0100 Subject: [PATCH] Reuse graph-bound metric samplers for demo chart series The full API race run timed out while cloning the entire mock estate and rebuilding every persona for each chart series. Cache one immutable sampler against the canonical graph data version under its publication lock. Preserve values, current revisions, disabled-mode fallback and isolation from private/global persona updates. Pin zero-allocation warm acquisition, reference equivalence and concurrent snapshot binding; compare both paths with the same thousand-VM sample. Contract-Neutral: Avoid repeated canonical graph cloning and persona reconstruction without changing metric formulas, identity, graph freshness, retention, endpoints or permissions. Change-source: pulse-maintainer --- internal/mock/metric_runtime.go | 31 +++++ internal/mock/metric_runtime_test.go | 118 ++++++++++++++++++++ internal/monitoring/mock_metrics_history.go | 2 +- 3 files changed, 150 insertions(+), 1 deletion(-) diff --git a/internal/mock/metric_runtime.go b/internal/mock/metric_runtime.go index 0cd58f5391..a9583d404c 100644 --- a/internal/mock/metric_runtime.go +++ b/internal/mock/metric_runtime.go @@ -3,6 +3,7 @@ package mock import ( "math" "strings" + "sync" "time" "github.com/rcourtman/pulse-go-rewrite/internal/mockmodel" @@ -218,6 +219,36 @@ type MetricSampler struct { roles map[string]string } +// runtimeMetricSampler caches only derived, immutable roles. The graph data +// version changes on both metric ticks and structural updates, so new series +// observe the current canonical fixture while existing samplers remain bound +// to their captured graph. Private graph/global persona updates cannot replace +// this canonical snapshot. +var runtimeMetricSampler struct { + sync.Mutex + version uint64 + sampler MetricSampler +} + +// CurrentMetricSampler avoids cloning the entire demo estate and resolving +// every resource's persona for every chart series. Graph publication and the +// version read share dataMu; the cached role map is never mutated after return. +func CurrentMetricSampler() MetricSampler { + dataMu.RLock() + defer dataMu.RUnlock() + if !enabled.Load() { + return MetricSampler{} + } + runtimeMetricSampler.Lock() + defer runtimeMetricSampler.Unlock() + version := fixtureDataVersion.Load() + if runtimeMetricSampler.sampler.roles == nil || runtimeMetricSampler.version != version { + runtimeMetricSampler.sampler = NewMetricSampler(mockGraph) + runtimeMetricSampler.version = version + } + return runtimeMetricSampler.sampler +} + func NewMetricSampler(graph FixtureGraph) MetricSampler { return MetricSampler{roles: buildMetricRoleRegistry(graph)} } diff --git a/internal/mock/metric_runtime_test.go b/internal/mock/metric_runtime_test.go index 775d2ec8b8..276305dc66 100644 --- a/internal/mock/metric_runtime_test.go +++ b/internal/mock/metric_runtime_test.go @@ -1,7 +1,10 @@ package mock import ( + "fmt" "math" + "reflect" + "sync" "testing" "time" @@ -89,3 +92,118 @@ func TestMetricSamplerRemainsBoundToFixtureGraph(t *testing.T) { } } } + +// Keep the graph and its version coherent, just like runtime publication. Do +// not restore an old version: existing readers may still hold its sampler. +func installMetricSamplerGraphForTest(tb testing.TB, graph FixtureGraph) { + tb.Helper() + dataMu.Lock() + previousGraph, previousEnabled := mockGraph, enabled.Load() + mockGraph = graph + enabled.Store(true) + fixtureDataVersion.Add(1) + dataMu.Unlock() + tb.Cleanup(func() { + dataMu.Lock() + mockGraph = previousGraph + enabled.Store(previousEnabled) + fixtureDataVersion.Add(1) + dataMu.Unlock() + }) +} + +func TestCurrentMetricSamplerTracksGraphVersionWithoutGlobalPersonaLeak(t *testing.T) { + graph := FixtureGraph{State: models.StateSnapshot{Containers: []models.Container{{ID: "neutral-155", Name: "backup-orchestrator"}}}} + installMetricSamplerGraphForTest(t, graph) + at := time.Date(2026, time.July, 19, 12, 0, 0, 0, time.UTC) + timestamps := []time.Time{at.Add(-time.Minute), at, at.Add(time.Minute)} + initial := CurrentMetricSampler() + want := NewMetricSampler(CurrentFixtureGraph()).SampleMetricSeries("container", "neutral-155", "memory", timestamps) + if !reflect.DeepEqual(initial.SampleMetricSeries("container", "neutral-155", "memory", timestamps), want) { + t.Fatal("cached sampler differs from graph-bound reference") + } + previousRegistry := currentMetricRoleRegistry() + t.Cleanup(func() { setMetricRoleRegistry(previousRegistry) }) + setMetricRoleRegistry(map[string]string{metricRoleRegistryKey("container", "neutral-155"): metricRoleDatabase}) + if !reflect.DeepEqual(CurrentMetricSampler().SampleMetricSeries("container", "neutral-155", "memory", timestamps), want) { + t.Fatal("private/global persona update replaced canonical graph sampling") + } + if allocs := testing.AllocsPerRun(100, func() { _ = CurrentMetricSampler() }); allocs != 0 { + t.Fatalf("warm sampler acquisition allocates %v times; graph must not be cloned per series", allocs) + } + dataMu.Lock() + mockGraph.State.Containers[0].Name = "database-primary" + fixtureDataVersion.Add(1) + dataMu.Unlock() + updated := CurrentMetricSampler().SampleMetricSeries("container", "neutral-155", "memory", timestamps) + reference := NewMetricSampler(CurrentFixtureGraph()).SampleMetricSeries("container", "neutral-155", "memory", timestamps) + if !reflect.DeepEqual(updated, reference) || reflect.DeepEqual(updated, want) { + t.Fatal("new graph version did not refresh its immutable persona snapshot") + } + if !reflect.DeepEqual(initial.SampleMetricSeries("container", "neutral-155", "memory", timestamps), want) { + t.Fatal("publishing a new sampler mutated an already-returned sampler") + } + enabled.Store(false) + if got := CurrentMetricSampler(); got.roles != nil { + t.Fatal("disabled mock mode retained canonical roles") + } +} + +func TestCurrentMetricSamplerConcurrentSeriesRemainSnapshotBound(t *testing.T) { + graph := FixtureGraph{State: models.StateSnapshot{Containers: []models.Container{{ID: "neutral-155", Name: "backup-orchestrator"}}}} + backup := NewMetricSampler(graph) + graph.State.Containers[0].Name = "database-primary" + database := NewMetricSampler(graph) + installMetricSamplerGraphForTest(t, graph) + at := time.Date(2026, time.July, 19, 12, 0, 0, 0, time.UTC) + times := []time.Time{at.Add(-time.Minute), at, at.Add(time.Minute)} + first, second := backup.SampleMetricSeries("container", "neutral-155", "memory", times), database.SampleMetricSeries("container", "neutral-155", "memory", times) + var workers sync.WaitGroup + for i := 0; i < 8; i++ { + workers.Add(1) + go func() { + defer workers.Done() + for j := 0; j < 50; j++ { + got := CurrentMetricSampler().SampleMetricSeries("container", "neutral-155", "memory", times) + if !reflect.DeepEqual(got, first) && !reflect.DeepEqual(got, second) { + t.Error("one series mixed two graph revisions") + return + } + } + }() + } + for i := 0; i < 50; i++ { + name := "backup-orchestrator" + if i%2 == 0 { + name = "database-primary" + } + dataMu.Lock() + mockGraph.State.Containers[0].Name = name + fixtureDataVersion.Add(1) + dataMu.Unlock() + } + workers.Wait() +} + +func BenchmarkCanonicalMetricSeriesSamplerReuse(b *testing.B) { + graph := FixtureGraph{State: models.StateSnapshot{VMs: make([]models.VM, 1000)}} + for i := range graph.State.VMs { + graph.State.VMs[i] = models.VM{ID: fmt.Sprintf("neutral-%d", i), Name: "database-primary"} + } + installMetricSamplerGraphForTest(b, graph) + at := time.Date(2026, time.July, 19, 12, 0, 0, 0, time.UTC) + times := []time.Time{at.Add(-time.Minute), at, at.Add(time.Minute)} + _ = CurrentMetricSampler() + b.Run("legacy-cloned-graph-per-series", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = NewMetricSampler(CurrentFixtureGraph()).SampleMetricSeries("vm", "neutral-1", "memory", times) + } + }) + b.Run("current-immutable-sampler", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = CurrentMetricSampler().SampleMetricSeries("vm", "neutral-1", "memory", times) + } + }) +} diff --git a/internal/monitoring/mock_metrics_history.go b/internal/monitoring/mock_metrics_history.go index 6c3b360426..6b10b55612 100644 --- a/internal/monitoring/mock_metrics_history.go +++ b/internal/monitoring/mock_metrics_history.go @@ -525,7 +525,7 @@ func normalizeMockMetricTimestamp(at time.Time, interval time.Duration) time.Tim func canonicalMetricSeries(resourceType, resourceID, metric string, timestamps []time.Time) []float64 { return canonicalMetricSeriesWithSampler( - mock.NewMetricSampler(mock.CurrentFixtureGraph()), + mock.CurrentMetricSampler(), resourceType, resourceID, metric,