Skip to content

Commit

Permalink
Support metrics in ResourceMonitoring for Windows (#5290)
Browse files Browse the repository at this point in the history
Fixes #4477

Support metrics in ResourceMonitoring for Windows
  • Loading branch information
evgenyfedorov2 authored Jul 22, 2024
1 parent ff43443 commit c016405
Show file tree
Hide file tree
Showing 7 changed files with 698 additions and 245 deletions.
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Blittable version of Windows BOOL type. It is convenient in situations where
/// manual marshalling is required, or to avoid overhead of regular bool marshalling.
/// </summary>
/// <remarks>
/// 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 .
/// </remarks>
#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,
}

Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// GlobalMemoryStatusEx.
/// </summary>
/// <param name="memoryStatus">Memory Status structure.</param>
/// <returns>Success or failure.</returns>
[DllImportAttr("kernel32.dll", SetLastError = true)]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
public static unsafe
#if NET8_0_OR_GREATER
partial
#else
extern
#endif
BOOL GlobalMemoryStatusEx(MEMORYSTATUSEX* memoryStatus);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,8 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop;
/// Native memory interop methods.
/// </summary>
[ExcludeFromCodeCoverage]
internal sealed class MemoryInfo : IMemoryInfo
internal sealed partial class MemoryInfo : IMemoryInfo
{
internal MemoryInfo()
{
}

/// <summary>
/// Get the memory status of the host.
/// </summary>
Expand All @@ -24,24 +20,16 @@ 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());
}

return info;
}

private static class SafeNativeMethods
private static partial class SafeNativeMethods
{
/// <summary>
/// GlobalMemoryStatusEx.
/// </summary>
/// <param name="memoryStatus">Memory Status structure.</param>
/// <returns>Success or failure.</returns>
[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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,83 +3,106 @@

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;

/// <summary>
/// A data source acquiring data from the kernel.
/// </summary>
internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider
{
internal TimeProvider TimeProvider = TimeProvider.System;
private const double Hundred = 100.0d;

/// <summary>
/// The memory status.
/// </summary>
private readonly Lazy<MEMORYSTATUSEX> _memoryStatus;

/// <summary>
/// This represents a factory method for creating the JobHandle.
/// </summary>
private readonly Func<IJobHandle> _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; }

/// <summary>
/// Initializes a new instance of the <see cref="WindowsContainerSnapshotProvider"/> class.
/// </summary>
public WindowsContainerSnapshotProvider(ILogger<WindowsContainerSnapshotProvider> logger)
public WindowsContainerSnapshotProvider(
ILogger<WindowsContainerSnapshotProvider> logger,
IMeterFactory meterFactory,
IOptions<ResourceMonitoringOptions> options)
: this(new MemoryInfo(), new SystemInfo(), new ProcessInfoWrapper(), logger, meterFactory,
static () => new JobHandleWrapper(), TimeProvider.System, options.Value)
{
Log.RunningInsideJobObject(logger);

_memoryStatus = new Lazy<MEMORYSTATUSEX>(
new MemoryInfo().GetMemoryStatus,
LazyThreadSafetyMode.ExecutionAndPublication);

var systemInfo = new Lazy<SYSTEM_INFO>(
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);
}

/// <summary>
/// Initializes a new instance of the <see cref="WindowsContainerSnapshotProvider"/> class.
/// </summary>
/// <param name="memoryInfo">A wrapper for the memory information retrieval object.</param>
/// <param name="systemInfoObject">A wrapper for the system information retrieval object.</param>
/// <param name="processInfo">A wrapper for the process info retrieval object.</param>
/// <param name="createJobHandleObject">A factory method that creates <see cref="IJobHandle"/> object.</param>
/// <remarks>This constructor enables the mocking the <see cref="WindowsContainerSnapshotProvider"/> dependencies for the purpose of Unit Testing only.</remarks>
internal WindowsContainerSnapshotProvider(IMemoryInfo memoryInfo, ISystemInfo systemInfoObject, IProcessInfo processInfo, Func<IJobHandle> createJobHandleObject)
/// <remarks>This constructor enables the mocking of <see cref="WindowsContainerSnapshotProvider"/> dependencies for the purpose of Unit Testing only.</remarks>
[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<WindowsContainerSnapshotProvider> logger,
IMeterFactory meterFactory,
Func<IJobHandle> createJobHandleObject,
TimeProvider timeProvider,
ResourceMonitoringOptions options)
{
_memoryStatus = new Lazy<MEMORYSTATUSEX>(memoryInfo.GetMemoryStatus, LazyThreadSafetyMode.ExecutionAndPublication);
var systemInfo = new Lazy<SYSTEM_INFO>(systemInfoObject.GetSystemInfo, LazyThreadSafetyMode.ExecutionAndPublication);
_processInfo = processInfo;
Log.RunningInsideJobObject(logger);

_memoryStatus = new Lazy<MEMORYSTATUSEX>(
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()
Expand All @@ -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<SYSTEM_INFO> 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.
Expand All @@ -115,9 +138,11 @@ private static double GetGuaranteedCpuUnits(IJobHandle jobHandle, Lazy<SYSTEM_IN
cpuRatio = cpuLimit.CpuRate / CpuCycles;
}

var systemInfoValue = systemInfo.GetSystemInfo();

// Multiply the cpu ratio by the number of processors to get you the portion
// of processors used from the system.
return cpuRatio * systemInfo.Value.NumberOfProcessors;
return cpuRatio * systemInfoValue.NumberOfProcessors;
}

/// <summary>
Expand Down Expand Up @@ -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;
}
}
}
Loading

0 comments on commit c016405

Please sign in to comment.