File: Windows\WindowsContainerSnapshotProviderTests.cs
Web Access
Project: src\test\Libraries\Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests\Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj (Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests)
// 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;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.Metrics.Testing;
using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Helpers;
using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Time.Testing;
using Moq;
using VerifyXunit;
using Xunit;
using static Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop.JobObjectInfo;
 
namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Test;
 
[UsesVerify]
public sealed class WindowsContainerSnapshotProviderTests
{
    private const string VerifiedDataDirectory = "Verified";
 
    private readonly FakeLogger<WindowsContainerSnapshotProvider> _logger;
    private readonly MEMORYSTATUSEX _memStatus;
 
    private readonly Mock<IMeterFactory> _meterFactory;
    private readonly Mock<IMemoryInfo> _memoryInfoMock = new();
    private readonly Mock<ISystemInfo> _systemInfoMock = new();
    private readonly Mock<IJobHandle> _jobHandleMock = new();
    private readonly Mock<IProcessInfo> _processInfoMock = new();
 
    private SYSTEM_INFO _sysInfo;
    private JOBOBJECT_BASIC_ACCOUNTING_INFORMATION _accountingInfo;
    private JOBOBJECT_CPU_RATE_CONTROL_INFORMATION _cpuLimit;
    private ulong _appMemoryUsage;
    private JOBOBJECT_EXTENDED_LIMIT_INFORMATION _limitInfo;
 
    public WindowsContainerSnapshotProviderTests()
    {
        using var meter = new Meter(nameof(WindowsContainerSnapshotProvider));
        _meterFactory = new Mock<IMeterFactory>();
        _meterFactory.Setup(x => x.Create(It.IsAny<MeterOptions>()))
            .Returns(meter);
 
        _logger = new FakeLogger<WindowsContainerSnapshotProvider>();
 
        _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);
 
        _appMemoryUsage = 1000UL;
        _processInfoMock.Setup(p => p.GetCurrentProcessMemoryUsage())
            .Returns(() => _appMemoryUsage);
    }
 
    [Theory]
    [InlineData(7_000, 1U, 0.7)]
    [InlineData(10_000, 1U, 1.0)]
    [InlineData(10_000, 2U, 2.0)]
    [InlineData(5_000, 2U, 1.0)]
    public void Resources_GetsCorrectSystemResourcesValues(uint cpuRate, uint numberOfProcessors, double expectedCpuUnits)
    {
        _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;
 
        // 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;
 
        var provider = new WindowsContainerSnapshotProvider(
            _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);
    }
 
    [Fact]
    public void GetSnapshot_ProducesCorrectSnapshot()
    {
        // The ControlFlags is customized to force the private method GetGuaranteedCpuUnits
        // to not use the value of CpuRate in the calculation.
        _cpuLimit.ControlFlags = 1;
 
        var source = new WindowsContainerSnapshotProvider(
            _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(_appMemoryUsage, data.MemoryUsageInBytes);
        Assert.True(data.MemoryUsageInBytes > 0);
    }
 
    [Fact]
    public void GetSnapshot_ProducesCorrectSnapshotForDifferentCpuRate()
    {
        _cpuLimit.ControlFlags = uint.MaxValue; // force all bits in ControlFlags to be 1.
 
        var source = new WindowsContainerSnapshotProvider(
            _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(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(_appMemoryUsage, data.MemoryUsageInBytes);
        Assert.True(data.MemoryUsageInBytes > 0);
    }
 
    [Fact]
    public void GetSnapshot_With_JobMemoryLimit_Set_To_Zero_ProducesCorrectSnapshot()
    {
        // This is customized to force the private method GetGuaranteedCpuUnits
        // to set the GuaranteedCpuUnits and MaximumCpuUnits to 1.0.
        _cpuLimit.ControlFlags = 4;
 
        _limitInfo.JobMemoryLimit = new UIntPtr(0);
 
        _appMemoryUsage = 3000UL;
 
        var source = new WindowsContainerSnapshotProvider(
            _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(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(_appMemoryUsage, data.MemoryUsageInBytes);
        Assert.True(data.MemoryUsageInBytes > 0);
    }
 
    [Theory]
    [InlineData(ResourceUtilizationInstruments.ProcessCpuUtilization)]
    [InlineData(ResourceUtilizationInstruments.ContainerCpuLimitUtilization)]
    public void SnapshotProvider_EmitsCpuMetrics(string instrumentName)
    {
        // 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<IMeterFactory>();
        meterFactoryMock.Setup(x => x.Create(It.IsAny<MeterOptions>()))
            .Returns(meter);
        using var metricCollector = new MetricCollector<double>(meter, instrumentName, 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);
 
        // Step #0 - state in the beginning:
        metricCollector.RecordObservableInstruments();
        Assert.NotNull(metricCollector.LastMeasurement);
        Assert.True(double.IsNaN(metricCollector.LastMeasurement.Value));
 
        // 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.
 
        // 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.
 
        // Step #3 - 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.
    }
 
    [Theory]
    [InlineData(ResourceUtilizationInstruments.ProcessMemoryUtilization)]
    [InlineData(ResourceUtilizationInstruments.ContainerMemoryLimitUtilization)]
    public void SnapshotProvider_EmitsMemoryMetrics(string instrumentName)
    {
        _appMemoryUsage = 200UL;
        ulong updatedAppMemoryUsage = 600UL;
 
        _processInfoMock.SetupSequence(p => p.GetCurrentProcessMemoryUsage())
            .Returns(() => _appMemoryUsage)
            .Returns(updatedAppMemoryUsage)
            .Throws(new InvalidOperationException("We shouldn't hit here..."));
 
        _processInfoMock.SetupSequence(p => p.GetMemoryUsage())
            .Returns(() => _appMemoryUsage)
            .Returns(updatedAppMemoryUsage)
            .Throws(new InvalidOperationException("We shouldn't hit here..."));
 
        var fakeClock = new FakeTimeProvider();
        using var meter = new Meter(nameof(SnapshotProvider_EmitsMemoryMetrics));
        var meterFactoryMock = new Mock<IMeterFactory>();
        meterFactoryMock.Setup(x => x.Create(It.IsAny<MeterOptions>()))
            .Returns(meter);
        using var metricCollector = new MetricCollector<double>(meter, instrumentName, 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);
 
        // Step #0 - state in the beginning:
        metricCollector.RecordObservableInstruments();
        Assert.NotNull(metricCollector.LastMeasurement?.Value);
        Assert.Equal(10, metricCollector.LastMeasurement.Value); // Consuming 10% of the memory initially.
 
        // 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.
 
        // 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]
    public Task 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();
 
        return Verifier.Verify(logRecords).UniqueForRuntime().UseDirectory(VerifiedDataDirectory);
    }
 
    [Fact]
    public void Provider_Creates_Meter_With_Correct_Name()
    {
        var options = Options.Options.Create<ResourceMonitoringOptions>(new());
        using var meterFactory = new TestMeterFactory();
 
        _ = new WindowsContainerSnapshotProvider(
            _memoryInfoMock.Object,
            _systemInfoMock.Object,
            _processInfoMock.Object,
            _logger,
            meterFactory,
            () => _jobHandleMock.Object,
            new FakeTimeProvider(),
            new());
 
        var meter = meterFactory.Meters.Single();
        Assert.Equal(ResourceUtilizationInstruments.MeterName, meter.Name);
    }
}