Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support metrics in ResourceMonitoring for Windows #5290

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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