File: Windows\WindowsContainerSnapshotProvider.cs
Web Access
Project: src\src\Libraries\Microsoft.Extensions.Diagnostics.ResourceMonitoring\Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj (Microsoft.Extensions.Diagnostics.ResourceMonitoring)
// 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.Collections.Generic;
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.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Shared.Instruments;
 
namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows;
 
internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider
{
    private const double One = 1.0d;
    private const double Hundred = 100.0d;
    private const double TicksPerSecondDouble = TimeSpan.TicksPerSecond;
 
    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 object _processMemoryLocker = new();
    private readonly TimeProvider _timeProvider;
    private readonly IProcessInfo _processInfo;
    private readonly ILogger<WindowsContainerSnapshotProvider> _logger;
    private readonly double _memoryLimit;
    private readonly double _cpuLimit;
    private readonly TimeSpan _cpuRefreshInterval;
    private readonly TimeSpan _memoryRefreshInterval;
    private readonly double _metricValueMultiplier;
 
    private long _oldCpuUsageTicks;
    private long _oldCpuTimeTicks;
    private DateTimeOffset _refreshAfterCpu;
    private DateTimeOffset _refreshAfterMemory;
    private DateTimeOffset _refreshAfterProcessMemory;
    private double _cpuPercentage = double.NaN;
    private ulong _memoryUsage;
    private double _processMemoryPercentage;
 
    public SystemResources Resources { get; }
 
    /// <summary>
    /// Initializes a new instance of the <see cref="WindowsContainerSnapshotProvider"/> class.
    /// </summary>
    public WindowsContainerSnapshotProvider(
        ILogger<WindowsContainerSnapshotProvider>? logger,
        IMeterFactory meterFactory,
        IOptions<ResourceMonitoringOptions> options)
        : this(new MemoryInfo(), new SystemInfo(), new ProcessInfo(), logger, meterFactory,
              static () => new JobHandleWrapper(), TimeProvider.System, options.Value)
    {
    }
 
    /// <summary>
    /// Initializes a new instance of the <see cref="WindowsContainerSnapshotProvider"/> class.
    /// </summary>
    /// <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)
    {
        _logger = logger ?? NullLogger<WindowsContainerSnapshotProvider>.Instance;
        _logger.RunningInsideJobObject();
 
        _metricValueMultiplier = options.UseZeroToOneRangeForMetrics ? One : Hundred;
 
        _memoryStatus = new Lazy<MEMORYSTATUSEX>(
            memoryInfo.GetMemoryStatus,
            LazyThreadSafetyMode.ExecutionAndPublication);
        _createJobHandleObject = createJobHandleObject;
        _processInfo = processInfo;
 
        _timeProvider = timeProvider;
 
        using IJobHandle jobHandle = _createJobHandleObject();
 
        ulong memoryLimitLong = GetMemoryLimit(jobHandle);
        _memoryLimit = memoryLimitLong;
        _cpuLimit = GetCpuLimit(jobHandle, systemInfo);
 
        // CPU request (aka guaranteed CPU units) is not supported on Windows, so we set it to the same value as CPU limit (aka maximum CPU units).
        // Memory request (aka guaranteed memory) is not supported on Windows, so we set it to the same value as memory limit (aka maximum memory).
        double cpuRequest = _cpuLimit;
        ulong memoryRequest = memoryLimitLong;
        Resources = new SystemResources(cpuRequest, _cpuLimit, memoryRequest, memoryLimitLong);
        _logger.SystemResourcesInfo(_cpuLimit, cpuRequest, memoryLimitLong, memoryRequest);
 
        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();
        _refreshAfterProcessMemory = _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
        Meter meter = meterFactory.Create(ResourceUtilizationInstruments.MeterName);
#pragma warning restore CA2000 // Dispose objects before losing scope
 
        // Container based metrics:
        _ = meter.CreateObservableCounter(
            name: ResourceUtilizationInstruments.ContainerCpuTime,
            observeValues: GetCpuTime,
            unit: "s",
            description: "CPU time used by the container.");
 
        _ = meter.CreateObservableGauge(
            name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization,
            observeValue: CpuPercentage);
 
        _ = meter.CreateObservableGauge(
            name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization,
            observeValue: () => Math.Min(_metricValueMultiplier, MemoryUsage() / _memoryLimit * _metricValueMultiplier));
 
        _ = meter.CreateObservableUpDownCounter(
            name: ResourceUtilizationInstruments.ContainerMemoryUsage,
            observeValue: () => (long)MemoryUsage(),
            unit: "By",
            description: "Memory usage of the container.");
 
        // Process based metrics:
        _ = meter.CreateObservableGauge(
            name: ResourceUtilizationInstruments.ProcessCpuUtilization,
            observeValue: CpuPercentage);
 
        _ = meter.CreateObservableGauge(
            name: ResourceUtilizationInstruments.ProcessMemoryUtilization,
            observeValue: ProcessMemoryPercentage);
    }
 
    public Snapshot GetSnapshot()
    {
        // Gather the information
        // Cpu kernel and user ticks
        using var jobHandle = _createJobHandleObject();
        var basicAccountingInfo = jobHandle.GetBasicAccountingInfo();
 
        return new Snapshot(
            TimeSpan.FromTicks(_timeProvider.GetUtcNow().Ticks),
            TimeSpan.FromTicks(basicAccountingInfo.TotalKernelTime),
            TimeSpan.FromTicks(basicAccountingInfo.TotalUserTime),
            _processInfo.GetCurrentProcessMemoryUsage());
    }
 
    private static double GetCpuLimit(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.
        const double CpuCycles = 10_000U;
 
        var cpuLimit = jobHandle.GetJobCpuLimitInfo();
        double cpuRatio = 1.0;
        if ((cpuLimit.ControlFlags & (uint)JobObjectInfo.JobCpuRateControlLimit.CpuRateControlEnable) != 0 &&
            (cpuLimit.ControlFlags & (uint)JobObjectInfo.JobCpuRateControlLimit.CpuRateControlHardCap) != 0)
        {
            // The CpuRate is represented as number of cycles during scheduling interval, where
            // a full cpu cycles number would equal 10_000, so for example if the CpuRate is 2_000,
            // that means that the application (or container) is assigned 20% of the total CPU available.
            // So, here we divide the CpuRate by 10_000 to convert it to a ratio (ex: 0.2 for 20% CPU).
            // For more info: https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-jobobject_cpu_rate_control_information?redirectedfrom=MSDN
            cpuRatio = cpuLimit.CpuRate / CpuCycles;
        }
 
        SYSTEM_INFO 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 * systemInfoValue.NumberOfProcessors;
    }
 
    /// <summary>
    /// Gets memory limit of the system.
    /// </summary>
    /// <returns>Memory limit allocated to the system in bytes.</returns>
    private ulong GetMemoryLimit(IJobHandle jobHandle)
    {
        var memoryLimitInBytes = jobHandle.GetExtendedLimitInfo().JobMemoryLimit.ToUInt64();
 
        if (memoryLimitInBytes <= 0)
        {
            MEMORYSTATUSEX memoryStatus = _memoryStatus.Value;
 
            // Technically, the unconstrained limit is memoryStatus.TotalPageFile.
            // Leaving this at physical as it is more understandable to consumers.
            memoryLimitInBytes = memoryStatus.TotalPhys;
        }
 
        return memoryLimitInBytes;
    }
 
    private double ProcessMemoryPercentage()
    {
        DateTimeOffset now = _timeProvider.GetUtcNow();
 
        lock (_processMemoryLocker)
        {
            if (now < _refreshAfterProcessMemory)
            {
                return _processMemoryPercentage;
            }
        }
 
        ulong processMemoryUsage = _processInfo.GetCurrentProcessMemoryUsage();
 
        lock (_processMemoryLocker)
        {
            if (now >= _refreshAfterProcessMemory)
            {
                _processMemoryPercentage = Math.Min(_metricValueMultiplier, processMemoryUsage / _memoryLimit * _metricValueMultiplier);
                _refreshAfterProcessMemory = now.Add(_memoryRefreshInterval);
 
                _logger.ProcessMemoryPercentageData(processMemoryUsage, _memoryLimit, _processMemoryPercentage);
            }
 
            return _processMemoryPercentage;
        }
    }
 
    private ulong MemoryUsage()
    {
        DateTimeOffset now = _timeProvider.GetUtcNow();
 
        lock (_memoryLocker)
        {
            if (now < _refreshAfterMemory)
            {
                return _memoryUsage;
            }
        }
 
        ulong memoryUsage = _processInfo.GetMemoryUsage();
 
        lock (_memoryLocker)
        {
            if (now >= _refreshAfterMemory)
            {
                _memoryUsage = memoryUsage;
                _refreshAfterMemory = now.Add(_memoryRefreshInterval);
                _logger.ContainerMemoryUsageData(_memoryUsage, _memoryLimit);
            }
 
            return _memoryUsage;
        }
    }
 
    private IEnumerable<Measurement<double>> GetCpuTime()
    {
        using IJobHandle jobHandle = _createJobHandleObject();
        var basicAccountingInfo = jobHandle.GetBasicAccountingInfo();
 
        yield return new Measurement<double>(basicAccountingInfo.TotalUserTime / TicksPerSecondDouble,
            [new KeyValuePair<string, object?>("cpu.mode", "user")]);
        yield return new Measurement<double>(basicAccountingInfo.TotalKernelTime / TicksPerSecondDouble,
            [new KeyValuePair<string, object?>("cpu.mode", "system")]);
    }
 
    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) * _cpuLimit;
                if (usageTickDelta > 0 && timeTickDelta > 0)
                {
                    // Don't change calculation order, otherwise precision is lost:
                    _cpuPercentage = Math.Min(_metricValueMultiplier, usageTickDelta / timeTickDelta * _metricValueMultiplier);
 
                    _logger.CpuContainerUsageData(
                        basicAccountingInfo.TotalKernelTime, basicAccountingInfo.TotalUserTime, _oldCpuUsageTicks, timeTickDelta, _cpuLimit, _cpuPercentage);
 
                    _oldCpuUsageTicks = currentCpuTicks;
                    _oldCpuTimeTicks = now.Ticks;
                    _refreshAfterCpu = now.Add(_cpuRefreshInterval);
                }
            }
 
            return _cpuPercentage;
        }
    }
}