Files
pulse/internal/monitoring/unified_structure_view.go
T
rcourtman 8823440f33 Stop mock-mode callers that read identity rebuilding the estate view
In mock mode the unified view is keyed on the fixture data version, which
advances on every metric tick, and building it for the default
1,776-resource estate costs about half a second of CPU (many seconds on a
starved process). #2778 moved the metrics-target lookup onto the fixture
structure revision; the other callers that read only what the estate
lists and how it connects still rebuilt the whole view after each tick.
At startup they made 42 view requests, all misses, and with no client
connected the monitor tick alone rebuilt the estate about every nine
seconds.

Callers that read identity and topology now share the view built for the
current structure revision and operator links (currentStructureUnifiedStateView,
GetUnifiedStructureReadState): the Docker-alert prune, the workload chart
prewarm, the demo Patrol resolver, and the host-agent evaluation, which
also lists nodes only when an offline host names a linked node. The
connection-degraded alert feed keeps only platform rows, so it no longer
reads the hosts, their desired-config fingerprints or the PBS instances
that only feed agent rows and a host alias its snapshot drops.

The mock storage chart is not one of them: a linked pool takes its
capacity from whichever source was seen last, which a tick can move
without a structure change. It keeps the data-version view, read once per
request instead of once per uncached pool. The per-node polling lookups
of a monitor that keeps real polling in mock mode (linkedHostForNode,
getHostAgentTemperatureForNode, carriedTemperatureOutlivesAgentLease)
read a host's status, disks and sensors, which a tick moves, so they stay
on the data-version view.

On pulse-dev (one pinned core, host load 30 to 110, probe builds): a
start built the estate view twice before the listener answered with the
wiring change alone and once with this; with no client connected the
process built the estate 33 times in 299 s and used 12.3 s of CPU over a
180 s window, against twice and 3.3 s with this change.
2026-10-10 12:37:42 +01:00

79 lines
3.0 KiB
Go

package monitoring
import (
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
)
// cachedMockStructureView returns the mock view the monitor has cached when it
// was built for the fixture's current structure revision and operator link
// list, however many metric ticks ago. The second result is false when there is
// no such view, and the first is then not for use.
func (m *Monitor) cachedMockStructureView() (monitorUnifiedStateView, bool) {
structure := mock.FixtureStructureRevision()
links := m.resourceStoreManualLinks()
m.mockUnifiedViewMu.Lock()
defer m.mockUnifiedViewMu.Unlock()
reusable := m.mockUnifiedViewValid &&
m.mockUnifiedViewStructure == structure &&
sameManualLinks(m.mockUnifiedViewLinks, links)
return m.mockUnifiedView, reusable
}
// currentStructureUnifiedStateView is currentUnifiedStateView for a caller that
// reads what the estate lists and how it connects: IDs, names, types, source
// mappings, parents, operator links and the metrics targets those yield. In
// mock mode that answers from the cached view for as long as the fixture
// structure and the link list are unchanged. The view is keyed on the data
// version otherwise, which advances on every metric tick, so a caller that
// needs only structure but asked for the data-version view built the whole
// estate once per tick: about half a second of CPU on an idle core, and on a
// starved process a build longer than the tick, so every call missed. A
// caller must not read what a tick moves (status, metric values, timestamps,
// sensors); the view it gets can be any number of ticks old. Outside mock mode
// this is currentUnifiedStateView.
func (m *Monitor) currentStructureUnifiedStateView() monitorUnifiedStateView {
if m == nil {
return monitorUnifiedStateView{}
}
if mock.IsMockEnabled() {
if view, ok := m.cachedMockStructureView(); ok {
return view
}
}
return m.currentUnifiedStateView()
}
// GetUnifiedStructureReadState is GetUnifiedReadStateOrSnapshot for a caller
// that reads identity and topology only (see currentStructureUnifiedStateView):
// in mock mode it can be several metric ticks old.
func (m *Monitor) GetUnifiedStructureReadState() unifiedresources.ReadState {
if m == nil {
return nil
}
if !mock.IsMockEnabled() {
return m.GetUnifiedReadStateOrSnapshot()
}
return m.currentStructureUnifiedStateView().readState
}
// currentModeStructureReadState is currentModeReadState for a caller that reads
// identity and topology only.
func (m *Monitor) currentModeStructureReadState() unifiedresources.ReadState {
if m == nil {
return nil
}
if mock.IsMockEnabled() {
return m.currentStructureUnifiedStateView().readState
}
return m.currentModeReadState()
}
// structureNodes is NodesSnapshot for a caller that reads the nodes' identity
// and links only (ID, instance, linked agent).
func (m *Monitor) structureNodes() []models.Node {
return nodesSnapshotFromReadState(m.GetUnifiedStructureReadState())
}