using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Core.Stability; /// /// Tier-agnostic memory-footprint tracker. Captures the post-initialize baseline /// from the first samples after IDriver.InitializeAsync, then classifies each /// subsequent sample against a hybrid soft/hard threshold per /// docs/v2/plan.md decision #146 — soft = max(multiplier × baseline, baseline + floor), /// hard = 2 × soft. /// /// /// Per decision #145, this tracker never kills a process. Soft and hard breaches /// log + surface to the Admin UI via DriverInstanceResilienceStatus. The matching /// process-level recycle protection lives in a separate MemoryRecycle that activates /// for Tier C drivers only (where the driver runs out-of-process behind a supervisor that /// can safely restart it without tearing down the OPC UA session or co-hosted in-proc /// drivers). /// /// Baseline capture: the tracker starts in for /// (default 5 min). During that window samples are collected; /// the baseline is computed as the median once the window elapses. Before that point every /// classification returns . /// public sealed class MemoryTracking { private readonly DriverTier _tier; private readonly TimeSpan _baselineWindow; private readonly List _warmupSamples = []; private long _baselineBytes; private TrackingPhase _phase = TrackingPhase.WarmingUp; private DateTime? _warmupStartUtc; /// Tier-default multiplier/floor constants per decision #146. public static (int Multiplier, long FloorBytes) GetTierConstants(DriverTier tier) => tier switch { DriverTier.A => (Multiplier: 3, FloorBytes: 50L * 1024 * 1024), DriverTier.B => (Multiplier: 3, FloorBytes: 100L * 1024 * 1024), DriverTier.C => (Multiplier: 2, FloorBytes: 500L * 1024 * 1024), _ => throw new ArgumentOutOfRangeException(nameof(tier), tier, $"No memory-tracking constants defined for tier {tier}."), }; /// Window over which post-init samples are collected to compute the baseline. public TimeSpan BaselineWindow => _baselineWindow; /// Current phase: or . public TrackingPhase Phase => _phase; /// Captured baseline; 0 until warmup completes. public long BaselineBytes => _baselineBytes; /// Effective soft threshold (zero while warming up). public long SoftThresholdBytes => _baselineBytes == 0 ? 0 : ComputeSoft(_tier, _baselineBytes); /// Effective hard threshold = 2 × soft (zero while warming up). public long HardThresholdBytes => _baselineBytes == 0 ? 0 : ComputeSoft(_tier, _baselineBytes) * 2; public MemoryTracking(DriverTier tier, TimeSpan? baselineWindow = null) { _tier = tier; _baselineWindow = baselineWindow ?? TimeSpan.FromMinutes(5); } /// /// Submit a memory-footprint sample. Returns the action the caller should surface. /// During warmup, always returns and accumulates /// samples; once the window elapses the first steady-phase sample triggers baseline capture /// (median of warmup samples). /// public MemoryTrackingAction Sample(long footprintBytes, DateTime utcNow) { if (_phase == TrackingPhase.WarmingUp) { _warmupStartUtc ??= utcNow; _warmupSamples.Add(footprintBytes); if (utcNow - _warmupStartUtc.Value >= _baselineWindow && _warmupSamples.Count > 0) { _baselineBytes = ComputeMedian(_warmupSamples); _phase = TrackingPhase.Steady; } else { return MemoryTrackingAction.Warming; } } if (footprintBytes >= HardThresholdBytes) return MemoryTrackingAction.HardBreach; if (footprintBytes >= SoftThresholdBytes) return MemoryTrackingAction.SoftBreach; return MemoryTrackingAction.None; } private static long ComputeSoft(DriverTier tier, long baseline) { var (multiplier, floor) = GetTierConstants(tier); return Math.Max(multiplier * baseline, baseline + floor); } private static long ComputeMedian(List samples) { var sorted = samples.Order().ToArray(); var mid = sorted.Length / 2; return sorted.Length % 2 == 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2; } } /// Phase of a lifecycle. public enum TrackingPhase { /// Collecting post-init samples; baseline not yet computed. WarmingUp, /// Baseline captured; every sample classified against soft/hard thresholds. Steady, } /// Classification the tracker returns per sample. public enum MemoryTrackingAction { /// Baseline not yet captured; sample collected, no threshold check. Warming, /// Below soft threshold. None, /// Between soft and hard thresholds — log + surface, no action. SoftBreach, /// /// ≥ hard threshold. Log + surface + (Tier C only, via MemoryRecycle) request /// process recycle via the driver supervisor. Tier A/B breach never invokes any /// kill path per decisions #145 and #74. /// HardBreach, }