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);
+ }
}