diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/BOOL.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/BOOL.cs new file mode 100644 index 00000000000..0b9a922d6df --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/BOOL.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop; + +/// +/// Blittable version of Windows BOOL type. It is convenient in situations where +/// manual marshalling is required, or to avoid overhead of regular bool marshalling. +/// +/// +/// Some Windows APIs return arbitrary integer values although the return type is defined +/// as BOOL. It is best to never compare BOOL to TRUE. Always use bResult != BOOL.FALSE +/// or bResult == BOOL.FALSE . +/// +#pragma warning disable S1939 // Inheritance list should not be redundant +internal enum BOOL : int +#pragma warning restore S1939 // Inheritance list should not be redundant +{ + FALSE = 0, + TRUE = 1, +} + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/MemoryInfo.Native.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/MemoryInfo.Native.cs new file mode 100644 index 00000000000..cd6e82b4c94 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/MemoryInfo.Native.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop; + +#if NET8_0_OR_GREATER +using DllImportAttr = System.Runtime.InteropServices.LibraryImportAttribute; // We trigger source-gen on .NET 7 and above +#else +using DllImportAttr = System.Runtime.InteropServices.DllImportAttribute; +#endif + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop; + +internal sealed partial class MemoryInfo +{ + private static partial class SafeNativeMethods + { + /// + /// GlobalMemoryStatusEx. + /// + /// Memory Status structure. + /// Success or failure. + [DllImportAttr("kernel32.dll", SetLastError = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + public static unsafe +#if NET8_0_OR_GREATER + partial +#else + extern +#endif + BOOL GlobalMemoryStatusEx(MEMORYSTATUSEX* memoryStatus); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/MemoryInfo.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/MemoryInfo.cs index 6c8a4de62fe..5f3676bbb1a 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/MemoryInfo.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/MemoryInfo.cs @@ -10,12 +10,8 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop; /// Native memory interop methods. /// [ExcludeFromCodeCoverage] -internal sealed class MemoryInfo : IMemoryInfo +internal sealed partial class MemoryInfo : IMemoryInfo { - internal MemoryInfo() - { - } - /// /// Get the memory status of the host. /// @@ -24,7 +20,7 @@ public unsafe MEMORYSTATUSEX GetMemoryStatus() { MEMORYSTATUSEX info = default; info.Length = (uint)sizeof(MEMORYSTATUSEX); - if (!SafeNativeMethods.GlobalMemoryStatusEx(ref info)) + if (SafeNativeMethods.GlobalMemoryStatusEx(&info) != BOOL.TRUE) { Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); } @@ -32,16 +28,8 @@ public unsafe MEMORYSTATUSEX GetMemoryStatus() return info; } - private static class SafeNativeMethods + private static partial class SafeNativeMethods { - /// - /// GlobalMemoryStatusEx. - /// - /// Memory Status structure. - /// Success or failure. - [DllImport("kernel32.dll", SetLastError = true)] - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX memoryStatus); + // the class is partial and empty for source gen to work correctly for GlobalMemoryStatusEx } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs index e00d2bedc8b..a966284eee5 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs @@ -3,22 +3,18 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; using System.Threading; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows; -/// -/// A data source acquiring data from the kernel. -/// internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider { - internal TimeProvider TimeProvider = TimeProvider.System; + private const double Hundred = 100.0d; - /// - /// The memory status. - /// private readonly Lazy _memoryStatus; /// @@ -26,60 +22,87 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider /// private readonly Func _createJobHandleObject; + private readonly object _cpuLocker = new(); + private readonly object _memoryLocker = new(); + private readonly TimeProvider _timeProvider; private readonly IProcessInfo _processInfo; + private readonly double _totalMemory; + private readonly double _cpuUnits; + private readonly TimeSpan _cpuRefreshInterval; + private readonly TimeSpan _memoryRefreshInterval; + + private long _oldCpuUsageTicks; + private long _oldCpuTimeTicks; + private DateTimeOffset _refreshAfterCpu; + private DateTimeOffset _refreshAfterMemory; + private double _cpuPercentage = double.NaN; + private double _memoryPercentage; public SystemResources Resources { get; } /// /// Initializes a new instance of the class. /// - public WindowsContainerSnapshotProvider(ILogger logger) + public WindowsContainerSnapshotProvider( + ILogger logger, + IMeterFactory meterFactory, + IOptions options) + : this(new MemoryInfo(), new SystemInfo(), new ProcessInfoWrapper(), logger, meterFactory, + static () => new JobHandleWrapper(), TimeProvider.System, options.Value) { - Log.RunningInsideJobObject(logger); - - _memoryStatus = new Lazy( - new MemoryInfo().GetMemoryStatus, - LazyThreadSafetyMode.ExecutionAndPublication); - - var systemInfo = new Lazy( - new SystemInfo().GetSystemInfo, - LazyThreadSafetyMode.ExecutionAndPublication); - - _createJobHandleObject = CreateJobHandle; - - _processInfo = new ProcessInfoWrapper(); - - // initialize system resources information - using var jobHandle = _createJobHandleObject(); - - var cpuUnits = GetGuaranteedCpuUnits(jobHandle, systemInfo); - var memory = GetMemoryLimits(jobHandle); - - Resources = new SystemResources(cpuUnits, cpuUnits, memory, memory); } /// /// Initializes a new instance of the class. /// - /// A wrapper for the memory information retrieval object. - /// A wrapper for the system information retrieval object. - /// A wrapper for the process info retrieval object. - /// A factory method that creates object. - /// This constructor enables the mocking the dependencies for the purpose of Unit Testing only. - internal WindowsContainerSnapshotProvider(IMemoryInfo memoryInfo, ISystemInfo systemInfoObject, IProcessInfo processInfo, Func createJobHandleObject) + /// This constructor enables the mocking of dependencies for the purpose of Unit Testing only. + [SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", Justification = "Dependencies for testing")] + internal WindowsContainerSnapshotProvider( + IMemoryInfo memoryInfo, + ISystemInfo systemInfo, + IProcessInfo processInfo, + ILogger logger, + IMeterFactory meterFactory, + Func createJobHandleObject, + TimeProvider timeProvider, + ResourceMonitoringOptions options) { - _memoryStatus = new Lazy(memoryInfo.GetMemoryStatus, LazyThreadSafetyMode.ExecutionAndPublication); - var systemInfo = new Lazy(systemInfoObject.GetSystemInfo, LazyThreadSafetyMode.ExecutionAndPublication); - _processInfo = processInfo; + Log.RunningInsideJobObject(logger); + + _memoryStatus = new Lazy( + memoryInfo.GetMemoryStatus, + LazyThreadSafetyMode.ExecutionAndPublication); _createJobHandleObject = createJobHandleObject; + _processInfo = processInfo; + + _timeProvider = timeProvider; // initialize system resources information using var jobHandle = _createJobHandleObject(); - var cpuUnits = GetGuaranteedCpuUnits(jobHandle, systemInfo); + _cpuUnits = GetGuaranteedCpuUnits(jobHandle, systemInfo); var memory = GetMemoryLimits(jobHandle); - Resources = new SystemResources(cpuUnits, cpuUnits, memory, memory); + Resources = new SystemResources(_cpuUnits, _cpuUnits, memory, memory); + + _totalMemory = memory; + var basicAccountingInfo = jobHandle.GetBasicAccountingInfo(); + _oldCpuUsageTicks = basicAccountingInfo.TotalKernelTime + basicAccountingInfo.TotalUserTime; + _oldCpuTimeTicks = _timeProvider.GetUtcNow().Ticks; + _cpuRefreshInterval = options.CpuConsumptionRefreshInterval; + _memoryRefreshInterval = options.MemoryConsumptionRefreshInterval; + _refreshAfterCpu = _timeProvider.GetUtcNow(); + _refreshAfterMemory = _timeProvider.GetUtcNow(); + +#pragma warning disable CA2000 // Dispose objects before losing scope + // We don't dispose the meter because IMeterFactory handles that + // An issue on analyzer side: https://github.com/dotnet/roslyn-analyzers/issues/6912 + // Related documentation: https://github.com/dotnet/docs/pull/37170 + var meter = meterFactory.Create("Microsoft.Extensions.Diagnostics.ResourceMonitoring"); +#pragma warning restore CA2000 // Dispose objects before losing scope + + _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.CpuUtilization, observeValue: CpuPercentage); + _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.MemoryUtilization, observeValue: MemoryPercentage); } public Snapshot GetSnapshot() @@ -90,13 +113,13 @@ public Snapshot GetSnapshot() var basicAccountingInfo = jobHandle.GetBasicAccountingInfo(); return new Snapshot( - TimeSpan.FromTicks(TimeProvider.GetUtcNow().Ticks), + TimeSpan.FromTicks(_timeProvider.GetUtcNow().Ticks), TimeSpan.FromTicks(basicAccountingInfo.TotalKernelTime), TimeSpan.FromTicks(basicAccountingInfo.TotalUserTime), GetMemoryUsage()); } - private static double GetGuaranteedCpuUnits(IJobHandle jobHandle, Lazy systemInfo) + private static double GetGuaranteedCpuUnits(IJobHandle jobHandle, ISystemInfo systemInfo) { // Note: This function convert the CpuRate from CPU cycles to CPU units, also it scales // the CPU units with the number of processors (cores) available in the system. @@ -115,9 +138,11 @@ private static double GetGuaranteedCpuUnits(IJobHandle jobHandle, Lazy @@ -151,9 +176,63 @@ private ulong GetMemoryUsage() return memoryInfo.TotalCommitUsage; } - [ExcludeFromCodeCoverage] - private JobHandleWrapper CreateJobHandle() + private double MemoryPercentage() + { + var now = _timeProvider.GetUtcNow(); + + lock (_memoryLocker) + { + if (now < _refreshAfterMemory) + { + return _memoryPercentage; + } + } + + var currentMemoryUsage = GetMemoryUsage(); + lock (_memoryLocker) + { + if (now >= _refreshAfterMemory) + { + _memoryPercentage = Math.Min(Hundred, currentMemoryUsage / _totalMemory * Hundred); // Don't change calculation order, otherwise we loose some precision + _refreshAfterMemory = now.Add(_memoryRefreshInterval); + } + + return _memoryPercentage; + } + } + + private double CpuPercentage() { - return new JobHandleWrapper(); + var now = _timeProvider.GetUtcNow(); + + lock (_cpuLocker) + { + if (now < _refreshAfterCpu) + { + return _cpuPercentage; + } + } + + using var jobHandle = _createJobHandleObject(); + var basicAccountingInfo = jobHandle.GetBasicAccountingInfo(); + var currentCpuTicks = basicAccountingInfo.TotalKernelTime + basicAccountingInfo.TotalUserTime; + + lock (_cpuLocker) + { + if (now >= _refreshAfterCpu) + { + var usageTickDelta = currentCpuTicks - _oldCpuUsageTicks; + var timeTickDelta = (now.Ticks - _oldCpuTimeTicks) * _cpuUnits; + if (usageTickDelta > 0 && timeTickDelta > 0) + { + _oldCpuUsageTicks = currentCpuTicks; + _oldCpuTimeTicks = now.Ticks; + _cpuPercentage = Math.Min(Hundred, usageTickDelta / timeTickDelta * Hundred); // Don't change calculation order, otherwise we loose some precision + _refreshAfterCpu = now.Add(_cpuRefreshInterval); + } + } + + return _cpuPercentage; + } } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs index 97cd645a946..c53b00886f5 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs @@ -3,36 +3,168 @@ using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows; internal sealed class WindowsSnapshotProvider : ISnapshotProvider { + private const double Hundred = 100.0d; + public SystemResources Resources { get; } - internal TimeProvider TimeProvider = TimeProvider.System; + private readonly int _cpuUnits; + private readonly object _cpuLocker = new(); + private readonly object _memoryLocker = new(); + private readonly TimeProvider _timeProvider; + private readonly Func _getCpuTicksFunc; + private readonly Func _getMemoryUsageFunc; + private readonly double _totalMemory; + private readonly TimeSpan _cpuRefreshInterval; + private readonly TimeSpan _memoryRefreshInterval; + + private long _oldCpuUsageTicks; + private long _oldCpuTimeTicks; + private DateTimeOffset _refreshAfterCpu; + private DateTimeOffset _refreshAfterMemory; + private double _cpuPercentage = double.NaN; + private double _memoryPercentage; + + public WindowsSnapshotProvider(ILogger logger, IMeterFactory meterFactory, IOptions options) + : this(logger, meterFactory, options.Value, TimeProvider.System, GetCpuUnits, GetCpuTicks, GetMemoryUsageInBytes, GetTotalMemoryInBytes) + { + } - public WindowsSnapshotProvider(ILogger logger) + [SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", Justification = "Dependencies for testing")] + internal WindowsSnapshotProvider( + ILogger logger, + IMeterFactory meterFactory, + ResourceMonitoringOptions options, + TimeProvider timeProvider, + Func getCpuUnitsFunc, + Func getCpuTicksFunc, + Func getMemoryUsageFunc, + Func getTotalMemoryInBytesFunc) { Log.RunningOutsideJobObject(logger); - var memoryStatus = new MemoryInfo().GetMemoryStatus(); - var cpuUnits = Environment.ProcessorCount; - Resources = new SystemResources(cpuUnits, cpuUnits, memoryStatus.TotalPhys, memoryStatus.TotalPhys); + _cpuUnits = getCpuUnitsFunc(); + var totalMemory = getTotalMemoryInBytesFunc(); + Resources = new SystemResources(_cpuUnits, _cpuUnits, totalMemory, totalMemory); + + _timeProvider = timeProvider; + _getCpuTicksFunc = getCpuTicksFunc; + _getMemoryUsageFunc = getMemoryUsageFunc; + _totalMemory = totalMemory; // "long" totalMemory => "double" _totalMemory - to calculate percentage later + + _oldCpuUsageTicks = getCpuTicksFunc(); + _oldCpuTimeTicks = timeProvider.GetUtcNow().Ticks; + _cpuRefreshInterval = options.CpuConsumptionRefreshInterval; + _memoryRefreshInterval = options.MemoryConsumptionRefreshInterval; + _refreshAfterCpu = timeProvider.GetUtcNow(); + _refreshAfterMemory = timeProvider.GetUtcNow(); + +#pragma warning disable CA2000 // Dispose objects before losing scope + // We don't dispose the meter because IMeterFactory handles that + // An issue on analyzer side: https://github.com/dotnet/roslyn-analyzers/issues/6912 + // Related documentation: https://github.com/dotnet/docs/pull/37170 + var meter = meterFactory.Create("Microsoft.Extensions.Diagnostics.ResourceMonitoring"); +#pragma warning restore CA2000 // Dispose objects before losing scope + + _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.CpuUtilization, observeValue: CpuPercentage); + _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.MemoryUtilization, observeValue: MemoryPercentage); } public Snapshot GetSnapshot() { - // Gather the information - // Get CPU kernel and user ticks - var process = Process.GetCurrentProcess(); + using var process = Process.GetCurrentProcess(); return new Snapshot( - TimeSpan.FromTicks(TimeProvider.GetUtcNow().Ticks), - TimeSpan.FromTicks(process.PrivilegedProcessorTime.Ticks), - TimeSpan.FromTicks(process.UserProcessorTime.Ticks), - (ulong)process.WorkingSet64); + totalTimeSinceStart: TimeSpan.FromTicks(_timeProvider.GetUtcNow().Ticks), + kernelTimeSinceStart: process.PrivilegedProcessorTime, + userTimeSinceStart: process.UserProcessorTime, + memoryUsageInBytes: (ulong)process.WorkingSet64); + } + + internal static long GetCpuTicks() + { + using var process = Process.GetCurrentProcess(); + return process.TotalProcessorTime.Ticks; + } + + internal static int GetCpuUnits() => Environment.ProcessorCount; + + internal static long GetMemoryUsageInBytes() + { + using var process = Process.GetCurrentProcess(); + return process.WorkingSet64; + } + + internal static ulong GetTotalMemoryInBytes() + { + var memoryStatus = new MemoryInfo().GetMemoryStatus(); + return memoryStatus.TotalPhys; + } + + private double MemoryPercentage() + { + var now = _timeProvider.GetUtcNow(); + + lock (_memoryLocker) + { + if (now < _refreshAfterMemory) + { + return _memoryPercentage; + } + } + + var currentMemoryUsage = _getMemoryUsageFunc(); + lock (_memoryLocker) + { + if (now >= _refreshAfterMemory) + { + _memoryPercentage = Math.Min(Hundred, currentMemoryUsage / _totalMemory * Hundred); // Don't change calculation order, otherwise we loose some precision + _refreshAfterMemory = now.Add(_memoryRefreshInterval); + } + + return _memoryPercentage; + } + } + + private double CpuPercentage() + { + var now = _timeProvider.GetUtcNow(); + + lock (_cpuLocker) + { + if (now < _refreshAfterCpu) + { + return _cpuPercentage; + } + } + + var currentCpuTicks = _getCpuTicksFunc(); + + lock (_cpuLocker) + { + if (now >= _refreshAfterCpu) + { + var usageTickDelta = currentCpuTicks - _oldCpuUsageTicks; + var timeTickDelta = (now.Ticks - _oldCpuTimeTicks) * _cpuUnits; + if (usageTickDelta > 0 && timeTickDelta > 0) + { + _oldCpuUsageTicks = currentCpuTicks; + _oldCpuTimeTicks = now.Ticks; + _cpuPercentage = Math.Min(Hundred, usageTickDelta / (double)timeTickDelta * Hundred); // Don't change calculation order, otherwise we loose some precision + _refreshAfterCpu = now.Add(_cpuRefreshInterval); + } + } + + return _cpuPercentage; + } } } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs index 924b5feed43..7057fd5d810 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs @@ -2,7 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop; +using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Time.Testing; using Moq; using Xunit; using static Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop.JobObjectInfo; @@ -11,6 +15,56 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Test; public sealed class WindowsContainerSnapshotProviderTests { + private readonly FakeLogger _logger; + private readonly MEMORYSTATUSEX _memStatus; + + private readonly Mock _meterFactory; + private readonly Mock _memoryInfoMock = new(); + private readonly Mock _systemInfoMock = new(); + private readonly Mock _jobHandleMock = new(); + private readonly Mock _processInfoMock = new(); + + private SYSTEM_INFO _sysInfo; + private JOBOBJECT_BASIC_ACCOUNTING_INFORMATION _accountingInfo; + private JOBOBJECT_CPU_RATE_CONTROL_INFORMATION _cpuLimit; + private ProcessInfo.APP_MEMORY_INFORMATION _appMemoryInfo; + private JOBOBJECT_EXTENDED_LIMIT_INFORMATION _limitInfo; + + public WindowsContainerSnapshotProviderTests() + { + using var meter = new Meter(nameof(WindowsContainerSnapshotProvider)); + _meterFactory = new Mock(); + _meterFactory.Setup(x => x.Create(It.IsAny())) + .Returns(meter); + + _logger = new FakeLogger(); + + _memStatus.TotalPhys = 3000UL; + _memoryInfoMock.Setup(m => m.GetMemoryStatus()) + .Returns(() => _memStatus); + + _sysInfo.NumberOfProcessors = 1; + _systemInfoMock.Setup(s => s.GetSystemInfo()) + .Returns(() => _sysInfo); + + _accountingInfo.TotalKernelTime = 1000; + _accountingInfo.TotalUserTime = 1000; + _jobHandleMock.Setup(j => j.GetBasicAccountingInfo()) + .Returns(() => _accountingInfo); + + _cpuLimit.CpuRate = 7_000; + _jobHandleMock.Setup(j => j.GetJobCpuLimitInfo()) + .Returns(() => _cpuLimit); + + _limitInfo.JobMemoryLimit = new UIntPtr(2000); + _jobHandleMock.Setup(j => j.GetExtendedLimitInfo()) + .Returns(() => _limitInfo); + + _appMemoryInfo.TotalCommitUsage = 1000UL; + _processInfoMock.Setup(p => p.GetCurrentAppMemoryInfo()) + .Returns(() => _appMemoryInfo); + } + [Theory] [InlineData(7_000, 1U, 0.7)] [InlineData(10_000, 1U, 1.0)] @@ -18,219 +72,250 @@ public sealed class WindowsContainerSnapshotProviderTests [InlineData(5_000, 2U, 1.0)] public void Resources_GetsCorrectSystemResourcesValues(uint cpuRate, uint numberOfProcessors, double expectedCpuUnits) { - MEMORYSTATUSEX memStatus = default; - memStatus.TotalPhys = 3000UL; - - SYSTEM_INFO sysInfo = default; - sysInfo.NumberOfProcessors = numberOfProcessors; - - JOBOBJECT_CPU_RATE_CONTROL_INFORMATION cpuLimit = default; + _sysInfo.NumberOfProcessors = numberOfProcessors; // This is customized to force the private method GetGuaranteedCpuUnits - // to use the value of CpuRate and divide it by 10_000. - cpuLimit.ControlFlags = 5; + // to use the value of CpuRate and divide it by 10_000. + _cpuLimit.ControlFlags = 5; // The CpuRate is the Cpu percentage multiplied by 100, check this: // https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-jobobject_cpu_rate_control_information - cpuLimit.CpuRate = cpuRate; - - JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accountingInfo = default; - accountingInfo.TotalKernelTime = 1000; - accountingInfo.TotalUserTime = 1000; - - JOBOBJECT_EXTENDED_LIMIT_INFORMATION limitInfo = default; - limitInfo.JobMemoryLimit = new UIntPtr(2000); - - ProcessInfo.APP_MEMORY_INFORMATION appMemoryInfo = default; - appMemoryInfo.TotalCommitUsage = 1000UL; - - var memoryInfoMock = new Mock(); - memoryInfoMock.Setup(m => m.GetMemoryStatus()).Returns(memStatus); - - var systemInfoMock = new Mock(); - systemInfoMock.Setup(s => s.GetSystemInfo()).Returns(sysInfo); - - var processInfoMock = new Mock(); - processInfoMock.Setup(p => p.GetCurrentAppMemoryInfo()).Returns(appMemoryInfo); - - var jobHandleMock = new Mock(); - jobHandleMock.Setup(j => j.GetJobCpuLimitInfo()).Returns(cpuLimit); - jobHandleMock.Setup(j => j.GetBasicAccountingInfo()).Returns(accountingInfo); - jobHandleMock.Setup(j => j.GetExtendedLimitInfo()).Returns(limitInfo); + _cpuLimit.CpuRate = cpuRate; var provider = new WindowsContainerSnapshotProvider( - memoryInfoMock.Object, - systemInfoMock.Object, - processInfoMock.Object, - () => jobHandleMock.Object); + _memoryInfoMock.Object, + _systemInfoMock.Object, + _processInfoMock.Object, + _logger, + _meterFactory.Object, + () => _jobHandleMock.Object, + new FakeTimeProvider(), + new()); Assert.Equal(expectedCpuUnits, provider.Resources.GuaranteedCpuUnits); Assert.Equal(expectedCpuUnits, provider.Resources.MaximumCpuUnits); - Assert.Equal(limitInfo.JobMemoryLimit.ToUInt64(), provider.Resources.GuaranteedMemoryInBytes); - Assert.Equal(limitInfo.JobMemoryLimit.ToUInt64(), provider.Resources.MaximumMemoryInBytes); + Assert.Equal(_limitInfo.JobMemoryLimit.ToUInt64(), provider.Resources.GuaranteedMemoryInBytes); + Assert.Equal(_limitInfo.JobMemoryLimit.ToUInt64(), provider.Resources.MaximumMemoryInBytes); } [Fact] public void GetSnapshot_ProducesCorrectSnapshot() { - MEMORYSTATUSEX memStatus = default; - memStatus.TotalPhys = 3000UL; - - SYSTEM_INFO sysInfo = default; - sysInfo.NumberOfProcessors = 1; - - JOBOBJECT_CPU_RATE_CONTROL_INFORMATION cpuLimit = default; - // The ControlFlags is customized to force the private method GetGuaranteedCpuUnits // to not use the value of CpuRate in the calculation. - cpuLimit.ControlFlags = 1; - cpuLimit.CpuRate = 7_000; - - JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accountingInfo = default; - accountingInfo.TotalKernelTime = 1000; - accountingInfo.TotalUserTime = 1000; - - JOBOBJECT_EXTENDED_LIMIT_INFORMATION limitInfo = default; - limitInfo.JobMemoryLimit = new UIntPtr(2000); - - ProcessInfo.APP_MEMORY_INFORMATION appMemoryInfo = default; - appMemoryInfo.TotalCommitUsage = 1000UL; - - var memoryInfoMock = new Mock(); - memoryInfoMock.Setup(m => m.GetMemoryStatus()).Returns(memStatus); - - var systemInfoMock = new Mock(); - systemInfoMock.Setup(s => s.GetSystemInfo()).Returns(sysInfo); - - var processInfoMock = new Mock(); - processInfoMock.Setup(p => p.GetCurrentAppMemoryInfo()).Returns(appMemoryInfo); - - var jobHandleMock = new Mock(); - jobHandleMock.Setup(j => j.GetJobCpuLimitInfo()).Returns(cpuLimit); - jobHandleMock.Setup(j => j.GetBasicAccountingInfo()).Returns(accountingInfo); - jobHandleMock.Setup(j => j.GetExtendedLimitInfo()).Returns(limitInfo); + _cpuLimit.ControlFlags = 1; var source = new WindowsContainerSnapshotProvider( - memoryInfoMock.Object, - systemInfoMock.Object, - processInfoMock.Object, - () => jobHandleMock.Object); + _memoryInfoMock.Object, + _systemInfoMock.Object, + _processInfoMock.Object, + _logger, + _meterFactory.Object, + () => _jobHandleMock.Object, + new FakeTimeProvider(), + new()); + var data = source.GetSnapshot(); - Assert.Equal(accountingInfo.TotalKernelTime, data.KernelTimeSinceStart.Ticks); - Assert.Equal(accountingInfo.TotalUserTime, data.UserTimeSinceStart.Ticks); - Assert.Equal(limitInfo.JobMemoryLimit.ToUInt64(), source.Resources.GuaranteedMemoryInBytes); - Assert.Equal(limitInfo.JobMemoryLimit.ToUInt64(), source.Resources.MaximumMemoryInBytes); - Assert.Equal(appMemoryInfo.TotalCommitUsage, data.MemoryUsageInBytes); + Assert.Equal(_accountingInfo.TotalKernelTime, data.KernelTimeSinceStart.Ticks); + Assert.Equal(_accountingInfo.TotalUserTime, data.UserTimeSinceStart.Ticks); + Assert.Equal(_limitInfo.JobMemoryLimit.ToUInt64(), source.Resources.GuaranteedMemoryInBytes); + Assert.Equal(_limitInfo.JobMemoryLimit.ToUInt64(), source.Resources.MaximumMemoryInBytes); + Assert.Equal(_appMemoryInfo.TotalCommitUsage, data.MemoryUsageInBytes); Assert.True(data.MemoryUsageInBytes > 0); } [Fact] public void GetSnapshot_ProducesCorrectSnapshotForDifferentCpuRate() { - MEMORYSTATUSEX memStatus = default; - memStatus.TotalPhys = 3000UL; - - SYSTEM_INFO sysInfo = default; - sysInfo.NumberOfProcessors = 1; - - JOBOBJECT_CPU_RATE_CONTROL_INFORMATION cpuLimit = default; - cpuLimit.ControlFlags = uint.MaxValue; // force all bits in ControlFlags to be 1. - cpuLimit.CpuRate = 7_000; - - JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accountingInfo = default; - accountingInfo.TotalKernelTime = 1000; - accountingInfo.TotalUserTime = 1000; - - JOBOBJECT_EXTENDED_LIMIT_INFORMATION limitInfo = default; - limitInfo.JobMemoryLimit = new UIntPtr(2000); - - ProcessInfo.APP_MEMORY_INFORMATION appMemoryInfo = default; - appMemoryInfo.TotalCommitUsage = 1000UL; - - var memoryInfoMock = new Mock(); - memoryInfoMock.Setup(m => m.GetMemoryStatus()).Returns(memStatus); - - var systemInfoMock = new Mock(); - systemInfoMock.Setup(s => s.GetSystemInfo()).Returns(sysInfo); - - var processInfoMock = new Mock(); - processInfoMock.Setup(p => p.GetCurrentAppMemoryInfo()).Returns(appMemoryInfo); - - var jobHandleMock = new Mock(); - jobHandleMock.Setup(j => j.GetJobCpuLimitInfo()).Returns(cpuLimit); - jobHandleMock.Setup(j => j.GetBasicAccountingInfo()).Returns(accountingInfo); - jobHandleMock.Setup(j => j.GetExtendedLimitInfo()).Returns(limitInfo); + _cpuLimit.ControlFlags = uint.MaxValue; // force all bits in ControlFlags to be 1. var source = new WindowsContainerSnapshotProvider( - memoryInfoMock.Object, - systemInfoMock.Object, - processInfoMock.Object, - () => jobHandleMock.Object); + _memoryInfoMock.Object, + _systemInfoMock.Object, + _processInfoMock.Object, + _logger, + _meterFactory.Object, + () => _jobHandleMock.Object, + new FakeTimeProvider(), + new()); + var data = source.GetSnapshot(); - Assert.Equal(accountingInfo.TotalKernelTime, data.KernelTimeSinceStart.Ticks); - Assert.Equal(accountingInfo.TotalUserTime, data.UserTimeSinceStart.Ticks); + Assert.Equal(_accountingInfo.TotalKernelTime, data.KernelTimeSinceStart.Ticks); + Assert.Equal(_accountingInfo.TotalUserTime, data.UserTimeSinceStart.Ticks); Assert.Equal(0.7, source.Resources.GuaranteedCpuUnits); Assert.Equal(0.7, source.Resources.MaximumCpuUnits); - Assert.Equal(limitInfo.JobMemoryLimit.ToUInt64(), source.Resources.GuaranteedMemoryInBytes); - Assert.Equal(limitInfo.JobMemoryLimit.ToUInt64(), source.Resources.MaximumMemoryInBytes); - Assert.Equal(appMemoryInfo.TotalCommitUsage, data.MemoryUsageInBytes); + Assert.Equal(_limitInfo.JobMemoryLimit.ToUInt64(), source.Resources.GuaranteedMemoryInBytes); + Assert.Equal(_limitInfo.JobMemoryLimit.ToUInt64(), source.Resources.MaximumMemoryInBytes); + Assert.Equal(_appMemoryInfo.TotalCommitUsage, data.MemoryUsageInBytes); Assert.True(data.MemoryUsageInBytes > 0); } [Fact] public void GetSnapshot_With_JobMemoryLimit_Set_To_Zero_ProducesCorrectSnapshot() { - MEMORYSTATUSEX memStatus = default; - memStatus.TotalPhys = 3000UL; - - SYSTEM_INFO sysInfo = default; - sysInfo.NumberOfProcessors = 1; - - JOBOBJECT_CPU_RATE_CONTROL_INFORMATION cpuLimit = default; - // This is customized to force the private method GetGuaranteedCpuUnits // to set the GuaranteedCpuUnits and MaximumCpuUnits to 1.0. - cpuLimit.ControlFlags = 4; - cpuLimit.CpuRate = 7_000; - - JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accountingInfo = default; - accountingInfo.TotalKernelTime = 1000; - accountingInfo.TotalUserTime = 1000; - - JOBOBJECT_EXTENDED_LIMIT_INFORMATION limitInfo = default; - limitInfo.JobMemoryLimit = new UIntPtr(0); + _cpuLimit.ControlFlags = 4; - ProcessInfo.APP_MEMORY_INFORMATION appMemoryInfo = default; - appMemoryInfo.TotalCommitUsage = 3000UL; + _limitInfo.JobMemoryLimit = new UIntPtr(0); - var memoryInfoMock = new Mock(); - memoryInfoMock.Setup(m => m.GetMemoryStatus()).Returns(memStatus); - - var systemInfoMock = new Mock(); - systemInfoMock.Setup(s => s.GetSystemInfo()).Returns(sysInfo); - - var processInfoMock = new Mock(); - processInfoMock.Setup(p => p.GetCurrentAppMemoryInfo()).Returns(appMemoryInfo); - - var jobHandleMock = new Mock(); - jobHandleMock.Setup(j => j.GetJobCpuLimitInfo()).Returns(cpuLimit); - jobHandleMock.Setup(j => j.GetBasicAccountingInfo()).Returns(accountingInfo); - jobHandleMock.Setup(j => j.GetExtendedLimitInfo()).Returns(limitInfo); + _appMemoryInfo.TotalCommitUsage = 3000UL; var source = new WindowsContainerSnapshotProvider( - memoryInfoMock.Object, - systemInfoMock.Object, - processInfoMock.Object, - () => jobHandleMock.Object); + _memoryInfoMock.Object, + _systemInfoMock.Object, + _processInfoMock.Object, + _logger, + _meterFactory.Object, + () => _jobHandleMock.Object, + new FakeTimeProvider(), + new()); + var data = source.GetSnapshot(); - Assert.Equal(accountingInfo.TotalKernelTime, data.KernelTimeSinceStart.Ticks); - Assert.Equal(accountingInfo.TotalUserTime, data.UserTimeSinceStart.Ticks); + Assert.Equal(_accountingInfo.TotalKernelTime, data.KernelTimeSinceStart.Ticks); + Assert.Equal(_accountingInfo.TotalUserTime, data.UserTimeSinceStart.Ticks); Assert.Equal(1.0, source.Resources.GuaranteedCpuUnits); Assert.Equal(1.0, source.Resources.MaximumCpuUnits); - Assert.Equal(memStatus.TotalPhys, source.Resources.GuaranteedMemoryInBytes); - Assert.Equal(memStatus.TotalPhys, source.Resources.MaximumMemoryInBytes); - Assert.Equal(appMemoryInfo.TotalCommitUsage, data.MemoryUsageInBytes); + Assert.Equal(_memStatus.TotalPhys, source.Resources.GuaranteedMemoryInBytes); + Assert.Equal(_memStatus.TotalPhys, source.Resources.MaximumMemoryInBytes); + Assert.Equal(_appMemoryInfo.TotalCommitUsage, data.MemoryUsageInBytes); Assert.True(data.MemoryUsageInBytes > 0); } + + [Fact] + public void SnapshotProvider_EmitsCpuMetrics() + { + // Simulating 10% CPU usage (2 CPUs, 2000 ticks initially, 4000 ticks after 1 ms): + JOBOBJECT_BASIC_ACCOUNTING_INFORMATION updatedAccountingInfo = default; + updatedAccountingInfo.TotalKernelTime = 2500; + updatedAccountingInfo.TotalUserTime = 1500; + + _jobHandleMock.SetupSequence(j => j.GetBasicAccountingInfo()) + .Returns(() => _accountingInfo) + .Returns(updatedAccountingInfo) + .Returns(updatedAccountingInfo) + .Returns(updatedAccountingInfo) + .Throws(new InvalidOperationException("We shouldn't hit here...")); + + _sysInfo.NumberOfProcessors = 2; + + var fakeClock = new FakeTimeProvider(); + using var meter = new Meter(nameof(SnapshotProvider_EmitsCpuMetrics)); + var meterFactoryMock = new Mock(); + meterFactoryMock.Setup(x => x.Create(It.IsAny())) + .Returns(meter); + using var metricCollector = new MetricCollector(meter, ResourceUtilizationInstruments.CpuUtilization, fakeClock); + + var options = new ResourceMonitoringOptions { CpuConsumptionRefreshInterval = TimeSpan.FromMilliseconds(2) }; + + var snapshotProvider = new WindowsContainerSnapshotProvider( + _memoryInfoMock.Object, + _systemInfoMock.Object, + _processInfoMock.Object, + _logger, + meterFactoryMock.Object, + () => _jobHandleMock.Object, + fakeClock, + options); + + // Step #0 - state in the beginning: + metricCollector.RecordObservableInstruments(); + Assert.NotNull(metricCollector.LastMeasurement); + Assert.True(double.IsNaN(metricCollector.LastMeasurement.Value)); + + // Step #1 - simulate 1 millisecond passing and collect metrics again: + fakeClock.Advance(TimeSpan.FromMilliseconds(1)); + metricCollector.RecordObservableInstruments(); + + Assert.Equal(10, metricCollector.LastMeasurement.Value); // Consumed 10% of the CPU. + + // Step #2 - simulate 1 millisecond passing and collect metrics again: + fakeClock.Advance(TimeSpan.FromMilliseconds(1)); + metricCollector.RecordObservableInstruments(); + + // CPU usage should be the same as before, as we didn't recalculate it: + Assert.Equal(10, metricCollector.LastMeasurement.Value); // Still consuming 10% as gauge wasn't updated. + + // Step #3 - simulate 1 millisecond passing and collect metrics again: + fakeClock.Advance(TimeSpan.FromMilliseconds(1)); + metricCollector.RecordObservableInstruments(); + + // CPU usage should be the same as before, as we're not simulating any CPU usage: + Assert.Equal(10, metricCollector.LastMeasurement.Value); // Consumed 10% of the CPU. + } + + [Fact] + public void SnapshotProvider_EmitsMemoryMetrics() + { + _appMemoryInfo.TotalCommitUsage = 200UL; + + ProcessInfo.APP_MEMORY_INFORMATION updatedAppMemoryInfo = default; + updatedAppMemoryInfo.TotalCommitUsage = 600UL; + _processInfoMock.SetupSequence(p => p.GetCurrentAppMemoryInfo()) + .Returns(() => _appMemoryInfo) + .Returns(updatedAppMemoryInfo) + .Throws(new InvalidOperationException("We shouldn't hit here...")); + + var fakeClock = new FakeTimeProvider(); + using var meter = new Meter(nameof(SnapshotProvider_EmitsMemoryMetrics)); + var meterFactoryMock = new Mock(); + meterFactoryMock.Setup(x => x.Create(It.IsAny())) + .Returns(meter); + using var metricCollector = new MetricCollector(meter, ResourceUtilizationInstruments.MemoryUtilization, fakeClock); + + var options = new ResourceMonitoringOptions { MemoryConsumptionRefreshInterval = TimeSpan.FromMilliseconds(2) }; + + var snapshotProvider = new WindowsContainerSnapshotProvider( + _memoryInfoMock.Object, + _systemInfoMock.Object, + _processInfoMock.Object, + _logger, + meterFactoryMock.Object, + () => _jobHandleMock.Object, + fakeClock, + options); + + // Step #0 - state in the beginning: + metricCollector.RecordObservableInstruments(); + Assert.NotNull(metricCollector.LastMeasurement?.Value); + Assert.Equal(10, metricCollector.LastMeasurement.Value); // Consuming 10% of the memory initially. + + // Step #1 - simulate 1 millisecond passing and collect metrics again: + fakeClock.Advance(options.MemoryConsumptionRefreshInterval - TimeSpan.FromMilliseconds(1)); + metricCollector.RecordObservableInstruments(); + Assert.Equal(10, metricCollector.LastMeasurement.Value); // Still consuming 10% as gauge wasn't updated. + + // Step #2 - simulate 2 milliseconds passing and collect metrics again: + fakeClock.Advance(TimeSpan.FromMilliseconds(1)); + metricCollector.RecordObservableInstruments(); + Assert.Equal(30, metricCollector.LastMeasurement.Value); // Consuming 30% of the memory afterwards. + } + + [Fact] + public void SnapshotProvider_EmitsLogRecord() + { + var snapshotProvider = new WindowsContainerSnapshotProvider( + _memoryInfoMock.Object, + _systemInfoMock.Object, + _processInfoMock.Object, + _logger, + _meterFactory.Object, + () => _jobHandleMock.Object, + new FakeTimeProvider(), + new()); + + var logRecords = _logger.Collector.GetSnapshot(); + var logRecord = Assert.Single(logRecords); + Assert.StartsWith("Resource Monitoring is running inside a Job Object", logRecord.Message); + } + + [Fact] + public void Provider_Throws_WhenLoggerIsNull() + { + // This is a synthetic test to have full test coverage, + // using [ExcludeFromCodeCoverage] on a constructor doesn't cover ": this(...)" call. + Assert.Throws(() => + new WindowsContainerSnapshotProvider(null!, _meterFactory.Object, Microsoft.Extensions.Options.Options.Create(new()))); + } } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsSnapshotProviderTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsSnapshotProviderTests.cs index 9c70139c02c..7dd0beb18eb 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsSnapshotProviderTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsSnapshotProviderTests.cs @@ -2,8 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Time.Testing; using Microsoft.TestUtilities; using Moq; using Xunit; @@ -13,11 +17,25 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Test; [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "Windows specific.")] public sealed class WindowsSnapshotProviderTests { + private readonly Mock _meterFactoryMock; + private readonly FakeLogger _fakeLogger; + private readonly IOptions _options; + + public WindowsSnapshotProviderTests() + { + _options = Options.Options.Create(new()); + using var meter = new Meter(nameof(BasicConstructor)); + _meterFactoryMock = new Mock(); + _meterFactoryMock.Setup(x => x.Create(It.IsAny())) + .Returns(meter); + + _fakeLogger = new FakeLogger(); + } + [ConditionalFact] public void BasicConstructor() { - var loggerMock = new Mock>(); - var provider = new WindowsSnapshotProvider(loggerMock.Object); + var provider = new WindowsSnapshotProvider(_fakeLogger, _meterFactoryMock.Object, _options); var memoryStatus = new MemoryInfo().GetMemoryStatus(); Assert.Equal(Environment.ProcessorCount, provider.Resources.GuaranteedCpuUnits); @@ -29,10 +47,105 @@ public void BasicConstructor() [ConditionalFact] public void GetSnapshot_DoesNotThrowExceptions() { - var loggerMock = new Mock>(); - var provider = new WindowsSnapshotProvider(loggerMock.Object); + var provider = new WindowsSnapshotProvider(_fakeLogger, _meterFactoryMock.Object, _options); var exception = Record.Exception(() => provider.GetSnapshot()); Assert.Null(exception); } + + [ConditionalFact] + public void SnapshotProvider_EmitsLogRecord() + { + var provider = new WindowsSnapshotProvider(_fakeLogger, _meterFactoryMock.Object, _options); + var logRecords = _fakeLogger.Collector.GetSnapshot(); + var logRecord = Assert.Single(logRecords); + Assert.StartsWith("Resource Monitoring is running outside of Job Object", logRecord.Message); + } + + [ConditionalFact] + public void SnapshotProvider_EmitsCpuMetrics() + { + var fakeClock = new FakeTimeProvider(); + var cpuTicks = 500L; + var options = new ResourceMonitoringOptions { CpuConsumptionRefreshInterval = TimeSpan.FromMilliseconds(2) }; + using var meter = new Meter(nameof(SnapshotProvider_EmitsCpuMetrics)); + var meterFactoryMock = new Mock(); + meterFactoryMock.Setup(x => x.Create(It.IsAny())).Returns(meter); + + var snapshotProvider = new WindowsSnapshotProvider(_fakeLogger, meterFactoryMock.Object, options, fakeClock, + static () => 2, () => cpuTicks, static () => 0L, static () => 1UL); + + cpuTicks = 1_500L; + + using var metricCollector = new MetricCollector(meter, ResourceUtilizationInstruments.CpuUtilization, fakeClock); + + // Step #0 - state in the beginning: + metricCollector.RecordObservableInstruments(); + Assert.NotNull(metricCollector.LastMeasurement); + Assert.True(double.IsNaN(metricCollector.LastMeasurement.Value)); + + // Step #1 - simulate 1 millisecond passing and collect metrics again: + fakeClock.Advance(TimeSpan.FromMilliseconds(1)); + metricCollector.RecordObservableInstruments(); + Assert.Equal(5, metricCollector.LastMeasurement?.Value); // Consuming 5% of the CPU (2 CPUs, 1000 ticks, 1ms). + + // Step #2 - simulate another 1 millisecond passing and collect metrics again: + fakeClock.Advance(TimeSpan.FromMilliseconds(1)); + metricCollector.RecordObservableInstruments(); + + // CPU usage should be the same as before, as we're not simulating any CPU usage: + Assert.Equal(5, metricCollector.LastMeasurement?.Value); // Still consuming 5% of the CPU + } + + [ConditionalFact] + public void SnapshotProvider_EmitsMemoryMetrics() + { + var fakeClock = new FakeTimeProvider(); + long memoryUsed = 300L; + var options = new ResourceMonitoringOptions { MemoryConsumptionRefreshInterval = TimeSpan.FromMilliseconds(2) }; + using var meter = new Meter(nameof(SnapshotProvider_EmitsMemoryMetrics)); + var meterFactoryMock = new Mock(); + meterFactoryMock.Setup(x => x.Create(It.IsAny())) + .Returns(meter); + + var snapshotProvider = new WindowsSnapshotProvider(_fakeLogger, meterFactoryMock.Object, options, fakeClock, static () => 1, static () => 0, () => memoryUsed, static () => 3000UL); + + using var metricCollector = new MetricCollector(meter, ResourceUtilizationInstruments.MemoryUtilization, fakeClock); + + // Step #0 - state in the beginning: + metricCollector.RecordObservableInstruments(); + Assert.NotNull(metricCollector.LastMeasurement); + Assert.Equal(10, metricCollector.LastMeasurement.Value); // Consuming 5% of the memory initially + + memoryUsed = 900L; // Simulate 30% memory usage. + + // Step #1 - simulate 1 millisecond passing and collect metrics again: + fakeClock.Advance(TimeSpan.FromMilliseconds(1)); + metricCollector.RecordObservableInstruments(); + + Assert.Equal(10, metricCollector.LastMeasurement.Value); // Still consuming 10% as gauge wasn't updated. + + // Step #2 - simulate 1 millisecond passing and collect metrics again: + fakeClock.Advance(TimeSpan.FromMilliseconds(1)); + metricCollector.RecordObservableInstruments(); + + Assert.Equal(30, metricCollector.LastMeasurement.Value); // Consuming 30% of the memory afterwards + + memoryUsed = 3_100L; // Simulate more than 100% memory usage + + // Step #3 - simulate 1 millisecond passing and collect metrics again: + fakeClock.Advance(options.MemoryConsumptionRefreshInterval); + metricCollector.RecordObservableInstruments(); + + // Memory usage should be the same as before, as we're not simulating any CPU usage: + Assert.Equal(100, metricCollector.LastMeasurement.Value); // Consuming 100% of the memory + } + + [ConditionalFact] + public void Provider_Returns_MemoryConsumption() + { + // This is a synthetic test to have full test coverage: + var usage = WindowsSnapshotProvider.GetMemoryUsageInBytes(); + Assert.InRange(usage, 0, long.MaxValue); + } }