diff --git a/src/AcDream.Headless/Diagnostics/HeadlessDiagnosticWriter.cs b/src/AcDream.Headless/Diagnostics/HeadlessDiagnosticWriter.cs index 0b88c304..8290a2d2 100644 --- a/src/AcDream.Headless/Diagnostics/HeadlessDiagnosticWriter.cs +++ b/src/AcDream.Headless/Diagnostics/HeadlessDiagnosticWriter.cs @@ -79,6 +79,8 @@ internal sealed class HeadlessDiagnosticWriter { ArgumentException.ThrowIfNullOrWhiteSpace(state); HeadlessProcessContentSnapshot? content = snapshot.Content; + HeadlessProcessResourceEnvelopeSnapshot envelope = + snapshot.Envelope; Write(new { kind = "resources", @@ -111,6 +113,54 @@ internal sealed class HeadlessDiagnosticWriter content.Value.MappedVirtualBytes, content.Value.IsConverged, }, + resourceEnvelope = new + { + envelope.Ceilings.Profile, + envelope.HasRateSample, + envelope.WithinCeilings, + formula = new + { + envelope.Ceilings.SharedPrivateBytes, + envelope.Ceilings.PrivateBytesPerSession, + envelope.Ceilings.SharedWorkingSetBytes, + envelope.Ceilings.WorkingSetBytesPerSession, + envelope.Ceilings.SharedManagedLiveBytes, + envelope.Ceilings.ManagedLiveBytesPerSession, + envelope.Ceilings.SharedManagedHeapBytes, + envelope.Ceilings.ManagedHeapBytesPerSession, + envelope.Ceilings.SharedCpuCorePercent, + envelope.Ceilings.CpuCorePercentPerSession, + envelope.Ceilings.SharedHandleCount, + envelope.Ceilings.HandleCountPerSession, + envelope.Ceilings.SharedDescriptorCount, + envelope.Ceilings.DescriptorCountPerSession, + envelope.Ceilings.SharedSocketCount, + envelope.Ceilings.SocketCountPerSession, + }, + limits = new + { + envelope.MaximumPrivateBytes, + envelope.MaximumWorkingSetBytes, + envelope.MaximumManagedLiveBytes, + envelope.MaximumManagedHeapBytes, + envelope.MaximumCpuCorePercent, + envelope.MaximumThreadCount, + envelope.MaximumHandleCount, + envelope.MaximumDescriptorCount, + envelope.MaximumSocketCount, + envelope.MaximumWaitsPerSecond, + envelope.MaximumCatchUpsPerSecond, + envelope.MaximumMeanLatenessMilliseconds, + envelope.MaximumLatenessMilliseconds, + }, + observedRates = new + { + envelope.WaitsPerSecond, + envelope.TurnsPerSecond, + envelope.CatchUpsPerSecond, + }, + envelope.Violations, + }, }); } diff --git a/src/AcDream.Headless/Diagnostics/HeadlessProcessResourceEnvelope.cs b/src/AcDream.Headless/Diagnostics/HeadlessProcessResourceEnvelope.cs new file mode 100644 index 00000000..6f5442a1 --- /dev/null +++ b/src/AcDream.Headless/Diagnostics/HeadlessProcessResourceEnvelope.cs @@ -0,0 +1,396 @@ +using AcDream.Headless.Hosting; + +namespace AcDream.Headless.Diagnostics; + +internal readonly record struct HeadlessProcessResourceCeilings( + string Profile, + long SharedPrivateBytes, + long PrivateBytesPerSession, + long SharedWorkingSetBytes, + long WorkingSetBytesPerSession, + long SharedManagedLiveBytes, + long ManagedLiveBytesPerSession, + long SharedManagedHeapBytes, + long ManagedHeapBytesPerSession, + double SharedCpuCorePercent, + double CpuCorePercentPerSession, + int MaximumThreadCount, + int SharedHandleCount, + int HandleCountPerSession, + int SharedDescriptorCount, + int DescriptorCountPerSession, + int SharedSocketCount, + int SocketCountPerSession, + double MaximumWaitsPerSecond, + double MaximumCatchUpsPerSecond, + double MaximumMeanLatenessMilliseconds, + double MaximumLatenessMilliseconds) +{ + internal const long MiB = 1024L * 1024L; + + /// + /// Conservative K4 Linux envelope derived from the fixed-binary native + /// two-session Rynthid baseline and the automated 1/5/10/30-root gates. + /// It is diagnostic-only: violations are observable but never alter + /// simulation, network, or teardown behavior. + /// + internal static HeadlessProcessResourceCeilings K4Linux { get; } = + new( + Profile: "k4-linux-30-session", + SharedPrivateBytes: 224L * MiB, + PrivateBytesPerSession: 64L * MiB, + SharedWorkingSetBytes: 224L * MiB, + WorkingSetBytesPerSession: 64L * MiB, + SharedManagedLiveBytes: 64L * MiB, + ManagedLiveBytesPerSession: 48L * MiB, + SharedManagedHeapBytes: 64L * MiB, + ManagedHeapBytesPerSession: 48L * MiB, + SharedCpuCorePercent: 2d, + CpuCorePercentPerSession: 3d, + MaximumThreadCount: 48, + SharedHandleCount: 128, + HandleCountPerSession: 3, + SharedDescriptorCount: 128, + DescriptorCountPerSession: 3, + SharedSocketCount: 1, + SocketCountPerSession: 2, + MaximumWaitsPerSecond: 250d, + MaximumCatchUpsPerSecond: 1d, + MaximumMeanLatenessMilliseconds: 10d, + MaximumLatenessMilliseconds: 750d); + + internal long MaximumPrivateBytes(int sessionCount) => + Scale(SharedPrivateBytes, PrivateBytesPerSession, sessionCount); + + internal long MaximumWorkingSetBytes(int sessionCount) => + Scale(SharedWorkingSetBytes, WorkingSetBytesPerSession, sessionCount); + + internal long MaximumManagedLiveBytes(int sessionCount) => + Scale( + SharedManagedLiveBytes, + ManagedLiveBytesPerSession, + sessionCount); + + internal long MaximumManagedHeapBytes(int sessionCount) => + Scale( + SharedManagedHeapBytes, + ManagedHeapBytesPerSession, + sessionCount); + + internal double MaximumCpuCorePercent(int sessionCount) => + SharedCpuCorePercent + + CpuCorePercentPerSession * Math.Max(0, sessionCount); + + internal int MaximumHandleCount(int sessionCount) => + Scale(SharedHandleCount, HandleCountPerSession, sessionCount); + + internal int MaximumDescriptorCount(int sessionCount) => + Scale( + SharedDescriptorCount, + DescriptorCountPerSession, + sessionCount); + + internal int MaximumSocketCount(int sessionCount) => + Scale(SharedSocketCount, SocketCountPerSession, sessionCount); + + private static long Scale( + long shared, + long perSession, + int sessionCount) => + checked(shared + perSession * Math.Max(0, sessionCount)); + + private static int Scale( + int shared, + int perSession, + int sessionCount) => + checked(shared + perSession * Math.Max(0, sessionCount)); +} + +internal readonly record struct HeadlessProcessResourceEnvelopeSnapshot( + HeadlessProcessResourceCeilings Ceilings, + bool HasRateSample, + bool WithinCeilings, + long MaximumPrivateBytes, + long MaximumWorkingSetBytes, + long MaximumManagedLiveBytes, + long MaximumManagedHeapBytes, + double MaximumCpuCorePercent, + int MaximumThreadCount, + int MaximumHandleCount, + int MaximumDescriptorCount, + int MaximumSocketCount, + double MaximumWaitsPerSecond, + double MaximumCatchUpsPerSecond, + double MaximumMeanLatenessMilliseconds, + double MaximumLatenessMilliseconds, + double WaitsPerSecond, + double TurnsPerSecond, + double CatchUpsPerSecond, + string[] Violations); + +/// +/// Stateful low-frequency evaluator for the committed K4 resource envelope. +/// It owns only prior cumulative counters needed to calculate interval rates. +/// +internal sealed class HeadlessProcessResourceEnvelopeEvaluator +{ + private readonly HeadlessProcessResourceCeilings _ceilings; + private long _previousWaitCount; + private long _previousTurnCount; + private long _previousCatchUpCount; + private bool _hasPreviousCounters; + + internal HeadlessProcessResourceEnvelopeEvaluator( + HeadlessProcessResourceCeilings? ceilings = null) + { + _ceilings = ceilings + ?? HeadlessProcessResourceCeilings.K4Linux; + } + + internal HeadlessProcessResourceEnvelopeSnapshot Evaluate( + string state, + in HeadlessProcessUsageSnapshot process, + in HeadlessManagedUsageSnapshot managed, + in HeadlessSessionUsageSnapshot sessions, + in HeadlessSchedulerSnapshot scheduler, + HeadlessProcessContentSnapshot? content) + { + ArgumentException.ThrowIfNullOrWhiteSpace(state); + int sessionCount = sessions.ConfiguredCount; + long maximumPrivateBytes = + _ceilings.MaximumPrivateBytes(sessionCount); + long maximumWorkingSetBytes = + _ceilings.MaximumWorkingSetBytes(sessionCount); + long maximumManagedLiveBytes = + _ceilings.MaximumManagedLiveBytes(sessionCount); + long maximumManagedHeapBytes = + _ceilings.MaximumManagedHeapBytes(sessionCount); + double maximumCpuCorePercent = + _ceilings.MaximumCpuCorePercent(sessionCount); + int maximumHandleCount = + _ceilings.MaximumHandleCount(sessionCount); + int maximumDescriptorCount = + _ceilings.MaximumDescriptorCount(sessionCount); + int maximumSocketCount = + _ceilings.MaximumSocketCount(sessionCount); + + bool hasRateSample = + _hasPreviousCounters + && process.SampleIntervalMilliseconds > 0d; + double intervalSeconds = + process.SampleIntervalMilliseconds / 1000d; + double waitsPerSecond = hasRateSample + ? Delta(scheduler.WaitCount, _previousWaitCount) + / intervalSeconds + : 0d; + double turnsPerSecond = hasRateSample + ? Delta(scheduler.TurnCount, _previousTurnCount) + / intervalSeconds + : 0d; + double catchUpsPerSecond = hasRateSample + ? Delta( + scheduler.CatchUpCollapseCount, + _previousCatchUpCount) + / intervalSeconds + : 0d; + _previousWaitCount = scheduler.WaitCount; + _previousTurnCount = scheduler.TurnCount; + _previousCatchUpCount = scheduler.CatchUpCollapseCount; + _hasPreviousCounters = true; + + var violations = new List(); + AddIfAbove( + violations, + "private-bytes", + process.PrivateBytes, + maximumPrivateBytes); + AddIfAbove( + violations, + "working-set-bytes", + process.WorkingSetBytes, + maximumWorkingSetBytes); + AddIfAbove( + violations, + "managed-live-bytes", + managed.LiveBytes, + maximumManagedLiveBytes); + AddIfAbove( + violations, + "managed-heap-bytes", + managed.HeapSizeBytes, + maximumManagedHeapBytes); + if (process.SampleIntervalMilliseconds > 0d) + { + AddIfAbove( + violations, + "cpu-core-percent", + process.CpuCorePercent, + maximumCpuCorePercent); + } + AddIfAbove( + violations, + "thread-count", + process.ThreadCount, + _ceilings.MaximumThreadCount); + AddIfSupportedAbove( + violations, + "handle-count", + process.HandleCount, + maximumHandleCount); + AddIfSupportedAbove( + violations, + "descriptor-count", + process.FileDescriptorCount, + maximumDescriptorCount); + AddIfSupportedAbove( + violations, + "socket-count", + process.SocketDescriptorCount, + maximumSocketCount); + if (hasRateSample) + { + AddIfAbove( + violations, + "scheduler-waits-per-second", + waitsPerSecond, + _ceilings.MaximumWaitsPerSecond); + AddIfAbove( + violations, + "scheduler-catchups-per-second", + catchUpsPerSecond, + _ceilings.MaximumCatchUpsPerSecond); + } + AddIfAbove( + violations, + "scheduler-mean-lateness-milliseconds", + scheduler.MeanLatenessMilliseconds, + _ceilings.MaximumMeanLatenessMilliseconds); + AddIfAbove( + violations, + "scheduler-maximum-lateness-milliseconds", + scheduler.MaximumLatenessMilliseconds, + _ceilings.MaximumLatenessMilliseconds); + if (sessions.FaultedCount != 0 + || scheduler.FaultedSessionCount != 0) + { + violations.Add("faulted-session-debt"); + } + + if (string.Equals( + state, + "disposed", + StringComparison.Ordinal)) + { + EvaluateTerminal( + violations, + sessions, + content); + } + else + { + EvaluateRunning( + violations, + sessions, + scheduler, + content); + } + + return new HeadlessProcessResourceEnvelopeSnapshot( + _ceilings, + hasRateSample, + violations.Count == 0, + maximumPrivateBytes, + maximumWorkingSetBytes, + maximumManagedLiveBytes, + maximumManagedHeapBytes, + maximumCpuCorePercent, + _ceilings.MaximumThreadCount, + maximumHandleCount, + maximumDescriptorCount, + maximumSocketCount, + _ceilings.MaximumWaitsPerSecond, + _ceilings.MaximumCatchUpsPerSecond, + _ceilings.MaximumMeanLatenessMilliseconds, + _ceilings.MaximumLatenessMilliseconds, + waitsPerSecond, + turnsPerSecond, + catchUpsPerSecond, + [.. violations]); + } + + private static void EvaluateRunning( + List violations, + in HeadlessSessionUsageSnapshot sessions, + in HeadlessSchedulerSnapshot scheduler, + HeadlessProcessContentSnapshot? content) + { + if (scheduler.SessionCount != sessions.ConfiguredCount) + violations.Add("scheduler-session-count"); + if (sessions.HostLeaseCount != sessions.ConfiguredCount) + violations.Add("runtime-host-lease-debt"); + if (sessions.InWorldCount + sessions.PendingReconnectCount + < sessions.ConfiguredCount) + { + violations.Add("unaccounted-session-lifecycle"); + } + if (content is { } current + && (current.LeaseCount != sessions.ConfiguredCount + || current.IsDisposeRequested + || current.IsDisposed)) + { + violations.Add("shared-content-lease-debt"); + } + } + + private static void EvaluateTerminal( + List violations, + in HeadlessSessionUsageSnapshot sessions, + HeadlessProcessContentSnapshot? content) + { + if (sessions.InWorldCount != 0 + || sessions.PendingReconnectCount != 0 + || sessions.ConvergedRuntimeCount + != sessions.ConfiguredCount + || sessions.EntityCount != 0 + || sessions.InventoryObjectCount != 0 + || sessions.HostLeaseCount != 0) + { + violations.Add("runtime-terminal-debt"); + } + if (content is { IsConverged: false }) + violations.Add("shared-content-terminal-debt"); + } + + private static long Delta(long current, long previous) => + Math.Max(0L, current - previous); + + private static void AddIfSupportedAbove( + List violations, + string name, + int value, + int ceiling) + { + if (value >= 0) + AddIfAbove(violations, name, value, ceiling); + } + + private static void AddIfAbove( + List violations, + string name, + long value, + long ceiling) + { + if (value > ceiling) + violations.Add(name); + } + + private static void AddIfAbove( + List violations, + string name, + double value, + double ceiling) + { + if (!double.IsFinite(value) || value > ceiling) + violations.Add(name); + } +} diff --git a/src/AcDream.Headless/Diagnostics/HeadlessProcessResourceSampler.cs b/src/AcDream.Headless/Diagnostics/HeadlessProcessResourceSampler.cs index b28a119d..5e092b0f 100644 --- a/src/AcDream.Headless/Diagnostics/HeadlessProcessResourceSampler.cs +++ b/src/AcDream.Headless/Diagnostics/HeadlessProcessResourceSampler.cs @@ -48,7 +48,8 @@ internal readonly record struct HeadlessProcessResourceSnapshot( HeadlessManagedUsageSnapshot Managed, HeadlessSessionUsageSnapshot Sessions, HeadlessSchedulerSnapshot Scheduler, - HeadlessProcessContentSnapshot? Content); + HeadlessProcessContentSnapshot? Content, + HeadlessProcessResourceEnvelopeSnapshot Envelope); /// /// Captures one low-frequency process-wide resource sample. The sampler owns @@ -58,6 +59,7 @@ internal sealed class HeadlessProcessResourceSampler : IDisposable { private readonly TimeProvider _timeProvider; private readonly Process _process; + private readonly HeadlessProcessResourceEnvelopeEvaluator _envelope; private long _sequence; private long _previousTimestamp; private long _previousProcessorTicks; @@ -70,14 +72,17 @@ internal sealed class HeadlessProcessResourceSampler : IDisposable { _timeProvider = timeProvider ?? TimeProvider.System; _process = process ?? Process.GetCurrentProcess(); + _envelope = new HeadlessProcessResourceEnvelopeEvaluator(); } internal HeadlessProcessResourceSnapshot Capture( + string state, IReadOnlyList sessions, HeadlessSchedulerSnapshot scheduler, HeadlessProcessContentSnapshot? content) { ObjectDisposedException.ThrowIf(_disposed, this); + ArgumentException.ThrowIfNullOrWhiteSpace(state); ArgumentNullException.ThrowIfNull(sessions); long now = _timeProvider.GetTimestamp(); @@ -179,6 +184,14 @@ internal sealed class HeadlessProcessResourceSampler : IDisposable entityCount, inventoryObjectCount, hostLeaseCount); + HeadlessProcessResourceEnvelopeSnapshot envelope = + _envelope.Evaluate( + state, + process, + managed, + sessionUsage, + scheduler, + content); return new HeadlessProcessResourceSnapshot( checked(++_sequence), now, @@ -186,7 +199,8 @@ internal sealed class HeadlessProcessResourceSampler : IDisposable managed, sessionUsage, scheduler, - content); + content, + envelope); } public void Dispose() diff --git a/src/AcDream.Headless/Hosting/HeadlessProcessHost.cs b/src/AcDream.Headless/Hosting/HeadlessProcessHost.cs index 629c1a86..4008d259 100644 --- a/src/AcDream.Headless/Hosting/HeadlessProcessHost.cs +++ b/src/AcDream.Headless/Hosting/HeadlessProcessHost.cs @@ -233,6 +233,7 @@ internal sealed class HeadlessProcessHost : IDisposable HeadlessProcessContentOwner? content) { HeadlessProcessResourceSnapshot snapshot = resources.Capture( + state, _sessions, scheduler, content?.CaptureSnapshot()); diff --git a/tests/AcDream.Headless.Tests/HeadlessProcessResourceEnvelopeTests.cs b/tests/AcDream.Headless.Tests/HeadlessProcessResourceEnvelopeTests.cs new file mode 100644 index 00000000..4d04b384 --- /dev/null +++ b/tests/AcDream.Headless.Tests/HeadlessProcessResourceEnvelopeTests.cs @@ -0,0 +1,322 @@ +using AcDream.Headless.Diagnostics; +using AcDream.Headless.Hosting; + +namespace AcDream.Headless.Tests; + +public sealed class HeadlessProcessResourceEnvelopeTests +{ + [Fact] + public void K4ProfileScalesOnlyPerSessionDimensions() + { + HeadlessProcessResourceCeilings ceilings = + HeadlessProcessResourceCeilings.K4Linux; + + Assert.Equal( + 288L * HeadlessProcessResourceCeilings.MiB, + ceilings.MaximumPrivateBytes(1)); + Assert.Equal( + 352L * HeadlessProcessResourceCeilings.MiB, + ceilings.MaximumPrivateBytes(2)); + Assert.Equal( + 2144L * HeadlessProcessResourceCeilings.MiB, + ceilings.MaximumPrivateBytes(30)); + Assert.Equal(8d, ceilings.MaximumCpuCorePercent(2)); + Assert.Equal(92d, ceilings.MaximumCpuCorePercent(30)); + Assert.Equal( + 160L * HeadlessProcessResourceCeilings.MiB, + ceilings.MaximumManagedHeapBytes(2)); + Assert.Equal(48, ceilings.MaximumThreadCount); + Assert.Equal(5, ceilings.MaximumSocketCount(2)); + Assert.Equal(61, ceilings.MaximumSocketCount(30)); + } + + [Fact] + public void FixedBinaryTwoSessionRynthidSamplePasses() + { + var evaluator = + new HeadlessProcessResourceEnvelopeEvaluator(); + _ = evaluator.Evaluate( + "running-start", + Process( + privateBytes: 207_966_208L, + workingSetBytes: 190_509_056L), + Managed(71_036_416L), + RunningSessions(2), + Scheduler(2), + RunningContent(2)); + + HeadlessProcessResourceEnvelopeSnapshot result = + evaluator.Evaluate( + "periodic", + Process( + privateBytes: 313_847_808L, + workingSetBytes: 302_817_280L, + cpuCorePercent: 6.2811d, + sampleIntervalMilliseconds: 30_000d, + threadCount: 17, + handleCount: 115, + descriptorCount: 116, + socketCount: 3), + Managed(122_179_536L), + RunningSessions(2), + Scheduler( + 2, + waitCount: 2918L, + turnCount: 3974L, + catchUpCount: 2L, + lateCount: 3974L, + meanLatenessMilliseconds: 3.31d, + maximumLatenessMilliseconds: 302.16d), + RunningContent(2)); + + Assert.True(result.HasRateSample); + Assert.True(result.WithinCeilings); + Assert.Empty(result.Violations); + Assert.Equal(97.2667d, result.WaitsPerSecond, precision: 4); + Assert.Equal(132.4667d, result.TurnsPerSecond, precision: 4); + Assert.Equal(0.0667d, result.CatchUpsPerSecond, precision: 4); + } + + [Fact] + public void EveryResourceAndRuntimeDebtIsNamed() + { + var evaluator = + new HeadlessProcessResourceEnvelopeEvaluator(); + _ = evaluator.Evaluate( + "running-start", + Process(1L, 1L), + Managed(1L), + RunningSessions(2), + Scheduler(2), + RunningContent(2)); + HeadlessProcessResourceCeilings ceilings = + HeadlessProcessResourceCeilings.K4Linux; + var sessions = new HeadlessSessionUsageSnapshot( + ConfiguredCount: 2, + InWorldCount: 0, + FaultedCount: 1, + PendingReconnectCount: 0, + ConvergedRuntimeCount: 0, + EntityCount: 0, + InventoryObjectCount: 0, + HostLeaseCount: 0); + + HeadlessProcessResourceEnvelopeSnapshot result = + evaluator.Evaluate( + "periodic", + Process( + ceilings.MaximumPrivateBytes(2) + 1L, + ceilings.MaximumWorkingSetBytes(2) + 1L, + cpuCorePercent: + ceilings.MaximumCpuCorePercent(2) + 0.01d, + sampleIntervalMilliseconds: 30_000d, + threadCount: ceilings.MaximumThreadCount + 1, + handleCount: + ceilings.MaximumHandleCount(2) + 1, + descriptorCount: + ceilings.MaximumDescriptorCount(2) + 1, + socketCount: + ceilings.MaximumSocketCount(2) + 1), + Managed( + ceilings.MaximumManagedLiveBytes(2) + 1L), + sessions, + Scheduler( + sessionCount: 1, + waitCount: 7501L, + turnCount: 1L, + catchUpCount: 31L, + lateCount: 1L, + meanLatenessMilliseconds: 10.01d, + maximumLatenessMilliseconds: 750.01d, + faultedCount: 1), + new HeadlessProcessContentSnapshot( + 0, + IsDisposeRequested: true, + IsDisposed: false, + MappedVirtualBytes: 1L)); + + Assert.False(result.WithinCeilings); + string[] expected = + [ + "private-bytes", + "working-set-bytes", + "managed-live-bytes", + "managed-heap-bytes", + "cpu-core-percent", + "thread-count", + "handle-count", + "descriptor-count", + "socket-count", + "scheduler-waits-per-second", + "scheduler-catchups-per-second", + "scheduler-mean-lateness-milliseconds", + "scheduler-maximum-lateness-milliseconds", + "faulted-session-debt", + "scheduler-session-count", + "runtime-host-lease-debt", + "unaccounted-session-lifecycle", + "shared-content-lease-debt", + ]; + Assert.Equal(expected, result.Violations); + } + + [Fact] + public void DisposedSampleRequiresExactRuntimeAndContentConvergence() + { + var failing = + new HeadlessProcessResourceEnvelopeEvaluator(); + HeadlessProcessResourceEnvelopeSnapshot debt = + failing.Evaluate( + "disposed", + Process(1L, 1L), + Managed(1L), + RunningSessions(2), + Scheduler(2), + RunningContent(2)); + + Assert.False(debt.WithinCeilings); + Assert.Contains("runtime-terminal-debt", debt.Violations); + Assert.Contains( + "shared-content-terminal-debt", + debt.Violations); + + var passing = + new HeadlessProcessResourceEnvelopeEvaluator(); + HeadlessProcessResourceEnvelopeSnapshot converged = + passing.Evaluate( + "disposed", + Process(1L, 1L), + Managed(1L), + new HeadlessSessionUsageSnapshot( + ConfiguredCount: 2, + InWorldCount: 0, + FaultedCount: 0, + PendingReconnectCount: 0, + ConvergedRuntimeCount: 2, + EntityCount: 0, + InventoryObjectCount: 0, + HostLeaseCount: 0), + Scheduler(2), + new HeadlessProcessContentSnapshot( + 0, + IsDisposeRequested: true, + IsDisposed: true, + MappedVirtualBytes: 0L)); + + Assert.True(converged.WithinCeilings); + Assert.Empty(converged.Violations); + } + + [Fact] + public void NonFiniteResourceRatesFailClosed() + { + var evaluator = + new HeadlessProcessResourceEnvelopeEvaluator(); + + HeadlessProcessResourceEnvelopeSnapshot result = + evaluator.Evaluate( + "periodic", + Process( + 1L, + 1L, + cpuCorePercent: double.NaN, + sampleIntervalMilliseconds: 30_000d), + Managed(1L), + RunningSessions(1), + Scheduler(1), + RunningContent(1)); + + Assert.False(result.WithinCeilings); + Assert.Contains("cpu-core-percent", result.Violations); + } + + private static HeadlessProcessUsageSnapshot Process( + long privateBytes, + long workingSetBytes, + double cpuCorePercent = 0d, + double sampleIntervalMilliseconds = 0d, + int threadCount = 16, + int handleCount = 113, + int descriptorCount = 112, + int socketCount = 3) => + new( + WorkingSetBytes: workingSetBytes, + PrivateBytes: privateBytes, + VirtualBytes: 30_000_000_000L, + TotalProcessorTimeTicks: 0L, + SampleIntervalMilliseconds: sampleIntervalMilliseconds, + CpuCorePercent: cpuCorePercent, + CpuMachinePercent: cpuCorePercent / 16d, + ProcessorCount: 16, + ThreadCount: threadCount, + HandleCount: handleCount, + FileDescriptorCount: descriptorCount, + SocketDescriptorCount: socketCount); + + private static HeadlessManagedUsageSnapshot Managed( + long liveBytes) => + new( + LiveBytes: liveBytes, + HeapSizeBytes: liveBytes, + FragmentedBytes: 0L, + CommittedBytes: liveBytes, + TotalAllocatedBytes: liveBytes, + MemoryLoadBytes: liveBytes, + HighMemoryLoadThresholdBytes: long.MaxValue, + PauseTimePercentage: 0d, + Gen0Collections: 0, + Gen1Collections: 0, + Gen2Collections: 0); + + private static HeadlessSessionUsageSnapshot RunningSessions( + int sessionCount) => + new( + ConfiguredCount: sessionCount, + InWorldCount: sessionCount, + FaultedCount: 0, + PendingReconnectCount: 0, + ConvergedRuntimeCount: 0, + EntityCount: sessionCount, + InventoryObjectCount: sessionCount, + HostLeaseCount: sessionCount); + + private static HeadlessSchedulerSnapshot Scheduler( + int sessionCount, + long waitCount = 0L, + long turnCount = 0L, + long catchUpCount = 0L, + long lateCount = 0L, + double meanLatenessMilliseconds = 0d, + double maximumLatenessMilliseconds = 0d, + int faultedCount = 0) + { + const long frequency = TimeSpan.TicksPerSecond; + long totalLatenessTicks = checked((long)( + meanLatenessMilliseconds + * TimeSpan.TicksPerMillisecond + * lateCount)); + long maximumLatenessTicks = checked((long)( + maximumLatenessMilliseconds + * TimeSpan.TicksPerMillisecond)); + return new HeadlessSchedulerSnapshot( + SessionCount: sessionCount, + WaitCount: waitCount, + TurnCount: turnCount, + CatchUpCollapseCount: catchUpCount, + LateDeadlineCount: lateCount, + TotalLatenessTicks: totalLatenessTicks, + MaximumLatenessTicks: maximumLatenessTicks, + TimestampFrequency: frequency, + ActiveSessionCount: sessionCount - faultedCount, + FaultedSessionCount: faultedCount, + NextDeadline: 0L); + } + + private static HeadlessProcessContentSnapshot RunningContent( + int sessionCount) => + new( + LeaseCount: sessionCount, + IsDisposeRequested: false, + IsDisposed: false, + MappedVirtualBytes: 29_908_271_024L); +} diff --git a/tests/AcDream.Headless.Tests/HeadlessProcessSchedulerTests.cs b/tests/AcDream.Headless.Tests/HeadlessProcessSchedulerTests.cs index a0a418d8..532251a7 100644 --- a/tests/AcDream.Headless.Tests/HeadlessProcessSchedulerTests.cs +++ b/tests/AcDream.Headless.Tests/HeadlessProcessSchedulerTests.cs @@ -345,6 +345,14 @@ public sealed class HeadlessProcessSchedulerTests "\"configuredCount\":2", diagnostics.ToString(), StringComparison.Ordinal); + Assert.Contains( + "\"resourceEnvelope\":", + diagnostics.ToString(), + StringComparison.Ordinal); + Assert.Contains( + "\"profile\":\"k4-linux-30-session\"", + diagnostics.ToString(), + StringComparison.Ordinal); host.Dispose(); @@ -356,6 +364,10 @@ public sealed class HeadlessProcessSchedulerTests "\"convergedRuntimeCount\":2", diagnostics.ToString(), StringComparison.Ordinal); + Assert.Contains( + "\"violations\":", + diagnostics.ToString(), + StringComparison.Ordinal); } [Fact]