From 9552185626b40d6982bd24d8d9cfac2b93f4bc53 Mon Sep 17 00:00:00 2001 From: R9 Fundamentals Date: Mon, 15 Jul 2024 16:54:31 +0200 Subject: [PATCH 01/10] Initial commit --- .../Windows/Interop/MemoryInfo.Native.cs | 34 ++ .../Windows/Interop/MemoryInfo.cs | 21 +- .../WindowsContainerSnapshotProvider.cs | 167 +++++-- .../Windows/WindowsSnapshotProvider.cs | 157 ++++++- ...iagnostics.ResourceMonitoring.Tests.csproj | 1 + .../WindowsContainerSnapshotProviderTests.cs | 428 +++++++++++------- .../Windows/WindowsSnapshotProviderTests.cs | 123 ++++- 7 files changed, 689 insertions(+), 242 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/MemoryInfo.Native.cs 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..8f1198953ba --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/MemoryInfo.Native.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. + +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)] + [return: MarshalAs(UnmanagedType.Bool)] + public static +#if NET8_0_OR_GREATER + partial +#else + extern +#endif + bool GlobalMemoryStatusEx(ref 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..2b7dbf7e1d0 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/MemoryInfo.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/MemoryInfo.cs @@ -10,20 +10,16 @@ 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. /// /// Memory status information. - public unsafe MEMORYSTATUSEX GetMemoryStatus() + public MEMORYSTATUSEX GetMemoryStatus() { MEMORYSTATUSEX info = default; - info.Length = (uint)sizeof(MEMORYSTATUSEX); + info.Length = (uint)Marshal.SizeOf(); if (!SafeNativeMethods.GlobalMemoryStatusEx(ref info)) { Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); @@ -32,16 +28,7 @@ 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); } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs index e00d2bedc8b..e1fdaab2c41 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs @@ -3,9 +3,11 @@ 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; @@ -14,7 +16,7 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows; /// internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider { - internal TimeProvider TimeProvider = TimeProvider.System; + private const double Hundred = 100.0d; /// /// The memory status. @@ -26,60 +28,89 @@ 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) + [ExcludeFromCodeCoverage] + public WindowsContainerSnapshotProvider( + ILogger logger, + IMeterFactory meterFactory, + IOptions options) + : this(new MemoryInfo(), new SystemInfo(), new ProcessInfoWrapper(), logger, meterFactory, + [ExcludeFromCodeCoverage] 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 +121,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 +146,11 @@ private static double GetGuaranteedCpuUnits(IJobHandle jobHandle, Lazy @@ -151,9 +184,63 @@ private ulong GetMemoryUsage() return memoryInfo.TotalCommitUsage; } - [ExcludeFromCodeCoverage] - private JobHandleWrapper CreateJobHandle() + private double MemoryPercentage() { - return new JobHandleWrapper(); + 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() + { + 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..b47cbdb5502 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs @@ -3,36 +3,169 @@ 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; // This is "double" - 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/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj index b7fcf503d96..9c17812b75b 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj @@ -18,6 +18,7 @@ + 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..2157b3efa0c 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs @@ -1,8 +1,11 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) Microsoft Corporation. All Rights Reserved. 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 +14,55 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Test; public sealed class WindowsContainerSnapshotProviderTests { + private readonly Mock _meterFactory; + private readonly FakeLogger _logger; + private readonly MEMORYSTATUSEX _memStatus; + 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 +70,259 @@ 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); - - ProcessInfo.APP_MEMORY_INFORMATION appMemoryInfo = default; - appMemoryInfo.TotalCommitUsage = 3000UL; + _cpuLimit.ControlFlags = 4; - var memoryInfoMock = new Mock(); - memoryInfoMock.Setup(m => m.GetMemoryStatus()).Returns(memStatus); + _limitInfo.JobMemoryLimit = new UIntPtr(0); - 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); + + metricCollector.RecordObservableInstruments(); + + Assert.NotNull(metricCollector.LastMeasurement); + + // 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. + + // 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. + + // 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); + + metricCollector.RecordObservableInstruments(); + var memoryValues = metricCollector.LastMeasurement; + + Assert.NotNull(metricCollector.LastMeasurement?.Value); + Assert.Equal(10, metricCollector.LastMeasurement.Value); // Consuming 10% of the memory initially. + + // 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. + + // 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..abcffbe277d 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,26 @@ 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 +48,104 @@ 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); } + + [Fact] + 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); + } + + [Fact] + 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); + + Assert.Null(metricCollector.LastMeasurement); + + // 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). + + // 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(5, metricCollector.LastMeasurement?.Value); // Still consuming 5% of the CPU + } + + [Fact] + 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_EmitsCpuMetrics)); + 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); + metricCollector.RecordObservableInstruments(); + + Assert.NotNull(metricCollector.LastMeasurement); + Assert.Equal(10, metricCollector.LastMeasurement.Value); // Consuming 5% of the memory initially + + memoryUsed = 900L; // Simulate 30% memory usage. + + // 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. + + // 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 + + // 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 + } + + [Fact] + 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); + } } From 3b7db2e6abb69838f213b6a48e6c4dd50040124a Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:36:59 +0200 Subject: [PATCH 02/10] fix build --- .../Windows/WindowsSnapshotProviderTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 abcffbe277d..ce037c1505d 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsSnapshotProviderTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsSnapshotProviderTests.cs @@ -54,7 +54,7 @@ public void GetSnapshot_DoesNotThrowExceptions() Assert.Null(exception); } - [Fact] + [ConditionalFact] public void SnapshotProvider_EmitsLogRecord() { var provider = new WindowsSnapshotProvider(_fakeLogger, _meterFactoryMock.Object, _options); @@ -63,7 +63,7 @@ public void SnapshotProvider_EmitsLogRecord() Assert.StartsWith("Resource Monitoring is running outside of Job Object", logRecord.Message); } - [Fact] + [ConditionalFact] public void SnapshotProvider_EmitsCpuMetrics() { var fakeClock = new FakeTimeProvider(); @@ -98,7 +98,7 @@ public void SnapshotProvider_EmitsCpuMetrics() Assert.Equal(5, metricCollector.LastMeasurement?.Value); // Still consuming 5% of the CPU } - [Fact] + [ConditionalFact] public void SnapshotProvider_EmitsMemoryMetrics() { var fakeClock = new FakeTimeProvider(); @@ -141,7 +141,7 @@ public void SnapshotProvider_EmitsMemoryMetrics() Assert.Equal(100, metricCollector.LastMeasurement.Value); // Consuming 100% of the memory } - [Fact] + [ConditionalFact] public void Provider_Returns_MemoryConsumption() { // This is a synthetic test to have full test coverage: From e82940c8b720f2cd98a5095412a342d0b7ad1aa8 Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Mon, 15 Jul 2024 18:11:39 +0200 Subject: [PATCH 03/10] improve comments --- .../Windows/WindowsSnapshotProvider.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs index b47cbdb5502..c53b00886f5 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs @@ -59,7 +59,7 @@ internal WindowsSnapshotProvider( _timeProvider = timeProvider; _getCpuTicksFunc = getCpuTicksFunc; _getMemoryUsageFunc = getMemoryUsageFunc; - _totalMemory = totalMemory; // This is "double" - to calculate percentage later + _totalMemory = totalMemory; // "long" totalMemory => "double" _totalMemory - to calculate percentage later _oldCpuUsageTicks = getCpuTicksFunc(); _oldCpuTimeTicks = timeProvider.GetUtcNow().Ticks; @@ -96,8 +96,7 @@ internal static long GetCpuTicks() return process.TotalProcessorTime.Ticks; } - internal static int GetCpuUnits() - => Environment.ProcessorCount; + internal static int GetCpuUnits() => Environment.ProcessorCount; internal static long GetMemoryUsageInBytes() { From 675ac6aa50dea5474f103087715ee4b939dcaf37 Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Mon, 15 Jul 2024 18:14:21 +0200 Subject: [PATCH 04/10] Fix csproj --- ...rosoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj index 9c17812b75b..b7fcf503d96 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj @@ -18,7 +18,6 @@ - From 16026006d46a5269ae4076e0aa22e8c02e8d8977 Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Mon, 15 Jul 2024 18:20:57 +0200 Subject: [PATCH 05/10] fix comments --- .../Windows/WindowsContainerSnapshotProvider.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs index e1fdaab2c41..a966284eee5 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs @@ -11,16 +11,10 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows; -/// -/// A data source acquiring data from the kernel. -/// internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider { private const double Hundred = 100.0d; - /// - /// The memory status. - /// private readonly Lazy _memoryStatus; /// @@ -49,13 +43,12 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider /// /// Initializes a new instance of the class. /// - [ExcludeFromCodeCoverage] public WindowsContainerSnapshotProvider( ILogger logger, IMeterFactory meterFactory, IOptions options) : this(new MemoryInfo(), new SystemInfo(), new ProcessInfoWrapper(), logger, meterFactory, - [ExcludeFromCodeCoverage] static () => new JobHandleWrapper(), TimeProvider.System, options.Value) + static () => new JobHandleWrapper(), TimeProvider.System, options.Value) { } @@ -110,7 +103,6 @@ internal WindowsContainerSnapshotProvider( _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.CpuUtilization, observeValue: CpuPercentage); _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.MemoryUtilization, observeValue: MemoryPercentage); - } public Snapshot GetSnapshot() From e21d4570ab9f5ad2691bb59b0e651429ceb806fb Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Mon, 15 Jul 2024 18:44:51 +0200 Subject: [PATCH 06/10] cosmetic fixes --- .../WindowsContainerSnapshotProviderTests.cs | 35 ++++++++----------- .../Windows/WindowsSnapshotProviderTests.cs | 32 ++++++++--------- 2 files changed, 30 insertions(+), 37 deletions(-) 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 2157b3efa0c..7057fd5d810 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs @@ -1,4 +1,5 @@ -// Copyright (c) Microsoft Corporation. All Rights Reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics.Metrics; @@ -14,9 +15,10 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Test; public sealed class WindowsContainerSnapshotProviderTests { - private readonly Mock _meterFactory; 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(); @@ -205,10 +207,7 @@ public void SnapshotProvider_EmitsCpuMetrics() .Returns(meter); using var metricCollector = new MetricCollector(meter, ResourceUtilizationInstruments.CpuUtilization, fakeClock); - var options = new ResourceMonitoringOptions - { - CpuConsumptionRefreshInterval = TimeSpan.FromMilliseconds(2) - }; + var options = new ResourceMonitoringOptions { CpuConsumptionRefreshInterval = TimeSpan.FromMilliseconds(2) }; var snapshotProvider = new WindowsContainerSnapshotProvider( _memoryInfoMock.Object, @@ -220,24 +219,25 @@ public void SnapshotProvider_EmitsCpuMetrics() fakeClock, options); + // Step #0 - state in the beginning: metricCollector.RecordObservableInstruments(); - Assert.NotNull(metricCollector.LastMeasurement); + Assert.True(double.IsNaN(metricCollector.LastMeasurement.Value)); - // Simulate 1 millisecond passing and collect metrics again: + // 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. - // Simulate 1 millisecond passing and collect metrics again: + // 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. - // Simulate 1 millisecond passing and collect metrics again: + // Step #3 - simulate 1 millisecond passing and collect metrics again: fakeClock.Advance(TimeSpan.FromMilliseconds(1)); metricCollector.RecordObservableInstruments(); @@ -264,10 +264,7 @@ public void SnapshotProvider_EmitsMemoryMetrics() .Returns(meter); using var metricCollector = new MetricCollector(meter, ResourceUtilizationInstruments.MemoryUtilization, fakeClock); - var options = new ResourceMonitoringOptions - { - MemoryConsumptionRefreshInterval = TimeSpan.FromMilliseconds(2) - }; + var options = new ResourceMonitoringOptions { MemoryConsumptionRefreshInterval = TimeSpan.FromMilliseconds(2) }; var snapshotProvider = new WindowsContainerSnapshotProvider( _memoryInfoMock.Object, @@ -279,24 +276,20 @@ public void SnapshotProvider_EmitsMemoryMetrics() fakeClock, options); + // Step #0 - state in the beginning: metricCollector.RecordObservableInstruments(); - var memoryValues = metricCollector.LastMeasurement; - Assert.NotNull(metricCollector.LastMeasurement?.Value); Assert.Equal(10, metricCollector.LastMeasurement.Value); // Consuming 10% of the memory initially. - // Simulate 1 millisecond passing and collect metrics again: + // 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. - // Simulate 2 milliseconds passing and collect metrics again: + // 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] 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 ce037c1505d..7dd0beb18eb 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsSnapshotProviderTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsSnapshotProviderTests.cs @@ -30,7 +30,6 @@ public WindowsSnapshotProviderTests() .Returns(meter); _fakeLogger = new FakeLogger(); - } [ConditionalFact] @@ -67,30 +66,30 @@ public void SnapshotProvider_EmitsLogRecord() 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); + 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); + 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); - Assert.Null(metricCollector.LastMeasurement); + // Step #0 - state in the beginning: + metricCollector.RecordObservableInstruments(); + Assert.NotNull(metricCollector.LastMeasurement); + Assert.True(double.IsNaN(metricCollector.LastMeasurement.Value)); - // Simulate 1 millisecond passing and collect metrics again: + // 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). - // Simulate 1 millisecond passing and collect metrics again: + // Step #2 - simulate another 1 millisecond passing and collect metrics again: fakeClock.Advance(TimeSpan.FromMilliseconds(1)); metricCollector.RecordObservableInstruments(); @@ -102,30 +101,31 @@ public void SnapshotProvider_EmitsCpuMetrics() 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_EmitsCpuMetrics)); + 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); - metricCollector.RecordObservableInstruments(); + // 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. - // Simulate 1 millisecond passing and collect metrics again: + // 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. - // Simulate 1 millisecond passing and collect metrics again: + // Step #2 - simulate 1 millisecond passing and collect metrics again: fakeClock.Advance(TimeSpan.FromMilliseconds(1)); metricCollector.RecordObservableInstruments(); @@ -133,7 +133,7 @@ public void SnapshotProvider_EmitsMemoryMetrics() memoryUsed = 3_100L; // Simulate more than 100% memory usage - // Simulate 1 millisecond passing and collect metrics again: + // Step #3 - simulate 1 millisecond passing and collect metrics again: fakeClock.Advance(options.MemoryConsumptionRefreshInterval); metricCollector.RecordObservableInstruments(); From 6e850d3d72aa263de91accee92f654a81ff0e637 Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Tue, 16 Jul 2024 10:59:04 +0200 Subject: [PATCH 07/10] PR comments --- .../Windows/Interop/BOOL.cs | 20 +++++++++++++++++++ .../Windows/Interop/MemoryInfo.Native.cs | 5 ++--- .../Windows/Interop/MemoryInfo.cs | 6 +++--- 3 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/BOOL.cs 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..422d7d93027 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/BOOL.cs @@ -0,0 +1,20 @@ +// 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 . +/// +internal enum BOOL : int +{ + 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 index 8f1198953ba..bad44e24701 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/MemoryInfo.Native.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/MemoryInfo.Native.cs @@ -22,13 +22,12 @@ private static partial class SafeNativeMethods /// Success or failure. [DllImportAttr("kernel32.dll", SetLastError = true)] [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] - [return: MarshalAs(UnmanagedType.Bool)] - public static + public static unsafe #if NET8_0_OR_GREATER partial #else extern #endif - bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX memoryStatus); + 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 2b7dbf7e1d0..1d8a95ea388 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/MemoryInfo.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/MemoryInfo.cs @@ -16,11 +16,11 @@ internal sealed partial class MemoryInfo : IMemoryInfo /// Get the memory status of the host. /// /// Memory status information. - public MEMORYSTATUSEX GetMemoryStatus() + public unsafe MEMORYSTATUSEX GetMemoryStatus() { MEMORYSTATUSEX info = default; - info.Length = (uint)Marshal.SizeOf(); - if (!SafeNativeMethods.GlobalMemoryStatusEx(ref info)) + info.Length = (uint)sizeof(MEMORYSTATUSEX); + if (SafeNativeMethods.GlobalMemoryStatusEx(&info) != BOOL.TRUE) { Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); } From 3486254cdacc03f764dc2679a8736b293893a9a3 Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Tue, 16 Jul 2024 11:00:03 +0200 Subject: [PATCH 08/10] update copyright --- .../Windows/Interop/MemoryInfo.Native.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index bad44e24701..cd6e82b4c94 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/MemoryInfo.Native.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/MemoryInfo.Native.cs @@ -1,4 +1,5 @@ -// Copyright (c) Microsoft Corporation. All Rights Reserved. +// 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; From ae618967c494fef3501d4305d2d1f81742070d17 Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Tue, 16 Jul 2024 11:13:09 +0200 Subject: [PATCH 09/10] suppress a warning --- .../Windows/Interop/BOOL.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/BOOL.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/BOOL.cs index 422d7d93027..0b9a922d6df 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/BOOL.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/BOOL.cs @@ -12,7 +12,9 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop; /// 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, From fe83ff50968fcbbff3731754f6b612b75db1ee78 Mon Sep 17 00:00:00 2001 From: Evgeny Fedorov <25526458+evgenyfedorov2@users.noreply.github.com> Date: Wed, 17 Jul 2024 10:52:23 +0200 Subject: [PATCH 10/10] Add comments --- .../Windows/Interop/MemoryInfo.cs | 1 + 1 file changed, 1 insertion(+) 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 1d8a95ea388..5f3676bbb1a 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/MemoryInfo.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Interop/MemoryInfo.cs @@ -30,5 +30,6 @@ public unsafe MEMORYSTATUSEX GetMemoryStatus() private static partial class SafeNativeMethods { + // the class is partial and empty for source gen to work correctly for GlobalMemoryStatusEx } }