Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ internal interface ILinuxUtilizationParser
/// <returns>nanoseconds.</returns>
long GetCgroupCpuUsageInNanoseconds();

/// <summary>
/// Reads the file cpu.stat based on /proc/self/cgroup, which is part of the cgroup v2 CPU controller.
/// It provides statistics about the CPU usage of a cgroup from its actual slice.
/// </summary>
/// <returns>nanoseconds.</returns>
long GetCgroupCpuUsageInNanosecondsWithoutHost();

/// <summary>
/// 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.
Expand All @@ -33,6 +40,16 @@ internal interface ILinuxUtilizationParser
/// <returns>cpuUnits.</returns>
float GetCgroupLimitedCpus();

/// <summary>
/// Reads the file cpu.max based on /proc/self/cgroup, 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 from actual slice.
/// 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.
/// </summary>
/// <returns>cpuUnits.</returns>
float GetCgroupLimitedCpusWithoutHost();

/// <summary>
/// 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.
Expand Down Expand Up @@ -66,4 +83,10 @@ internal interface ILinuxUtilizationParser
/// </summary>
/// <returns>cpuPodRequest.</returns>
float GetCgroupRequestCpu();

/// <summary>
/// Reads the file cpu.weight based on /proc/self/cgroup. And calculates the Pod CPU Request in millicores based on actual slice.
/// </summary>
/// <returns>cpuPodRequest.</returns>
float GetCgroupRequestCpuWithoutHost();
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ public LinuxUtilizationParserCgroupV1(IFileSystem fileSystem, IUserHz userHz)
_userHz = userHz.Value;
}

public long GetCgroupCpuUsageInNanosecondsWithoutHost() => throw new NotSupportedException();
public float GetCgroupLimitedCpusWithoutHost() => throw new NotSupportedException();
public float GetCgroupRequestCpuWithoutHost() => throw new NotSupportedException();

