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
This commit is contained in:
pulse-triage[bot] 2026-10-01 07:33:33 +01:00
parent 3fb4b0456a
commit cf12c37e63
3 changed files with 150 additions and 1 deletions

View file

@ -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)}
}

View file

@ -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)
}
})
}

View file

@ -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,