From a06edbd6fad6556542a3bdffb408add4875e5cb0 Mon Sep 17 00:00:00 2001 From: Igor Velikorossov Date: Wed, 29 May 2024 08:26:20 +1000 Subject: [PATCH] Avoid buffer race conditions in CGroups (#5129) * Correct tests decorations * Avoid buffer race conditions in CGroups Fixes #5114 --- .../Linux/LinuxUtilizationParserCgroupV1.cs | 158 ++++++++-------- .../Linux/LinuxUtilizationParserCgroupV2.cs | 178 +++++++++--------- .../ReturnableBufferWriter.cs | 45 +++++ .../Linux/AcceptanceTest.cs | 12 +- .../Linux/LinuxCountersTests.cs | 2 +- ...=> LinuxUtilizationParserCgroupV1Tests.cs} | 50 ++++- .../LinuxUtilizationParserCgroupV2Tests.cs | 47 ++++- .../Linux/OSFileSystemTests.cs | 2 +- 8 files changed, 314 insertions(+), 180 deletions(-) create mode 100644 src/Shared/BufferWriterPool/ReturnableBufferWriter.cs rename test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/{LinuxUtilizationParserTests.cs => LinuxUtilizationParserCgroupV1Tests.cs} (89%) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV1.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV1.cs index 622a316743f..00ada5dc8ae 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV1.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV1.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.IO; +using Microsoft.Extensions.ObjectPool; using Microsoft.Shared.Diagnostics; using Microsoft.Shared.Pools; @@ -17,6 +18,7 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux; internal sealed class LinuxUtilizationParserCgroupV1 : ILinuxUtilizationParser { private const float CpuShares = 1024; + private static readonly ObjectPool> _sharedBufferWriterPool = BufferWriterPool.CreateBufferWriterPool(); /// /// File contains the amount of CPU time (in microseconds) available to the group during each accounting period. @@ -85,7 +87,6 @@ internal sealed class LinuxUtilizationParserCgroupV1 : ILinuxUtilizationParser private readonly IFileSystem _fileSystem; private readonly long _userHz; - private readonly BufferWriter _buffer = new(); public LinuxUtilizationParserCgroupV1(IFileSystem fileSystem, IUserHz userHz) { @@ -95,19 +96,18 @@ public LinuxUtilizationParserCgroupV1(IFileSystem fileSystem, IUserHz userHz) public long GetCgroupCpuUsageInNanoseconds() { - _fileSystem.ReadAll(_cpuacctUsage, _buffer); + using ReturnableBufferWriter bufferWriter = new(_sharedBufferWriterPool); + _fileSystem.ReadAll(_cpuacctUsage, bufferWriter.Buffer); - var usage = _buffer.WrittenSpan; + ReadOnlySpan usage = bufferWriter.Buffer.WrittenSpan; - _ = GetNextNumber(usage, out var nanoseconds); + _ = GetNextNumber(usage, out long nanoseconds); if (nanoseconds == -1) { Throw.InvalidOperationException($"Could not get cpu usage from '{_cpuacctUsage}'. Expected positive number, but got '{new string(usage)}'."); } - _buffer.Reset(); - return nanoseconds; } @@ -117,21 +117,22 @@ public long GetHostCpuUsageInNanoseconds() const int NumberOfColumnsRepresentingCpuUsage = 8; const int NanosecondsInSecond = 1_000_000_000; - _fileSystem.ReadFirstLine(_procStat, _buffer); + using ReturnableBufferWriter bufferWriter = new(_sharedBufferWriterPool); + _fileSystem.ReadFirstLine(_procStat, bufferWriter.Buffer); - var stat = _buffer.WrittenSpan; - var total = 0L; + ReadOnlySpan stat = bufferWriter.Buffer.WrittenSpan; + long total = 0L; - if (!_buffer.WrittenSpan.StartsWith(StartingTokens)) + if (!bufferWriter.Buffer.WrittenSpan.StartsWith(StartingTokens)) { - Throw.InvalidOperationException($"Expected proc/stat to start with '{StartingTokens}' but it was '{new string(_buffer.WrittenSpan)}'."); + Throw.InvalidOperationException($"Expected proc/stat to start with '{StartingTokens}' but it was '{new string(bufferWriter.Buffer.WrittenSpan)}'."); } stat = stat.Slice(StartingTokens.Length, stat.Length - StartingTokens.Length); - for (var i = 0; i < NumberOfColumnsRepresentingCpuUsage; i++) + for (int i = 0; i < NumberOfColumnsRepresentingCpuUsage; i++) { - var next = GetNextNumber(stat, out var number); + int next = GetNextNumber(stat, out long number); if (number != -1) { @@ -147,8 +148,6 @@ public long GetHostCpuUsageInNanoseconds() stat = stat.Slice(next, stat.Length - next); } - _buffer.Reset(); - return (long)(total / (double)_userHz * NanosecondsInSecond); } @@ -159,7 +158,7 @@ public long GetHostCpuUsageInNanoseconds() /// public float GetCgroupLimitedCpus() { - if (TryGetCpuUnitsFromCgroups(_fileSystem, out var cpus)) + if (TryGetCpuUnitsFromCgroups(_fileSystem, out float cpus)) { return cpus; } @@ -169,7 +168,7 @@ public float GetCgroupLimitedCpus() public float GetCgroupRequestCpu() { - if (TryGetCgroupRequestCpu(_fileSystem, out var cpuUnits)) + if (TryGetCgroupRequestCpu(_fileSystem, out float cpuUnits)) { return cpuUnits; } @@ -180,19 +179,22 @@ public float GetCgroupRequestCpu() public ulong GetAvailableMemoryInBytes() { const long UnsetCgroupMemoryLimit = 9_223_372_036_854_771_712; + long maybeMemory = UnsetCgroupMemoryLimit; - _fileSystem.ReadAll(_memoryLimitInBytes, _buffer); + // Constrain the scope of the buffer because GetHostAvailableMemory is allocating its own buffer. + using (ReturnableBufferWriter bufferWriter = new(_sharedBufferWriterPool)) + { + _fileSystem.ReadAll(_memoryLimitInBytes, bufferWriter.Buffer); - var memoryBuffer = _buffer.WrittenSpan; - _ = GetNextNumber(memoryBuffer, out var maybeMemory); + ReadOnlySpan memoryBuffer = bufferWriter.Buffer.WrittenSpan; + _ = GetNextNumber(memoryBuffer, out maybeMemory); - if (maybeMemory == -1) - { - Throw.InvalidOperationException($"Could not parse '{_memoryLimitInBytes}' content. Expected to find available memory in bytes but got '{new string(memoryBuffer)}' instead."); + 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; @@ -202,30 +204,31 @@ public ulong GetMemoryUsageInBytes() { const string TotalInactiveFile = "total_inactive_file"; - _fileSystem.ReadAll(_memoryStat, _buffer); - var memoryFile = _buffer.WrittenSpan; + using ReturnableBufferWriter bufferWriter = new(_sharedBufferWriterPool); + _fileSystem.ReadAll(_memoryStat, bufferWriter.Buffer); + ReadOnlySpan memoryFile = bufferWriter.Buffer.WrittenSpan; - var index = memoryFile.IndexOf(TotalInactiveFile.AsSpan()); + int index = memoryFile.IndexOf(TotalInactiveFile.AsSpan()); if (index == -1) { Throw.InvalidOperationException($"Unable to find total_inactive_file from '{_memoryStat}'."); } - var inactiveMemorySlice = memoryFile.Slice(index + TotalInactiveFile.Length, memoryFile.Length - index - TotalInactiveFile.Length); - _ = GetNextNumber(inactiveMemorySlice, out var inactiveMemory); + ReadOnlySpan inactiveMemorySlice = memoryFile.Slice(index + TotalInactiveFile.Length, memoryFile.Length - index - TotalInactiveFile.Length); + _ = GetNextNumber(inactiveMemorySlice, out long inactiveMemory); if (inactiveMemory == -1) { Throw.InvalidOperationException($"The value of total_inactive_file found in '{_memoryStat}' is not a positive number: '{new string(inactiveMemorySlice)}'."); } - _buffer.Reset(); + bufferWriter.Buffer.Reset(); - _fileSystem.ReadAll(_memoryUsageInBytes, _buffer); + _fileSystem.ReadAll(_memoryUsageInBytes, bufferWriter.Buffer); - var containerMemoryUsageFile = _buffer.WrittenSpan; - var next = GetNextNumber(containerMemoryUsageFile, out var containerMemoryUsage); + ReadOnlySpan containerMemoryUsageFile = bufferWriter.Buffer.WrittenSpan; + int next = GetNextNumber(containerMemoryUsageFile, out long containerMemoryUsage); // this file format doesn't expect to contain anything after the number. if (containerMemoryUsage == -1) @@ -234,9 +237,9 @@ public ulong GetMemoryUsageInBytes() $"We tried to read '{_memoryUsageInBytes}', and we expected to get a positive number but instead it was: '{new string(containerMemoryUsageFile)}'."); } - _buffer.Reset(); + bufferWriter.Buffer.Reset(); - var memoryUsage = containerMemoryUsage - inactiveMemory; + long memoryUsage = containerMemoryUsage - inactiveMemory; if (memoryUsage < 0) { @@ -253,17 +256,18 @@ 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; + using ReturnableBufferWriter bufferWriter = new(_sharedBufferWriterPool); + _fileSystem.ReadFirstLine(_memInfo, bufferWriter.Buffer); + ReadOnlySpan firstLine = bufferWriter.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); + ReadOnlySpan totalMemory = firstLine.Slice(MemTotal.Length, firstLine.Length - MemTotal.Length); - var next = GetNextNumber(totalMemory, out var totalMemoryAvailable); + int next = GetNextNumber(totalMemory, out long totalMemoryAvailable); if (totalMemoryAvailable == -1) { @@ -275,10 +279,10 @@ public ulong GetHostAvailableMemory() 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; + ReadOnlySpan unit = totalMemory.Slice(totalMemory.Length - 2, 2); + ulong memory = (ulong)totalMemoryAvailable; - var u = unit switch + ulong u = unit switch { "kB" => memory << 10, "MB" => memory << 20, @@ -288,8 +292,6 @@ public ulong GetHostAvailableMemory() $"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; } @@ -299,29 +301,30 @@ public ulong GetHostAvailableMemory() /// public float GetHostCpuCount() { - _fileSystem.ReadFirstLine(_cpuSetCpus, _buffer); - var stats = _buffer.WrittenSpan; + using ReturnableBufferWriter bufferWriter = new(_sharedBufferWriterPool); + _fileSystem.ReadFirstLine(_cpuSetCpus, bufferWriter.Buffer); + ReadOnlySpan stats = bufferWriter.Buffer.WrittenSpan; if (stats.IsEmpty) { ThrowException(stats); } - var cpuCount = 0L; + long cpuCount = 0L; // Iterate over groups (comma-separated) while (true) { - var groupIndex = stats.IndexOf(','); + int groupIndex = stats.IndexOf(','); - var group = groupIndex == -1 ? stats : stats.Slice(0, groupIndex); + ReadOnlySpan group = groupIndex == -1 ? stats : stats.Slice(0, groupIndex); - var rangeIndex = group.IndexOf('-'); + int rangeIndex = group.IndexOf('-'); if (rangeIndex == -1) { // Single number - _ = GetNextNumber(group, out var singleCpu); + _ = GetNextNumber(group, out long singleCpu); if (singleCpu == -1) { @@ -333,11 +336,11 @@ public float GetHostCpuCount() else { // Range - var first = group.Slice(0, rangeIndex); - _ = GetNextNumber(first, out var startCpu); + ReadOnlySpan first = group.Slice(0, rangeIndex); + _ = GetNextNumber(first, out long startCpu); - var second = group.Slice(rangeIndex + 1); - var next = GetNextNumber(second, out var endCpu); + ReadOnlySpan second = group.Slice(rangeIndex + 1); + int next = GetNextNumber(second, out long endCpu); if (endCpu == -1 || startCpu == -1 || endCpu < startCpu || next != -1) { @@ -355,8 +358,6 @@ public float GetHostCpuCount() stats = stats.Slice(groupIndex + 1); } - _buffer.Reset(); - return cpuCount; static void ThrowException(ReadOnlySpan content) => @@ -371,7 +372,7 @@ static void ThrowException(ReadOnlySpan content) => 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; + int numberStart = 0; while (numberStart < buffer.Length && char.IsWhiteSpace(buffer[numberStart])) { @@ -384,12 +385,12 @@ private static int GetNextNumber(ReadOnlySpan buffer, out long number) return -1; } - var numberEnd = numberStart; + int numberEnd = numberStart; number = 0; while (numberEnd < buffer.Length && char.IsDigit(buffer[numberEnd])) { - var current = buffer[numberEnd] - '0'; + int current = buffer[numberEnd] - '0'; number *= 10; number += current; numberEnd++; @@ -398,47 +399,44 @@ private static int GetNextNumber(ReadOnlySpan buffer, out long number) return numberEnd < buffer.Length ? numberEnd : -1; } - private bool TryGetCpuUnitsFromCgroups(IFileSystem fileSystem, out float cpuUnits) + private static bool TryGetCpuUnitsFromCgroups(IFileSystem fileSystem, out float cpuUnits) { - fileSystem.ReadFirstLine(_cpuCfsQuotaUs, _buffer); + using ReturnableBufferWriter bufferWriter = new(_sharedBufferWriterPool); + fileSystem.ReadFirstLine(_cpuCfsQuotaUs, bufferWriter.Buffer); - var quotaBuffer = _buffer.WrittenSpan; + ReadOnlySpan quotaBuffer = bufferWriter.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); + int nextQuota = GetNextNumber(quotaBuffer, out long quota); if (quota == -1 || nextQuota != -1) { Throw.InvalidOperationException($"Could not parse '{_cpuCfsQuotaUs}'. Expected an integer but got: '{new string(quotaBuffer)}'."); } - _buffer.Reset(); + bufferWriter.Buffer.Reset(); - fileSystem.ReadFirstLine(_cpuCfsPeriodUs, _buffer); - var periodBuffer = _buffer.WrittenSpan; + fileSystem.ReadFirstLine(_cpuCfsPeriodUs, bufferWriter.Buffer); + ReadOnlySpan periodBuffer = bufferWriter.Buffer.WrittenSpan; if (periodBuffer.IsEmpty || (periodBuffer.Length == 2 && periodBuffer[0] == '-' && periodBuffer[1] == '1')) { - _buffer.Reset(); cpuUnits = -1; return false; } - var nextPeriod = GetNextNumber(periodBuffer, out var period); + int nextPeriod = GetNextNumber(periodBuffer, out long period); if (period == -1 || nextPeriod != -1) { Throw.InvalidOperationException($"Could not parse '{_cpuCfsPeriodUs}'. Expected to get an integer but got: '{new string(periodBuffer)}'."); } - _buffer.Reset(); - cpuUnits = (float)quota / period; return true; } @@ -451,25 +449,25 @@ private bool TryGetCpuUnitsFromCgroups(IFileSystem fileSystem, out float cpuUnit /// 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) + private static bool TryGetCgroupRequestCpu(IFileSystem fileSystem, out float cpuUnits) { - if (!_fileSystem.Exists(_cpuPodWeight)) + if (!fileSystem.Exists(_cpuPodWeight)) { cpuUnits = 0; return false; } - fileSystem.ReadFirstLine(_cpuPodWeight, _buffer); - var cpuPodWeightBuffer = _buffer.WrittenSpan; - _ = GetNextNumber(cpuPodWeightBuffer, out var cpuPodWeight); + using ReturnableBufferWriter bufferWriter = new(_sharedBufferWriterPool); + fileSystem.ReadFirstLine(_cpuPodWeight, bufferWriter.Buffer); + ReadOnlySpan cpuPodWeightBuffer = bufferWriter.Buffer.WrittenSpan; + _ = GetNextNumber(cpuPodWeightBuffer, out long cpuPodWeight); 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(); - var result = cpuPodWeight / CpuShares; + float 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 index c9435623fc9..061b1111548 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; +using Microsoft.Extensions.ObjectPool; using Microsoft.Shared.Diagnostics; using Microsoft.Shared.Pools; @@ -20,6 +21,7 @@ internal sealed class LinuxUtilizationParserCgroupV2 : ILinuxUtilizationParser { private const int Thousand = 1000; private const int CpuShares = 1024; + private static readonly ObjectPool> _sharedBufferWriterPool = BufferWriterPool.CreateBufferWriterPool(); /// /// File contains the amount of CPU time (in microseconds) available to the group during each accounting period. @@ -86,7 +88,6 @@ internal sealed class LinuxUtilizationParserCgroupV2 : ILinuxUtilizationParser private readonly IFileSystem _fileSystem; private readonly long _userHz; - private readonly BufferWriter _buffer = new(); public LinuxUtilizationParserCgroupV2(IFileSystem fileSystem, IUserHz userHz) { @@ -105,25 +106,24 @@ public long GetCgroupCpuUsageInNanoseconds() return GetHostCpuUsageInNanoseconds(); } - _fileSystem.ReadAll(_cpuacctUsage, _buffer); - var usage = _buffer.WrittenSpan; + using ReturnableBufferWriter bufferWriter = new(_sharedBufferWriterPool); + _fileSystem.ReadAll(_cpuacctUsage, bufferWriter.Buffer); + ReadOnlySpan usage = bufferWriter.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); + ReadOnlySpan cpuUsage = usage.Slice(Usage_usec.Length, usage.Length - Usage_usec.Length); - var next = GetNextNumber(cpuUsage, out var microseconds); + int next = GetNextNumber(cpuUsage, out long 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; @@ -135,21 +135,22 @@ public long GetHostCpuUsageInNanoseconds() const int NumberOfColumnsRepresentingCpuUsage = 8; const int NanosecondsInSecond = 1_000_000_000; - _fileSystem.ReadFirstLine(_procStat, _buffer); + using ReturnableBufferWriter bufferWriter = new(_sharedBufferWriterPool); + _fileSystem.ReadFirstLine(_procStat, bufferWriter.Buffer); - var stat = _buffer.WrittenSpan; - var total = 0L; + ReadOnlySpan stat = bufferWriter.Buffer.WrittenSpan; + long total = 0L; - if (!_buffer.WrittenSpan.StartsWith(StartingTokens)) + if (!bufferWriter.Buffer.WrittenSpan.StartsWith(StartingTokens)) { - Throw.InvalidOperationException($"Expected proc/stat to start with '{StartingTokens}' but it was '{new string(_buffer.WrittenSpan)}'."); + Throw.InvalidOperationException($"Expected proc/stat to start with '{StartingTokens}' but it was '{new string(bufferWriter.Buffer.WrittenSpan)}'."); } stat = stat.Slice(StartingTokens.Length, stat.Length - StartingTokens.Length); - for (var i = 0; i < NumberOfColumnsRepresentingCpuUsage; i++) + for (int i = 0; i < NumberOfColumnsRepresentingCpuUsage; i++) { - var next = GetNextNumber(stat, out var number); + int next = GetNextNumber(stat, out long number); if (number != -1) { @@ -165,8 +166,6 @@ public long GetHostCpuUsageInNanoseconds() stat = stat.Slice(next, stat.Length - next); } - _buffer.Reset(); - return (long)(total / (double)_userHz * NanosecondsInSecond); } @@ -177,7 +176,7 @@ public long GetHostCpuUsageInNanoseconds() /// public float GetCgroupLimitedCpus() { - if (TryGetCpuUnitsFromCgroups(_fileSystem, out var cpus)) + if (LinuxUtilizationParserCgroupV2.TryGetCpuUnitsFromCgroups(_fileSystem, out float cpus)) { return cpus; } @@ -191,7 +190,7 @@ public float GetCgroupLimitedCpus() /// public float GetCgroupRequestCpu() { - if (TryGetCgroupRequestCpu(_fileSystem, out var cpuPodRequest)) + if (TryGetCgroupRequestCpu(_fileSystem, out float cpuPodRequest)) { return cpuPodRequest / CpuShares; } @@ -204,24 +203,27 @@ public float GetCgroupRequestCpu() /// public ulong GetAvailableMemoryInBytes() { - const long UnsetCgroupMemoryLimit = 9_223_372_036_854_771_712; - if (!_fileSystem.Exists(_memoryLimitInBytes)) { return GetHostAvailableMemory(); } - _fileSystem.ReadAll(_memoryLimitInBytes, _buffer); - - var memoryBuffer = _buffer.WrittenSpan; - _ = GetNextNumber(memoryBuffer, out var maybeMemory); + const long UnsetCgroupMemoryLimit = 9_223_372_036_854_771_712; + long maybeMemory = UnsetCgroupMemoryLimit; - if (maybeMemory == -1) + // Constrain the scope of the buffer because GetHostAvailableMemory is allocating its own buffer. + using (ReturnableBufferWriter bufferWriter = new(_sharedBufferWriterPool)) { - Throw.InvalidOperationException($"Could not parse '{_memoryLimitInBytes}' content. Expected to find available memory in bytes but got '{new string(memoryBuffer)}' instead."); - } + _fileSystem.ReadAll(_memoryLimitInBytes, bufferWriter.Buffer); + + ReadOnlySpan memoryBuffer = bufferWriter.Buffer.WrittenSpan; + _ = GetNextNumber(memoryBuffer, out maybeMemory); - _buffer.Reset(); + if (maybeMemory == -1) + { + Throw.InvalidOperationException($"Could not parse '{_memoryLimitInBytes}' content. Expected to find available memory in bytes but got '{new string(memoryBuffer)}' instead."); + } + } return maybeMemory == UnsetCgroupMemoryLimit ? GetHostAvailableMemory() @@ -235,18 +237,19 @@ public long GetMemoryUsageInBytesFromSlices(string pattern) long memoryUsageInBytesTotal = 0; + using ReturnableBufferWriter bufferWriter = new(_sharedBufferWriterPool); foreach (string path in memoryUsageInBytesSlicesPath) { - var memoryUsageInBytesFile = new FileInfo(path + "/memory.current"); + FileInfo memoryUsageInBytesFile = new(Path.Combine(path, "memory.current")); if (!_fileSystem.Exists(memoryUsageInBytesFile)) { continue; } - _fileSystem.ReadAll(memoryUsageInBytesFile, _buffer); + _fileSystem.ReadAll(memoryUsageInBytesFile, bufferWriter.Buffer); - var memoryUsageFile = _buffer.WrittenSpan; - var next = GetNextNumber(memoryUsageFile, out var containerMemoryUsage); + ReadOnlySpan memoryUsageFile = bufferWriter.Buffer.WrittenSpan; + int next = GetNextNumber(memoryUsageFile, out long containerMemoryUsage); if (containerMemoryUsage == 0 || containerMemoryUsage == -1) { @@ -257,7 +260,7 @@ public long GetMemoryUsageInBytesFromSlices(string pattern) memoryUsageInBytesTotal += containerMemoryUsage; - _buffer.Reset(); + bufferWriter.Buffer.Reset(); } return memoryUsageInBytesTotal; @@ -278,27 +281,29 @@ public ulong GetMemoryUsageInBytes() return GetHostAvailableMemory(); } - _fileSystem.ReadAll(_memoryStat, _buffer); - var memoryFile = _buffer.WrittenSpan; + ReadOnlySpan memoryFile; + using (ReturnableBufferWriter bufferWriter = new(_sharedBufferWriterPool)) + { + _fileSystem.ReadAll(_memoryStat, bufferWriter.Buffer); + memoryFile = bufferWriter.Buffer.WrittenSpan; + } - var index = memoryFile.IndexOf(InactiveFile.AsSpan()); + int 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); + ReadOnlySpan inactiveMemorySlice = memoryFile.Slice(index + InactiveFile.Length, memoryFile.Length - index - InactiveFile.Length); - _ = GetNextNumber(inactiveMemorySlice, out var inactiveMemory); + _ = GetNextNumber(inactiveMemorySlice, out long 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 (!_fileSystem.Exists(_memoryUsageInBytes)) @@ -310,7 +315,7 @@ public ulong GetMemoryUsageInBytes() memoryUsage = GetMemoryUsageInBytesPod(); } - var memoryUsageTotal = memoryUsage - inactiveMemory; + long memoryUsageTotal = memoryUsage - inactiveMemory; if (memoryUsageTotal < 0) { @@ -327,17 +332,18 @@ 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; + using ReturnableBufferWriter bufferWriter = new(_sharedBufferWriterPool); + _fileSystem.ReadFirstLine(_memInfo, bufferWriter.Buffer); + ReadOnlySpan firstLine = bufferWriter.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); + ReadOnlySpan totalMemory = firstLine.Slice(MemTotal.Length, firstLine.Length - MemTotal.Length); - var next = GetNextNumber(totalMemory, out var totalMemoryAvailable); + int next = GetNextNumber(totalMemory, out long totalMemoryAvailable); if (totalMemoryAvailable == -1) { @@ -349,10 +355,10 @@ public ulong GetHostAvailableMemory() 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; + ReadOnlySpan unit = totalMemory.Slice(totalMemory.Length - 2, 2); + ulong memory = (ulong)totalMemoryAvailable; - var u = unit switch + ulong u = unit switch { "kB" => memory << 10, "MB" => memory << 20, @@ -362,8 +368,6 @@ public ulong GetHostAvailableMemory() $"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; } @@ -373,29 +377,30 @@ public ulong GetHostAvailableMemory() /// public float GetHostCpuCount() { - _fileSystem.ReadFirstLine(_cpuSetCpus, _buffer); - var stats = _buffer.WrittenSpan; + using ReturnableBufferWriter bufferWriter = new(_sharedBufferWriterPool); + _fileSystem.ReadFirstLine(_cpuSetCpus, bufferWriter.Buffer); + ReadOnlySpan stats = bufferWriter.Buffer.WrittenSpan; if (stats.IsEmpty) { ThrowException(stats); } - var cpuCount = 0L; + long cpuCount = 0L; // Iterate over groups (comma-separated) while (true) { - var groupIndex = stats.IndexOf(','); + int groupIndex = stats.IndexOf(','); - var group = groupIndex == -1 ? stats : stats.Slice(0, groupIndex); + ReadOnlySpan group = groupIndex == -1 ? stats : stats.Slice(0, groupIndex); - var rangeIndex = group.IndexOf('-'); + int rangeIndex = group.IndexOf('-'); if (rangeIndex == -1) { // Single number - _ = GetNextNumber(group, out var singleCpu); + _ = GetNextNumber(group, out long singleCpu); if (singleCpu == -1) { @@ -407,11 +412,11 @@ public float GetHostCpuCount() else { // Range - var first = group.Slice(0, rangeIndex); - _ = GetNextNumber(first, out var startCpu); + ReadOnlySpan first = group.Slice(0, rangeIndex); + _ = GetNextNumber(first, out long startCpu); - var second = group.Slice(rangeIndex + 1); - var next = GetNextNumber(second, out var endCpu); + ReadOnlySpan second = group.Slice(rangeIndex + 1); + int next = GetNextNumber(second, out long endCpu); if (endCpu == -1 || startCpu == -1 || endCpu < startCpu || next != -1) { @@ -429,8 +434,6 @@ public float GetHostCpuCount() stats = stats.Slice(groupIndex + 1); } - _buffer.Reset(); - return cpuCount; static void ThrowException(ReadOnlySpan content) => @@ -445,7 +448,7 @@ static void ThrowException(ReadOnlySpan content) => 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; + int numberStart = 0; while (numberStart < buffer.Length && char.IsWhiteSpace(buffer[numberStart])) { @@ -458,12 +461,12 @@ private static int GetNextNumber(ReadOnlySpan buffer, out long number) return -1; } - var numberEnd = numberStart; + int numberEnd = numberStart; number = 0; while (numberEnd < buffer.Length && char.IsDigit(buffer[numberEnd])) { - var current = buffer[numberEnd] - '0'; + int current = buffer[numberEnd] - '0'; number *= 10; number += current; numberEnd++; @@ -475,65 +478,65 @@ private static int GetNextNumber(ReadOnlySpan buffer, out long number) /// /// 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) + private static bool TryGetCpuUnitsFromCgroups(IFileSystem fileSystem, out float cpuUnits) { - if (!_fileSystem.Exists(_cpuCfsQuaotaPeriodUs)) + if (!fileSystem.Exists(_cpuCfsQuaotaPeriodUs)) { cpuUnits = 0; return false; } - fileSystem.ReadFirstLine(_cpuCfsQuaotaPeriodUs, _buffer); + using ReturnableBufferWriter bufferWriter = new(_sharedBufferWriterPool); + fileSystem.ReadFirstLine(_cpuCfsQuaotaPeriodUs, bufferWriter.Buffer); - var quotaBuffer = _buffer.WrittenSpan; + ReadOnlySpan quotaBuffer = bufferWriter.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); + int nextQuota = GetNextNumber(quotaBuffer, out long quota); if (quota == -1) { Throw.InvalidOperationException($"Could not parse '{_cpuCfsQuaotaPeriodUs}'. Expected an integer but got: '{new string(quotaBuffer)}'."); } - 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); + string quotaString = quota.ToString(CultureInfo.CurrentCulture); + int index = quotaBuffer.IndexOf(quotaString.AsSpan()); + ReadOnlySpan cpuPeriodSlice = quotaBuffer.Slice(index + quotaString.Length, quotaBuffer.Length - index - quotaString.Length); + _ = GetNextNumber(cpuPeriodSlice, out long 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) + private static bool TryGetCgroupRequestCpu(IFileSystem fileSystem, out float cpuUnits) { - if (!_fileSystem.Exists(_cpuPodWeight)) + if (!fileSystem.Exists(_cpuPodWeight)) { cpuUnits = 0; return false; } - fileSystem.ReadFirstLine(_cpuPodWeight, _buffer); - var cpuPodWeightBuffer = _buffer.WrittenSpan; + using ReturnableBufferWriter bufferWriter = new(_sharedBufferWriterPool); + fileSystem.ReadFirstLine(_cpuPodWeight, bufferWriter.Buffer); + ReadOnlySpan cpuPodWeightBuffer = bufferWriter.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."); } - _ = GetNextNumber(cpuPodWeightBuffer, out var cpuPodWeight); + _ = GetNextNumber(cpuPodWeightBuffer, out long cpuPodWeight); if (cpuPodWeight == -1) { @@ -543,23 +546,23 @@ private bool TryGetCgroupRequestCpu(IFileSystem fileSystem, out float cpuUnits) // 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; + long cpuPodShare = ((cpuPodWeight * 262142) + 19997) / 9999; if (cpuPodShare == -1) { Throw.InvalidOperationException($"Could not calculate CPU share from CPU weight '{cpuPodShare}'"); } - _buffer.Reset(); cpuUnits = cpuPodShare; return true; } private long GetMemoryUsageInBytesPod() { - _fileSystem.ReadAll(_memoryUsageInBytes, _buffer); + using ReturnableBufferWriter bufferWriter = new(_sharedBufferWriterPool); + _fileSystem.ReadAll(_memoryUsageInBytes, bufferWriter.Buffer); - var memoryUsageFile = _buffer.WrittenSpan; - var next = GetNextNumber(memoryUsageFile, out long memoryUsage); + ReadOnlySpan memoryUsageFile = bufferWriter.Buffer.WrittenSpan; + int next = GetNextNumber(memoryUsageFile, out long memoryUsage); // this file format doesn't expect to contain anything after the number. if (memoryUsage == -1) @@ -568,7 +571,6 @@ private long GetMemoryUsageInBytesPod() $"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/Shared/BufferWriterPool/ReturnableBufferWriter.cs b/src/Shared/BufferWriterPool/ReturnableBufferWriter.cs new file mode 100644 index 00000000000..4675f84349e --- /dev/null +++ b/src/Shared/BufferWriterPool/ReturnableBufferWriter.cs @@ -0,0 +1,45 @@ +// 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.Extensions.ObjectPool; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Pools; +#pragma warning restore CA1716 + +/// +/// Represents a buffer writer that can be automatically returned to an object pool upon dispose. +/// +/// The type of the elements in the buffer. +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif +internal readonly struct ReturnableBufferWriter : IDisposable +{ + private readonly ObjectPool> _pool; + + /// + /// Initializes a new instance of the struct. + /// + /// The object pool to return the buffer writer to. + public ReturnableBufferWriter(ObjectPool> pool) + { + _pool = pool; + Buffer = pool.Get(); + } + + /// + /// Gets the buffer writer. + /// + public BufferWriter Buffer { get; } + + /// + /// Disposes the buffer writer and returns it to the object pool. + /// + public readonly void Dispose() + { + Buffer.Reset(); + _pool.Return(Buffer); + } +} 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 50fdf13e651..32fba39697b 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs @@ -24,7 +24,7 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; public sealed class AcceptanceTest { [ConditionalFact] - [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific package.")] + [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")] public void Adding_Linux_Resource_Utilization_Allows_To_Query_Snapshot_Provider() { using var services = new ServiceCollection() @@ -38,7 +38,7 @@ public void Adding_Linux_Resource_Utilization_Allows_To_Query_Snapshot_Provider( } [ConditionalFact] - [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific package.")] + [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")] [SuppressMessage("Minor Code Smell", "S3257:Declarations and initializations should be as concise as possible", Justification = "Broken analyzer.")] public void Adding_Linux_Resource_Utilization_Can_Be_Configured_With_Section() { @@ -68,7 +68,7 @@ public void Adding_Linux_Resource_Utilization_Can_Be_Configured_With_Section() } [ConditionalFact] - [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific package.")] + [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")] public void Adding_Linux_Resource_Utilization_Can_Be_Configured_With_Action() { var cpuRefresh = TimeSpan.FromMinutes(13); @@ -90,7 +90,7 @@ public void Adding_Linux_Resource_Utilization_Can_Be_Configured_With_Action() } [ConditionalFact] - [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific package.")] + [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")] [SuppressMessage("Minor Code Smell", "S3257:Declarations and initializations should be as concise as possible", Justification = "Broken analyzer.")] public void Adding_Linux_Resource_Utilization_With_Section_Registers_SnapshotProvider_Cgroupv1() { @@ -139,7 +139,7 @@ public void Adding_Linux_Resource_Utilization_With_Section_Registers_SnapshotPro } [ConditionalFact] - [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific package.")] + [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")] [SuppressMessage("Minor Code Smell", "S3257:Declarations and initializations should be as concise as possible", Justification = "Broken analyzer.")] public void Adding_Linux_Resource_Utilization_With_Section_Registers_SnapshotProvider_Cgroupv2() { @@ -187,7 +187,7 @@ public void Adding_Linux_Resource_Utilization_With_Section_Registers_SnapshotPro } [ConditionalFact(Skip = "Flaky test, see https://github.com/dotnet/extensions/issues/3997")] - [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific package.")] + [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")] public Task ResourceUtilizationTracker_Reports_The_Same_Values_As_One_Can_Observe_From_Gauges() { var cpuRefresh = TimeSpan.FromMinutes(13); 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 27a161ff99e..3fb10a74865 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs @@ -11,7 +11,7 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; -[OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific package")] +[OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")] public sealed class LinuxCountersTests { [ConditionalFact] diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV1Tests.cs similarity index 89% rename from test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserTests.cs rename to test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV1Tests.cs index d1fe509323d..4fa6aaafe3d 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV1Tests.cs @@ -6,14 +6,16 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; -using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; +using System.Threading.Tasks; +using Microsoft.Shared.Pools; using Microsoft.TestUtilities; +using Moq; using Xunit; namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; -[OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Windows specific.")] -public sealed class LinuxUtilizationParserTests +[OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")] +public sealed class LinuxUtilizationParserCgroupV1Tests { [ConditionalTheory] [InlineData("DFIJEUWGHFWGBWEFWOMDOWKSLA")] @@ -348,4 +350,46 @@ public void Parser_Throws_When_Cgroup_Cpu_Shares_Files_Contain_Invalid_Data(stri Assert.IsAssignableFrom(r); Assert.Contains("/sys/fs/cgroup/cpu/cpu.shares", r.Message); } + + [ConditionalFact] + public async Task ThreadSafetyAsync() + { + var f1 = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/proc/stat"), "cpu 6163 0 3853 4222848 614 0 1155 0 0 0\r\ncpu0 240 0 279 210987 59 0 927 0 0 0" }, + }); + var f2 = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/proc/stat"), "cpu 9137 0 9296 13972503 1148 0 2786 0 0 0\r\ncpu0 297 0 431 698663 59 0 2513 0 0 0" }, + }); + + int callCount = 0; + Mock fs = new(); + fs.Setup(x => x.ReadFirstLine(It.IsAny(), It.IsAny>())) + .Callback>((fileInfo, buffer) => + { + callCount++; + if (callCount % 2 == 0) + { + f1.ReadFirstLine(fileInfo, buffer); + } + else + { + f2.ReadFirstLine(fileInfo, buffer); + } + }) + .Verifiable(); + + var p = new LinuxUtilizationParserCgroupV1(fs.Object, new FakeUserHz(100)); + + Task[] tasks = new Task[1_000]; + for (int i = 0; i < tasks.Length; i++) + { + tasks[i] = Task.Run(p.GetHostCpuUsageInNanoseconds); + } + + await Task.WhenAll(tasks); + + Assert.True(true); + } } 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 0be39c612d8..0df5c1caca5 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserCgroupV2Tests.cs @@ -6,13 +6,16 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; +using System.Threading.Tasks; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; +using Microsoft.Shared.Pools; using Microsoft.TestUtilities; +using Moq; using Xunit; namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; -[OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Windows specific.")] +[OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")] public sealed class LinuxUtilizationParserCgroupV2Tests { [ConditionalTheory] @@ -425,4 +428,46 @@ public void Parser_Throws_When_Cgroup_Cpu_Weight_Files_Contain_Invalid_Data(stri Assert.IsAssignableFrom(r); Assert.Contains("/sys/fs/cgroup/cpu.weight", r.Message); } + + [ConditionalFact] + public async Task ThreadSafetyAsync() + { + var f1 = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/proc/stat"), "cpu 6163 0 3853 4222848 614 0 1155 0 0 0\r\ncpu0 240 0 279 210987 59 0 927 0 0 0" }, + }); + var f2 = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/proc/stat"), "cpu 9137 0 9296 13972503 1148 0 2786 0 0 0\r\ncpu0 297 0 431 698663 59 0 2513 0 0 0" }, + }); + + int callCount = 0; + Mock fs = new(); + fs.Setup(x => x.ReadFirstLine(It.IsAny(), It.IsAny>())) + .Callback>((fileInfo, buffer) => + { + callCount++; + if (callCount % 2 == 0) + { + f1.ReadFirstLine(fileInfo, buffer); + } + else + { + f2.ReadFirstLine(fileInfo, buffer); + } + }) + .Verifiable(); + + var p = new LinuxUtilizationParserCgroupV2(fs.Object, new FakeUserHz(100)); + + Task[] tasks = new Task[1_000]; + for (int i = 0; i < tasks.Length; i++) + { + tasks[i] = Task.Run(p.GetHostCpuUsageInNanoseconds); + } + + await Task.WhenAll(tasks); + + Assert.True(true); + } } 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..2c9ee280414 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTests.cs @@ -9,7 +9,7 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; -[OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Windows specific.")] +[OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")] public sealed class OSFileSystemTests { [ConditionalFact]