public long GetCgroupCpuUsageInNanoseconds()
{
using ReturnableBufferWriter<char> bufferWriter = new(_sharedBufferWriterPool);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
{
private const int Thousand = 1000;
private const int CpuShares = 1024;
private const string PathPrefix = "/sys/fs/cgroup";
private const string CpuStat = "cpu.stat"; // File containing CPU usage in nanoseconds.
private const string CpuLimit = "cpu.max"; // File with amount of CPU time available to the group along with the accounting period in microseconds.
private const string CpuRequest = "cpu.weight"; // CPU weights, also known as shares in cgroup v1, is used for resource allocation.
private static readonly ObjectPool<BufferWriter<char>> _sharedBufferWriterPool = BufferWriterPool.CreateBufferWriterPool<char>();

/// <remarks>
Expand Down Expand Up @@ -86,15 +90,53 @@
/// </summary>
private static readonly FileInfo _cpuPodWeight = new("/sys/fs/cgroup/cpu.weight");

private static readonly FileInfo _cpuActualSelfSliceProcFile = new("/proc/self/cgroup");

private readonly IFileSystem _fileSystem;
private readonly long _userHz;
// Cache for the trimmed path string to avoid repeated file reads and processing

Check failure on line 97 in src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs

View check run for this annotation

Azure Pipelines / extensions-ci (Correctness WarningsCheck)

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs#L97

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs(97,5): error SA1515: (NETCORE_ENGINEERING_TELEMETRY=Build) Single-line comment should be preceded by blank line (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1515.md)

Check failure on line 97 in src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs

View check run for this annotation

Azure Pipelines / extensions-ci

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs#L97

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs(97,5): error SA1515: (NETCORE_ENGINEERING_TELEMETRY=Build) Single-line comment should be preceded by blank line (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1515.md)
private string _cachedCgroupPath;

public LinuxUtilizationParserCgroupV2(IFileSystem fileSystem, IUserHz userHz)

Check failure on line 100 in src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs

View check run for this annotation

Azure Pipelines / extensions-ci (Correctness WarningsCheck)

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs#L100

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs(100,12): error CS8618: (NETCORE_ENGINEERING_TELEMETRY=Build) Non-nullable field '_cachedCgroupPath' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check failure on line 100 in src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs

View check run for this annotation

Azure Pipelines / extensions-ci (Correctness WarningsCheck)

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs#L100

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs(100,12): error CS8618: (NETCORE_ENGINEERING_TELEMETRY=Build) Non-nullable field '_cachedCgroupPath' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check failure on line 100 in src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs

View check run for this annotation

Azure Pipelines / extensions-ci

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs#L100

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs(100,12): error CS8618: (NETCORE_ENGINEERING_TELEMETRY=Build) Non-nullable field '_cachedCgroupPath' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check failure on line 100 in src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs

View check run for this annotation

Azure Pipelines / extensions-ci

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs#L100

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs(100,12): error CS8618: (NETCORE_ENGINEERING_TELEMETRY=Build) Non-nullable field '_cachedCgroupPath' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.
{
_fileSystem = fileSystem;
_userHz = userHz.Value;
}

public string GetCgroupActualSlicePath(string filename)
{
// If we've already parsed the path, use the cached value
if (_cachedCgroupPath != null)
{
return $"{PathPrefix}{_cachedCgroupPath}{filename}";
}

using ReturnableBufferWriter<char> bufferWriter = new(_sharedBufferWriterPool);

// Read the content of the file
_fileSystem.ReadFirstLine(_cpuActualSelfSliceProcFile, bufferWriter.Buffer);
ReadOnlySpan<char> fileContent = bufferWriter.Buffer.WrittenSpan;

// Ensure the file content is not empty
if (fileContent.IsEmpty)
{
Throw.InvalidOperationException($"The file '{_cpuActualSelfSliceProcFile}' is empty or could not be read.");
}

// Find the index of the first colon (:)
int colonIndex = fileContent.LastIndexOf(':');
if (colonIndex == -1 || colonIndex + 1 >= fileContent.Length)
{
Throw.InvalidOperationException($"Invalid format in file '{_cpuActualSelfSliceProcFile}'. Expected content with ':' separator.");
}

// Extract the part after the last colon and cache it for future use
ReadOnlySpan<char> trimmedPath = fileContent.Slice(colonIndex + 1);
_cachedCgroupPath = trimmedPath.ToString().TrimEnd('/') + "/";

Check failure on line 136 in src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs

View check run for this annotation

Azure Pipelines / extensions-ci (Correctness WarningsCheck)

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs#L136

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs(136,1): error IDE0055: (NETCORE_ENGINEERING_TELEMETRY=Build) Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 136 in src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs

View check run for this annotation

Azure Pipelines / extensions-ci (Correctness WarningsCheck)

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs#L136

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs(136,1): error IDE0055: (NETCORE_ENGINEERING_TELEMETRY=Build) Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 136 in src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs

View check run for this annotation

Azure Pipelines / extensions-ci

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs#L136

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs(136,1): error IDE0055: (NETCORE_ENGINEERING_TELEMETRY=Build) Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)
return $"{PathPrefix}{_cachedCgroupPath}{filename}";
}

public long GetCgroupCpuUsageInNanoseconds()
{
// The value we are interested in starts with this. We just want to make sure it is true.
Expand Down Expand Up @@ -129,6 +171,44 @@
return microseconds * Thousand;
}

public long GetCgroupCpuUsageInNanosecondsWithoutHost()
{
// The value we are interested in starts with this. We just want to make sure it is true.
const string Usage_usec = "usage_usec";

using ReturnableBufferWriter<char> bufferWriter = new(_sharedBufferWriterPool);

FileInfo cpuUsageFile = new(GetCgroupActualSlicePath(CpuStat));

// If the file doesn't exist, we assume that the system is a Host and we read the CPU usage from /proc/stat.
if (!_fileSystem.Exists(cpuUsageFile))
{
return GetHostCpuUsageInNanoseconds();
}

_fileSystem.ReadAll(cpuUsageFile, bufferWriter.Buffer);

ReadOnlySpan<char> usage = bufferWriter.Buffer.WrittenSpan;

if (!usage.StartsWith(Usage_usec))
{
Throw.InvalidOperationException($"Could not parse '{cpuUsageFile}'. We expected first line of the file to start with '{Usage_usec}' but it was '{new string(usage)}' instead.");
}

ReadOnlySpan<char> cpuUsage = usage.Slice(Usage_usec.Length, usage.Length - Usage_usec.Length);

int next = GetNextNumber(cpuUsage, out long microseconds);

if (microseconds == -1)
{
Throw.InvalidOperationException($"Could not get cpu usage from '{cpuUsageFile}'. Expected positive number, but got '{new string(usage)}'.");
}

// 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 ";
Expand Down Expand Up @@ -184,6 +264,22 @@
return GetHostCpuCount();
}

/// <remarks>
/// 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.
/// </remarks>
public float GetCgroupLimitedCpusWithoutHost()
{
FileInfo cpuLimitsFile = new(GetCgroupActualSlicePath(CpuLimit));
if (LinuxUtilizationParserCgroupV2.TryGetCpuUnitsFromCgroupsWithoutHost(_fileSystem, cpuLimitsFile, out float cpus))
{
return cpus;
}

return GetHostCpuCount();
}

/// <remarks>
/// 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.
Expand All @@ -198,6 +294,21 @@
return GetHostCpuCount();
}

/// <remarks>
/// 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.
/// </remarks>
public float GetCgroupRequestCpuWithoutHost()
{
FileInfo cpuRequestsFile = new(GetCgroupActualSlicePath(CpuRequest));
if (TryGetCgroupRequestCpuWithoutHost(_fileSystem, cpuRequestsFile, out float cpuPodRequest))
{
return cpuPodRequest / CpuShares;
}

return GetHostCpuCount();
}

/// <remarks>
/// If the file doesn't exist, we assume that the system is a Host and we read the memory from /proc/meminfo.
/// </remarks>
Expand Down Expand Up @@ -531,6 +642,56 @@
return true;
}

