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.Metrics;
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;
 
    /// <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 TimeSpan _cpuRefreshInterval;
    private readonly TimeSpan _memoryRefreshInterval;
    private readonly double _metricValueMultiplier;
 
    private double _memoryLimit;
    private double _cpuLimit;
#pragma warning disable S1450 // Private fields only used as local variables in methods should become local variables. Those will be used once we bring relevant meters.
    private double _memoryRequest;
    private double _cpuRequest;
#pragma warning restore S1450 // Private fields only used as local variables in methods should become local variables
 
    private long _oldCpuUsageTicks;
    private long _oldCpuTimeTicks;
    private DateTimeOffset _refreshAfterCpu;
    private DateTimeOffset _refreshAfterMemory;
    private DateTimeOffset _refreshAfterProcessMemory;
    private long _cachedUsageTickDelta;
    private long _cachedTimeTickDelta;
    private double _processMemoryPercentage;
    private ulong _memoryUsage;
 
    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,
        ResourceQuotaProvider resourceQuotaProvider)
        : this(new ProcessInfo(), logger, meterFactory,
              static () => new JobHandleWrapper(), TimeProvider.System, options.Value, resourceQuotaProvider)
    {
    }
 
    /// <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>
    internal WindowsContainerSnapshotProvider(
        IProcessInfo processInfo,
        ILogger<WindowsContainerSnapshotProvider>? logger,
        IMeterFactory meterFactory,
        Func<IJobHandle> createJobHandleObject,
        TimeProvider timeProvider,
        ResourceMonitoringOptions options,
        ResourceQuotaProvider resourceQuotaProvider)
    {
        _logger = logger ?? NullLogger<WindowsContainerSnapshotProvider>.Instance;
        _logger.RunningInsideJobObject();
 
        _metricValueMultiplier = options.UseZeroToOneRangeForMetrics ? One : Hundred;
 
        _createJobHandleObject = createJobHandleObject;
        _processInfo = processInfo;
 
        _timeProvider = timeProvider;
 
        using IJobHandle jobHandle = _createJobHandleObject();
 
        var quota = resourceQuotaProvider.GetResourceQuota();
        _cpuLimit = quota.MaxCpuInCores;
        _memoryLimit = quota.MaxMemoryInBytes;
        _cpuRequest = quota.BaselineCpuInCores;
        _memoryRequest = quota.BaselineMemoryInBytes;
 
        Resources = new SystemResources(_cpuRequest, _cpuLimit, quota.BaselineMemoryInBytes, quota.MaxMemoryInBytes);
        _logger.SystemResourcesInfo(_cpuLimit, _cpuRequest, quota.MaxMemoryInBytes, quota.BaselineMemoryInBytes);
 
        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
 
        _ = meter.CreateObservableCounter(
            name: ResourceUtilizationInstruments.ContainerCpuTime,
            observeValues: GetCpuTime,
            unit: "s",
            description: "CPU time used by the container.");
 
        _ = meter.CreateObservableGauge(
            name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization,
            observeValue: () => CpuPercentage(_cpuLimit));
 
        _ = meter.CreateObservableGauge(
            name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization,
            observeValue: () => Math.Min(_metricValueMultiplier, MemoryUsage() / _memoryLimit * _metricValueMultiplier));
 
        _ = meter.CreateObservableGauge(
            name: ResourceUtilizationInstruments.ContainerCpuRequestUtilization,
            observeValue: () => CpuPercentage(_cpuRequest));
 
        _ = meter.CreateObservableGauge(
            name: ResourceUtilizationInstruments.ContainerMemoryRequestUtilization,
            observeValue: () => Math.Min(_metricValueMultiplier, MemoryUsage() / _memoryRequest * _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(_cpuLimit));
 
        _ = 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 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, _memoryRequest);
            }
 
            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(double denominator)
    {
        var now = _timeProvider.GetUtcNow();
        lock (_cpuLocker)
        {
            if (now >= _refreshAfterCpu)
            {
                using var jobHandle = _createJobHandleObject();
                var basicAccountingInfo = jobHandle.GetBasicAccountingInfo();
                var currentCpuTicks = basicAccountingInfo.TotalKernelTime + basicAccountingInfo.TotalUserTime;
 
                var usageTickDelta = currentCpuTicks - _oldCpuUsageTicks;
                var timeTickDelta = now.Ticks - _oldCpuTimeTicks;
 
                if (usageTickDelta > 0 && timeTickDelta > 0)
                {
                    _cachedUsageTickDelta = usageTickDelta;
                    _cachedTimeTickDelta = timeTickDelta;
 
                    _logger.CpuContainerUsageData(
                        basicAccountingInfo.TotalKernelTime, basicAccountingInfo.TotalUserTime, _oldCpuUsageTicks, timeTickDelta, denominator, double.NaN);
 
                    _oldCpuUsageTicks = currentCpuTicks;
                    _oldCpuTimeTicks = now.Ticks;
                    _refreshAfterCpu = now.Add(_cpuRefreshInterval);
                }
            }
 
            if (_cachedUsageTickDelta > 0 && _cachedTimeTickDelta > 0)
            {
                var timeTickDeltaWithDenominator = _cachedTimeTickDelta * denominator;
 
                // Don't change calculation order, otherwise precision is lost:
                return Math.Min(_metricValueMultiplier, _cachedUsageTickDelta / timeTickDeltaWithDenominator * _metricValueMultiplier);
            }
 
            return double.NaN;
        }
    }
}