mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 21:47:20 +00:00
cf12c37e63
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
353 lines
9.5 KiB
Go
353 lines
9.5 KiB
Go
package mock
|
|
|
|
import (
|
|
"math"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/mockmodel"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
)
|
|
|
|
func normalizeMetricClass(resourceClass string) string {
|
|
normalized := strings.ToLower(strings.TrimSpace(resourceClass))
|
|
normalized = strings.ReplaceAll(normalized, "-", "")
|
|
normalized = strings.ReplaceAll(normalized, "_", "")
|
|
return normalized
|
|
}
|
|
|
|
func MetricSeed(resourceClass, resourceID, metric string) uint64 {
|
|
classKey := normalizeMetricClass(resourceClass)
|
|
idKey := strings.TrimSpace(resourceID)
|
|
metricKey := strings.ToLower(strings.TrimSpace(metric))
|
|
return mockStableHash64(classKey, idKey, metricKey)
|
|
}
|
|
|
|
func MetricBounds(resourceClass, metric string) (float64, float64) {
|
|
classKey := normalizeMetricClass(resourceClass)
|
|
metricKey := strings.ToLower(strings.TrimSpace(metric))
|
|
|
|
switch metricKey {
|
|
case "cpu":
|
|
switch classKey {
|
|
case "node":
|
|
return 5, 85
|
|
case "vm":
|
|
return 1, 85
|
|
case "container":
|
|
return 1, 75
|
|
case "agent":
|
|
return 4, 97
|
|
case "dockerhost":
|
|
return 3, 99
|
|
case "dockercontainer":
|
|
return 0, 190
|
|
case "pbs", "pbsdatastore":
|
|
return 6, 62
|
|
default:
|
|
return 0, 100
|
|
}
|
|
case "memory":
|
|
switch classKey {
|
|
case "node", "vm":
|
|
return 10, 85
|
|
case "container":
|
|
return 5, 85
|
|
case "agent", "dockerhost":
|
|
return 12, 96
|
|
case "dockercontainer":
|
|
return 1, 97
|
|
case "pbs", "pbsdatastore":
|
|
return 24, 78
|
|
default:
|
|
return 0, 100
|
|
}
|
|
case "disk":
|
|
switch classKey {
|
|
case "node":
|
|
return 5, 95
|
|
case "vm":
|
|
return 10, 90
|
|
case "container":
|
|
return 5, 85
|
|
case "agent":
|
|
return 5, 98
|
|
case "dockerhost":
|
|
return 8, 92
|
|
case "dockercontainer":
|
|
return 5, 72
|
|
default:
|
|
return 0, 100
|
|
}
|
|
case "usage":
|
|
switch classKey {
|
|
case "pbs", "pbsdatastore":
|
|
return 12, 78
|
|
case "storage", "pool", "dataset":
|
|
return 8, 88
|
|
default:
|
|
return 0, 100
|
|
}
|
|
case "diskread":
|
|
switch classKey {
|
|
case "container", "dockercontainer":
|
|
return 0, 30 * 1024 * 1024
|
|
case "vm":
|
|
return 0, 100 * 1024 * 1024
|
|
case "agent", "dockerhost":
|
|
return 0, 120 * 1024 * 1024
|
|
default:
|
|
return 0, 80 * 1024 * 1024
|
|
}
|
|
case "diskwrite":
|
|
switch classKey {
|
|
case "container", "dockercontainer":
|
|
return 0, 18 * 1024 * 1024
|
|
case "vm":
|
|
return 0, 60 * 1024 * 1024
|
|
case "agent", "dockerhost":
|
|
return 0, 90 * 1024 * 1024
|
|
default:
|
|
return 0, 48 * 1024 * 1024
|
|
}
|
|
case "netin":
|
|
switch classKey {
|
|
case "container", "dockercontainer":
|
|
return 0, 40 * 1024 * 1024
|
|
case "vm":
|
|
return 0, 80 * 1024 * 1024
|
|
case "agent", "dockerhost":
|
|
return 0, 250 * 1024 * 1024
|
|
default:
|
|
return 0, 64 * 1024 * 1024
|
|
}
|
|
case "netout":
|
|
switch classKey {
|
|
case "container", "dockercontainer":
|
|
return 0, 24 * 1024 * 1024
|
|
case "vm":
|
|
return 0, 48 * 1024 * 1024
|
|
case "agent", "dockerhost":
|
|
return 0, 200 * 1024 * 1024
|
|
default:
|
|
return 0, 48 * 1024 * 1024
|
|
}
|
|
case "smart_temp", "temperature", "temp":
|
|
return 25, 95
|
|
default:
|
|
return 0, 100
|
|
}
|
|
}
|
|
|
|
func MetricBoundsForResource(resourceClass, resourceID, metric string) (float64, float64) {
|
|
return metricBoundsForRole(resourceClass, metric, MetricRole(resourceClass, resourceID))
|
|
}
|
|
|
|
func metricBoundsForRole(resourceClass, metric, role string) (float64, float64) {
|
|
min, max := MetricBounds(resourceClass, metric)
|
|
metricKey := strings.ToLower(strings.TrimSpace(metric))
|
|
|
|
switch metricKey {
|
|
case "cpu":
|
|
switch role {
|
|
case metricRoleDatabase:
|
|
min, max = maxFloat(min, 8), max*1.05
|
|
case metricRoleCI, metricRoleBatch:
|
|
max *= 1.18
|
|
case metricRoleBackup, metricRoleStorage:
|
|
max *= 0.9
|
|
case metricRoleWeb, metricRoleAPI, metricRoleIngress, metricRoleMedia:
|
|
max *= 1.1
|
|
}
|
|
case "memory":
|
|
switch role {
|
|
case metricRoleDatabase, metricRoleCache:
|
|
min, max = maxFloat(min, 24), max*1.08
|
|
case metricRoleMonitoring:
|
|
min, max = maxFloat(min, 18), max*1.03
|
|
case metricRoleBackup:
|
|
max *= 0.85
|
|
case metricRoleWeb, metricRoleIngress:
|
|
max *= 0.92
|
|
}
|
|
case "disk", "usage":
|
|
switch role {
|
|
case metricRoleBackup, metricRoleStorage:
|
|
min, max = maxFloat(min, 12), max*1.05
|
|
case metricRoleCache, metricRoleIngress:
|
|
max *= 0.82
|
|
}
|
|
case "diskread", "diskwrite":
|
|
switch role {
|
|
case metricRoleBackup, metricRoleDatabase, metricRoleBatch, metricRoleStorage:
|
|
max *= 1.4
|
|
case metricRoleWeb, metricRoleIngress:
|
|
max *= 0.75
|
|
case metricRoleCache:
|
|
max *= 0.65
|
|
}
|
|
case "netin", "netout":
|
|
switch role {
|
|
case metricRoleWeb, metricRoleAPI, metricRoleIngress, metricRoleMedia, metricRoleSecurity:
|
|
max *= 1.35
|
|
case metricRoleBackup:
|
|
max *= 1.2
|
|
case metricRoleDatabase, metricRoleStorage:
|
|
max *= 0.78
|
|
}
|
|
case "smart_temp", "temperature", "temp":
|
|
switch role {
|
|
case metricRoleMedia, metricRoleCI, metricRoleDatabase:
|
|
max += 4
|
|
case metricRoleBackup:
|
|
max -= 2
|
|
}
|
|
}
|
|
|
|
if max <= min {
|
|
max = min + 1
|
|
}
|
|
return min, max
|
|
}
|
|
|
|
// MetricSampler binds metric persona resolution to one immutable fixture
|
|
// graph snapshot. Historical seeding and its live continuation must not depend
|
|
// on the process-global persona registry changing while another monitor or
|
|
// fixture update is initializing.
|
|
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)}
|
|
}
|
|
|
|
func (s MetricSampler) role(resourceClass, resourceID string) string {
|
|
if role := strings.TrimSpace(s.roles[metricRoleRegistryKey(resourceClass, resourceID)]); role != "" {
|
|
return role
|
|
}
|
|
return inferMetricRole(resourceClass, resourceID)
|
|
}
|
|
|
|
func (s MetricSampler) SampleMetric(resourceClass, resourceID, metric string, at time.Time) float64 {
|
|
role := s.role(resourceClass, resourceID)
|
|
min, max := metricBoundsForRole(resourceClass, metric, role)
|
|
seed := MetricSeed(resourceClass, resourceID, metric)
|
|
return mockmodel.ValueAtMetricWithRole(seed, min, max, metric, metricSpeed(metric), role, at)
|
|
}
|
|
|
|
func (s MetricSampler) SampleMetricSeries(resourceClass, resourceID, metric string, timestamps []time.Time) []float64 {
|
|
if len(timestamps) == 0 {
|
|
return nil
|
|
}
|
|
|
|
role := s.role(resourceClass, resourceID)
|
|
min, max := metricBoundsForRole(resourceClass, metric, role)
|
|
seed := MetricSeed(resourceClass, resourceID, metric)
|
|
speed := metricSpeed(metric)
|
|
|
|
values := make([]float64, len(timestamps))
|
|
for i, at := range timestamps {
|
|
values[i] = mockmodel.ValueAtMetricWithRole(seed, min, max, metric, speed, role, at)
|
|
}
|
|
return values
|
|
}
|
|
|
|
func metricSpeed(metric string) float64 {
|
|
switch strings.ToLower(strings.TrimSpace(metric)) {
|
|
case "memory":
|
|
return 0.5
|
|
case "disk", "usage", "storage", "capacity", "smart_temp", "temperature", "temp":
|
|
return 0.12
|
|
default:
|
|
return 1.0
|
|
}
|
|
}
|
|
|
|
func SampleMetric(resourceClass, resourceID, metric string, at time.Time) float64 {
|
|
min, max := MetricBoundsForResource(resourceClass, resourceID, metric)
|
|
return sampleNaturalMetric(resourceClass, resourceID, metric, min, max, metricSpeed(metric), at)
|
|
}
|
|
|
|
// SampleMetricSeries projects one canonical metric across a timestamp
|
|
// sequence. The resource identity, bounds, seed, speed, and role are invariant
|
|
// for the entire series, so resolve them once instead of repeating string
|
|
// normalization, hashing, and registry lookups for every historical point.
|
|
func SampleMetricSeries(resourceClass, resourceID, metric string, timestamps []time.Time) []float64 {
|
|
if len(timestamps) == 0 {
|
|
return nil
|
|
}
|
|
|
|
min, max := MetricBoundsForResource(resourceClass, resourceID, metric)
|
|
seed := MetricSeed(resourceClass, resourceID, metric)
|
|
speed := metricSpeed(metric)
|
|
role := MetricRole(resourceClass, resourceID)
|
|
|
|
values := make([]float64, len(timestamps))
|
|
for i, at := range timestamps {
|
|
values[i] = mockmodel.ValueAtMetricWithRole(seed, min, max, metric, speed, role, at)
|
|
}
|
|
return values
|
|
}
|
|
|
|
func SampleMetricInt(resourceClass, resourceID, metric string, at time.Time) int64 {
|
|
return int64(math.Round(SampleMetric(resourceClass, resourceID, metric, at)))
|
|
}
|
|
|
|
func ApplyStorageUsage(storage *models.Storage, usage float64) {
|
|
if storage == nil || storage.Total <= 0 {
|
|
return
|
|
}
|
|
|
|
storage.Usage = clampFloat(usage, 0, 100)
|
|
storage.Used = int64(float64(storage.Total) * (storage.Usage / 100.0))
|
|
if storage.Used < 0 {
|
|
storage.Used = 0
|
|
}
|
|
if storage.Used > storage.Total {
|
|
storage.Used = storage.Total
|
|
}
|
|
storage.Free = storage.Total - storage.Used
|
|
if storage.Free < 0 {
|
|
storage.Free = 0
|
|
}
|
|
}
|
|
|
|
func maxFloat(a, b float64) float64 {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|