/// <remarks>
/// If the file doesn't exist, we assume that the system is a Host and we read the CPU usage from /proc/stat.
/// </remarks>
private static bool TryGetCpuUnitsFromCgroupsWithoutHost(IFileSystem fileSystem, FileInfo cpuLimitsFile, out float cpuUnits)
{
if (!fileSystem.Exists(cpuLimitsFile))
{
cpuUnits = 0;
return false;
}

using ReturnableBufferWriter<char> bufferWriter = new(_sharedBufferWriterPool);
fileSystem.ReadFirstLine(cpuLimitsFile, bufferWriter.Buffer);

ReadOnlySpan<char> quotaBuffer = bufferWriter.Buffer.WrittenSpan;

if (quotaBuffer.IsEmpty || (quotaBuffer.Length == 2 && quotaBuffer[0] == '-' && quotaBuffer[1] == '1'))
{
cpuUnits = -1;
return false;
}

if (quotaBuffer.StartsWith("max", StringComparison.InvariantCulture))
{
cpuUnits = 0;
return false;
}

_ = GetNextNumber(quotaBuffer, out long quota);

if (quota == -1)
{
Throw.InvalidOperationException($"Could not parse '{cpuLimitsFile}'. Expected an integer but got: '{new string(quotaBuffer)}'.");
}

string quotaString = quota.ToString(CultureInfo.CurrentCulture);
int index = quotaBuffer.IndexOf(quotaString.AsSpan());
ReadOnlySpan<char> cpuPeriodSlice = quotaBuffer.Slice(index + quotaString.Length, quotaBuffer.Length - index - quotaString.Length);
_ = GetNextNumber(cpuPeriodSlice, out long period);

if (period == -1)
{
Throw.InvalidOperationException($"Could not parse '{cpuLimitsFile}'. Expected to get an integer but got: '{new string(cpuPeriodSlice)}'.");
}

cpuUnits = (float)quota / period;

return true;
}

private static bool TryGetCgroupRequestCpu(IFileSystem fileSystem, out float cpuUnits)
{
const long CpuPodWeightPossibleMax = 10_000;
Expand Down Expand Up @@ -578,6 +739,53 @@
return true;
}

private static bool TryGetCgroupRequestCpuWithoutHost(IFileSystem fileSystem, FileInfo cpuRequestsFile, out float cpuUnits)
{
const long CpuPodWeightPossibleMax = 10_000;
const long CpuPodWeightPossibleMin = 1;

if (!fileSystem.Exists(cpuRequestsFile))
{
cpuUnits = 0;
return false;
}

using ReturnableBufferWriter<char> bufferWriter = new(_sharedBufferWriterPool);
fileSystem.ReadFirstLine(cpuRequestsFile, bufferWriter.Buffer);
ReadOnlySpan<char> cpuPodWeightBuffer = bufferWriter.Buffer.WrittenSpan;

if (cpuPodWeightBuffer.IsEmpty || (cpuPodWeightBuffer.Length == 2 && cpuPodWeightBuffer[0] == '-' && cpuPodWeightBuffer[1] == '1'))
{
Throw.InvalidOperationException(
$"Could not parse '{cpuRequestsFile}' content. Expected to find CPU weight but got '{new string(cpuPodWeightBuffer)}' instead.");
}

_ = GetNextNumber(cpuPodWeightBuffer, out long cpuPodWeight);

if (cpuPodWeight == -1)
{
Throw.InvalidOperationException(
$"Could not parse '{cpuRequestsFile}' content. Expected to get an integer but got: '{cpuPodWeightBuffer}'.");
}

if (cpuPodWeight < CpuPodWeightPossibleMin || cpuPodWeight > CpuPodWeightPossibleMax)
{
Throw.ArgumentOutOfRangeException("CPU weight",
$"Expected to find CPU weight in range [{CpuPodWeightPossibleMin}-{CpuPodWeightPossibleMax}] in '{cpuRequestsFile}', but got '{cpuPodWeight}' instead.");
}

// The formula to calculate CPU pod weight (measured in millicores) from CPU share:
// y = (1 + ((x - 2) * 9999) / 262142),
// where y is the CPU pod weight (e.g. cpuPodWeight) and x is the CPU share of cgroup v1 (e.g. cpuUnits).
// https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/2254-cgroup-v2#phase-1-convert-from-cgroups-v1-settings-to-v2
// We invert the formula to calculate CPU share from CPU pod weight:
#pragma warning disable S109 // Magic numbers should not be used - using the formula, forgive.
cpuUnits = ((cpuPodWeight - 1) * 262142 / 9999) + 2;
#pragma warning restore S109 // Magic numbers should not be used

return true;
}

private long GetMemoryUsageInBytesPod()
{
using ReturnableBufferWriter<char> bufferWriter = new(_sharedBufferWriterPool);
Expand Down
Loading
Loading