From 87261f3aebe420b200bd64ee554f80cafd9a0bf4 Mon Sep 17 00:00:00 2001 From: vitalid Date: Tue, 26 Mar 2024 18:31:51 +0200 Subject: [PATCH 01/31] Add support for Linux cgrpoup v2, issue-4885 --- .../Linux/ILinuxUtilizationParser.cs | 69 +++ .../Linux/LinuxUtilizationParser.cs | 56 +- .../Linux/LinuxUtilizationParserCgroupV2.cs | 565 ++++++++++++++++++ .../Linux/LinuxUtilizationProvider.cs | 14 +- ...ceMonitoringServiceCollectionExtensions.cs | 43 +- .../SystemResources.cs | 26 + 6 files changed, 767 insertions(+), 6 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/ILinuxUtilizationParser.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/ILinuxUtilizationParser.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/ILinuxUtilizationParser.cs new file mode 100644 index 00000000000..7aca57aeb2b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/ILinuxUtilizationParser.cs @@ -0,0 +1,69 @@ +// 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.Linux; + +/// +/// An interface to be implemented by a parser that reads files and extracts resource utilization data from them. +/// For both versions of cgroup controllers, the parser reads files from the /sys/fs/cgroup directory. +/// +internal interface ILinuxUtilizationParser +{ + /// + /// Reads file /sys/fs/cgroup/memory.max, which is used to check the maximum amount of memory that can be used by a cgroup. + /// It is part of the cgroup v2 memory controller. + /// + /// maybeMemory. + ulong GetAvailableMemoryInBytes(); + + /// + /// Reads the file /sys/fs/cgroup/cpu.stat, which is part of the cgroup v2 CPU controller. + /// It provides statistics about the CPU usage of a cgroup. + /// + /// nanoseconds. + long GetCgroupCpuUsageInNanoseconds(); + + /// + /// Reads the file /sys/fs/cgroup/cpu.max, which is part of the cgroup v2 CPU controller. + /// It is used to set the maximum amount of CPU time that can be used by a cgroup. + /// The file contains two fields, separated by a space. + /// The first field is the quota, which specifies the maximum amount of CPU time (in microseconds) that can be used by the cgroup during one period. + /// The second value is the period, which specifies the length of a period in microseconds. + /// + /// cpuUnits. + float GetCgroupLimitedCpus(); + + /// + /// Reads the file /proc/stat, which provides information about the system’s memory usage. + /// It contains information about the total amount of installed memory, the amount of free and used memory, and the amount of memory used by the kernel and buffers/cache. + /// + /// memory. + ulong GetHostAvailableMemory(); + + /// + /// Reads the file /sys/fs/cgroup/cpuset.cpus.effective, which is part of the cgroup v2 cpuset controller. + /// It shows the effective cpus that the cgroup can use. + /// + /// cpuCount. + float GetHostCpuCount(); + + /// + /// Reads the file /sys/fs/cgroup/cpu.stat, which is part of the cgroup v2 CPU controller. + /// It provides statistics about the CPU usage of a cgroup. + /// The file contains several fields, including usage_usec, which shows the total CPU time (in microseconds). + /// + /// total / (double)_userHz * NanosecondsInSecond. + long GetHostCpuUsageInNanoseconds(); + + /// + /// Reads the file /sys/fs/cgroup/memory.current, which is a file that contains the current memory usage of a cgroup in bytes. + /// + /// memoryUsage. + ulong GetMemoryUsageInBytes(); + + /// + /// Reads the file /sys/fs/cgroup/cpu.weight. And calculates the Pod CPU Request in millicores. + /// + /// cpuPodRequest. + float GetCgroupRequestCpu(); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs index 69708073722..1b31fc85db6 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs @@ -14,8 +14,10 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux; /// This class is not thread safe. /// When the same instance is called by multiple threads it may return corrupted data. /// -internal sealed class LinuxUtilizationParser +internal sealed class LinuxUtilizationParser : ILinuxUtilizationParser { + private const float CpuShares = 1024; + /// /// File contains the amount of CPU time (in microseconds) available to the group during each accounting period. /// @@ -76,6 +78,11 @@ internal sealed class LinuxUtilizationParser /// private static readonly FileInfo _cpuacctUsage = new("/sys/fs/cgroup/cpuacct/cpuacct.usage"); + /// + /// CPU weights, also known as shares incgroup v1, is used for resource allocation. + /// + private static readonly FileInfo _cpuPodWeight = new("/sys/fs/cgroup/cpuacct/cpu.shares"); + private readonly IFileSystem _fileSystem; private readonly long _userHz; private readonly BufferWriter _buffer = new(); @@ -150,6 +157,7 @@ public long GetHostCpuUsageInNanoseconds() /// It should be 99% of the cases when app is hosted in the container environment. /// Otherwise, we assume that all host's CPUs are available, which we read from proc/stat file. /// + public float GetCgroupLimitedCpus() { if (TryGetCpuUnitsFromCgroups(_fileSystem, out var cpus)) @@ -160,6 +168,17 @@ public float GetCgroupLimitedCpus() return GetHostCpuCount(); } + public float GetCgroupRequestCpu() + { + if (TryGetCgroupRequestCpu(_fileSystem, out var cpuUnits)) + { + return cpuUnits; + } + + // If we can't read the CPU weight, we assume that the pod request is 1 core. + return 1; + } + public ulong GetAvailableMemoryInBytes() { const long UnsetCgroupMemoryLimit = 9_223_372_036_854_771_712; @@ -280,6 +299,7 @@ public ulong GetHostAvailableMemory() /// Comma-separated list of integers, with dashes ("-") to represent ranges. For example "0-1,5", or "0", or "1,2,3". /// Each value represents the zero-based index of a CPU. /// + public float GetHostCpuCount() { _fileSystem.ReadFirstLine(_cpuSetCpus, _buffer); @@ -425,4 +445,38 @@ private bool TryGetCpuUnitsFromCgroups(IFileSystem fileSystem, out float cpuUnit cpuUnits = (float)quota / period; return true; } + + /// + /// In cgroup v1 the CPU shares is used to determine the CPU allocation. + /// in cgroup v2 the CPU weight is used to determine the CPU allocation. + /// To calculete CPU request in cgroup v2 we need to read the CPU weight and convert it to CPU shares. + /// But for cgroup v1 we can read the CPU shares directly from the file. + /// 1024 equals 1 CPU core. + /// In cgroup v1 on some systems the location of the CPU shares file is different. + /// + private bool TryGetCgroupRequestCpu(IFileSystem fileSystem, out float cpuUnits) + { + if (!_cpuPodWeight.Exists) + { + cpuUnits = 1; + return false; + } + + fileSystem.ReadFirstLine(_cpuPodWeight, _buffer); + var cpuPodWeightBuffer = _buffer.WrittenSpan; + _ = GetNextNumber(cpuPodWeightBuffer, out var cpuPodWeight); + + if (cpuPodWeightBuffer.IsEmpty || (cpuPodWeightBuffer.Length == 2 && cpuPodWeightBuffer[0] == '-' && cpuPodWeightBuffer[1] == '1')) + { + _buffer.Reset(); + Throw.InvalidOperationException($"Could not parse '{_cpuPodWeight}' content. Expected to find CPU weight but got '{new string(cpuPodWeightBuffer)}' instead."); + cpuUnits = -1; + return false; + } + + _buffer.Reset(); + var result = cpuPodWeight / CpuShares; + cpuUnits = result; + return true; + } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs new file mode 100644 index 00000000000..3280e04df6f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs @@ -0,0 +1,565 @@ +// 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.CodeAnalysis; +using System.IO; +using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Pools; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux; + +/// +/// Parses Linux files to retrieve resource utilization data. +/// This class is not thread safe. +/// When the same instance is called by multiple threads it may return corrupted data. +/// +internal sealed class LinuxUtilizationParserCgroupV2 : ILinuxUtilizationParser +{ + private const int Thousand = 1000; + private const int CpuShares = 1024; + + /// + /// File contains the amount of CPU time (in microseconds) available to the group during each accounting period. + /// and the length of the accounting period in microseconds. + /// In Cgroup V1 : /sys/fs/cgroup/cpu/cpu.cfs_quota_us and /sys/fs/cgroup/cpu/cpu.cfs_period_us. + /// + private static readonly FileInfo _cpuCfsQuaotaPeriodUs = new("/sys/fs/cgroup/cpu.max"); + + /// + /// Stat file contains information about all CPUs and their time. + /// + /// + /// The file has format of whitespace separated values. Each value has its own meaning and unit. + /// To know which value we read, why and what it means refer to proc (5) man page (its POSIX). + /// + private static readonly FileInfo _procStat = new("/proc/stat"); + + /// + /// File that contains information about available memory. + /// + private static readonly FileInfo _memInfo = new("/proc/meminfo"); + + /// + /// List of available CPUs for host. + /// In Cgroup v1 : /sys/fs/cgroup/cpuset/cpuset.cpus. + /// + private static readonly FileInfo _cpuSetCpus = new("/sys/fs/cgroup/cpuset.cpus.effective"); + + /// + /// Cgroup memory limit. + /// + private static readonly FileInfo _memoryLimitInBytes = new("/sys/fs/cgroup/memory.max"); + + /// + /// Cgroup memory stats. + /// + /// + /// Single line representing used memory by cgroup in bytes. + /// + private static readonly FileInfo _memoryUsageInBytes = new("/sys/fs/cgroup/memory.current"); + + /// + /// Cgroup memory stats. + /// + /// + /// This file contains the details about memory usage. + /// The format is (type of memory spent) (value) (unit of measure). + /// + private static readonly FileInfo _memoryStat = new("/sys/fs/cgroup/memory.stat"); + + /// + /// File containing usage in nanoseconds. + /// + /// + /// This value refers to the container/cgroup utilization. + /// The format is single line with one number value. + /// + private static readonly FileInfo _cpuacctUsage = new("/sys/fs/cgroup/cpu.stat"); + + /// + /// CPU weights, also known as shares in cgroup v1, is used for resource allocation. + /// + private static readonly FileInfo _cpuPodWeight = new("/sys/fs/cgroup/cpu.weight"); + + private readonly IFileSystem _fileSystem; + private readonly long _userHz; + private readonly BufferWriter _buffer = new(); + + public LinuxUtilizationParserCgroupV2(IFileSystem fileSystem, IUserHz userHz) + { + _fileSystem = fileSystem; + _userHz = userHz.Value; + } + + public long GetCgroupCpuUsageInNanoseconds() + { + // The value we are interested in starts with this. We just want to make sure it is true. + const string Usage_usec = "usage_usec"; + + // If the file doesn't exist, we assume that the system is a Host and we read the CPU usage from /proc/stat. + if (!_cpuacctUsage.Exists) + { + return GetHostCpuUsageInNanoseconds(); + } + + _fileSystem.ReadAll(_cpuacctUsage, _buffer); + var usage = _buffer.WrittenSpan; + + if (!usage.StartsWith(Usage_usec)) + { + Throw.InvalidOperationException($"Could not parse '{_cpuacctUsage}'. We expected first line of the file to start with '{Usage_usec}' but it was '{new string(usage)}' instead."); + } + + var cpuUsage = usage.Slice(Usage_usec.Length, usage.Length - Usage_usec.Length); + + var next = GetNextNumber(cpuUsage, out var microseconds); + + if (microseconds == -1) + { + Throw.InvalidOperationException($"Could not get cpu usage from '{_cpuacctUsage}'. Expected positive number, but got '{new string(usage)}'."); + } + + _buffer.Reset(); + + // In cgroup v2, the Units are microseconds for usage_usec. + // We multiply by 1000 to convert to nanoseconds to keep the common calculation logic. + return microseconds * Thousand; + } + + public long GetHostCpuUsageInNanoseconds() + { + const string StartingTokens = "cpu "; + const int NumberOfColumnsRepresentingCpuUsage = 8; + const int NanosecondsInSecond = 1_000_000_000; + + _fileSystem.ReadFirstLine(_procStat, _buffer); + + var stat = _buffer.WrittenSpan; + var total = 0L; + + if (!_buffer.WrittenSpan.StartsWith(StartingTokens)) + { + Throw.InvalidOperationException($"Expected proc/stat to start with '{StartingTokens}' but it was '{new string(_buffer.WrittenSpan)}'."); + } + + stat = stat.Slice(StartingTokens.Length, stat.Length - StartingTokens.Length); + + for (var i = 0; i < NumberOfColumnsRepresentingCpuUsage; i++) + { + var next = GetNextNumber(stat, out var number); + + if (number != -1) + { + total += number; + } + + if (next == -1) + { + Throw.InvalidOperationException( + $"'{_procStat}' should contain whitespace separated values according to POSIX. We've failed trying to get {i}th value. File content: '{new string(stat)}'."); + } + + stat = stat.Slice(next, stat.Length - next); + } + + _buffer.Reset(); + + return (long)(total / (double)_userHz * NanosecondsInSecond); + } + + /// + /// When CGroup limits are set, we can calculate number of cores based on the file settings. + /// It should be 99% of the cases when app is hosted in the container environment. + /// Otherwise, we assume that all host's CPUs are available, which we read from proc/stat file. + /// + public float GetCgroupLimitedCpus() + { + if (TryGetCpuUnitsFromCgroups(_fileSystem, out var cpus)) + { + return cpus; + } + return GetHostCpuCount(); + } + + /// + /// If we are able to read the CPU share, we calculate the CPU request based on the weight by dividing it by 1024. + /// If we can't read the CPU weight, we assume that the pod/vm cpu request is 1 core by default. + /// + public float GetCgroupRequestCpu() + { + if (TryGetCgroupRequestCpu(_fileSystem, out var cpuPodRequest)) + { + return cpuPodRequest / CpuShares; + } + + return cpuPodRequest; + } + + /// + /// If the file doesn't exist, we assume that the system is a Host and we read the memory from /proc/meminfo. + /// + public ulong GetAvailableMemoryInBytes() + { + const long UnsetCgroupMemoryLimit = 9_223_372_036_854_771_712; + + if (!_memoryLimitInBytes.Exists) + { + return GetHostAvailableMemory(); + } + + _fileSystem.ReadAll(_memoryLimitInBytes, _buffer); + + var memoryBuffer = _buffer.WrittenSpan; + _ = GetNextNumber(memoryBuffer, out var maybeMemory); + + if (maybeMemory == -1) + { + Throw.InvalidOperationException($"Could not parse '{_memoryLimitInBytes}' content. Expected to find available memory in bytes but got '{new string(memoryBuffer)}' instead."); + } + + _buffer.Reset(); + + return maybeMemory == UnsetCgroupMemoryLimit + ? GetHostAvailableMemory() + : (ulong)maybeMemory; + } + + public long GetMemoryUsageInBytesFromSlices() + { + string[] memoryUsageInBytesSlicesPath = Directory.GetDirectories(@"/sys/fs/cgroup", "*.slice", SearchOption.TopDirectoryOnly); + long memoryUsageInBytesTotal = 0; + + foreach (string path in memoryUsageInBytesSlicesPath) + { + var memoryUsageInBytesFile = new FileInfo(path + "/memory.current"); + if (!memoryUsageInBytesFile.Exists) + { + continue; + } + + _fileSystem.ReadAll(memoryUsageInBytesFile, _buffer); + + var memoryUsageFile = _buffer.WrittenSpan; + var next = GetNextNumber(memoryUsageFile, out var containerMemoryUsage); + + // this file format doesn't expect to contain anything after the number. + if (containerMemoryUsage == -1) + { + Throw.InvalidOperationException( + $"We tried to read '{memoryUsageInBytesFile}', and we expected to get a positive number but instead it was: '{containerMemoryUsage}'."); + } + + memoryUsageInBytesTotal += containerMemoryUsage; + + _buffer.Reset(); + } + + return memoryUsageInBytesTotal; + } + + /// + /// If the file doesn't exist, we assume that the system is a Host and we read the memory from /proc/meminfo. + /// + public ulong GetMemoryUsageInBytes() + { + const string InactiveFile = "inactive_file"; + + if (!_memoryStat.Exists) + { + return GetHostAvailableMemory(); + } + + _fileSystem.ReadAll(_memoryStat, _buffer); + var memoryFile = _buffer.WrittenSpan; + + var index = memoryFile.IndexOf(InactiveFile.AsSpan()); + + if (index == -1) + { + Throw.InvalidOperationException($"Unable to find inactive_file from '{_memoryStat}'."); + } + + var inactiveMemorySlice = memoryFile.Slice(index + InactiveFile.Length, memoryFile.Length - index - InactiveFile.Length); + + _ = GetNextNumber(inactiveMemorySlice, out var inactiveMemory); + + if (inactiveMemory == -1) + { + Throw.InvalidOperationException($"The value of inactive_file found in '{_memoryStat}' is not a positive number: '{new string(inactiveMemorySlice)}'."); + } + + _buffer.Reset(); + + long memoryUsage = 0; + if (!_memoryUsageInBytes.Exists) + { + memoryUsage = GetMemoryUsageInBytesFromSlices(); + } + else + { + memoryUsage = GetMemoryUsageInBytesPod(); + } + + if (memoryUsage < 0) + { + Throw.InvalidOperationException($"The total memory usage read from '{_memoryUsageInBytes}' is lesser than inactive memory usage read from '{_memoryStat}'."); + } + + var memoryUsageTotal = memoryUsage - inactiveMemory; + + return (ulong)memoryUsageTotal; + } + + [SuppressMessage("Major Code Smell", "S109:Magic numbers should not be used", + Justification = "Shifting bits left by number n is multiplying the value by 2 to the power of n.")] + public ulong GetHostAvailableMemory() + { + // The value we are interested in starts with this. We just want to make sure it is true. + const string MemTotal = "MemTotal:"; + + _fileSystem.ReadFirstLine(_memInfo, _buffer); + var firstLine = _buffer.WrittenSpan; + + if (!firstLine.StartsWith(MemTotal)) + { + Throw.InvalidOperationException($"Could not parse '{_memInfo}'. We expected first line of the file to start with '{MemTotal}' but it was '{new string(firstLine)}' instead."); + } + + var totalMemory = firstLine.Slice(MemTotal.Length, firstLine.Length - MemTotal.Length); + + var next = GetNextNumber(totalMemory, out var totalMemoryAvailable); + + if (totalMemoryAvailable == -1) + { + Throw.InvalidOperationException($"Could not parse '{_memInfo}'. We expected to get total memory usage on first line but we've got: '{new string(firstLine)}'."); + } + + if (next == -1 || totalMemory.Length - next < 2) + { + Throw.InvalidOperationException($"Could not parse '{_memInfo}'. We expected to get memory usage followed by the unit (kB, MB, GB) but found no unit: '{new string(firstLine)}'."); + } + + var unit = totalMemory.Slice(totalMemory.Length - 2, 2); + var memory = (ulong)totalMemoryAvailable; + + var u = unit switch + { + "kB" => memory << 10, + "MB" => memory << 20, + "GB" => memory << 30, + "TB" => memory << 40, + _ => throw new InvalidOperationException( + $"We tried to convert total memory usage value from '{_memInfo}' to bytes, but we've got a unit that we don't recognize: '{new string(unit)}'.") + }; + + _buffer.Reset(); + + return u; + } + + /// + /// Comma-separated list of integers, with dashes ("-") to represent ranges. For example "0-1,5", or "0", or "1,2,3". + /// Each value represents the zero-based index of a CPU. + /// + public float GetHostCpuCount() + { + _fileSystem.ReadFirstLine(_cpuSetCpus, _buffer); + var stats = _buffer.WrittenSpan; + + if (stats.IsEmpty) + { + ThrowException(stats); + } + + var cpuCount = 0L; + + // Iterate over groups (comma-separated) + while (true) + { + var groupIndex = stats.IndexOf(','); + + var group = groupIndex == -1 ? stats : stats.Slice(0, groupIndex); + + var rangeIndex = group.IndexOf('-'); + + if (rangeIndex == -1) + { + // Single number + _ = GetNextNumber(group, out var singleCpu); + + if (singleCpu == -1) + { + ThrowException(stats); + } + + cpuCount += 1; + } + else + { + // Range + var first = group.Slice(0, rangeIndex); + _ = GetNextNumber(first, out var startCpu); + + var second = group.Slice(rangeIndex + 1); + var next = GetNextNumber(second, out var endCpu); + + if (endCpu == -1 || startCpu == -1 || endCpu < startCpu || next != -1) + { + ThrowException(stats); + } + + cpuCount += endCpu - startCpu + 1; + } + + if (groupIndex == -1) + { + break; + } + + stats = stats.Slice(groupIndex + 1); + } + + _buffer.Reset(); + + return cpuCount; + + static void ThrowException(ReadOnlySpan content) => + Throw.InvalidOperationException( + $"Could not parse '{_cpuSetCpus}'. Expected comma-separated list of integers, with dashes (\"-\") based ranges (\"0\", \"2-6,12\") but got '{new string(content)}'."); + } + + /// + /// The input must contain only number. If there is something more than whitespace before the number, it will return failure (-1). + /// + [SuppressMessage("Major Code Smell", "S109:Magic numbers should not be used", + Justification = "We are adding another digit, so we need to multiply by ten.")] + private static int GetNextNumber(ReadOnlySpan buffer, out long number) + { + var numberStart = 0; + + while (numberStart < buffer.Length && char.IsWhiteSpace(buffer[numberStart])) + { + numberStart++; + } + + if (numberStart == buffer.Length || !char.IsDigit(buffer[numberStart])) + { + number = -1; + return -1; + } + + var numberEnd = numberStart; + number = 0; + + while (numberEnd < buffer.Length && char.IsDigit(buffer[numberEnd])) + { + var current = buffer[numberEnd] - '0'; + number *= 10; + number += current; + numberEnd++; + } + + return numberEnd < buffer.Length ? numberEnd : -1; + } + + /// + /// If the file doesn't exist, we assume that the system is a Host and we read the CPU usage from /proc/stat. + /// + private bool TryGetCpuUnitsFromCgroups(IFileSystem fileSystem, out float cpuUnits) + { + if (!_cpuCfsQuaotaPeriodUs.Exists) + { + cpuUnits = 0; + return false; + } + + fileSystem.ReadFirstLine(_cpuCfsQuaotaPeriodUs, _buffer); + + var quotaBuffer = _buffer.WrittenSpan; + + if (quotaBuffer.IsEmpty || (quotaBuffer.Length == 2 && quotaBuffer[0] == '-' && quotaBuffer[1] == '1')) + { + _buffer.Reset(); + cpuUnits = -1; + return false; + } + + var nextQuota = GetNextNumber(quotaBuffer, out var quota); + + if (quota == -1) + { + Throw.InvalidOperationException($"Could not parse '{_cpuCfsQuaotaPeriodUs}'. Expected an integer but got: '{new string(quotaBuffer)}'."); + } + + var index = quotaBuffer.IndexOf(quota.ToString().AsSpan()); + var cpuPeriodSlice = quotaBuffer.Slice(index + quota.ToString().Length, quotaBuffer.Length - index - quota.ToString().Length); + _ = GetNextNumber(cpuPeriodSlice, out var period); + + if (period == -1) + { + Throw.InvalidOperationException($"Could not parse '{_cpuCfsQuaotaPeriodUs}'. Expected to get an integer but got: '{new string(cpuPeriodSlice)}'."); + } + + _buffer.Reset(); + cpuUnits = (float)quota / period; + + return true; + } + + private bool TryGetCgroupRequestCpu(IFileSystem fileSystem, out float cpuUnits) + { + // If the file doesn't exist, we assume that the system is a Host and we assign the default 1 CPU core. + if (!_cpuPodWeight.Exists) + { + cpuUnits = 1; + return false; + } + + fileSystem.ReadFirstLine(_cpuPodWeight, _buffer); + var cpuPodWeightBuffer = _buffer.WrittenSpan; + _ = GetNextNumber(cpuPodWeightBuffer, out var cpuPodWeight); + + if (cpuPodWeightBuffer.IsEmpty || (cpuPodWeightBuffer.Length == 2 && cpuPodWeightBuffer[0] == '-' && cpuPodWeightBuffer[1] == '1')) + { + _buffer.Reset(); + Throw.InvalidOperationException($"Could not parse '{_cpuPodWeight}' content. Expected to find CPU weight but got '{new string(cpuPodWeightBuffer)}' instead."); + cpuUnits = -1; + return false; + } + + // Calculate CPU pod request in millicores based on the weight, using the formula: + // y = (1 + ((x - 2) * 9999) / 262142), where y is the CPU weight and x is the CPU share (cgroup v1) + // https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/2254-cgroup-v2#phase-1-convert-from-cgroups-v1-settings-to-v2 + var cpuPodShare = ((cpuPodWeight * 262142) + 19997) / 9999; + if (cpuPodShare == -1) + { + _buffer.Reset(); + Throw.InvalidOperationException($"Could not calculate CPU share from CPU weight '{cpuPodShare}'"); + cpuUnits = -1; + return false; + } + + _buffer.Reset(); + cpuUnits = cpuPodShare; + return true; + } + + private long GetMemoryUsageInBytesPod() + { + _fileSystem.ReadAll(_memoryUsageInBytes, _buffer); + + var memoryUsageFile = _buffer.WrittenSpan; + var next = GetNextNumber(memoryUsageFile, out long memoryUsage); + + // this file format doesn't expect to contain anything after the number. + if (memoryUsage == -1) + { + Throw.InvalidOperationException( + $"We tried to read '{_memoryUsageInBytes}', and we expected to get a positive number but instead it was: '{memoryUsage}'."); + } + + _buffer.Reset(); + return memoryUsage; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs index b2c2f259216..bb97277a562 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs @@ -14,7 +14,7 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider private readonly object _cpuLocker = new(); private readonly object _memoryLocker = new(); - private readonly LinuxUtilizationParser _parser; + private readonly ILinuxUtilizationParser _parser; private readonly ulong _totalMemoryInBytes; private readonly TimeSpan _cpuRefreshInterval; private readonly TimeSpan _memoryRefreshInterval; @@ -32,7 +32,7 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider public SystemResources Resources { get; } - public LinuxUtilizationProvider(IOptions options, LinuxUtilizationParser parser, + public LinuxUtilizationProvider(IOptions options, ILinuxUtilizationParser parser, IMeterFactory meterFactory, TimeProvider? timeProvider = null) { _parser = parser; @@ -49,7 +49,7 @@ public LinuxUtilizationProvider(IOptions options, Lin var hostMemory = _parser.GetHostAvailableMemory(); var hostCpus = _parser.GetHostCpuCount(); var availableCpus = _parser.GetCgroupLimitedCpus(); - + var cpuGuaranteedRequest = _parser.GetCgroupRequestCpu(); _scale = hostCpus / availableCpus; _scaleForTrackerApi = hostCpus / availableCpus; @@ -63,7 +63,13 @@ public LinuxUtilizationProvider(IOptions options, Lin _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.CpuUtilization, observeValue: CpuUtilization, unit: "1"); _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.MemoryUtilization, observeValue: MemoryUtilization, unit: "1"); - Resources = new SystemResources(1, hostCpus, _totalMemoryInBytes, hostMemory); + // 1 - Min available cpu for HOST + // hostCpus - Max available cpu for HOST + // _totalMemoryInBytes - guaranteedMemoryInBytes for POD + // hostMemory - Max available memory for HOST + // availableCpus is a Cpu limit for Pod or for a HOST. + // cpuGuaranteedRequest is a Cpu request for POD. + Resources = new SystemResources(1, availableCpus, cpuGuaranteedRequest, _totalMemoryInBytes, hostMemory); } public double CpuUtilization() diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs index 92d61192ef6..d273baeb705 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.IO; using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Diagnostics.ResourceMonitoring; @@ -118,9 +119,49 @@ private static ResourceMonitorBuilder AddLinuxProvider(this ResourceMonitorBuild builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); + + bool injectParserV2 = GetCgroupType(); + + if (injectParserV2) + { + builder.Services.TryAddSingleton(); + } + else + { + builder.Services.TryAddSingleton(); + } return builder; } + + private static bool GetCgroupType() + { + DriveInfo[] allDrives = DriveInfo.GetDrives(); + var injectParserV2 = false; + const string CgroupVersion = "cgroup2fs"; + const string UnifiedCgroupPath = "/sys/fs/cgroup/unified"; + + // We check which cgroup version is mounted in the system and based on that we inject the parser. + foreach (DriveInfo d in allDrives) + { + // Currently there are some OS'es which mount both cgroup v1 and v2. And v2 is mounted under /sys/fs/cgroup/unified + // So, we are checking for the unified cgroup and fallback to cgroup v1, because the path for cgroup v2 is different. + // This is mostly to support WSL/WSL2, where both cgroup v1 and v2 are mounted and make debugging for Linux easier in VS. + // https://systemd.io/CGROUP_DELEGATION/#three-different-tree-setups + if (d.DriveType == DriveType.Ram && d.DriveFormat == CgroupVersion && d.VolumeLabel == UnifiedCgroupPath) + { + injectParserV2 = false; + break; + } + + if (d.DriveType == DriveType.Ram && d.DriveFormat == CgroupVersion) + { + injectParserV2 = true; + break; + } + } + + return injectParserV2; + } #endif } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs index 725ff1ed305..a037a634bec 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs @@ -1,6 +1,7 @@ // 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 Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; @@ -32,6 +33,16 @@ public readonly struct SystemResources /// public double MaximumCpuUnits { get; } + /// + /// Gets CPU request value in millicores in Kubernetes terminology. + /// + /// + /// This value calculates CPU pod request in millicores based on the weight, using the formula. + /// y = (1 + ((x - 2) * 9999) / 262142), where y is the CPU weight and x is the CPU share (cgroup v1). + /// https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/2254-cgroup-v2#phase-1-convert-from-cgroups-v1-settings-to-v2. + /// + public double GuaranteedPodCpuUnits { get; } + /// /// Gets the memory allocated to the system in bytes. /// @@ -56,4 +67,19 @@ public SystemResources(double guaranteedCpuUnits, double maximumCpuUnits, ulong GuaranteedMemoryInBytes = Throw.IfLessThan(guaranteedMemoryInBytes, 1UL); MaximumMemoryInBytes = Throw.IfLessThan(maximumMemoryInBytes, 1UL); } + + /// + /// Initializes a new instance of the struct. + /// Metric guaranteedPodCpuUnits is calculated for CPU request in millicores only for Linux. + /// + /// The CPU units available in the system. + /// The maximum CPU units available in the system. + /// The memory allocated to the system in bytes. + /// The maximum memory allocated to the system in bytes. + /// The CPU request value in millicores. + public SystemResources(double guaranteedCpuUnits, double maximumCpuUnits, double guaranteedPodCpuUnits, ulong guaranteedMemoryInBytes, ulong maximumMemoryInBytes) + : this(guaranteedCpuUnits, maximumCpuUnits, guaranteedMemoryInBytes, maximumMemoryInBytes) + { + GuaranteedPodCpuUnits = Throw.IfLessThanOrEqual(guaranteedPodCpuUnits, 0.00); + } } From f32c9adc4d90a167118484ae2046026188785755 Mon Sep 17 00:00:00 2001 From: vitalid Date: Wed, 27 Mar 2024 11:37:18 +0200 Subject: [PATCH 02/31] Removed unnecessary using, ordered using, removed blank lines --- .../Linux/LinuxUtilizationParser.cs | 1 - .../Linux/LinuxUtilizationParserCgroupV2.cs | 1 + .../ResourceMonitoringServiceCollectionExtensions.cs | 2 +- .../SystemResources.cs | 6 +----- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs index 1b31fc85db6..48a812e0765 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs @@ -157,7 +157,6 @@ public long GetHostCpuUsageInNanoseconds() /// It should be 99% of the cases when app is hosted in the container environment. /// Otherwise, we assume that all host's CPUs are available, which we read from proc/stat file. /// - public float GetCgroupLimitedCpus() { if (TryGetCpuUnitsFromCgroups(_fileSystem, out var cpus)) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs index 3280e04df6f..802029e5a91 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs @@ -179,6 +179,7 @@ public float GetCgroupLimitedCpus() { return cpus; } + return GetHostCpuCount(); } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs index d273baeb705..eeec693db4f 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs @@ -2,8 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.IO; using System.Diagnostics.CodeAnalysis; +using System.IO; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Diagnostics.ResourceMonitoring; #if !NETFRAMEWORK diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs index a037a634bec..d7f0b404bc3 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs @@ -1,8 +1,4 @@ -// 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 Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; From da44f5f2a5d77848c82f4676e7525b2e380e787d Mon Sep 17 00:00:00 2001 From: vitalid Date: Wed, 27 Mar 2024 12:55:03 +0200 Subject: [PATCH 03/31] Changed path for /sys/fs/cgroup/cpu/cpu.shares --- .../Linux/LinuxUtilizationParser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs index 48a812e0765..eb80506ba61 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs @@ -79,9 +79,9 @@ internal sealed class LinuxUtilizationParser : ILinuxUtilizationParser private static readonly FileInfo _cpuacctUsage = new("/sys/fs/cgroup/cpuacct/cpuacct.usage"); /// - /// CPU weights, also known as shares incgroup v1, is used for resource allocation. + /// CPU weights, also known as shares in cgroup v1, is used for resource allocation. /// - private static readonly FileInfo _cpuPodWeight = new("/sys/fs/cgroup/cpuacct/cpu.shares"); + private static readonly FileInfo _cpuPodWeight = new("/sys/fs/cgroup/cpu/cpu.shares"); private readonly IFileSystem _fileSystem; private readonly long _userHz; From 3f75b404e5dfa51cdcca1b3b8a561b2ee96f71f4 Mon Sep 17 00:00:00 2001 From: vitalid Date: Wed, 27 Mar 2024 13:44:06 +0200 Subject: [PATCH 04/31] Experimental attribute for GuaranteedPodCpuUnits added, removed blank lines --- .../Linux/LinuxUtilizationParser.cs | 1 - .../Linux/LinuxUtilizationParserCgroupV2.cs | 7 +++++-- .../SystemResources.cs | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs index eb80506ba61..7bdecb37048 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs @@ -298,7 +298,6 @@ public ulong GetHostAvailableMemory() /// Comma-separated list of integers, with dashes ("-") to represent ranges. For example "0-1,5", or "0", or "1,2,3". /// Each value represents the zero-based index of a CPU. /// - public float GetHostCpuCount() { _fileSystem.ReadFirstLine(_cpuSetCpus, _buffer); diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs index 802029e5a91..87154800346 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs @@ -3,7 +3,9 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.IO; +using System.Xml.Linq; using Microsoft.Shared.Diagnostics; using Microsoft.Shared.Pools; @@ -493,8 +495,9 @@ private bool TryGetCpuUnitsFromCgroups(IFileSystem fileSystem, out float cpuUnit Throw.InvalidOperationException($"Could not parse '{_cpuCfsQuaotaPeriodUs}'. Expected an integer but got: '{new string(quotaBuffer)}'."); } - var index = quotaBuffer.IndexOf(quota.ToString().AsSpan()); - var cpuPeriodSlice = quotaBuffer.Slice(index + quota.ToString().Length, quotaBuffer.Length - index - quota.ToString().Length); + var quotaString = quota.ToString(CultureInfo.CurrentCulture); + var index = quotaBuffer.IndexOf(quotaString.AsSpan()); + var cpuPeriodSlice = quotaBuffer.Slice(index + quotaString.Length, quotaBuffer.Length - index - quotaString.Length); _ = GetNextNumber(cpuPeriodSlice, out var period); if (period == -1) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs index d7f0b404bc3..606622ae8f2 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs @@ -1,4 +1,5 @@ -using Microsoft.Shared.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; @@ -37,6 +38,7 @@ public readonly struct SystemResources /// y = (1 + ((x - 2) * 9999) / 262142), where y is the CPU weight and x is the CPU share (cgroup v1). /// https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/2254-cgroup-v2#phase-1-convert-from-cgroups-v1-settings-to-v2. /// + [Experimental("CS8058")] public double GuaranteedPodCpuUnits { get; } /// From 87e64b86d4e666303606f651a8f217b27d287cee Mon Sep 17 00:00:00 2001 From: vitalid Date: Wed, 27 Mar 2024 13:59:22 +0200 Subject: [PATCH 05/31] Exprerimental attributes added --- .../Linux/LinuxUtilizationParserCgroupV2.cs | 1 - .../SystemResources.cs | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs index 87154800346..aa8f264c537 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs @@ -5,7 +5,6 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; -using System.Xml.Linq; using Microsoft.Shared.Diagnostics; using Microsoft.Shared.Pools; diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs index 606622ae8f2..4af25744981 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs @@ -75,9 +75,12 @@ public SystemResources(double guaranteedCpuUnits, double maximumCpuUnits, ulong /// The memory allocated to the system in bytes. /// The maximum memory allocated to the system in bytes. /// The CPU request value in millicores. + [Experimental("CS8058")] public SystemResources(double guaranteedCpuUnits, double maximumCpuUnits, double guaranteedPodCpuUnits, ulong guaranteedMemoryInBytes, ulong maximumMemoryInBytes) : this(guaranteedCpuUnits, maximumCpuUnits, guaranteedMemoryInBytes, maximumMemoryInBytes) { +#pragma warning disable CS8058 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. GuaranteedPodCpuUnits = Throw.IfLessThanOrEqual(guaranteedPodCpuUnits, 0.00); +#pragma warning restore CS8058 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. } } From cda49b5b25bb80540af97dbef64a76a61c3a4a9b Mon Sep 17 00:00:00 2001 From: vitalid Date: Thu, 28 Mar 2024 18:46:13 +0200 Subject: [PATCH 06/31] Removed the experimental parameter, agreed to reuse cpuGuaranteedRequest --- .../Linux/LinuxUtilizationProvider.cs | 10 +++--- .../SystemResources.cs | 34 +++---------------- 2 files changed, 8 insertions(+), 36 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs index bb97277a562..2a634409514 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs @@ -63,13 +63,11 @@ public LinuxUtilizationProvider(IOptions options, ILi _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.CpuUtilization, observeValue: CpuUtilization, unit: "1"); _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.MemoryUtilization, observeValue: MemoryUtilization, unit: "1"); - // 1 - Min available cpu for HOST - // hostCpus - Max available cpu for HOST - // _totalMemoryInBytes - guaranteedMemoryInBytes for POD - // hostMemory - Max available memory for HOST + // cpuGuaranteedRequest is a Cpu request for POD, for HOST its 1 Core // availableCpus is a Cpu limit for Pod or for a HOST. - // cpuGuaranteedRequest is a Cpu request for POD. - Resources = new SystemResources(1, availableCpus, cpuGuaranteedRequest, _totalMemoryInBytes, hostMemory); + // 1 - Currently value in /sys/fs/cgroup/memory.min per container is 0 + // _totalMemoryInBytes - Resource Memory Limit (in k8s terms) + Resources = new SystemResources(cpuGuaranteedRequest, availableCpus, _totalMemoryInBytes, _totalMemoryInBytes); } public double CpuUtilization() diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs index 4af25744981..1a70df54243 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs @@ -17,6 +17,9 @@ public readonly struct SystemResources /// This value corresponds to the number of the guaranteed CPUs as described by Kubernetes CPU request parameter, each 1000 CPU units /// represent 1 CPU or 1 Core. For example, if the POD is configured with 1500m units as the CPU request, this property will be assigned /// to 1.5 which means one and a half CPU will be dedicated for the POD. + /// For POD this value is calculated based on the cgroupv2 weight, using the formula. + /// y = (1 + ((x - 2) * 9999) / 262142), where y is the CPU weight and x is the CPU share (cgroup v1). + /// https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/2254-cgroup-v2#phase-1-convert-from-cgroups-v1-settings-to-v2. /// public double GuaranteedCpuUnits { get; } @@ -30,24 +33,13 @@ public readonly struct SystemResources /// public double MaximumCpuUnits { get; } - /// - /// Gets CPU request value in millicores in Kubernetes terminology. - /// - /// - /// This value calculates CPU pod request in millicores based on the weight, using the formula. - /// y = (1 + ((x - 2) * 9999) / 262142), where y is the CPU weight and x is the CPU share (cgroup v1). - /// https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/2254-cgroup-v2#phase-1-convert-from-cgroups-v1-settings-to-v2. - /// - [Experimental("CS8058")] - public double GuaranteedPodCpuUnits { get; } - /// /// Gets the memory allocated to the system in bytes. /// public ulong GuaranteedMemoryInBytes { get; } /// - /// Gets the maximum memory allocated to the system in bytes. + /// Gets the Container's Request Memory Limit or the Maximum allocated for the VM. /// public ulong MaximumMemoryInBytes { get; } @@ -65,22 +57,4 @@ public SystemResources(double guaranteedCpuUnits, double maximumCpuUnits, ulong GuaranteedMemoryInBytes = Throw.IfLessThan(guaranteedMemoryInBytes, 1UL); MaximumMemoryInBytes = Throw.IfLessThan(maximumMemoryInBytes, 1UL); } - - /// - /// Initializes a new instance of the struct. - /// Metric guaranteedPodCpuUnits is calculated for CPU request in millicores only for Linux. - /// - /// The CPU units available in the system. - /// The maximum CPU units available in the system. - /// The memory allocated to the system in bytes. - /// The maximum memory allocated to the system in bytes. - /// The CPU request value in millicores. - [Experimental("CS8058")] - public SystemResources(double guaranteedCpuUnits, double maximumCpuUnits, double guaranteedPodCpuUnits, ulong guaranteedMemoryInBytes, ulong maximumMemoryInBytes) - : this(guaranteedCpuUnits, maximumCpuUnits, guaranteedMemoryInBytes, maximumMemoryInBytes) - { -#pragma warning disable CS8058 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - GuaranteedPodCpuUnits = Throw.IfLessThanOrEqual(guaranteedPodCpuUnits, 0.00); -#pragma warning restore CS8058 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - } } From a09bfc83099ceddfb2546ccb6db90557aee600a7 Mon Sep 17 00:00:00 2001 From: vitalid Date: Thu, 4 Apr 2024 17:06:16 +0300 Subject: [PATCH 07/31] Acceptance test to support cgroupv2 --- .../SystemResources.cs | 3 +- .../Linux/AcceptanceTest.cs | 64 +++++++++++++++++-- .../Linux/LinuxCountersTests.cs | 57 +++++++++++++++++ 3 files changed, 118 insertions(+), 6 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs index 1a70df54243..7fda19cd9dd 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs @@ -1,5 +1,4 @@ -using System.Diagnostics.CodeAnalysis; -using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs index c0e22bdb1c2..0a1e88bce52 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs @@ -8,8 +8,12 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using FluentAssertions.Common; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Linux.Resources; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting.Testing; using Microsoft.Extensions.Options; @@ -19,6 +23,7 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; +//[CollectionDefinition("Get cgroup version")] public sealed class AcceptanceTest { [ConditionalFact] @@ -90,7 +95,7 @@ public void Adding_Linux_Resource_Utilization_Can_Be_Configured_With_Action() [ConditionalFact] [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific package.")] [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() + public void Adding_Linux_Resource_Utilization_With_Section_Registers_SnapshotProvider_Cgroupv1() { var cpuRefresh = TimeSpan.FromMinutes(13); var memoryRefresh = TimeSpan.FromMinutes(14); @@ -117,17 +122,68 @@ public void Adding_Linux_Resource_Utilization_With_Section_Registers_SnapshotPro { 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 which tests cgroup v1. + .Replace(ServiceDescriptor.Singleton()) .BuildServiceProvider(); var provider = services.GetService(); - Assert.NotNull(provider); Assert.Equal(1, provider.Resources.GuaranteedCpuUnits); // hack to make hardcoded calculation in resource utilization main package work. - Assert.Equal(20.0d, provider.Resources.MaximumCpuUnits); // read from cpuset.cpus + Assert.Equal(2.0d, provider.Resources.MaximumCpuUnits); // read from cpuset.cpus Assert.Equal(100_000UL, provider.Resources.GuaranteedMemoryInBytes); // read from memory.limit_in_bytes - Assert.Equal(104_767_488UL, provider.Resources.MaximumMemoryInBytes); // meminfo * 1024 + + // 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 package.")] + [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[] + { + 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(new FakeUserHz(100)) + .AddSingleton(new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/proc/stat"), "cpu 10 10 10 10 10 10 10 10 10 10"}, + { new FileInfo("/sys/fs/cgroup/cpu.stat"), "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"), "0.2"}, + { new FileInfo("/sys/fs/cgroup/cpu/cpu.weight"), "4"}, + { new FileInfo("/sys/fs/cgroup/memory.max"), "100000" } + })) + .AddResourceMonitoring(x => x.ConfigureMonitor(section)) + .Replace(ServiceDescriptor.Singleton()) // Ingesting LinuxUtilizationParser which tests cgroup v1. + .BuildServiceProvider(); + + var provider = services.GetService(); + 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.max + + // 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(Skip = "Flaky test, see https://github.com/dotnet/extensions/issues/3997")] diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs index 2a0c286d0ea..d1c4f8620e6 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs @@ -35,6 +35,7 @@ public void LinuxCounters_Registers_Instruments() { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_period_us"), "6"}, { new FileInfo("/sys/fs/cgroup/memory/memory.stat"), "total_inactive_file 0"}, { new FileInfo("/sys/fs/cgroup/memory/memory.usage_in_bytes"), "524288"}, + { new FileInfo("/sys/fs/cgroup/cpu/cpu.shares"), "1024"}, }); var parser = new LinuxUtilizationParser(fileSystem: fileSystem, new FakeUserHz(100)); @@ -69,4 +70,60 @@ public void LinuxCounters_Registers_Instruments() Assert.Equal(ResourceUtilizationInstruments.MemoryUtilization, samples[1].instrument.Name); Assert.Equal(0.5, samples[1].value); } + + [ConditionalFact] + public void LinuxCounters_Registers_Instruments_CgroupV2() + { + var meterName = Guid.NewGuid().ToString(); + var options = Options.Options.Create(new()); + using var meter = new Meter(nameof(LinuxCounters_Registers_Instruments)); + var meterFactoryMock = new Mock(); + meterFactoryMock.Setup(x => x.Create(It.IsAny())) + .Returns(meter); + + var fileSystem = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/memory.max"), "9223372036854771712" }, + { new FileInfo("/proc/stat"), "cpu 10 10 10 10 10 10 10 10 10 10"}, + { new FileInfo("/sys/fs/cgroup/cpu.stat"), "50"}, + { new FileInfo("/proc/meminfo"), "MemTotal: 1024 kB"}, + { new FileInfo("/sys/fs/cgroup/cpuset.cpus.effective"), "0-19"}, + { new FileInfo("/sys/fs/cgroup/cpu/cpu.max"), "60"}, + { new FileInfo("/sys/fs/cgroup/memory.stat"), "total_inactive_file 0"}, + { new FileInfo("/sys/fs/cgroup/memory.current"), "524288"}, + { new FileInfo("/sys/fs/cgroup/cpu/cpu.weight"), "4"}, + }); + + var parser = new LinuxUtilizationParserCgroupV2(fileSystem: fileSystem, new FakeUserHz(100)); + var provider = new LinuxUtilizationProvider(options, parser, meterFactoryMock.Object, TimeProvider.System); + + using var listener = new MeterListener + { + InstrumentPublished = (instrument, listener) => + { + if (ReferenceEquals(meter, instrument.Meter)) + { + listener.EnableMeasurementEvents(instrument); + } + } + }; + + var samples = new List<(Instrument instrument, double value)>(); + listener.SetMeasurementEventCallback((instrument, value, _, _) => + { + if (ReferenceEquals(meter, instrument.Meter)) + { + samples.Add((instrument, value)); + } + }); + + listener.Start(); + listener.RecordObservableInstruments(); + + Assert.Equal(2, samples.Count); + Assert.Equal(ResourceUtilizationInstruments.CpuUtilization, samples[0].instrument.Name); + Assert.Equal(double.NaN, samples[0].value); + Assert.Equal(ResourceUtilizationInstruments.MemoryUtilization, samples[1].instrument.Name); + Assert.Equal(1, samples[1].value); + } } From 0bc23ba490b9172ba9a960bd910351abc8f71d82 Mon Sep 17 00:00:00 2001 From: vitalid Date: Thu, 4 Apr 2024 18:19:26 +0300 Subject: [PATCH 08/31] Removed unnessecary using --- .../Linux/AcceptanceTest.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs index 0a1e88bce52..fc30a66e49a 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs @@ -8,12 +8,10 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using FluentAssertions.Common; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; -using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Linux.Resources; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting.Testing; using Microsoft.Extensions.Options; @@ -23,7 +21,6 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; -//[CollectionDefinition("Get cgroup version")] public sealed class AcceptanceTest { [ConditionalFact] From e80f7fb3b62170b4ac2bc8a6e80d78a153755c30 Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Thu, 4 Apr 2024 10:06:37 -0600 Subject: [PATCH 09/31] Update src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs Co-authored-by: Igor Velikorossov --- .../Linux/LinuxUtilizationProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs index 2a634409514..5d729ba9505 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs @@ -63,8 +63,8 @@ public LinuxUtilizationProvider(IOptions options, ILi _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.CpuUtilization, observeValue: CpuUtilization, unit: "1"); _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.MemoryUtilization, observeValue: MemoryUtilization, unit: "1"); - // cpuGuaranteedRequest is a Cpu request for POD, for HOST its 1 Core - // availableCpus is a Cpu limit for Pod or for a HOST. + // cpuGuaranteedRequest is a CPU request for pod, for host its 1 core + // available CPUs is a CPU limit for a pod or for a host. // 1 - Currently value in /sys/fs/cgroup/memory.min per container is 0 // _totalMemoryInBytes - Resource Memory Limit (in k8s terms) Resources = new SystemResources(cpuGuaranteedRequest, availableCpus, _totalMemoryInBytes, _totalMemoryInBytes); From de0ca6b9561aa8765e71f51208b14bfbb538f3f5 Mon Sep 17 00:00:00 2001 From: vitalid Date: Fri, 5 Apr 2024 15:50:21 +0300 Subject: [PATCH 10/31] Added Exists method for HardcodedValueFileSystem --- .../Linux/IFileSystem.cs | 6 ++++ .../Linux/LinuxUtilizationParser.cs | 2 +- .../Linux/LinuxUtilizationParserCgroupV2.cs | 14 ++++---- .../Linux/OSFileSystem.cs | 5 +++ .../Linux/AcceptanceTest.cs | 15 ++++---- .../Linux/LinuxCountersTests.cs | 8 ++--- .../Resources/FileNamesOnlyFileSystem.cs | 4 +++ .../Resources/HardcodedValueFileSystem.cs | 5 +++ .../Resources/PathReturningFileSystem.cs | 34 ------------------- 9 files changed, 41 insertions(+), 52 deletions(-) delete mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/PathReturningFileSystem.cs diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/IFileSystem.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/IFileSystem.cs index 99844335609..bd64b9d4259 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/IFileSystem.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/IFileSystem.cs @@ -12,6 +12,12 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux; /// internal interface IFileSystem { + /// + /// Checks for file existence. + /// + /// True/False. + bool Exists(FileInfo fileInfo); + /// /// Reads content from the file. /// diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs index 7bdecb37048..7aa25a41dba 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs @@ -454,7 +454,7 @@ private bool TryGetCpuUnitsFromCgroups(IFileSystem fileSystem, out float cpuUnit /// private bool TryGetCgroupRequestCpu(IFileSystem fileSystem, out float cpuUnits) { - if (!_cpuPodWeight.Exists) + if (!_fileSystem.Exists(_cpuPodWeight)) { cpuUnits = 1; return false; diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs index aa8f264c537..cbfe10b128f 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs @@ -99,7 +99,7 @@ public long GetCgroupCpuUsageInNanoseconds() const string Usage_usec = "usage_usec"; // If the file doesn't exist, we assume that the system is a Host and we read the CPU usage from /proc/stat. - if (!_cpuacctUsage.Exists) + if (!_fileSystem.Exists(_cpuacctUsage)) { return GetHostCpuUsageInNanoseconds(); } @@ -205,7 +205,7 @@ public ulong GetAvailableMemoryInBytes() { const long UnsetCgroupMemoryLimit = 9_223_372_036_854_771_712; - if (!_memoryLimitInBytes.Exists) + if (!_fileSystem.Exists(_memoryLimitInBytes)) { return GetHostAvailableMemory(); } @@ -235,7 +235,7 @@ public long GetMemoryUsageInBytesFromSlices() foreach (string path in memoryUsageInBytesSlicesPath) { var memoryUsageInBytesFile = new FileInfo(path + "/memory.current"); - if (!memoryUsageInBytesFile.Exists) + if (!_fileSystem.Exists(memoryUsageInBytesFile)) { continue; } @@ -267,7 +267,7 @@ public ulong GetMemoryUsageInBytes() { const string InactiveFile = "inactive_file"; - if (!_memoryStat.Exists) + if (!_fileSystem.Exists(_memoryStat)) { return GetHostAvailableMemory(); } @@ -294,7 +294,7 @@ public ulong GetMemoryUsageInBytes() _buffer.Reset(); long memoryUsage = 0; - if (!_memoryUsageInBytes.Exists) + if (!_fileSystem.Exists(_memoryUsageInBytes)) { memoryUsage = GetMemoryUsageInBytesFromSlices(); } @@ -470,7 +470,7 @@ private static int GetNextNumber(ReadOnlySpan buffer, out long number) /// private bool TryGetCpuUnitsFromCgroups(IFileSystem fileSystem, out float cpuUnits) { - if (!_cpuCfsQuaotaPeriodUs.Exists) + if (!_fileSystem.Exists(_cpuCfsQuaotaPeriodUs)) { cpuUnits = 0; return false; @@ -513,7 +513,7 @@ private bool TryGetCpuUnitsFromCgroups(IFileSystem fileSystem, out float cpuUnit private bool TryGetCgroupRequestCpu(IFileSystem fileSystem, out float cpuUnits) { // If the file doesn't exist, we assume that the system is a Host and we assign the default 1 CPU core. - if (!_cpuPodWeight.Exists) + if (!_fileSystem.Exists(_cpuPodWeight)) { cpuUnits = 1; return false; diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/OSFileSystem.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/OSFileSystem.cs index 6b950871445..e90a1c1eb37 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/OSFileSystem.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/OSFileSystem.cs @@ -16,6 +16,11 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux; /// internal sealed class OSFileSystem : IFileSystem { + public bool Exists(FileInfo fileInfo) + { + return fileInfo.Exists; + } + public int Read(FileInfo file, int length, Span destination) { using var stream = file.OpenRead(); diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs index fc30a66e49a..801baa443a1 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs @@ -123,7 +123,7 @@ public void Adding_Linux_Resource_Utilization_With_Section_Registers_SnapshotPro })) .AddResourceMonitoring(x => x.ConfigureMonitor(section)) - // Ingesting LinuxUtilizationParser which tests cgroup v1. + // Ingesting LinuxUtilizationParser with cgroup v1 support. .Replace(ServiceDescriptor.Singleton()) .BuildServiceProvider(); @@ -137,6 +137,7 @@ public void Adding_Linux_Resource_Utilization_With_Section_Registers_SnapshotPro // 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 package.")] [SuppressMessage("Minor Code Smell", "S3257:Declarations and initializations should be as concise as possible", Justification = "Broken analyzer.")] @@ -161,25 +162,27 @@ public void Adding_Linux_Resource_Utilization_With_Section_Registers_SnapshotPro .AddSingleton(new HardcodedValueFileSystem(new Dictionary { { new FileInfo("/proc/stat"), "cpu 10 10 10 10 10 10 10 10 10 10"}, - { new FileInfo("/sys/fs/cgroup/cpu.stat"), "102312"}, + { 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"), "0.2"}, + { new FileInfo("/sys/fs/cgroup/cpu.max"), "20000 100000"}, { new FileInfo("/sys/fs/cgroup/cpu/cpu.weight"), "4"}, { new FileInfo("/sys/fs/cgroup/memory.max"), "100000" } })) .AddResourceMonitoring(x => x.ConfigureMonitor(section)) - .Replace(ServiceDescriptor.Singleton()) // Ingesting LinuxUtilizationParser which tests cgroup v1. + + // Ingesting LinuxUtilizationParser with cgroup v2 support. + .Replace(ServiceDescriptor.Singleton()) .BuildServiceProvider(); var provider = services.GetService(); 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(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 contaract with the main package, we need to set the maximum memory to the same value as the guaranteed memory. + // To not break the contaract with the main package, we need to set the maximum memory value to the guaranteed memory. Assert.Equal(100_000UL, provider.Resources.MaximumMemoryInBytes); } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs index d1c4f8620e6..a56eb8fbd2b 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs @@ -85,11 +85,11 @@ public void LinuxCounters_Registers_Instruments_CgroupV2() { { new FileInfo("/sys/fs/cgroup/memory.max"), "9223372036854771712" }, { new FileInfo("/proc/stat"), "cpu 10 10 10 10 10 10 10 10 10 10"}, - { new FileInfo("/sys/fs/cgroup/cpu.stat"), "50"}, + { new FileInfo("/sys/fs/cgroup/cpu.stat"), "usage_usec 102312"}, { new FileInfo("/proc/meminfo"), "MemTotal: 1024 kB"}, { new FileInfo("/sys/fs/cgroup/cpuset.cpus.effective"), "0-19"}, - { new FileInfo("/sys/fs/cgroup/cpu/cpu.max"), "60"}, - { new FileInfo("/sys/fs/cgroup/memory.stat"), "total_inactive_file 0"}, + { new FileInfo("/sys/fs/cgroup/cpu/cpu.max"), "20000 100000"}, + { new FileInfo("/sys/fs/cgroup/memory.stat"), "inactive_file 0"}, { new FileInfo("/sys/fs/cgroup/memory.current"), "524288"}, { new FileInfo("/sys/fs/cgroup/cpu/cpu.weight"), "4"}, }); @@ -124,6 +124,6 @@ public void LinuxCounters_Registers_Instruments_CgroupV2() Assert.Equal(ResourceUtilizationInstruments.CpuUtilization, samples[0].instrument.Name); Assert.Equal(double.NaN, samples[0].value); Assert.Equal(ResourceUtilizationInstruments.MemoryUtilization, samples[1].instrument.Name); - Assert.Equal(1, samples[1].value); + Assert.Equal(0.5, samples[1].value); } } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FileNamesOnlyFileSystem.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FileNamesOnlyFileSystem.cs index 520d2398bf7..deb6e9be197 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FileNamesOnlyFileSystem.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FileNamesOnlyFileSystem.cs @@ -12,6 +12,10 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; internal sealed class FileNamesOnlyFileSystem : IFileSystem { private readonly string _directory; + public bool Exists(FileInfo fileInfo) + { + return fileInfo.Exists; + } public FileNamesOnlyFileSystem(string directory) { diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/HardcodedValueFileSystem.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/HardcodedValueFileSystem.cs index ce489ccfb43..d8507cca015 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/HardcodedValueFileSystem.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/HardcodedValueFileSystem.cs @@ -27,6 +27,11 @@ public HardcodedValueFileSystem(Dictionary fileContent, string _fallback = fallback; } + public bool Exists(FileInfo fileInfo) + { + return _fileContent.ContainsKey(fileInfo.FullName); + } + public void ReadFirstLine(FileInfo file, BufferWriter destination) { if (_fileContent.Count == 0 || !_fileContent.TryGetValue(file.FullName, out var content)) diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/PathReturningFileSystem.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/PathReturningFileSystem.cs deleted file mode 100644 index 9ddc43e4d1e..00000000000 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/PathReturningFileSystem.cs +++ /dev/null @@ -1,34 +0,0 @@ -// 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.Buffers; -using System.IO; -using Microsoft.Shared.Pools; - -namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; - -internal sealed class PathReturningFileSystem : IFileSystem -{ - public void ReadFirstLine(FileInfo file, BufferWriter destination) - { - destination.Write(file.FullName); - } - - public void ReadAll(FileInfo file, BufferWriter destination) - { - destination.Write(file.FullName); - } - - public int Read(FileInfo file, int length, Span destination) - { - var min = Math.Min(length, file.FullName.Length); - - for (var i = 0; i < min; i++) - { - destination[i] = file.FullName[i]; - } - - return min; - } -} From 89ee8fe30aadc75ed4e2bf0f32e15dddc407ea82 Mon Sep 17 00:00:00 2001 From: vitalid Date: Fri, 5 Apr 2024 16:26:40 +0300 Subject: [PATCH 11/31] Adjusted the Preprocessor symbol --- .../ResourceMonitoringServiceCollectionExtensions.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs index eeec693db4f..2033f333657 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs @@ -133,7 +133,7 @@ private static ResourceMonitorBuilder AddLinuxProvider(this ResourceMonitorBuild return builder; } - +#endif private static bool GetCgroupType() { DriveInfo[] allDrives = DriveInfo.GetDrives(); @@ -163,5 +163,4 @@ private static bool GetCgroupType() return injectParserV2; } -#endif } From c124d5a9dd75af41b0ae01450c56f6ec612066fb Mon Sep 17 00:00:00 2001 From: vitalid Date: Fri, 5 Apr 2024 17:45:37 +0300 Subject: [PATCH 12/31] Moved cgroup version detection to a separate class --- ...ions.Diagnostics.ResourceMonitoring.csproj | 1 + .../ResourceMonitoringLinuxCgroupVersion.cs | 44 +++++++++++++++++++ ...ceMonitoringServiceCollectionExtensions.cs | 32 +------------- 3 files changed, 46 insertions(+), 31 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringLinuxCgroupVersion.cs diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj index cbc5caad60a..2c91a8eba73 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj @@ -44,6 +44,7 @@ + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringLinuxCgroupVersion.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringLinuxCgroupVersion.cs new file mode 100644 index 00000000000..64273997a04 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringLinuxCgroupVersion.cs @@ -0,0 +1,44 @@ +using System.IO; +// 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.DependencyInjection; + +/// +/// Detects cgroup version on Linux OS. +/// +public static class ResourceMonitoringLinuxCgroupVersion +{ + /// + /// Get drive metadata for each drive in the system and detects the cgroup version. + /// + public static bool GetCgroupType() + { + DriveInfo[] allDrives = DriveInfo.GetDrives(); + var injectParserV2 = false; + const string CgroupVersion = "cgroup2fs"; + const string UnifiedCgroupPath = "/sys/fs/cgroup/unified"; + + // We check which cgroup version is mounted in the system and based on that we inject the parser. + foreach (DriveInfo d in allDrives) + { + // Currently there are some OS'es which mount both cgroup v1 and v2. And v2 is mounted under /sys/fs/cgroup/unified + // So, we are checking for the unified cgroup and fallback to cgroup v1, because the path for cgroup v2 is different. + // This is mostly to support WSL/WSL2, where both cgroup v1 and v2 are mounted and make debugging for Linux easier in VS. + // https://systemd.io/CGROUP_DELEGATION/#three-different-tree-setups + if (d.DriveType == DriveType.Ram && d.DriveFormat == CgroupVersion && d.VolumeLabel == UnifiedCgroupPath) + { + injectParserV2 = false; + break; + } + + if (d.DriveType == DriveType.Ram && d.DriveFormat == CgroupVersion) + { + injectParserV2 = true; + break; + } + } + + return injectParserV2; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs index 2033f333657..e6a8270c71c 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.IO; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Diagnostics.ResourceMonitoring; #if !NETFRAMEWORK @@ -120,7 +119,7 @@ private static ResourceMonitorBuilder AddLinuxProvider(this ResourceMonitorBuild builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); - bool injectParserV2 = GetCgroupType(); + bool injectParserV2 = ResourceMonitoringLinuxCgroupVersion.GetCgroupType(); if (injectParserV2) { @@ -134,33 +133,4 @@ private static ResourceMonitorBuilder AddLinuxProvider(this ResourceMonitorBuild return builder; } #endif - private static bool GetCgroupType() - { - DriveInfo[] allDrives = DriveInfo.GetDrives(); - var injectParserV2 = false; - const string CgroupVersion = "cgroup2fs"; - const string UnifiedCgroupPath = "/sys/fs/cgroup/unified"; - - // We check which cgroup version is mounted in the system and based on that we inject the parser. - foreach (DriveInfo d in allDrives) - { - // Currently there are some OS'es which mount both cgroup v1 and v2. And v2 is mounted under /sys/fs/cgroup/unified - // So, we are checking for the unified cgroup and fallback to cgroup v1, because the path for cgroup v2 is different. - // This is mostly to support WSL/WSL2, where both cgroup v1 and v2 are mounted and make debugging for Linux easier in VS. - // https://systemd.io/CGROUP_DELEGATION/#three-different-tree-setups - if (d.DriveType == DriveType.Ram && d.DriveFormat == CgroupVersion && d.VolumeLabel == UnifiedCgroupPath) - { - injectParserV2 = false; - break; - } - - if (d.DriveType == DriveType.Ram && d.DriveFormat == CgroupVersion) - { - injectParserV2 = true; - break; - } - } - - return injectParserV2; - } } From cca3d6025bb9c9ef0ec574eba9dbe949cd28ed6c Mon Sep 17 00:00:00 2001 From: vitalid Date: Fri, 5 Apr 2024 17:53:15 +0300 Subject: [PATCH 13/31] Summary added for ResourceMonitoringLinuxCgroupVersion class --- .../ResourceMonitoringLinuxCgroupVersion.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringLinuxCgroupVersion.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringLinuxCgroupVersion.cs index 64273997a04..af51496a61b 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringLinuxCgroupVersion.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringLinuxCgroupVersion.cs @@ -1,17 +1,19 @@ using System.IO; + // 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.DependencyInjection; -/// +/// /// Detects cgroup version on Linux OS. -/// +/// public static class ResourceMonitoringLinuxCgroupVersion { - /// + /// /// Get drive metadata for each drive in the system and detects the cgroup version. - /// + /// + /// True/False. public static bool GetCgroupType() { DriveInfo[] allDrives = DriveInfo.GetDrives(); From 1f9c9633fb6f73a29741c15a96aa4e7fed967ca0 Mon Sep 17 00:00:00 2001 From: vitalid Date: Mon, 8 Apr 2024 12:50:23 +0300 Subject: [PATCH 14/31] Made cgroup detection class internal --- .../ResourceMonitoringLinuxCgroupVersion.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringLinuxCgroupVersion.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringLinuxCgroupVersion.cs index af51496a61b..066624613fb 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringLinuxCgroupVersion.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringLinuxCgroupVersion.cs @@ -8,7 +8,7 @@ namespace Microsoft.Extensions.DependencyInjection; /// /// Detects cgroup version on Linux OS. /// -public static class ResourceMonitoringLinuxCgroupVersion +internal static class ResourceMonitoringLinuxCgroupVersion { /// /// Get drive metadata for each drive in the system and detects the cgroup version. From 742d0a46ba102693c984fbe616e305dacb69e929 Mon Sep 17 00:00:00 2001 From: vitalid Date: Mon, 8 Apr 2024 20:56:40 +0300 Subject: [PATCH 15/31] Additional tests to cover cgroupv2 parser --- .../Linux/LinuxUtilizationParserCgroupV2.cs | 13 +- .../Linux/AcceptanceTest.cs | 4 +- .../Linux/LinuxCountersTests.cs | 2 +- .../LinuxUtilizationParserCgroupV2Tests.cs | 327 ++++++++++++++++++ .../Linux/LinuxUtilizationParserTests.cs | 20 ++ .../Linux/Resources/TestResources.cs | 6 +- .../fixtures/cpu.max | 1 + .../fixtures/cpu.weight | 1 + .../fixtures/cpuset.cpus.effective | 1 + 9 files changed, 368 insertions(+), 7 deletions(-) create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpu.max create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpu.weight create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpuset.cpus.effective diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs index cbfe10b128f..98c22ca10f0 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs @@ -516,29 +516,36 @@ private bool TryGetCgroupRequestCpu(IFileSystem fileSystem, out float cpuUnits) if (!_fileSystem.Exists(_cpuPodWeight)) { cpuUnits = 1; + Throw.InvalidOperationException($"'{_cpuPodWeight}' file does not exist!"); return false; } fileSystem.ReadFirstLine(_cpuPodWeight, _buffer); var cpuPodWeightBuffer = _buffer.WrittenSpan; - _ = GetNextNumber(cpuPodWeightBuffer, out var cpuPodWeight); if (cpuPodWeightBuffer.IsEmpty || (cpuPodWeightBuffer.Length == 2 && cpuPodWeightBuffer[0] == '-' && cpuPodWeightBuffer[1] == '1')) { - _buffer.Reset(); Throw.InvalidOperationException($"Could not parse '{_cpuPodWeight}' content. Expected to find CPU weight but got '{new string(cpuPodWeightBuffer)}' instead."); + _buffer.Reset(); cpuUnits = -1; return false; } + _ = GetNextNumber(cpuPodWeightBuffer, out var cpuPodWeight); + + if (cpuPodWeight == -1) + { + Throw.InvalidOperationException($"Could not parse '{_cpuPodWeight}'. Expected to get an integer but got: '{cpuPodWeight}'."); + } + // Calculate CPU pod request in millicores based on the weight, using the formula: // y = (1 + ((x - 2) * 9999) / 262142), where y is the CPU weight and x is the CPU share (cgroup v1) // https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/2254-cgroup-v2#phase-1-convert-from-cgroups-v1-settings-to-v2 var cpuPodShare = ((cpuPodWeight * 262142) + 19997) / 9999; if (cpuPodShare == -1) { - _buffer.Reset(); Throw.InvalidOperationException($"Could not calculate CPU share from CPU weight '{cpuPodShare}'"); + _buffer.Reset(); cpuUnits = -1; return false; } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs index 801baa443a1..57a9d2f6b61 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs @@ -166,7 +166,7 @@ public void Adding_Linux_Resource_Utilization_With_Section_Registers_SnapshotPro { 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/cpu.weight"), "4"}, + { new FileInfo("/sys/fs/cgroup/cpu.weight"), "4"}, { new FileInfo("/sys/fs/cgroup/memory.max"), "100000" } })) .AddResourceMonitoring(x => x.ConfigureMonitor(section)) @@ -177,7 +177,7 @@ public void Adding_Linux_Resource_Utilization_With_Section_Registers_SnapshotPro var provider = services.GetService(); Assert.NotNull(provider); - Assert.Equal(1, provider.Resources.GuaranteedCpuUnits); // hack to make hardcoded calculation in resource utilization main package work. + 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 diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs index a56eb8fbd2b..4b87738676e 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs @@ -91,7 +91,7 @@ public void LinuxCounters_Registers_Instruments_CgroupV2() { new FileInfo("/sys/fs/cgroup/cpu/cpu.max"), "20000 100000"}, { new FileInfo("/sys/fs/cgroup/memory.stat"), "inactive_file 0"}, { new FileInfo("/sys/fs/cgroup/memory.current"), "524288"}, - { new FileInfo("/sys/fs/cgroup/cpu/cpu.weight"), "4"}, + { new FileInfo("/sys/fs/cgroup/cpu.weight"), "4"}, }); var parser = new LinuxUtilizationParserCgroupV2(fileSystem: fileSystem, new FakeUserHz(100)); diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs new file mode 100644 index 00000000000..994dbbcd31f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs @@ -0,0 +1,327 @@ +// 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.Globalization; +using System.IO; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; +using Microsoft.TestUtilities; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; + +[OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Windows specific.")] +public sealed class LinuxUtilizationParserCgroupV2Tests +{ + [ConditionalTheory] + [InlineData("DFIJEUWGHFWGBWEFWOMDOWKSLA")] + [InlineData("")] + [InlineData("________________________Asdasdasdas dd")] + [InlineData(" ")] + [InlineData("!@#!$%!@")] + public void Parser_Throws_When_Data_Is_Invalid(string line) + { + var parser = new LinuxUtilizationParserCgroupV2(new HardcodedValueFileSystem(line), new FakeUserHz(100)); + + Assert.Throws(() => parser.GetHostAvailableMemory()); + Assert.Throws(() => parser.GetAvailableMemoryInBytes()); + Assert.Throws(() => parser.GetMemoryUsageInBytes()); + Assert.Throws(() => parser.GetCgroupLimitedCpus()); + Assert.Throws(() => parser.GetHostCpuUsageInNanoseconds()); + Assert.Throws(() => parser.GetHostCpuCount()); + Assert.Throws(() => parser.GetCgroupCpuUsageInNanoseconds()); + Assert.Throws(() => parser.GetCgroupRequestCpu()); + } + + [ConditionalFact] + public void Parser_Can_Read_Host_And_Cgroup_Available_Cpu_Count() + { + var parser = new LinuxUtilizationParserCgroupV2(new FileNamesOnlyFileSystem(TestResources.TestFilesLocation), new FakeUserHz(100)); + var hostCpuCount = parser.GetHostCpuCount(); + var cgroupCpuCount = parser.GetCgroupLimitedCpus(); + + Assert.Equal(2.0, hostCpuCount); + Assert.Equal(2.0, cgroupCpuCount); + } + + [ConditionalFact] + public void Parser_Provides_Total_Available_Memory_In_Bytes() + { + var fs = new FileNamesOnlyFileSystem(TestResources.TestFilesLocation); + var parser = new LinuxUtilizationParserCgroupV2(fs, new FakeUserHz(100)); + + var totalMem = parser.GetHostAvailableMemory(); + + Assert.Equal(16_233_760UL * 1024, totalMem); + } + + [ConditionalTheory] + [InlineData("----------------------")] + [InlineData("@ @#dddada")] + [InlineData("1231234124124")] + [InlineData("1024 KB")] + [InlineData("1024 KB d \n\r 1024")] + [InlineData("\n\r")] + [InlineData("")] + [InlineData("Suspicious")] + [InlineData("string@")] + [InlineData("string12312")] + [InlineData("total-inactive-file")] + [InlineData("total_inactive-file")] + [InlineData("total_active_file")] + [InlineData("total_inactive_file:_ 213912")] + [InlineData("Total_Inactive_File 2")] + public void When_Calling_GetMemoryUsageInBytes_Parser_Throws_When_MemoryStat_Doesnt_Contain_Total_Inactive_File_Section(string content) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/memory.stat"), content } + }); + + var p = new LinuxUtilizationParserCgroupV2(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetMemoryUsageInBytes()); + + Assert.IsAssignableFrom(r); + Assert.Contains("/sys/fs/cgroup/memory.stat", r.Message); + Assert.Contains("inactive_file", r.Message); + } + + [ConditionalTheory] + [InlineData("----------------------")] + [InlineData("@ @#dddada")] + [InlineData("_1231234124124")] + [InlineData("eee 1024 KB")] + [InlineData("\n\r")] + [InlineData("")] + [InlineData("Suspicious")] + [InlineData("Suspicious12312312")] + [InlineData("string@")] + [InlineData("string12312")] + public void When_Calling_GetMemoryUsageInBytes_Parser_Throws_When_UsageInBytes_Doesnt_Contain_Just_A_Number(string content) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/memory.stat"), "inactive_file 0" }, + { new FileInfo("/sys/fs/cgroup/memory.current"), content } + }); + + var p = new LinuxUtilizationParserCgroupV2(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetMemoryUsageInBytes()); + + Assert.IsAssignableFrom(r); + Assert.Contains("/sys/fs/cgroup/memory.current", r.Message); + } + + [ConditionalTheory] + [InlineData(10, 1)] + [InlineData(23, 22)] + [InlineData(100000, 10000)] + public void When_Calling_GetMemoryUsageInBytes_Parser_Throws_When_Inactive_Memory_Is_Bigger_Than_Total_Memory(int inactive, int total) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/memory/memory.stat"), $"total_inactive_file {inactive}" }, + { new FileInfo("/sys/fs/cgroup/memory/memory.usage_in_bytes"), total.ToString(CultureInfo.CurrentCulture) } + }); + + var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetMemoryUsageInBytes()); + + Assert.IsAssignableFrom(r); + Assert.Contains("lesser than", r.Message); + } + + [ConditionalTheory] + [InlineData("Mem")] + [InlineData("MemTotal:")] + [InlineData("MemTotal: 120")] + [InlineData("MemTotal: kb")] + [InlineData("MemTotal: KB")] + [InlineData("MemTotal: PB")] + [InlineData("MemTotal: 1024 PB")] + [InlineData("MemTotal: 1024 ")] + [InlineData("MemTotal: 1024 @@ ")] + [InlineData("MemoryTotal: 1024 MB ")] + [InlineData("MemoryTotal: 123123123123123123")] + public void When_Calling_GetHostAvailableMemory_Parser_Throws_When_MemInfo_Does_Not_Contain_TotalMemory(string totalMemory) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/proc/meminfo"), totalMemory }, + }); + + var p = new LinuxUtilizationParserCgroupV2(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetHostAvailableMemory()); + + Assert.IsAssignableFrom(r); + Assert.Contains("/proc/meminfo", r.Message); + } + + [ConditionalTheory] + [InlineData("kB", 231, 236544)] + [InlineData("MB", 287, 300_941_312)] + [InlineData("GB", 372, 399_431_958_528)] + [InlineData("TB", 2, 219_902_325_555_2)] + [SuppressMessage("Critical Code Smell", "S3937:Number patterns should be regular", Justification = "Its OK.")] + public void When_Calling_GetHostAvailableMemory_Parser_Correctly_Transforms_Supported_Units_To_Bytes(string unit, int value, ulong bytes) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/proc/meminfo"), $"MemTotal: {value} {unit}" }, + }); + + var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var memory = p.GetHostAvailableMemory(); + + Assert.Equal(bytes, memory); + } + + [ConditionalTheory] + [InlineData("0-11", 12)] + [InlineData("0", 1)] + [InlineData("1000", 1)] + [InlineData("0,1", 2)] + [InlineData("0,1,2", 3)] + [InlineData("0,1,2,4", 4)] + [InlineData("0,1-2,3", 4)] + [InlineData("0,1,2-3,4", 5)] + [InlineData("0-1,2-3", 4)] + [InlineData("0-1,2-3,4-5", 6)] + [InlineData("0-2,3-5,6-8", 9)] + public void When_No_Cgroup_Cpu_Limits_Are_Not_Set_We_Get_Available_Cpus_From_CpuSetCpus(string content, int result) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/cpuset.cpus.effective"), content }, + { new FileInfo("/sys/fs/cgroup/cpu.max"), "-1" }, + }); + + var p = new LinuxUtilizationParserCgroupV2(f, new FakeUserHz(100)); + var cpus = p.GetCgroupLimitedCpus(); + + Assert.Equal(result, cpus); + } + + [ConditionalTheory] + [InlineData("-11")] + [InlineData("0-")] + [InlineData("d-22")] + [InlineData("22-d")] + [InlineData("22-18")] + [InlineData("aaaa")] + [InlineData(" d 182-1923")] + [InlineData("")] + [InlineData("1-18-22")] + [InlineData("1-18 \r\n")] + [InlineData("\r\n")] + public void Parser_Throws_When_CpuSet_Has_Invalid_Content(string content) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/cpuset.cpus.effective"), content }, + { new FileInfo("/sys/fs/cgroup/cpu.max"), "-1" } + }); + + var p = new LinuxUtilizationParserCgroupV2(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetHostCpuCount()); + + Assert.IsAssignableFrom(r); + Assert.Contains("/sys/fs/cgroup/cpuset.cpus.effective", r.Message); + } + + [ConditionalFact] + public void When_Quota_And_Period_Are_Minus_One_It_Fallbacks_To_Cpuset() + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/cpuset.cpus.effective"), "@" }, + { new FileInfo("/sys/fs/cgroup/cpu.max"), "-1" } + }); + + var p = new LinuxUtilizationParserCgroupV2(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetCgroupLimitedCpus()); + + Assert.IsAssignableFrom(r); + Assert.Contains("/sys/fs/cgroup/cpuset.cpus.effective", r.Message); + } + + [ConditionalTheory] + [InlineData("dd1d", "18")] + [InlineData("-18", "18")] + [InlineData("\r\r\r\r\r", "18")] + [InlineData("123", "\r\r\r\r\r")] + [InlineData("-", "d'")] + [InlineData("-", "d/:")] + [InlineData("2", "d/:")] + [InlineData("2d2d2d", "e3")] + [InlineData("3d", "d3")] + [InlineData(" 12", "eeeee 12")] + [InlineData("1 2", "eeeee 12")] + [InlineData("12 ", "")] + public void Parser_Throws_When_Cgroup_Cpu_Files_Contain_Invalid_Data(string quota, string period) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_quota_us"), quota }, + { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_period_us"), period } + }); + + var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetCgroupLimitedCpus()); + + Assert.IsAssignableFrom(r); + Assert.Contains("/sys/fs/cgroup/cpu/cpu.cfs_", r.Message); + } + + [ConditionalFact] + public void ReadingCpuUsage_Does_Not_Throw_For_Valid_Input() + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/proc/stat"), "cpu 2569530 36700 245693 4860924 82283 0 4360 0 0 0" } + }); + + var p = new LinuxUtilizationParserCgroupV2(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetHostCpuUsageInNanoseconds()); + + Assert.Null(r); + } + + [ConditionalFact] + public void ReadingTotalMemory_Does_Not_Throw_For_Valid_Input() + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/memory/memory.usage_in_bytes"), "32493514752\r\n" }, + { new FileInfo("/sys/fs/cgroup/memory/memory.stat"), "total_inactive_file 100" } + }); + + var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetMemoryUsageInBytes()); + + Assert.Null(r); + } + + [ConditionalTheory] + [InlineData("2569530367000")] + [InlineData(" 2569530 36700 245693 4860924 82283 0 4360 0dsa 0 0 asdasd @@@@")] + [InlineData("asdasd 2569530 36700 245693 4860924 82283 0 4360 0 0 0")] + [InlineData(" 2569530 36700 245693")] + [InlineData("cpu 2569530 36700 245693")] + [InlineData(" 2")] + public void ReadingCpuUsage_Does_Throws_For_Valid_Input(string content) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/proc/stat"), content } + }); + + var p = new LinuxUtilizationParserCgroupV2(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetHostCpuUsageInNanoseconds()); + + Assert.IsAssignableFrom(r); + Assert.Contains("proc/stat", r.Message); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserTests.cs index 559ac792e08..434e61551f4 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserTests.cs @@ -34,6 +34,26 @@ public void Parser_Throws_When_Data_Is_Invalid(string line) Assert.Throws(() => parser.GetCgroupCpuUsageInNanoseconds()); } + [ConditionalTheory] + [InlineData("DFIJEUWGHFWGBWEFWOMDOWKSLA")] + [InlineData("")] + [InlineData("________________________Asdasdasdas dd")] + [InlineData(" ")] + [InlineData("!@#!$%!@")] + public void Cgroupv2_Parser_Throws_When_Data_Is_Invalid(string line) + { + var parser = new LinuxUtilizationParserCgroupV2(new HardcodedValueFileSystem(line), new FakeUserHz(100)); + + Assert.Throws(() => parser.GetHostAvailableMemory()); + Assert.Throws(() => parser.GetAvailableMemoryInBytes()); + Assert.Throws(() => parser.GetMemoryUsageInBytes()); + Assert.Throws(() => parser.GetCgroupLimitedCpus()); + Assert.Throws(() => parser.GetHostCpuUsageInNanoseconds()); + Assert.Throws(() => parser.GetHostCpuCount()); + Assert.Throws(() => parser.GetCgroupCpuUsageInNanoseconds()); + Assert.Throws(() => parser.GetCgroupLimitedCpus()); + } + [ConditionalFact] public void Parser_Can_Read_Host_And_Cgroup_Available_Cpu_Count() { diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/TestResources.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/TestResources.cs index a7377fe019f..f0910866f68 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/TestResources.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/TestResources.cs @@ -15,15 +15,19 @@ internal sealed class TestResources : IDisposable private static readonly Dictionary _files = new(StringComparer.OrdinalIgnoreCase) { { "/sys/fs/cgroup/cpu/cpu.shares", ""}, - { "/sys/fs/cgroup/cpuset/cpuset.cpus", "0-1"}, { "/sys/fs/cgroup/memory/memory.limit_in_bytes", "1024"}, + { "/sys/fs/cgroup/memory/memory.max", "1024"}, { "/sys/fs/cgroup/cpu/cpu.cfs_quota_us", "1"}, { "/sys/fs/cgroup/cpu/cpu.cfs_period_us", "1" }, + { "/sys/fs/cgroup/cpu.max", "1"}, { "/proc/meminfo", "MemTotal: 1 kB\r\n"}, + { "/sys/fs/cgroup/cpuset.cpus.effective", "0-3"}, + { "/sys/fs/cgroup/cpu/cpu.wight", "512"} }; private static readonly string[] _namesOfDirectories = { + "/sys/fs/cgroup", "/sys/fs/cgroup/memory", "/sys/fs/cgroup/cpu", "/proc" diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpu.max b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpu.max new file mode 100644 index 00000000000..2ec118cf113 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpu.max @@ -0,0 +1 @@ +20000 100000 diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpu.weight b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpu.weight new file mode 100644 index 00000000000..29d6383b52c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpu.weight @@ -0,0 +1 @@ +100 diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpuset.cpus.effective b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpuset.cpus.effective new file mode 100644 index 00000000000..8b0fab869c1 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpuset.cpus.effective @@ -0,0 +1 @@ +0-1 From c188df2a20de845d3a43bd2e8a0b38aca1e1e274 Mon Sep 17 00:00:00 2001 From: vitalid Date: Thu, 11 Apr 2024 15:20:26 +0300 Subject: [PATCH 16/31] New method to get directory names with pattern and multiple test fixes --- .../Linux/IFileSystem.cs | 6 ++ .../Linux/LinuxUtilizationParser.cs | 1 + .../Linux/LinuxUtilizationParserCgroupV2.cs | 15 ++-- .../Linux/OSFileSystem.cs | 14 ++++ .../Linux/LinuxCountersTests.cs | 6 +- .../LinuxUtilizationParserCgroupV2Tests.cs | 82 +++++++++++++++---- .../Linux/LinuxUtilizationParserTests.cs | 38 ++++----- .../Resources/FileNamesOnlyFileSystem.cs | 12 +++ .../Resources/HardcodedValueFileSystem.cs | 11 +++ .../Linux/Resources/TestResources.cs | 4 +- 10 files changed, 142 insertions(+), 47 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/IFileSystem.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/IFileSystem.cs index bd64b9d4259..9e0b4c50f9e 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/IFileSystem.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/IFileSystem.cs @@ -18,6 +18,12 @@ internal interface IFileSystem /// True/False. bool Exists(FileInfo fileInfo); + /// + /// Get directory name on the filesystem. + /// + /// string. + string[] GetDirectoryName(string directory, string pattern); + /// /// Reads content from the file. /// diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs index 7aa25a41dba..bb173ec2fca 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs @@ -457,6 +457,7 @@ private bool TryGetCgroupRequestCpu(IFileSystem fileSystem, out float cpuUnits) if (!_fileSystem.Exists(_cpuPodWeight)) { cpuUnits = 1; + Throw.InvalidOperationException($"'{_cpuPodWeight}' file does not exist!"); return false; } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs index 98c22ca10f0..73177de7930 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs @@ -229,7 +229,8 @@ public ulong GetAvailableMemoryInBytes() public long GetMemoryUsageInBytesFromSlices() { - string[] memoryUsageInBytesSlicesPath = Directory.GetDirectories(@"/sys/fs/cgroup", "*.slice", SearchOption.TopDirectoryOnly); + string[] memoryUsageInBytesSlicesPath = _fileSystem.GetDirectoryName("/sys/fs/cgroup/", @"\w+.slice"); + long memoryUsageInBytesTotal = 0; foreach (string path in memoryUsageInBytesSlicesPath) @@ -245,8 +246,7 @@ public long GetMemoryUsageInBytesFromSlices() var memoryUsageFile = _buffer.WrittenSpan; var next = GetNextNumber(memoryUsageFile, out var containerMemoryUsage); - // this file format doesn't expect to contain anything after the number. - if (containerMemoryUsage == -1) + if (containerMemoryUsage == 0 || containerMemoryUsage == -1) { Throw.InvalidOperationException( $"We tried to read '{memoryUsageInBytesFile}', and we expected to get a positive number but instead it was: '{containerMemoryUsage}'."); @@ -294,6 +294,7 @@ public ulong GetMemoryUsageInBytes() _buffer.Reset(); long memoryUsage = 0; + if (!_fileSystem.Exists(_memoryUsageInBytes)) { memoryUsage = GetMemoryUsageInBytesFromSlices(); @@ -303,13 +304,13 @@ public ulong GetMemoryUsageInBytes() memoryUsage = GetMemoryUsageInBytesPod(); } - if (memoryUsage < 0) + var memoryUsageTotal = memoryUsage - inactiveMemory; + + if (memoryUsageTotal < 0) { - Throw.InvalidOperationException($"The total memory usage read from '{_memoryUsageInBytes}' is lesser than inactive memory usage read from '{_memoryStat}'."); + Throw.InvalidOperationException($"The total memory usage read from '{_memoryUsageInBytes}' is lesser than inactive memory read from '{_memoryStat}'."); } - var memoryUsageTotal = memoryUsage - inactiveMemory; - return (ulong)memoryUsageTotal; } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/OSFileSystem.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/OSFileSystem.cs index e90a1c1eb37..e6b007255e1 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/OSFileSystem.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/OSFileSystem.cs @@ -2,9 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Runtime.CompilerServices; using System.Text; +using System.Text.RegularExpressions; using Microsoft.Shared.Pools; namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux; @@ -16,11 +19,22 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux; /// internal sealed class OSFileSystem : IFileSystem { + private readonly Dictionary _fileContent; public bool Exists(FileInfo fileInfo) { return fileInfo.Exists; } + public string[] GetDirectoryName(string directory, string pattern) + { + var m = Regex.Match(directory, pattern, RegexOptions.IgnoreCase); + + return _fileContent.Keys + .Where(x => x.StartsWith(directory, StringComparison.OrdinalIgnoreCase)) + .Select(x => x.Substring(0, 27)) + .ToArray(); + } + public int Read(FileInfo file, int length, Span destination) { using var stream = file.OpenRead(); diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs index 4b87738676e..bbc179f0174 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs @@ -89,8 +89,8 @@ public void LinuxCounters_Registers_Instruments_CgroupV2() { new FileInfo("/proc/meminfo"), "MemTotal: 1024 kB"}, { new FileInfo("/sys/fs/cgroup/cpuset.cpus.effective"), "0-19"}, { new FileInfo("/sys/fs/cgroup/cpu/cpu.max"), "20000 100000"}, - { new FileInfo("/sys/fs/cgroup/memory.stat"), "inactive_file 0"}, - { new FileInfo("/sys/fs/cgroup/memory.current"), "524288"}, + { new FileInfo("/sys/fs/cgroup/memory.stat"), "inactive_file 312312"}, + { new FileInfo("/sys/fs/cgroup/memory.current"), "524288423423"}, { new FileInfo("/sys/fs/cgroup/cpu.weight"), "4"}, }); @@ -124,6 +124,6 @@ public void LinuxCounters_Registers_Instruments_CgroupV2() Assert.Equal(ResourceUtilizationInstruments.CpuUtilization, samples[0].instrument.Name); Assert.Equal(double.NaN, samples[0].value); Assert.Equal(ResourceUtilizationInstruments.MemoryUtilization, samples[1].instrument.Name); - Assert.Equal(0.5, samples[1].value); + Assert.Equal(1, samples[1].value); } } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs index 994dbbcd31f..c7513c903f3 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; +using System.Text.RegularExpressions; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; using Microsoft.TestUtilities; using Xunit; @@ -73,6 +74,7 @@ public void Parser_Provides_Total_Available_Memory_In_Bytes() [InlineData("total_active_file")] [InlineData("total_inactive_file:_ 213912")] [InlineData("Total_Inactive_File 2")] + [InlineData("string@ -1")] public void When_Calling_GetMemoryUsageInBytes_Parser_Throws_When_MemoryStat_Doesnt_Contain_Total_Inactive_File_Section(string content) { var f = new HardcodedValueFileSystem(new Dictionary @@ -103,7 +105,7 @@ public void When_Calling_GetMemoryUsageInBytes_Parser_Throws_When_UsageInBytes_D { var f = new HardcodedValueFileSystem(new Dictionary { - { new FileInfo("/sys/fs/cgroup/memory.stat"), "inactive_file 0" }, + { new FileInfo("/sys/fs/cgroup/memory.stat"), "inactive_file 14340" }, { new FileInfo("/sys/fs/cgroup/memory.current"), content } }); @@ -114,19 +116,34 @@ public void When_Calling_GetMemoryUsageInBytes_Parser_Throws_When_UsageInBytes_D Assert.Contains("/sys/fs/cgroup/memory.current", r.Message); } + [ConditionalFact] + public void When_Calling_GetMemoryUsageInBytesFromSlices_Parser_Throws_When_UsageInBytes_Doesnt_Contain_A_Number() + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/system.slice/memory.current"), "dasda"}, + }); + + var p = new LinuxUtilizationParserCgroupV2(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetMemoryUsageInBytesFromSlices()); + + Assert.IsAssignableFrom(r); + Assert.Contains("/sys/fs/cgroup/system.slice/memory.current", r.Message); + } + [ConditionalTheory] - [InlineData(10, 1)] - [InlineData(23, 22)] - [InlineData(100000, 10000)] + [InlineData(104343, 1)] + [InlineData(23423, 22)] + [InlineData(10000, 100)] public void When_Calling_GetMemoryUsageInBytes_Parser_Throws_When_Inactive_Memory_Is_Bigger_Than_Total_Memory(int inactive, int total) { var f = new HardcodedValueFileSystem(new Dictionary { - { new FileInfo("/sys/fs/cgroup/memory/memory.stat"), $"total_inactive_file {inactive}" }, - { new FileInfo("/sys/fs/cgroup/memory/memory.usage_in_bytes"), total.ToString(CultureInfo.CurrentCulture) } + { new FileInfo("/sys/fs/cgroup/memory.stat"), $"inactive_file {inactive}" }, + { new FileInfo("/sys/fs/cgroup/memory.current"), total.ToString(CultureInfo.CurrentCulture) } }); - var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var p = new LinuxUtilizationParserCgroupV2(f, new FakeUserHz(100)); var r = Record.Exception(() => p.GetMemoryUsageInBytes()); Assert.IsAssignableFrom(r); @@ -172,7 +189,7 @@ public void When_Calling_GetHostAvailableMemory_Parser_Correctly_Transforms_Supp { new FileInfo("/proc/meminfo"), $"MemTotal: {value} {unit}" }, }); - var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var p = new LinuxUtilizationParserCgroupV2(f, new FakeUserHz(100)); var memory = p.GetHostAvailableMemory(); Assert.Equal(bytes, memory); @@ -258,21 +275,19 @@ public void When_Quota_And_Period_Are_Minus_One_It_Fallbacks_To_Cpuset() [InlineData("2d2d2d", "e3")] [InlineData("3d", "d3")] [InlineData(" 12", "eeeee 12")] - [InlineData("1 2", "eeeee 12")] [InlineData("12 ", "")] public void Parser_Throws_When_Cgroup_Cpu_Files_Contain_Invalid_Data(string quota, string period) { var f = new HardcodedValueFileSystem(new Dictionary { - { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_quota_us"), quota }, - { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_period_us"), period } + { new FileInfo("/sys/fs/cgroup/cpu.max"), $"{quota} {period}"}, }); - var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var p = new LinuxUtilizationParserCgroupV2(f, new FakeUserHz(100)); var r = Record.Exception(() => p.GetCgroupLimitedCpus()); Assert.IsAssignableFrom(r); - Assert.Contains("/sys/fs/cgroup/cpu/cpu.cfs_", r.Message); + Assert.Contains("/sys/fs/cgroup/cpu.max", r.Message); } [ConditionalFact] @@ -294,11 +309,11 @@ public void ReadingTotalMemory_Does_Not_Throw_For_Valid_Input() { var f = new HardcodedValueFileSystem(new Dictionary { - { new FileInfo("/sys/fs/cgroup/memory/memory.usage_in_bytes"), "32493514752\r\n" }, - { new FileInfo("/sys/fs/cgroup/memory/memory.stat"), "total_inactive_file 100" } + { new FileInfo("/sys/fs/cgroup/memory.current"), "32493514752" }, + { new FileInfo("/sys/fs/cgroup/memory.stat"), "inactive_file 100" } }); - var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var p = new LinuxUtilizationParserCgroupV2(f, new FakeUserHz(100)); var r = Record.Exception(() => p.GetMemoryUsageInBytes()); Assert.Null(r); @@ -324,4 +339,39 @@ public void ReadingCpuUsage_Does_Throws_For_Valid_Input(string content) Assert.IsAssignableFrom(r); Assert.Contains("proc/stat", r.Message); } + + [ConditionalTheory] + [InlineData("usage_", 12222)] + [InlineData("dasd", -1)] + [InlineData("@#dddada", 342322)] + public void Parser_Throws_When_CpuAcctUsage_Has_Invalid_Content(string content, int value) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/cpu.stat"), $"{content} {value}"}, + }); + + var p = new LinuxUtilizationParserCgroupV2(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetCgroupCpuUsageInNanoseconds()); + + Assert.IsAssignableFrom(r); + Assert.Contains("/sys/fs/cgroup/cpu.stat", r.Message); + } + + [ConditionalTheory] + [InlineData("-1")] + [InlineData("")] + public void Parser_Throws_When_Cgroup_Cpu_Weight_Files_Contain_Invalid_Data(string content) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/cpu/cpu.weight"), content }, + }); + + var p = new LinuxUtilizationParserCgroupV2(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetCgroupRequestCpu()); + + Assert.IsAssignableFrom(r); + Assert.Contains("/sys/fs/cgroup/cpu.weight", r.Message); + } } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserTests.cs index 434e61551f4..df2d856098b 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserTests.cs @@ -32,26 +32,7 @@ public void Parser_Throws_When_Data_Is_Invalid(string line) Assert.Throws(() => parser.GetHostCpuUsageInNanoseconds()); Assert.Throws(() => parser.GetHostCpuCount()); Assert.Throws(() => parser.GetCgroupCpuUsageInNanoseconds()); - } - - [ConditionalTheory] - [InlineData("DFIJEUWGHFWGBWEFWOMDOWKSLA")] - [InlineData("")] - [InlineData("________________________Asdasdasdas dd")] - [InlineData(" ")] - [InlineData("!@#!$%!@")] - public void Cgroupv2_Parser_Throws_When_Data_Is_Invalid(string line) - { - var parser = new LinuxUtilizationParserCgroupV2(new HardcodedValueFileSystem(line), new FakeUserHz(100)); - - Assert.Throws(() => parser.GetHostAvailableMemory()); - Assert.Throws(() => parser.GetAvailableMemoryInBytes()); - Assert.Throws(() => parser.GetMemoryUsageInBytes()); - Assert.Throws(() => parser.GetCgroupLimitedCpus()); - Assert.Throws(() => parser.GetHostCpuUsageInNanoseconds()); - Assert.Throws(() => parser.GetHostCpuCount()); - Assert.Throws(() => parser.GetCgroupCpuUsageInNanoseconds()); - Assert.Throws(() => parser.GetCgroupLimitedCpus()); + Assert.Throws(() => parser.GetCgroupRequestCpu()); } [ConditionalFact] @@ -350,4 +331,21 @@ public void ReadingCpuUsage_Does_Throws_For_Valid_Input(string content) Assert.IsAssignableFrom(r); Assert.Contains("proc/stat", r.Message); } + + [ConditionalTheory] + [InlineData("-1")] + [InlineData("")] + public void Parser_Throws_When_Cgroup_Cpu_Shares_Files_Contain_Invalid_Data(string content) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/cpu/cpu.shares"), content }, + }); + + var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetCgroupRequestCpu()); + + Assert.IsAssignableFrom(r); + Assert.Contains("/sys/fs/cgroup/cpu/cpu.shares", r.Message); + } } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FileNamesOnlyFileSystem.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FileNamesOnlyFileSystem.cs index deb6e9be197..7d1229e3a76 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FileNamesOnlyFileSystem.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FileNamesOnlyFileSystem.cs @@ -3,19 +3,31 @@ using System; using System.Buffers; +using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using Microsoft.Shared.Pools; namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; internal sealed class FileNamesOnlyFileSystem : IFileSystem { + private readonly Dictionary _fileContent; private readonly string _directory; public bool Exists(FileInfo fileInfo) { return fileInfo.Exists; } + public string[] GetDirectoryName(string directory, string pattern) + { + var m = Regex.Match(directory, pattern, RegexOptions.IgnoreCase); + + return _fileContent.Keys + .Where(x => x.StartsWith(directory, StringComparison.OrdinalIgnoreCase)) + .Select(x => x.Substring(0, 27)) + .ToArray(); + } public FileNamesOnlyFileSystem(string directory) { diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/HardcodedValueFileSystem.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/HardcodedValueFileSystem.cs index d8507cca015..cddff5d2ab7 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/HardcodedValueFileSystem.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/HardcodedValueFileSystem.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using Microsoft.Shared.Pools; namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; @@ -32,6 +33,16 @@ public bool Exists(FileInfo fileInfo) return _fileContent.ContainsKey(fileInfo.FullName); } + public string[] GetDirectoryName(string directory, string pattern) + { + var m = Regex.Match(directory, pattern, RegexOptions.IgnoreCase); + + return _fileContent.Keys + .Where(x => x.StartsWith(directory, StringComparison.OrdinalIgnoreCase)) + .Select(x => x.Substring(0, 27)) + .ToArray(); + } + public void ReadFirstLine(FileInfo file, BufferWriter destination) { if (_fileContent.Count == 0 || !_fileContent.TryGetValue(file.FullName, out var content)) diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/TestResources.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/TestResources.cs index f0910866f68..e7097f084fa 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/TestResources.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/TestResources.cs @@ -22,7 +22,8 @@ internal sealed class TestResources : IDisposable { "/sys/fs/cgroup/cpu.max", "1"}, { "/proc/meminfo", "MemTotal: 1 kB\r\n"}, { "/sys/fs/cgroup/cpuset.cpus.effective", "0-3"}, - { "/sys/fs/cgroup/cpu/cpu.wight", "512"} + { "/sys/fs/cgroup/cpu/cpu.wight", "512"}, + { "/sys/fs/cgroup/system.slice/memory.current", "dasda!@#"} }; private static readonly string[] _namesOfDirectories = @@ -30,6 +31,7 @@ internal sealed class TestResources : IDisposable "/sys/fs/cgroup", "/sys/fs/cgroup/memory", "/sys/fs/cgroup/cpu", + "/sys/fs/cgroup/system.slice", "/proc" }; From af86bc6be317ebd73429530849c20a055b656863 Mon Sep 17 00:00:00 2001 From: vitalid Date: Thu, 11 Apr 2024 19:28:51 +0300 Subject: [PATCH 17/31] GetDirectoryNames method regex implementation --- .../Linux/IFileSystem.cs | 2 +- .../Linux/LinuxUtilizationParserCgroupV2.cs | 11 ++--------- .../Linux/OSFileSystem.cs | 11 ++++------- .../Linux/LinuxUtilizationParserCgroupV2Tests.cs | 1 - .../Linux/Resources/FileNamesOnlyFileSystem.cs | 13 +++---------- .../Linux/Resources/HardcodedValueFileSystem.cs | 9 ++++----- 6 files changed, 14 insertions(+), 33 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/IFileSystem.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/IFileSystem.cs index 9e0b4c50f9e..623d961960d 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/IFileSystem.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/IFileSystem.cs @@ -22,7 +22,7 @@ internal interface IFileSystem /// Get directory name on the filesystem. /// /// string. - string[] GetDirectoryName(string directory, string pattern); + string[] GetDirectoryNames(DirectoryInfo directory, string pattern); /// /// Reads content from the file. diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs index 73177de7930..375eb812fed 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs @@ -229,7 +229,8 @@ public ulong GetAvailableMemoryInBytes() public long GetMemoryUsageInBytesFromSlices() { - string[] memoryUsageInBytesSlicesPath = _fileSystem.GetDirectoryName("/sys/fs/cgroup/", @"\w+.slice"); + // In cgroup v2, we need to read memory usage from all slices, which are directories in /sys/fs/cgroup/*.slice. + string[] memoryUsageInBytesSlicesPath = _fileSystem.GetDirectoryNames(new DirectoryInfo("/sys/fs/cgroup/"), @"\w+.slice"); long memoryUsageInBytesTotal = 0; @@ -513,14 +514,6 @@ private bool TryGetCpuUnitsFromCgroups(IFileSystem fileSystem, out float cpuUnit private bool TryGetCgroupRequestCpu(IFileSystem fileSystem, out float cpuUnits) { - // If the file doesn't exist, we assume that the system is a Host and we assign the default 1 CPU core. - if (!_fileSystem.Exists(_cpuPodWeight)) - { - cpuUnits = 1; - Throw.InvalidOperationException($"'{_cpuPodWeight}' file does not exist!"); - return false; - } - fileSystem.ReadFirstLine(_cpuPodWeight, _buffer); var cpuPodWeightBuffer = _buffer.WrittenSpan; diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/OSFileSystem.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/OSFileSystem.cs index e6b007255e1..c53239f4dea 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/OSFileSystem.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/OSFileSystem.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.CompilerServices; @@ -19,19 +18,17 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux; /// internal sealed class OSFileSystem : IFileSystem { - private readonly Dictionary _fileContent; public bool Exists(FileInfo fileInfo) { return fileInfo.Exists; } - public string[] GetDirectoryName(string directory, string pattern) + public string[] GetDirectoryNames(DirectoryInfo directory, string pattern) { - var m = Regex.Match(directory, pattern, RegexOptions.IgnoreCase); + var match = Regex.Match(directory.ToString(), pattern, RegexOptions.IgnoreCase); - return _fileContent.Keys - .Where(x => x.StartsWith(directory, StringComparison.OrdinalIgnoreCase)) - .Select(x => x.Substring(0, 27)) + return directory.GetDirectories(match.ToString()) + .Select(x => x.FullName) .ToArray(); } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs index c7513c903f3..91482277e93 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs @@ -6,7 +6,6 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; -using System.Text.RegularExpressions; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; using Microsoft.TestUtilities; using Xunit; diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FileNamesOnlyFileSystem.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FileNamesOnlyFileSystem.cs index 7d1229e3a76..56975e6a90e 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FileNamesOnlyFileSystem.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FileNamesOnlyFileSystem.cs @@ -3,30 +3,23 @@ using System; using System.Buffers; -using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using Microsoft.Shared.Pools; namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; internal sealed class FileNamesOnlyFileSystem : IFileSystem { - private readonly Dictionary _fileContent; private readonly string _directory; public bool Exists(FileInfo fileInfo) { return fileInfo.Exists; } - public string[] GetDirectoryName(string directory, string pattern) - { - var m = Regex.Match(directory, pattern, RegexOptions.IgnoreCase); - return _fileContent.Keys - .Where(x => x.StartsWith(directory, StringComparison.OrdinalIgnoreCase)) - .Select(x => x.Substring(0, 27)) - .ToArray(); + public string[] GetDirectoryNames(DirectoryInfo directory, string pattern) + { + throw new NotImplementedException(); } public FileNamesOnlyFileSystem(string directory) diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/HardcodedValueFileSystem.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/HardcodedValueFileSystem.cs index cddff5d2ab7..304076d8fe3 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/HardcodedValueFileSystem.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/HardcodedValueFileSystem.cs @@ -33,13 +33,12 @@ public bool Exists(FileInfo fileInfo) return _fileContent.ContainsKey(fileInfo.FullName); } - public string[] GetDirectoryName(string directory, string pattern) + public string[] GetDirectoryNames(DirectoryInfo directory, string pattern) { - var m = Regex.Match(directory, pattern, RegexOptions.IgnoreCase); - return _fileContent.Keys - .Where(x => x.StartsWith(directory, StringComparison.OrdinalIgnoreCase)) - .Select(x => x.Substring(0, 27)) + .Where(x => x.StartsWith(directory.ToString(), StringComparison.OrdinalIgnoreCase)) + .Select(x => Regex.Match(x, pattern, RegexOptions.IgnoreCase)) + .Select(x => Path.Combine(directory.ToString(), x.Value)) .ToArray(); } From 9ac1be505adeb3b42d9a6469d08a2459cb46c74f Mon Sep 17 00:00:00 2001 From: vitalid Date: Thu, 11 Apr 2024 22:22:31 +0300 Subject: [PATCH 18/31] refactored GetDirectoryNames for OSFIleSystem --- .../Linux/IFileSystem.cs | 2 +- .../Linux/LinuxUtilizationParserCgroupV2.cs | 21 ++++++++++++------- .../Linux/OSFileSystem.cs | 8 ++----- .../LinuxUtilizationParserCgroupV2Tests.cs | 7 ++++--- .../Resources/FileNamesOnlyFileSystem.cs | 4 ++-- .../Resources/HardcodedValueFileSystem.cs | 6 +++--- 6 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/IFileSystem.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/IFileSystem.cs index 623d961960d..c400821dfbb 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/IFileSystem.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/IFileSystem.cs @@ -22,7 +22,7 @@ internal interface IFileSystem /// Get directory name on the filesystem. /// /// string. - string[] GetDirectoryNames(DirectoryInfo directory, string pattern); + string[] GetDirectoryNames(string directory, string pattern); /// /// Reads content from the file. diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs index 375eb812fed..4fdb77e5e39 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; +using System.Xml.Schema; using Microsoft.Shared.Diagnostics; using Microsoft.Shared.Pools; @@ -195,7 +196,8 @@ public float GetCgroupRequestCpu() return cpuPodRequest / CpuShares; } - return cpuPodRequest; + // If we can't read the CPU weight, we assume that the pod request is 1 core. + return GetHostCpuCount(); } /// @@ -227,10 +229,10 @@ public ulong GetAvailableMemoryInBytes() : (ulong)maybeMemory; } - public long GetMemoryUsageInBytesFromSlices() + public long GetMemoryUsageInBytesFromSlices(string pattern) { // In cgroup v2, we need to read memory usage from all slices, which are directories in /sys/fs/cgroup/*.slice. - string[] memoryUsageInBytesSlicesPath = _fileSystem.GetDirectoryNames(new DirectoryInfo("/sys/fs/cgroup/"), @"\w+.slice"); + string[] memoryUsageInBytesSlicesPath = _fileSystem.GetDirectoryNames("/sys/fs/cgroup/", pattern); long memoryUsageInBytesTotal = 0; @@ -267,6 +269,8 @@ public long GetMemoryUsageInBytesFromSlices() public ulong GetMemoryUsageInBytes() { const string InactiveFile = "inactive_file"; + // Regex pattern for slice directory path in real file system + string? pattern = "*.slice"; if (!_fileSystem.Exists(_memoryStat)) { @@ -298,7 +302,7 @@ public ulong GetMemoryUsageInBytes() if (!_fileSystem.Exists(_memoryUsageInBytes)) { - memoryUsage = GetMemoryUsageInBytesFromSlices(); + memoryUsage = GetMemoryUsageInBytesFromSlices(pattern); } else { @@ -514,15 +518,18 @@ private bool TryGetCpuUnitsFromCgroups(IFileSystem fileSystem, out float cpuUnit private bool TryGetCgroupRequestCpu(IFileSystem fileSystem, out float cpuUnits) { + if (!_fileSystem.Exists(_cpuPodWeight)) + { + cpuUnits = 0; + return false; + } + fileSystem.ReadFirstLine(_cpuPodWeight, _buffer); var cpuPodWeightBuffer = _buffer.WrittenSpan; if (cpuPodWeightBuffer.IsEmpty || (cpuPodWeightBuffer.Length == 2 && cpuPodWeightBuffer[0] == '-' && cpuPodWeightBuffer[1] == '1')) { Throw.InvalidOperationException($"Could not parse '{_cpuPodWeight}' content. Expected to find CPU weight but got '{new string(cpuPodWeightBuffer)}' instead."); - _buffer.Reset(); - cpuUnits = -1; - return false; } _ = GetNextNumber(cpuPodWeightBuffer, out var cpuPodWeight); diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/OSFileSystem.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/OSFileSystem.cs index c53239f4dea..7a4604ec3a0 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/OSFileSystem.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/OSFileSystem.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Text; -using System.Text.RegularExpressions; using Microsoft.Shared.Pools; namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux; @@ -23,12 +22,9 @@ public bool Exists(FileInfo fileInfo) return fileInfo.Exists; } - public string[] GetDirectoryNames(DirectoryInfo directory, string pattern) + public string[] GetDirectoryNames(string directory, string pattern) { - var match = Regex.Match(directory.ToString(), pattern, RegexOptions.IgnoreCase); - - return directory.GetDirectories(match.ToString()) - .Select(x => x.FullName) + return Directory.GetDirectories(directory, pattern) .ToArray(); } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs index 91482277e93..57437f5b278 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs @@ -118,13 +118,14 @@ public void When_Calling_GetMemoryUsageInBytes_Parser_Throws_When_UsageInBytes_D [ConditionalFact] public void When_Calling_GetMemoryUsageInBytesFromSlices_Parser_Throws_When_UsageInBytes_Doesnt_Contain_A_Number() { + var regexPatternforSlices = @"\w+.slice"; var f = new HardcodedValueFileSystem(new Dictionary { { new FileInfo("/sys/fs/cgroup/system.slice/memory.current"), "dasda"}, }); var p = new LinuxUtilizationParserCgroupV2(f, new FakeUserHz(100)); - var r = Record.Exception(() => p.GetMemoryUsageInBytesFromSlices()); + var r = Record.Exception(() => p.GetMemoryUsageInBytesFromSlices(regexPatternforSlices)); Assert.IsAssignableFrom(r); Assert.Contains("/sys/fs/cgroup/system.slice/memory.current", r.Message); @@ -359,12 +360,12 @@ public void Parser_Throws_When_CpuAcctUsage_Has_Invalid_Content(string content, [ConditionalTheory] [InlineData("-1")] - [InlineData("")] + [InlineData("dasrz3424")] public void Parser_Throws_When_Cgroup_Cpu_Weight_Files_Contain_Invalid_Data(string content) { var f = new HardcodedValueFileSystem(new Dictionary { - { new FileInfo("/sys/fs/cgroup/cpu/cpu.weight"), content }, + { new FileInfo("/sys/fs/cgroup/cpu.weight"), content }, }); var p = new LinuxUtilizationParserCgroupV2(f, new FakeUserHz(100)); diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FileNamesOnlyFileSystem.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FileNamesOnlyFileSystem.cs index 56975e6a90e..0e2f645dc88 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FileNamesOnlyFileSystem.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FileNamesOnlyFileSystem.cs @@ -17,9 +17,9 @@ public bool Exists(FileInfo fileInfo) return fileInfo.Exists; } - public string[] GetDirectoryNames(DirectoryInfo directory, string pattern) + public string[] GetDirectoryNames(string directory, string pattern) { - throw new NotImplementedException(); + throw new NotSupportedException(); } public FileNamesOnlyFileSystem(string directory) diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/HardcodedValueFileSystem.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/HardcodedValueFileSystem.cs index 304076d8fe3..2ce3c93245a 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/HardcodedValueFileSystem.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/HardcodedValueFileSystem.cs @@ -33,12 +33,12 @@ public bool Exists(FileInfo fileInfo) return _fileContent.ContainsKey(fileInfo.FullName); } - public string[] GetDirectoryNames(DirectoryInfo directory, string pattern) + public string[] GetDirectoryNames(string directory, string pattern) { return _fileContent.Keys - .Where(x => x.StartsWith(directory.ToString(), StringComparison.OrdinalIgnoreCase)) + .Where(x => x.StartsWith(directory, StringComparison.OrdinalIgnoreCase)) .Select(x => Regex.Match(x, pattern, RegexOptions.IgnoreCase)) - .Select(x => Path.Combine(directory.ToString(), x.Value)) + .Select(x => Path.Combine(directory, x.Value)) .ToArray(); } From 5baf6ba7780a1bcd3345668a6ea0b5d1e98f93b7 Mon Sep 17 00:00:00 2001 From: R9 Fundamentals Date: Fri, 12 Apr 2024 11:51:23 +0200 Subject: [PATCH 19/31] Added test to OSFileSystemTests.cs for GetDirectoryNames --- .../Linux/OSFileSystemTests.cs | 9 +++++++++ .../fixtures/testing.slice/test.file | 0 2 files changed, 9 insertions(+) create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/testing.slice/test.file diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTests.cs index a88da1c0d2c..0813784d89b 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTests.cs @@ -12,6 +12,15 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Windows specific.")] public sealed class OSFileSystemTests { + [Fact] + public void GetDirectoryNames_ReturnsDirectoryNames() + { + var fileSystem = new OSFileSystem(); + var directoryNames = fileSystem.GetDirectoryNames("fixtures", "*.slice"); + + Assert.Collection(directoryNames, s => Assert.Equal("fixtures\\testing.slice", s)); + } + [ConditionalFact] public void Reading_First_File_Line_Works() { diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/testing.slice/test.file b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/testing.slice/test.file new file mode 100644 index 00000000000..e69de29bb2d From d950ad315775c391366bb95404b9e53a903aae46 Mon Sep 17 00:00:00 2001 From: Martin Obratil Date: Fri, 12 Apr 2024 11:57:12 +0200 Subject: [PATCH 20/31] Fix in OSFileSystemTests.cs --- .../Linux/OSFileSystemTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTests.cs index 0813784d89b..3c1207e56bc 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTests.cs @@ -12,7 +12,7 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Windows specific.")] public sealed class OSFileSystemTests { - [Fact] + [ConditionalFact] public void GetDirectoryNames_ReturnsDirectoryNames() { var fileSystem = new OSFileSystem(); From 91b0a4bd1d06b7950db66b415ffc682ba97ff232 Mon Sep 17 00:00:00 2001 From: Martin Obratil Date: Fri, 12 Apr 2024 11:59:20 +0200 Subject: [PATCH 21/31] Excluded from code coverage logic that identifies version of Cgroups --- .../ResourceMonitoringServiceCollectionExtensions.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs index e6a8270c71c..37ec2b438ad 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs @@ -118,9 +118,15 @@ private static ResourceMonitorBuilder AddLinuxProvider(this ResourceMonitorBuild builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.PickLinuxParser(); - bool injectParserV2 = ResourceMonitoringLinuxCgroupVersion.GetCgroupType(); + return builder; + } + [ExcludeFromCodeCoverage] + private static void PickLinuxParser(this ResourceMonitorBuilder builder) + { + var injectParserV2 = ResourceMonitoringLinuxCgroupVersion.GetCgroupType(); if (injectParserV2) { builder.Services.TryAddSingleton(); @@ -129,8 +135,6 @@ private static ResourceMonitorBuilder AddLinuxProvider(this ResourceMonitorBuild { builder.Services.TryAddSingleton(); } - - return builder; } #endif } From d24f868220e085e6ad83fdae24c003f4eada3649 Mon Sep 17 00:00:00 2001 From: vitalid Date: Fri, 12 Apr 2024 13:31:17 +0300 Subject: [PATCH 22/31] Removed unnecessary 'using', fix for GetCgroupRequestCpu to return cpuhostcount in some cases --- .../Linux/LinuxUtilizationParser.cs | 2 +- .../Linux/LinuxUtilizationParserCgroupV2.cs | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs index bb173ec2fca..3495fd2c0ad 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs @@ -175,7 +175,7 @@ public float GetCgroupRequestCpu() } // If we can't read the CPU weight, we assume that the pod request is 1 core. - return 1; + return GetHostCpuCount(); } public ulong GetAvailableMemoryInBytes() diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs index 4fdb77e5e39..c5d084f8e43 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs @@ -5,7 +5,6 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; -using System.Xml.Schema; using Microsoft.Shared.Diagnostics; using Microsoft.Shared.Pools; @@ -269,8 +268,9 @@ public long GetMemoryUsageInBytesFromSlices(string pattern) public ulong GetMemoryUsageInBytes() { const string InactiveFile = "inactive_file"; + // Regex pattern for slice directory path in real file system - string? pattern = "*.slice"; + const string pattern = "*.slice"; if (!_fileSystem.Exists(_memoryStat)) { @@ -546,9 +546,6 @@ private bool TryGetCgroupRequestCpu(IFileSystem fileSystem, out float cpuUnits) if (cpuPodShare == -1) { Throw.InvalidOperationException($"Could not calculate CPU share from CPU weight '{cpuPodShare}'"); - _buffer.Reset(); - cpuUnits = -1; - return false; } _buffer.Reset(); From e4c69ae2a3b4d3133218a83a586043718caef30d Mon Sep 17 00:00:00 2001 From: Martin Obratil Date: Fri, 12 Apr 2024 13:24:38 +0200 Subject: [PATCH 23/31] Analyzer error fix --- .../Linux/LinuxUtilizationParserCgroupV2.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs index c5d084f8e43..b4b172dfec4 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs @@ -270,7 +270,7 @@ public ulong GetMemoryUsageInBytes() const string InactiveFile = "inactive_file"; // Regex pattern for slice directory path in real file system - const string pattern = "*.slice"; + const string Pattern = "*.slice"; if (!_fileSystem.Exists(_memoryStat)) { @@ -302,7 +302,7 @@ public ulong GetMemoryUsageInBytes() if (!_fileSystem.Exists(_memoryUsageInBytes)) { - memoryUsage = GetMemoryUsageInBytesFromSlices(pattern); + memoryUsage = GetMemoryUsageInBytesFromSlices(Pattern); } else { From 46cc5bf1b1c2dc5fd856931af1b212edc3a59bd3 Mon Sep 17 00:00:00 2001 From: Martin Obratil Date: Fri, 12 Apr 2024 13:29:21 +0200 Subject: [PATCH 24/31] Test fix --- .../Linux/OSFileSystemTests.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTests.cs index 3c1207e56bc..510059ef4fc 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTests.cs @@ -16,9 +16,10 @@ public sealed class OSFileSystemTests public void GetDirectoryNames_ReturnsDirectoryNames() { var fileSystem = new OSFileSystem(); - var directoryNames = fileSystem.GetDirectoryNames("fixtures", "*.slice"); + var directoryNames = fileSystem.GetDirectoryNames( + Path.Combine(Directory.GetCurrentDirectory(), "fixtures"), "*.slice"); - Assert.Collection(directoryNames, s => Assert.Equal("fixtures\\testing.slice", s)); + Assert.Single(directoryNames); } [ConditionalFact] From 507f2ed89805630fa4e4ac83ebe8b321acc189ba Mon Sep 17 00:00:00 2001 From: Martin Obratil Date: Fri, 12 Apr 2024 14:38:45 +0200 Subject: [PATCH 25/31] Removed one test --- .../Linux/OSFileSystemTests.cs | 10 ---------- ...ensions.Diagnostics.ResourceMonitoring.Tests.csproj | 3 +++ .../fixtures/testing.slice/test.file | 0 3 files changed, 3 insertions(+), 10 deletions(-) delete mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/testing.slice/test.file diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTests.cs index 510059ef4fc..a88da1c0d2c 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTests.cs @@ -12,16 +12,6 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Windows specific.")] public sealed class OSFileSystemTests { - [ConditionalFact] - public void GetDirectoryNames_ReturnsDirectoryNames() - { - var fileSystem = new OSFileSystem(); - var directoryNames = fileSystem.GetDirectoryNames( - Path.Combine(Directory.GetCurrentDirectory(), "fixtures"), "*.slice"); - - Assert.Single(directoryNames); - } - [ConditionalFact] public void Reading_First_File_Line_Works() { diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj index 5f470b1aaf4..c25933cb637 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj @@ -14,6 +14,9 @@ PreserveNewest + + PreserveNewest + diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/testing.slice/test.file b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/testing.slice/test.file deleted file mode 100644 index e69de29bb2d..00000000000 From 4f6eb8bf6f9a9fb3a6cf0d434c66a1bf55aa8ed5 Mon Sep 17 00:00:00 2001 From: vitalid Date: Fri, 12 Apr 2024 19:37:21 +0300 Subject: [PATCH 26/31] updated config path to fixtures to make osfilesystem test pass --- .../Linux/OSFileSystemTests.cs | 10 ++++++++++ ...ensions.Diagnostics.ResourceMonitoring.Tests.csproj | 5 +---- .../fixtures/testing.slice/memory.current | 1 + 3 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/testing.slice/memory.current diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTests.cs index a88da1c0d2c..510059ef4fc 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTests.cs @@ -12,6 +12,16 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Windows specific.")] public sealed class OSFileSystemTests { + [ConditionalFact] + public void GetDirectoryNames_ReturnsDirectoryNames() + { + var fileSystem = new OSFileSystem(); + var directoryNames = fileSystem.GetDirectoryNames( + Path.Combine(Directory.GetCurrentDirectory(), "fixtures"), "*.slice"); + + Assert.Single(directoryNames); + } + [ConditionalFact] public void Reading_First_File_Line_Works() { diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj index c25933cb637..b7fcf503d96 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj @@ -11,10 +11,7 @@ - - PreserveNewest - - + PreserveNewest diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/testing.slice/memory.current b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/testing.slice/memory.current new file mode 100644 index 00000000000..5e5b583f293 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/testing.slice/memory.current @@ -0,0 +1 @@ +4234234 From 05a0994415d09bab57842f801d68b2a2ccfe4004 Mon Sep 17 00:00:00 2001 From: vitalid Date: Fri, 12 Apr 2024 20:24:46 +0300 Subject: [PATCH 27/31] Added ExcludeFromCodeCoverage for GetCgroupType based on nested setting --- .../ResourceMonitoringLinuxCgroupVersion.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringLinuxCgroupVersion.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringLinuxCgroupVersion.cs index 066624613fb..8bdedc860f4 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringLinuxCgroupVersion.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringLinuxCgroupVersion.cs @@ -1,4 +1,5 @@ -using System.IO; +using System.Diagnostics.CodeAnalysis; +using System.IO; // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. @@ -14,6 +15,7 @@ internal static class ResourceMonitoringLinuxCgroupVersion /// Get drive metadata for each drive in the system and detects the cgroup version. /// /// True/False. + [ExcludeFromCodeCoverage] public static bool GetCgroupType() { DriveInfo[] allDrives = DriveInfo.GetDrives(); From 54849608730bb570801613acd6fa5021082e1ed8 Mon Sep 17 00:00:00 2001 From: vitalid Date: Fri, 12 Apr 2024 21:30:21 +0300 Subject: [PATCH 28/31] Additional test for GetMemoryUsageInBytesFromSlices --- .../Linux/LinuxUtilizationParser.cs | 7 +------ .../Linux/LinuxUtilizationParserCgroupV2.cs | 2 +- .../Linux/LinuxUtilizationParserCgroupV2Tests.cs | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs index 3495fd2c0ad..b377bc0246d 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs @@ -174,7 +174,6 @@ public float GetCgroupRequestCpu() return cpuUnits; } - // If we can't read the CPU weight, we assume that the pod request is 1 core. return GetHostCpuCount(); } @@ -456,8 +455,7 @@ private bool TryGetCgroupRequestCpu(IFileSystem fileSystem, out float cpuUnits) { if (!_fileSystem.Exists(_cpuPodWeight)) { - cpuUnits = 1; - Throw.InvalidOperationException($"'{_cpuPodWeight}' file does not exist!"); + cpuUnits = 0; return false; } @@ -467,10 +465,7 @@ private bool TryGetCgroupRequestCpu(IFileSystem fileSystem, out float cpuUnits) if (cpuPodWeightBuffer.IsEmpty || (cpuPodWeightBuffer.Length == 2 && cpuPodWeightBuffer[0] == '-' && cpuPodWeightBuffer[1] == '1')) { - _buffer.Reset(); Throw.InvalidOperationException($"Could not parse '{_cpuPodWeight}' content. Expected to find CPU weight but got '{new string(cpuPodWeightBuffer)}' instead."); - cpuUnits = -1; - return false; } _buffer.Reset(); diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs index b4b172dfec4..8e405304c9d 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs @@ -195,7 +195,6 @@ public float GetCgroupRequestCpu() return cpuPodRequest / CpuShares; } - // If we can't read the CPU weight, we assume that the pod request is 1 core. return GetHostCpuCount(); } @@ -250,6 +249,7 @@ public long GetMemoryUsageInBytesFromSlices(string pattern) if (containerMemoryUsage == 0 || containerMemoryUsage == -1) { + memoryUsageInBytesTotal = 0; Throw.InvalidOperationException( $"We tried to read '{memoryUsageInBytesFile}', and we expected to get a positive number but instead it was: '{containerMemoryUsage}'."); } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs index 57437f5b278..81c329ac9e0 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs @@ -131,6 +131,21 @@ public void When_Calling_GetMemoryUsageInBytesFromSlices_Parser_Throws_When_Usag Assert.Contains("/sys/fs/cgroup/system.slice/memory.current", r.Message); } + [ConditionalFact] + public void When_Calling_GetMemoryUsageInBytesFromSlices_Parser_Does_Not_Throw() + { + var regexPatternforSlices = @"\w+.slice"; + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/system.slice/memory.current"), "5342342"}, + }); + + var p = new LinuxUtilizationParserCgroupV2(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetMemoryUsageInBytesFromSlices(regexPatternforSlices)); + + Assert.Null(r); + } + [ConditionalTheory] [InlineData(104343, 1)] [InlineData(23423, 22)] From 00f46f57f703dd49b8d6e2ba7be65866d35ebdea Mon Sep 17 00:00:00 2001 From: vitalid Date: Fri, 12 Apr 2024 23:27:40 +0300 Subject: [PATCH 29/31] Tests for GetCgroupCpuUsageInNanoseconds and for GetAvailableMemoryInBytes --- .../LinuxUtilizationParserCgroupV2Tests.cs | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs index 81c329ac9e0..0be39c612d8 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs @@ -115,6 +115,24 @@ public void When_Calling_GetMemoryUsageInBytes_Parser_Throws_When_UsageInBytes_D Assert.Contains("/sys/fs/cgroup/memory.current", r.Message); } + [ConditionalTheory] + [InlineData("Suspicious12312312")] + [InlineData("string@")] + [InlineData("string12312")] + public void When_Calling_GetAvailableMemoryInBytes_Parser_Throws_When_AvailableMemoryInBytes_Doesnt_Contain_Just_A_Number(string content) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/memory.max"), content }, + }); + + var p = new LinuxUtilizationParserCgroupV2(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetAvailableMemoryInBytes()); + + Assert.IsAssignableFrom(r); + Assert.Contains("/sys/fs/cgroup/memory.max", r.Message); + } + [ConditionalFact] public void When_Calling_GetMemoryUsageInBytesFromSlices_Parser_Throws_When_UsageInBytes_Doesnt_Contain_A_Number() { @@ -359,7 +377,7 @@ public void ReadingCpuUsage_Does_Throws_For_Valid_Input(string content) [InlineData("usage_", 12222)] [InlineData("dasd", -1)] [InlineData("@#dddada", 342322)] - public void Parser_Throws_When_CpuAcctUsage_Has_Invalid_Content(string content, int value) + public void Parser_Throws_When_CpuAcctUsage_Has_Invalid_Content_Both_Parts(string content, int value) { var f = new HardcodedValueFileSystem(new Dictionary { @@ -373,6 +391,24 @@ public void Parser_Throws_When_CpuAcctUsage_Has_Invalid_Content(string content, Assert.Contains("/sys/fs/cgroup/cpu.stat", r.Message); } + [ConditionalTheory] + [InlineData(-32131)] + [InlineData(-1)] + [InlineData(-15.323)] + public void Parser_Throws_When_Usage_Usec_Has_Negative_Valuet(int value) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/cpu.stat"), $"usage_usec {value}"}, + }); + + var p = new LinuxUtilizationParserCgroupV2(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetCgroupCpuUsageInNanoseconds()); + + Assert.IsAssignableFrom(r); + Assert.Contains("/sys/fs/cgroup/cpu.stat", r.Message); + } + [ConditionalTheory] [InlineData("-1")] [InlineData("dasrz3424")] From d0997704f22fc5cf3f72370d3eb3c53d599bb2fa Mon Sep 17 00:00:00 2001 From: nezdali Date: Mon, 15 Apr 2024 13:08:50 +0300 Subject: [PATCH 30/31] Update src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/IFileSystem.cs Co-authored-by: Nikita Balabaev --- .../Linux/IFileSystem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/IFileSystem.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/IFileSystem.cs index c400821dfbb..9799a45fd2e 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/IFileSystem.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/IFileSystem.cs @@ -19,7 +19,7 @@ internal interface IFileSystem bool Exists(FileInfo fileInfo); /// - /// Get directory name on the filesystem. + /// Get directory names on the filesystem based on the provided pattern. /// /// string. string[] GetDirectoryNames(string directory, string pattern); From 031caa7b530c4153f211cae0bfa2ebc741f00df1 Mon Sep 17 00:00:00 2001 From: vitalid Date: Tue, 16 Apr 2024 13:26:39 +0300 Subject: [PATCH 31/31] Renamed LinuxUtilizationParser to LinuxUtilizationParserCgroupV1 and minor fixes --- ...r.cs => LinuxUtilizationParserCgroupV1.cs} | 6 ++-- .../Linux/LinuxUtilizationParserCgroupV2.cs | 2 +- .../Linux/LinuxUtilizationProvider.cs | 2 +- ...ceMonitoringServiceCollectionExtensions.cs | 2 +- .../Linux/AcceptanceTest.cs | 2 +- .../Linux/LinuxCountersTests.cs | 2 +- .../Linux/LinuxUtilizationParserTests.cs | 32 +++++++++---------- 7 files changed, 24 insertions(+), 24 deletions(-) rename src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/{LinuxUtilizationParser.cs => LinuxUtilizationParserCgroupV1.cs} (98%) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV1.cs similarity index 98% rename from src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs rename to src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV1.cs index b377bc0246d..622a316743f 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParser.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV1.cs @@ -10,11 +10,11 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux; /// -/// Parses Linux files to retrieve resource utilization data. +/// Parses Linux cgroup v1 files to retrieve resource utilization data. /// This class is not thread safe. /// When the same instance is called by multiple threads it may return corrupted data. /// -internal sealed class LinuxUtilizationParser : ILinuxUtilizationParser +internal sealed class LinuxUtilizationParserCgroupV1 : ILinuxUtilizationParser { private const float CpuShares = 1024; @@ -87,7 +87,7 @@ internal sealed class LinuxUtilizationParser : ILinuxUtilizationParser private readonly long _userHz; private readonly BufferWriter _buffer = new(); - public LinuxUtilizationParser(IFileSystem fileSystem, IUserHz userHz) + public LinuxUtilizationParserCgroupV1(IFileSystem fileSystem, IUserHz userHz) { _fileSystem = fileSystem; _userHz = userHz.Value; diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs index 8e405304c9d..b1b1c5e7d51 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs @@ -11,7 +11,7 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux; /// -/// Parses Linux files to retrieve resource utilization data. +/// Parses Linux cgroups v2 files to retrieve resource utilization data. /// This class is not thread safe. /// When the same instance is called by multiple threads it may return corrupted data. /// diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs index 5d729ba9505..8d41b5fd167 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs @@ -65,8 +65,8 @@ public LinuxUtilizationProvider(IOptions options, ILi // cpuGuaranteedRequest is a CPU request for pod, for host its 1 core // available CPUs is a CPU limit for a pod or for a host. - // 1 - Currently value in /sys/fs/cgroup/memory.min per container is 0 // _totalMemoryInBytes - Resource Memory Limit (in k8s terms) + // _totalMemoryInBytes - To keep the contract, this parameter will get the Host available memory Resources = new SystemResources(cpuGuaranteedRequest, availableCpus, _totalMemoryInBytes, _totalMemoryInBytes); } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs index 37ec2b438ad..8b4d1915769 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs @@ -133,7 +133,7 @@ private static void PickLinuxParser(this ResourceMonitorBuilder builder) } else { - builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); } } #endif diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs index 57a9d2f6b61..50fdf13e651 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs @@ -124,7 +124,7 @@ public void Adding_Linux_Resource_Utilization_With_Section_Registers_SnapshotPro .AddResourceMonitoring(x => x.ConfigureMonitor(section)) // Ingesting LinuxUtilizationParser with cgroup v1 support. - .Replace(ServiceDescriptor.Singleton()) + .Replace(ServiceDescriptor.Singleton()) .BuildServiceProvider(); var provider = services.GetService(); diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs index bbc179f0174..27a161ff99e 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs @@ -38,7 +38,7 @@ public void LinuxCounters_Registers_Instruments() { new FileInfo("/sys/fs/cgroup/cpu/cpu.shares"), "1024"}, }); - var parser = new LinuxUtilizationParser(fileSystem: fileSystem, new FakeUserHz(100)); + var parser = new LinuxUtilizationParserCgroupV1(fileSystem: fileSystem, new FakeUserHz(100)); var provider = new LinuxUtilizationProvider(options, parser, meterFactoryMock.Object, TimeProvider.System); using var listener = new MeterListener diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserTests.cs index df2d856098b..d1fe509323d 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserTests.cs @@ -23,7 +23,7 @@ public sealed class LinuxUtilizationParserTests [InlineData("!@#!$%!@")] public void Parser_Throws_When_Data_Is_Invalid(string line) { - var parser = new LinuxUtilizationParser(new HardcodedValueFileSystem(line), new FakeUserHz(100)); + var parser = new LinuxUtilizationParserCgroupV1(new HardcodedValueFileSystem(line), new FakeUserHz(100)); Assert.Throws(() => parser.GetHostAvailableMemory()); Assert.Throws(() => parser.GetAvailableMemoryInBytes()); @@ -38,7 +38,7 @@ public void Parser_Throws_When_Data_Is_Invalid(string line) [ConditionalFact] public void Parser_Can_Read_Host_And_Cgroup_Available_Cpu_Count() { - var parser = new LinuxUtilizationParser(new FileNamesOnlyFileSystem(TestResources.TestFilesLocation), new FakeUserHz(100)); + var parser = new LinuxUtilizationParserCgroupV1(new FileNamesOnlyFileSystem(TestResources.TestFilesLocation), new FakeUserHz(100)); var hostCpuCount = parser.GetHostCpuCount(); var cgroupCpuCount = parser.GetCgroupLimitedCpus(); @@ -50,7 +50,7 @@ public void Parser_Can_Read_Host_And_Cgroup_Available_Cpu_Count() public void Parser_Provides_Total_Available_Memory_In_Bytes() { var fs = new FileNamesOnlyFileSystem(TestResources.TestFilesLocation); - var parser = new LinuxUtilizationParser(fs, new FakeUserHz(100)); + var parser = new LinuxUtilizationParserCgroupV1(fs, new FakeUserHz(100)); var totalMem = parser.GetHostAvailableMemory(); @@ -80,7 +80,7 @@ public void When_Calling_GetMemoryUsageInBytes_Parser_Throws_When_MemoryStat_Doe { new FileInfo("/sys/fs/cgroup/memory/memory.stat"), content } }); - var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100)); var r = Record.Exception(() => p.GetMemoryUsageInBytes()); Assert.IsAssignableFrom(r); @@ -107,7 +107,7 @@ public void When_Calling_GetMemoryUsageInBytes_Parser_Throws_When_UsageInBytes_D { new FileInfo("/sys/fs/cgroup/memory/memory.usage_in_bytes"), content } }); - var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100)); var r = Record.Exception(() => p.GetMemoryUsageInBytes()); Assert.IsAssignableFrom(r); @@ -126,7 +126,7 @@ public void When_Calling_GetMemoryUsageInBytes_Parser_Throws_When_Inactive_Memor { new FileInfo("/sys/fs/cgroup/memory/memory.usage_in_bytes"), total.ToString(CultureInfo.CurrentCulture) } }); - var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100)); var r = Record.Exception(() => p.GetMemoryUsageInBytes()); Assert.IsAssignableFrom(r); @@ -152,7 +152,7 @@ public void When_Calling_GetHostAvailableMemory_Parser_Throws_When_MemInfo_Does_ { new FileInfo("/proc/meminfo"), totalMemory }, }); - var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100)); var r = Record.Exception(() => p.GetHostAvailableMemory()); Assert.IsAssignableFrom(r); @@ -172,7 +172,7 @@ public void When_Calling_GetHostAvailableMemory_Parser_Correctly_Transforms_Supp { new FileInfo("/proc/meminfo"), $"MemTotal: {value} {unit}" }, }); - var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100)); var memory = p.GetHostAvailableMemory(); Assert.Equal(bytes, memory); @@ -199,7 +199,7 @@ public void When_No_Cgroup_Cpu_Limits_Are_Not_Set_We_Get_Available_Cpus_From_Cpu { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_period_us"), "-1" } }); - var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100)); var cpus = p.GetCgroupLimitedCpus(); Assert.Equal(result, cpus); @@ -226,7 +226,7 @@ public void Parser_Throws_When_CpuSet_Has_Invalid_Content(string content) { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_period_us"), "-1" } }); - var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100)); var r = Record.Exception(() => p.GetCgroupLimitedCpus()); Assert.IsAssignableFrom(r); @@ -247,7 +247,7 @@ public void When_Quota_And_Period_Are_Minus_One_It_Fallbacks_To_Cpuset(string qu { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_period_us"), period } }); - var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100)); var r = Record.Exception(() => p.GetCgroupLimitedCpus()); Assert.IsAssignableFrom(r); @@ -275,7 +275,7 @@ public void Parser_Throws_When_Cgroup_Cpu_Files_Contain_Invalid_Data(string quot { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_period_us"), period } }); - var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100)); var r = Record.Exception(() => p.GetCgroupLimitedCpus()); Assert.IsAssignableFrom(r); @@ -290,7 +290,7 @@ public void ReadingCpuUsage_Does_Not_Throw_For_Valid_Input() { new FileInfo("/proc/stat"), "cpu 2569530 36700 245693 4860924 82283 0 4360 0 0 0" } }); - var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100)); var r = Record.Exception(() => p.GetHostCpuUsageInNanoseconds()); Assert.Null(r); @@ -305,7 +305,7 @@ public void ReadingTotalMemory_Does_Not_Throw_For_Valid_Input() { new FileInfo("/sys/fs/cgroup/memory/memory.stat"), "total_inactive_file 100" } }); - var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100)); var r = Record.Exception(() => p.GetMemoryUsageInBytes()); Assert.Null(r); @@ -325,7 +325,7 @@ public void ReadingCpuUsage_Does_Throws_For_Valid_Input(string content) { new FileInfo("/proc/stat"), content } }); - var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100)); var r = Record.Exception(() => p.GetHostCpuUsageInNanoseconds()); Assert.IsAssignableFrom(r); @@ -342,7 +342,7 @@ public void Parser_Throws_When_Cgroup_Cpu_Shares_Files_Contain_Invalid_Data(stri { new FileInfo("/sys/fs/cgroup/cpu/cpu.shares"), content }, }); - var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100)); var r = Record.Exception(() => p.GetCgroupRequestCpu()); Assert.IsAssignableFrom(r);