File: Linux\AcceptanceTest.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.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Metrics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.Testing;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using Microsoft.TestUtilities;
using Xunit;
 
namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test;
 
public sealed class AcceptanceTest
{
    [ConditionalFact]
    [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")]
    public void Adding_Linux_Resource_Utilization_Allows_To_Query_Snapshot_Provider()
    {
        using var services = new ServiceCollection()
            .AddResourceMonitoring()
            .BuildServiceProvider();
 
        var provider = services.GetRequiredService<ISnapshotProvider>();
 
        Assert.NotEqual(default, provider.Resources);
        Assert.NotEqual(default, provider.GetSnapshot());
    }
 
    [ConditionalFact]
    [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")]
    [SuppressMessage("Minor Code Smell", "S3257:Declarations and initializations should be as concise as possible", Justification = "Broken analyzer.")]
    public void Adding_Linux_Resource_Utilization_Can_Be_Configured_With_Section()
    {
        var cpuRefresh = TimeSpan.FromMinutes(13);
        var memoryRefresh = TimeSpan.FromMinutes(14);
 
        var config = new KeyValuePair<string, string?>[]
            {
                new($"{nameof(ResourceMonitoringOptions)}:{nameof(ResourceMonitoringOptions.CpuConsumptionRefreshInterval)}", cpuRefresh.ToString()),
                new($"{nameof(ResourceMonitoringOptions)}:{nameof(ResourceMonitoringOptions.MemoryConsumptionRefreshInterval)}", memoryRefresh.ToString()),
            };
 
        var section = new ConfigurationBuilder()
            .AddInMemoryCollection(config)
            .Build()
            .GetSection(nameof(ResourceMonitoringOptions));
 
        using var services = new ServiceCollection()
            .AddResourceMonitoring(x => x.ConfigureMonitor(section))
            .BuildServiceProvider();
 
        var options = services.GetRequiredService<IOptions<ResourceMonitoringOptions>>();
 
        Assert.NotNull(options.Value);
        Assert.Equal(cpuRefresh, options.Value.CpuConsumptionRefreshInterval);
        Assert.Equal(memoryRefresh, options.Value.MemoryConsumptionRefreshInterval);
    }
 
    [ConditionalFact]
    [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")]
    public void Adding_Linux_Resource_Utilization_Can_Be_Configured_With_Action()
    {
        var cpuRefresh = TimeSpan.FromMinutes(13);
        var memoryRefresh = TimeSpan.FromMinutes(14);
 
        using var services = new ServiceCollection()
            .AddResourceMonitoring(x => x.ConfigureMonitor(options =>
            {
                options.CpuConsumptionRefreshInterval = cpuRefresh;
                options.MemoryConsumptionRefreshInterval = memoryRefresh;
            }))
            .BuildServiceProvider();
 
        var options = services.GetRequiredService<IOptions<ResourceMonitoringOptions>>();
 
        Assert.NotNull(options.Value);
        Assert.Equal(cpuRefresh, options.Value.CpuConsumptionRefreshInterval);
        Assert.Equal(memoryRefresh, options.Value.MemoryConsumptionRefreshInterval);
    }
 
    [ConditionalFact]
    [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")]
    [SuppressMessage("Minor Code Smell", "S3257:Declarations and initializations should be as concise as possible", Justification = "Broken analyzer.")]
    public void Adding_Linux_Resource_Utilization_With_Section_Registers_SnapshotProvider_Cgroupv1()
    {
        var cpuRefresh = TimeSpan.FromMinutes(13);
        var memoryRefresh = TimeSpan.FromMinutes(14);
 
        var config = new KeyValuePair<string, string?>[]
            {
                new($"{nameof(ResourceMonitoringOptions)}:{nameof(ResourceMonitoringOptions.CpuConsumptionRefreshInterval)}", cpuRefresh.ToString()),
                new($"{nameof(ResourceMonitoringOptions)}:{nameof(ResourceMonitoringOptions.MemoryConsumptionRefreshInterval)}", memoryRefresh.ToString()),
            };
 
        var section = new ConfigurationBuilder()
            .AddInMemoryCollection(config)
            .Build()
            .GetSection(nameof(ResourceMonitoringOptions));
 
        using var services = new ServiceCollection()
            .AddSingleton<IUserHz>(new FakeUserHz(100))
            .AddSingleton<IFileSystem>(new HardcodedValueFileSystem(new Dictionary<FileInfo, string>
            {
                { new FileInfo("/sys/fs/cgroup/memory/memory.limit_in_bytes"), "100000" },
                { new FileInfo("/proc/stat"), "cpu  10 10 10 10 10 10 10 10 10 10"},
                { new FileInfo("/sys/fs/cgroup/cpuacct/cpuacct.usage"), "102312"},
                { new FileInfo("/proc/meminfo"), "MemTotal: 102312 kB"},
                { new FileInfo("/sys/fs/cgroup/cpuset/cpuset.cpus"), "0-19"},
                { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_quota_us"), "12"},
                { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_period_us"), "6"},
                { new FileInfo("/sys/fs/cgroup/cpu/cpu.shares"), "1024"}
            }))
            .AddResourceMonitoring(x => x.ConfigureMonitor(section))
 
            // Ingesting LinuxUtilizationParser with cgroup v1 support.
            .Replace(ServiceDescriptor.Singleton<ILinuxUtilizationParser, LinuxUtilizationParserCgroupV1>())
            .BuildServiceProvider();
 
        var provider = services.GetService<ISnapshotProvider>();
        Assert.NotNull(provider);
        Assert.Equal(1, provider.Resources.GuaranteedCpuUnits); // hack to make hardcoded calculation in resource utilization main package work.
        Assert.Equal(2.0d, provider.Resources.MaximumCpuUnits); // read from cpuset.cpus
        Assert.Equal(100_000UL, provider.Resources.GuaranteedMemoryInBytes); // read from memory.limit_in_bytes
 
        // The main usage of this library is containers.
        // To not break the contaract with the main package, we need to set the maximum memory to the same value as the guaranteed memory.
        Assert.Equal(100_000UL, provider.Resources.MaximumMemoryInBytes);
    }
 
    [ConditionalFact]
    [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")]
    [SuppressMessage("Minor Code Smell", "S3257:Declarations and initializations should be as concise as possible", Justification = "Broken analyzer.")]
    public void Adding_Linux_Resource_Utilization_With_Section_Registers_SnapshotProvider_Cgroupv2()
    {
        var cpuRefresh = TimeSpan.FromMinutes(13);
        var memoryRefresh = TimeSpan.FromMinutes(14);
 
        var config = new KeyValuePair<string, string?>[]
            {
                new($"{nameof(ResourceMonitoringOptions)}:{nameof(ResourceMonitoringOptions.CpuConsumptionRefreshInterval)}", cpuRefresh.ToString()),
                new($"{nameof(ResourceMonitoringOptions)}:{nameof(ResourceMonitoringOptions.MemoryConsumptionRefreshInterval)}", memoryRefresh.ToString()),
            };
 
        var section = new ConfigurationBuilder()
            .AddInMemoryCollection(config)
            .Build()
            .GetSection(nameof(ResourceMonitoringOptions));
 
        using var services = new ServiceCollection()
            .AddSingleton<IUserHz>(new FakeUserHz(100))
            .AddSingleton<IFileSystem>(new HardcodedValueFileSystem(new Dictionary<FileInfo, string>
            {
                { new FileInfo("/proc/stat"), "cpu  10 10 10 10 10 10 10 10 10 10"},
                { new FileInfo("/sys/fs/cgroup/cpu.stat"), "usage_usec 102312"},
                { new FileInfo("/proc/meminfo"), "MemTotal: 102312 kB"},
                { new FileInfo("/sys/fs/cgroup/cpuset.cpus.effective"), "0-1"},
                { new FileInfo("/sys/fs/cgroup/cpu.max"), "20000 100000"},
                { new FileInfo("/sys/fs/cgroup/cpu.weight"), "4"},
                { new FileInfo("/sys/fs/cgroup/memory.max"), "100000" }
            }))
            .AddResourceMonitoring(x => x.ConfigureMonitor(section))
 
            // Ingesting LinuxUtilizationParser with cgroup v2 support.
            .Replace(ServiceDescriptor.Singleton<ILinuxUtilizationParser, LinuxUtilizationParserCgroupV2>())
            .BuildServiceProvider();
 
        var provider = services.GetService<ISnapshotProvider>();
        Assert.NotNull(provider);
        Assert.Equal(0.1, Math.Round(provider.Resources.GuaranteedCpuUnits, 1)); // hack to make hardcoded calculation in resource utilization main package work.
        Assert.Equal(0.2d, Math.Round(provider.Resources.MaximumCpuUnits, 1)); // read from cpuset.cpus
        Assert.Equal(100_000UL, provider.Resources.GuaranteedMemoryInBytes); // read from memory.max
 
        // The main usage of this library is containers.
        // To not break the contract with the main package, we need to set the maximum memory value to the guaranteed memory.
        Assert.Equal(100_000UL, provider.Resources.MaximumMemoryInBytes);
    }
 
    [ConditionalFact]
    [CombinatorialData]
    [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")]
    public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgroupsv1()
    {
        var cpuRefresh = TimeSpan.FromMinutes(13);
        var memoryRefresh = TimeSpan.FromMinutes(14);
        var fileSystem = new HardcodedValueFileSystem(new Dictionary<FileInfo, string>
        {
            { new FileInfo("/sys/fs/cgroup/memory/memory.limit_in_bytes"), "100000" },
            { new FileInfo("/sys/fs/cgroup/memory/memory.usage_in_bytes"), "450000" },
            { new FileInfo("/proc/stat"), "cpu  10 10 10 10 10 10 10 10 10 10"},
            { new FileInfo("/sys/fs/cgroup/cpuacct/cpuacct.usage"), "102312"},
            { new FileInfo("/proc/meminfo"), "MemTotal: 102312 kB"},
            { new FileInfo("/sys/fs/cgroup/cpuset/cpuset.cpus"), "0-19"},
            { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_quota_us"), "24"},
            { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_period_us"), "6"},
            { new FileInfo("/sys/fs/cgroup/cpu/cpu.shares"), "2048"},
            { new FileInfo("/sys/fs/cgroup/memory/memory.stat"), "total_inactive_file 100"},
        });
 
        using var listener = new MeterListener();
        var clock = new FakeTimeProvider(DateTimeOffset.UtcNow);
        var cpuFromGauge = 0.0d;
        var cpuLimitFromGauge = 0.0d;
        var cpuRequestFromGauge = 0.0d;
        var memoryFromGauge = 0.0d;
        var memoryLimitFromGauge = 0.0d;
        using var e = new ManualResetEventSlim();
 
        object? meterScope = null;
        listener.InstrumentPublished = (Instrument instrument, MeterListener meterListener)
            => OnInstrumentPublished(instrument, meterListener, meterScope);
        listener.SetMeasurementEventCallback<double>((m, f, _, _)
            => OnMeasurementReceived(m, f, ref cpuFromGauge, ref cpuLimitFromGauge, ref cpuRequestFromGauge, ref memoryFromGauge, ref memoryLimitFromGauge));
        listener.Start();
 
        using var host = FakeHost.CreateBuilder()
            .ConfigureServices(x =>
                x.AddLogging()
                .AddSingleton<TimeProvider>(clock)
                .AddSingleton<IUserHz>(new FakeUserHz(100))
                .AddSingleton<IFileSystem>(fileSystem)
                .AddSingleton<IResourceUtilizationPublisher>(new GenericPublisher(_ => e.Set()))
                .AddResourceMonitoring())
            .Build();
 
        meterScope = host.Services.GetRequiredService<IMeterFactory>();
        var tracker = host.Services.GetService<IResourceMonitor>();
        Assert.NotNull(tracker);
 
        _ = host.RunAsync();
 
        listener.RecordObservableInstruments();
 
        var utilization = tracker.GetUtilization(TimeSpan.FromSeconds(5));
 
        Assert.Equal(0, utilization.CpuUsedPercentage);
        Assert.Equal(100, utilization.MemoryUsedPercentage);
        Assert.True(double.IsNaN(cpuFromGauge));
 
        // gauge multiplied by 100 because gauges are in range [0, 1], and utilization is in range [0, 100]
        Assert.Equal(utilization.MemoryUsedPercentage, memoryFromGauge * 100);
 
        fileSystem.ReplaceFileContent(new FileInfo("/sys/fs/cgroup/memory/memory.usage_in_bytes"), "50100");
        fileSystem.ReplaceFileContent(new FileInfo("/proc/stat"), "cpu  11 10 10 10 10 10 10 10 10 10");
        fileSystem.ReplaceFileContent(new FileInfo("/sys/fs/cgroup/cpuacct/cpuacct.usage"), "112312");
 
        clock.Advance(TimeSpan.FromSeconds(6));
        listener.RecordObservableInstruments();
 
        e.Wait();
 
        utilization = tracker.GetUtilization(TimeSpan.FromSeconds(5));
 
        Assert.Equal(1, utilization.CpuUsedPercentage);
        Assert.Equal(50, utilization.MemoryUsedPercentage);
        Assert.Equal(0.5, cpuLimitFromGauge * 100);
        Assert.Equal(utilization.CpuUsedPercentage, cpuRequestFromGauge * 100);
        Assert.Equal(utilization.MemoryUsedPercentage, memoryLimitFromGauge * 100);
        Assert.Equal(utilization.CpuUsedPercentage, cpuFromGauge * 100);
        Assert.Equal(utilization.MemoryUsedPercentage, memoryFromGauge * 100);
 
        return Task.CompletedTask;
    }
 
    [ConditionalFact]
    [CombinatorialData]
    [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")]
    public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgroupsv2()
    {
        var cpuRefresh = TimeSpan.FromMinutes(13);
        var memoryRefresh = TimeSpan.FromMinutes(14);
        var fileSystem = new HardcodedValueFileSystem(new Dictionary<FileInfo, string>
        {
            { new FileInfo("/proc/stat"), "cpu  10 10 10 10 10 10 10 10 10 10"},
            { new FileInfo("/sys/fs/cgroup/cpu.stat"), "usage_usec 102"},
            { new FileInfo("/sys/fs/cgroup/memory.max"), "1048576" },
            { new FileInfo("/proc/meminfo"), "MemTotal: 1024 kB"},
            { new FileInfo("/sys/fs/cgroup/cpuset.cpus.effective"), "0-19"},
            { new FileInfo("/sys/fs/cgroup/cpu.max"), "40000 10000"},
            { new FileInfo("/sys/fs/cgroup/cpu.weight"), "79"}, // equals to 2046,9 CPU shares (cgroups v1) which is ~2 CPU units (2 * 1024), so have to use Math.Round() in Assertions down below.
        });
 
        using var listener = new MeterListener();
        var clock = new FakeTimeProvider(DateTimeOffset.UtcNow);
        var cpuFromGauge = 0.0d;
        var cpuLimitFromGauge = 0.0d;
        var cpuRequestFromGauge = 0.0d;
        var memoryFromGauge = 0.0d;
        var memoryLimitFromGauge = 0.0d;
        using var e = new ManualResetEventSlim();
 
        object? meterScope = null;
        listener.InstrumentPublished = (Instrument instrument, MeterListener meterListener)
            => OnInstrumentPublished(instrument, meterListener, meterScope);
        listener.SetMeasurementEventCallback<double>((m, f, _, _)
            => OnMeasurementReceived(m, f, ref cpuFromGauge, ref cpuLimitFromGauge, ref cpuRequestFromGauge, ref memoryFromGauge, ref memoryLimitFromGauge));
        listener.Start();
 
        using var host = FakeHost.CreateBuilder()
            .ConfigureServices(x =>
                x.AddLogging()
                .AddSingleton<TimeProvider>(clock)
                .AddSingleton<IUserHz>(new FakeUserHz(100))
                .AddSingleton<IFileSystem>(fileSystem)
                .AddSingleton<IResourceUtilizationPublisher>(new GenericPublisher(_ => e.Set()))
                .AddResourceMonitoring()
                .Replace(ServiceDescriptor.Singleton<ILinuxUtilizationParser, LinuxUtilizationParserCgroupV2>()))
            .Build();
 
        meterScope = host.Services.GetRequiredService<IMeterFactory>();
        var tracker = host.Services.GetService<IResourceMonitor>();
        Assert.NotNull(tracker);
 
        _ = host.RunAsync();
 
        listener.RecordObservableInstruments();
 
        var utilization = tracker.GetUtilization(TimeSpan.FromSeconds(5));
 
        Assert.Equal(0, utilization.CpuUsedPercentage);
        Assert.Equal(100, utilization.MemoryUsedPercentage);
        Assert.True(double.IsNaN(cpuFromGauge));
 
        // gauge multiplied by 100 because gauges are in range [0, 1], and utilization is in range [0, 100]
        Assert.Equal(utilization.MemoryUsedPercentage, memoryFromGauge * 100);
 
        fileSystem.ReplaceFileContent(new FileInfo("/proc/stat"), "cpu  11 10 10 10 10 10 10 10 10 10");
        fileSystem.ReplaceFileContent(new FileInfo("/sys/fs/cgroup/cpu.stat"), "usage_usec 112");
        fileSystem.ReplaceFileContent(new FileInfo("/sys/fs/cgroup/memory.current"), "524298");
        fileSystem.ReplaceFileContent(new FileInfo("/sys/fs/cgroup/memory.stat"), "inactive_file 10");
 
        clock.Advance(TimeSpan.FromSeconds(6));
        listener.RecordObservableInstruments();
 
        e.Wait();
 
        utilization = tracker.GetUtilization(TimeSpan.FromSeconds(5));
 
        var roundedCpuUsedPercentage = Math.Round(utilization.CpuUsedPercentage, 1);
 
        Assert.Equal(1, roundedCpuUsedPercentage);
        Assert.Equal(50, utilization.MemoryUsedPercentage);
        Assert.Equal(0.5, cpuLimitFromGauge * 100);
        Assert.Equal(roundedCpuUsedPercentage, Math.Round(cpuRequestFromGauge * 100));
        Assert.Equal(utilization.MemoryUsedPercentage, memoryLimitFromGauge * 100);
        Assert.Equal(roundedCpuUsedPercentage, Math.Round(cpuFromGauge * 100));
        Assert.Equal(utilization.MemoryUsedPercentage, memoryFromGauge * 100);
 
        return Task.CompletedTask;
    }
 
    private static void OnInstrumentPublished(Instrument instrument, MeterListener meterListener, object? meterScope)
    {
        if (!ReferenceEquals(instrument.Meter.Scope, meterScope))
        {
            return;
        }
 
#pragma warning disable S1067 // Expressions should not be too complex
        if (instrument.Name == ResourceUtilizationInstruments.ProcessCpuUtilization ||
            instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization ||
            instrument.Name == ResourceUtilizationInstruments.ContainerCpuRequestUtilization ||
            instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization ||
            instrument.Name == ResourceUtilizationInstruments.ContainerMemoryLimitUtilization)
        {
            meterListener.EnableMeasurementEvents(instrument);
        }
#pragma warning restore S1067 // Expressions should not be too complex
    }
 
    private static void OnMeasurementReceived(
        Instrument instrument, double value,
        ref double cpuFromGauge, ref double cpuLimitFromGauge, ref double cpuRequestFromGauge,
        ref double memoryFromGauge, ref double memoryLimitFromGauge)
    {
        if (instrument.Name == ResourceUtilizationInstruments.ProcessCpuUtilization)
        {
            cpuFromGauge = value;
        }
        else if (instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization)
        {
            memoryFromGauge = value;
        }
        else if (instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization)
        {
            cpuLimitFromGauge = value;
        }
        else if (instrument.Name == ResourceUtilizationInstruments.ContainerCpuRequestUtilization)
        {
            cpuRequestFromGauge = value;
        }
        else if (instrument.Name == ResourceUtilizationInstruments.ContainerMemoryLimitUtilization)
        {
            memoryLimitFromGauge = value;
        }
    }
}