diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesMetadata.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesMetadata.cs new file mode 100644 index 00000000000..6d8ec54fdda --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesMetadata.cs @@ -0,0 +1,65 @@ +// 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.Globalization; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes; + +internal class KubernetesMetadata +{ + /// + /// Gets or sets the resource memory limit the container is allowed to use in bytes. + /// + public ulong LimitsMemory { get; set; } + + /// + /// Gets or sets the resource CPU limit the container is allowed to use in milicores. + /// + public ulong LimitsCpu { get; set; } + + /// + /// Gets or sets the resource memory request the container is allowed to use in bytes. + /// + public ulong RequestsMemory { get; set; } + + /// + /// Gets or sets the resource CPU request the container is allowed to use in milicores. + /// + public ulong RequestsCpu { get; set; } + + private string _environmentVariablePrefix; + + public KubernetesMetadata(string environmentVariablePrefix) + { + _environmentVariablePrefix = environmentVariablePrefix; + } + + /// + /// Fills the object with data loaded from environment variables. + /// + public void Build() + { + LimitsMemory = GetEnvironmentVariableAsUInt64($"{_environmentVariablePrefix}LIMITS_MEMORY"); + LimitsCpu = GetEnvironmentVariableAsUInt64($"{_environmentVariablePrefix}LIMITS_CPU"); + RequestsMemory = GetEnvironmentVariableAsUInt64($"{_environmentVariablePrefix}REQUESTS_MEMORY"); + RequestsCpu = GetEnvironmentVariableAsUInt64($"{_environmentVariablePrefix}REQUESTS_CPU"); + } + + private static ulong GetEnvironmentVariableAsUInt64(string variableName) + { + var value = Environment.GetEnvironmentVariable(variableName); + + if (string.IsNullOrEmpty(value)) + { + throw new InvalidOperationException($"Environment variable '{variableName}' is not set or is empty."); + } + + if (!ulong.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out ulong result)) + { + throw new InvalidOperationException($"Environment variable '{variableName}' contains invalid value '{value}'. Expected a non-negative integer."); + } + + return result; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotaProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotaProvider.cs new file mode 100644 index 00000000000..41b8bdb42f4 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotaProvider.cs @@ -0,0 +1,31 @@ +// 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.Kubernetes; + +internal class KubernetesResourceQuotaProvider : ResourceQuotaProvider +{ + private const double MillicoresPerCore = 1000.0; + private KubernetesMetadata _kubernetesMetadata; + + public KubernetesResourceQuotaProvider(KubernetesMetadata kubernetesMetadata) + { + _kubernetesMetadata = kubernetesMetadata; + } + + public override ResourceQuota GetResourceQuota() + { + return new ResourceQuota + { + GuaranteedCpuInCores = ConvertMillicoreToCpuUnit(_kubernetesMetadata.RequestsCpu), + MaxCpuInCores = ConvertMillicoreToCpuUnit(_kubernetesMetadata.LimitsCpu), + GuaranteedMemoryInBytes = _kubernetesMetadata.RequestsMemory, + MaxMemoryInBytes = _kubernetesMetadata.LimitsMemory, + }; + } + + private static double ConvertMillicoreToCpuUnit(ulong millicores) + { + return millicores / MillicoresPerCore; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotaServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotaServiceCollectionExtensions.cs new file mode 100644 index 00000000000..27f0b8ef6ed --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotaServiceCollectionExtensions.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Lets you configure and register Kubernetes resource monitoring components. +/// +public static class KubernetesResourceQuotaServiceCollectionExtensions +{ + /// + /// Configures and adds an Kubernetes resource monitoring components to a service collection alltoghter with necessary basic resource monitoring components. + /// + /// The dependency injection container to add the Kubernetes resource monitoring to. + /// Optional value of prefix used to read environment variables in the container. + /// The value of . + /// + /// + /// If you have configured your Kubernetes container with Downward API to add environment variable MYCLUSTER_LIMITS_CPU with CPU limits, + /// then you should pass MYCLUSTER_ to parameter. Environment variables will be read during DI Container resolution. + /// + /// + /// Important: Do not call + /// if you are using this method, as it already includes all necessary resource monitoring components and registers a Kubernetes-specific + /// implementation. Calling both methods may result in conflicting service registrations. + /// + /// + public static IServiceCollection AddKubernetesResourceMonitoring( + this IServiceCollection services, + string? environmentVariablePrefix = default) + { + services.TryAddSingleton(serviceProvider => + { + var metadata = new KubernetesMetadata(environmentVariablePrefix ?? string.Empty); + metadata.Build(); + + return metadata; + }); + services.TryAddSingleton(); + + _ = services.AddResourceMonitoring(); + + return services; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.csproj new file mode 100644 index 00000000000..9fa77891fce --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.csproj @@ -0,0 +1,42 @@ + + + Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes + Measures processor and memory usage. + ResourceMonitoring + $(NoWarn);CS0436 + + + + true + + + + normal + 99 + 90 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.json b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/README.md b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/README.md new file mode 100644 index 00000000000..1430f916d08 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/README.md @@ -0,0 +1,24 @@ +# Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes + +Registers `ResourceQuota` implementation specific to Kubernetes. + +## Install the package + +From the command-line: + +```console +dotnet add package Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes +``` + +Or directly in the C# project file: + +```xml + + + +``` + + +## Feedback & Contributing + +We welcome feedback and contributions in [our GitHub repo](https://github.com/dotnet/extensions). diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxResourceQuotaProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxResourceQuotaProvider.cs new file mode 100644 index 00000000000..296977b7675 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxResourceQuotaProvider.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux; + +internal class LinuxResourceQuotaProvider : ResourceQuotaProvider +{ + private readonly ILinuxUtilizationParser _parser; + private bool _useLinuxCalculationV2; + + public LinuxResourceQuotaProvider(ILinuxUtilizationParser parser, IOptions options) + { + _parser = parser; + _useLinuxCalculationV2 = options.Value.UseLinuxCalculationV2; + } + + public override ResourceQuota GetResourceQuota() + { + var resourceQuota = new ResourceQuota(); + if (_useLinuxCalculationV2) + { + resourceQuota.MaxCpuInCores = _parser.GetCgroupLimitV2(); + resourceQuota.GuaranteedCpuInCores = _parser.GetCgroupRequestCpuV2(); + } + else + { + resourceQuota.MaxCpuInCores = _parser.GetCgroupLimitedCpus(); + resourceQuota.GuaranteedCpuInCores = _parser.GetCgroupRequestCpu(); + } + + resourceQuota.MaxMemoryInBytes = _parser.GetAvailableMemoryInBytes(); + resourceQuota.GuaranteedMemoryInBytes = resourceQuota.MaxMemoryInBytes; // TODO: use real value + + return resourceQuota; + } +} + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs index 611b96f4f1d..a7a0de86094 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs @@ -22,7 +22,6 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider private readonly object _memoryLocker = new(); private readonly ILogger _logger; private readonly ILinuxUtilizationParser _parser; - private readonly ulong _memoryLimit; private readonly long _cpuPeriodsInterval; private readonly TimeSpan _cpuRefreshInterval; private readonly TimeSpan _memoryRefreshInterval; @@ -33,6 +32,13 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider private DateTimeOffset _lastFailure = DateTimeOffset.MinValue; private int _measurementsUnavailable; + private double _memoryLimit; + private double _cpuLimit; +#pragma warning disable S1450 // Private fields only used as local variables in methods should become local variables. This will be used once we bring relevant meters. + private ulong _memoryRequest; +#pragma warning restore S1450 // Private fields only used as local variables in methods should become local variables + private double _cpuRequest; + private DateTimeOffset _refreshAfterCpu; private DateTimeOffset _refreshAfterMemory; private double _cpuPercentage = double.NaN; @@ -43,8 +49,13 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider private long _previousCgroupCpuPeriodCounter; public SystemResources Resources { get; } - public LinuxUtilizationProvider(IOptions options, ILinuxUtilizationParser parser, - IMeterFactory meterFactory, ILogger? logger = null, TimeProvider? timeProvider = null) + public LinuxUtilizationProvider( + IOptions options, + ILinuxUtilizationParser parser, + IMeterFactory meterFactory, + ResourceQuotaProvider resourceQuotaProvider, + ILogger? logger = null, + TimeProvider? timeProvider = null) { _parser = parser; _logger = logger ?? NullLogger.Instance; @@ -54,15 +65,18 @@ public LinuxUtilizationProvider(IOptions options, ILi _memoryRefreshInterval = options.Value.MemoryConsumptionRefreshInterval; _refreshAfterCpu = now; _refreshAfterMemory = now; - _memoryLimit = _parser.GetAvailableMemoryInBytes(); _previousHostCpuTime = _parser.GetHostCpuUsageInNanoseconds(); _previousCgroupCpuTime = _parser.GetCgroupCpuUsageInNanoseconds(); + var quota = resourceQuotaProvider.GetResourceQuota(); + _memoryLimit = quota.MaxMemoryInBytes; + _cpuLimit = quota.MaxCpuInCores; + _cpuRequest = quota.GuaranteedCpuInCores; + _memoryRequest = quota.GuaranteedMemoryInBytes; + float hostCpus = _parser.GetHostCpuCount(); - float cpuLimit = _parser.GetCgroupLimitedCpus(); - float cpuRequest = _parser.GetCgroupRequestCpu(); - float scaleRelativeToCpuLimit = hostCpus / cpuLimit; - float scaleRelativeToCpuRequest = hostCpus / cpuRequest; + double scaleRelativeToCpuLimit = hostCpus / _cpuLimit; + double scaleRelativeToCpuRequest = hostCpus / _cpuRequest; _scaleRelativeToCpuRequestForTrackerApi = hostCpus; // the division by cpuRequest is performed later on in the ResourceUtilization class #pragma warning disable CA2000 // Dispose objects before losing scope @@ -74,21 +88,18 @@ public LinuxUtilizationProvider(IOptions options, ILi if (options.Value.UseLinuxCalculationV2) { - cpuLimit = _parser.GetCgroupLimitV2(); - cpuRequest = _parser.GetCgroupRequestCpuV2(); - // Get Cpu periods interval from cgroup _cpuPeriodsInterval = _parser.GetCgroupPeriodsIntervalInMicroSecondsV2(); (_previousCgroupCpuTime, _previousCgroupCpuPeriodCounter) = _parser.GetCgroupCpuUsageInNanosecondsAndCpuPeriodsV2(); _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, - observeValues: () => GetMeasurementWithRetry(() => CpuUtilizationLimit(cpuLimit)), + observeValues: () => GetMeasurementWithRetry(() => CpuUtilizationLimit(_cpuLimit)), unit: "1"); _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ContainerCpuRequestUtilization, - observeValues: () => GetMeasurementWithRetry(() => CpuUtilizationRequest(cpuRequest)), + observeValues: () => GetMeasurementWithRetry(() => CpuUtilizationRequest(_cpuRequest)), unit: "1"); _ = meter.CreateObservableGauge( @@ -130,12 +141,9 @@ public LinuxUtilizationProvider(IOptions options, ILi observeValues: () => GetMeasurementWithRetry(MemoryPercentage), unit: "1"); - // cpuRequest is a CPU request (aka guaranteed number of CPU units) for pod, for host its 1 core - // cpuLimit is a CPU limit (aka max CPU units available) for a pod or for a host. - // _memoryLimit - Resource Memory Limit (in k8s terms) - // _memoryLimit - To keep the contract, this parameter will get the Host available memory - Resources = new SystemResources(cpuRequest, cpuLimit, _memoryLimit, _memoryLimit); - _logger.SystemResourcesInfo(cpuLimit, cpuRequest, _memoryLimit, _memoryLimit); + ulong memoryLimitRounded = (ulong)Math.Round(_memoryLimit); + Resources = new SystemResources(_cpuRequest, _cpuLimit, _memoryRequest, memoryLimitRounded); + _logger.SystemResourcesInfo(_cpuLimit, _cpuRequest, memoryLimitRounded, _memoryRequest); } public double CpuUtilizationV2() @@ -271,7 +279,7 @@ public Snapshot GetSnapshot() private double MemoryPercentage() { ulong memoryUsage = MemoryUsage(); - double memoryPercentage = Math.Min(One, (double)memoryUsage / _memoryLimit); + double memoryPercentage = Math.Min(One, memoryUsage / _memoryLimit); _logger.MemoryPercentageData(memoryUsage, _memoryLimit, memoryPercentage); return memoryPercentage; diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs index c368e2bff91..cb54bf219a8 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs @@ -6,6 +6,7 @@ using System.Runtime.Versioning; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Diagnostics.ResourceMonitoring; + #if !NETFRAMEWORK using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Disk; @@ -114,6 +115,7 @@ private static void PickWindowsSnapshotProvider(this ResourceMonitorBuilder buil if (JobObjectInfo.SafeJobHandle.IsProcessInJob()) { builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); } else { @@ -127,6 +129,7 @@ private static ResourceMonitorBuilder AddLinuxProvider(this ResourceMonitorBuild _ = Throw.IfNull(builder); builder.Services.TryAddActivatedSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(TimeProvider.System); builder.Services.TryAddSingleton(); diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuota.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuota.cs new file mode 100644 index 00000000000..bd53c20034c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuota.cs @@ -0,0 +1,32 @@ +// 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; + +/// +/// Represents resource quota information for a process or container, including CPU and memory constraints. +/// Maximum values define the upper limits of resource usage, while guaranteed values specify +/// the minimum assured resource allocations. +/// +public sealed class ResourceQuota +{ + /// + /// Gets or sets the maximum memory that can be used in bytes. + /// + public ulong MaxMemoryInBytes { get; set; } + + /// + /// Gets or sets the maximum CPU that can be used in cores. + /// + public double MaxCpuInCores { get; set; } + + /// + /// Gets or sets the guaranteed (minimum) memory allocation in bytes. + /// + public ulong GuaranteedMemoryInBytes { get; set; } + + /// + /// Gets or sets the guaranteed (minimum) CPU allocation in cores. + /// + public double GuaranteedCpuInCores { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuotaProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuotaProvider.cs new file mode 100644 index 00000000000..9c4ea2d92a0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuotaProvider.cs @@ -0,0 +1,25 @@ +// 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; + +/// +/// Provides resource quota information for resource monitoring purposes. +/// +/// +/// This abstract class defines a contract for retrieving resource quotas, which include +/// memory and CPU maximum and guaranteed allocations that may be imposed by container orchestrators, +/// resource management systems, or other runtime constraints. +/// +public abstract class ResourceQuotaProvider +{ + /// + /// Gets the current resource quota containing memory and CPU maximum and guaranteed allocations. + /// The returned is used in resource monitoring calculations. + /// + /// + /// A instance containing the current resource constraints + /// including maximum memory, maximum CPU, guaranteed memory, and guaranteed CPU. + /// + public abstract ResourceQuota GetResourceQuota(); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerResourceQuotaProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerResourceQuotaProvider.cs new file mode 100644 index 00000000000..e6844ab8b69 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerResourceQuotaProvider.cs @@ -0,0 +1,96 @@ +// 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.Threading; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows; + +internal class WindowsContainerResourceQuotaProvider : ResourceQuotaProvider +{ + private readonly ISystemInfo _systemInfo; + private readonly Lazy _memoryStatus; + private Func _createJobHandleObject; + + public WindowsContainerResourceQuotaProvider() + : this(new MemoryInfo(), new SystemInfo(), static () => new JobHandleWrapper()) + { + } + + public WindowsContainerResourceQuotaProvider(IMemoryInfo memoryInfo, ISystemInfo systemInfo, Func createJobHandleObject) + { + _systemInfo = systemInfo; + _createJobHandleObject = createJobHandleObject; + + _memoryStatus = new Lazy( + memoryInfo.GetMemoryStatus, + LazyThreadSafetyMode.ExecutionAndPublication); + } + + public override ResourceQuota GetResourceQuota() + { + // bring logic from WindowsContainerSnapshotProvider for limits and requests + using IJobHandle jobHandle = _createJobHandleObject(); + + var resourceQuota = new ResourceQuota + { + MaxCpuInCores = GetCpuLimit(jobHandle, _systemInfo), + MaxMemoryInBytes = GetMemoryLimit(jobHandle), + }; + + // CPU guaranteed (aka minimum CPU units) is not supported on Windows, so we set it to the same value as CPU maximum (aka limit CPU units). + // Memory guaranteed (aka minimum memory) is not supported on Windows, so we set it to the same value as memory maximum (aka limit memory). + resourceQuota.GuaranteedCpuInCores = resourceQuota.MaxCpuInCores; + resourceQuota.GuaranteedMemoryInBytes = resourceQuota.MaxMemoryInBytes; + + return resourceQuota; + } + + private static double GetCpuLimit(IJobHandle jobHandle, ISystemInfo systemInfo) + { + // Note: This function convert the CpuRate from CPU cycles to CPU units, also it scales + // the CPU units with the number of processors (cores) available in the system. + const double CpuCycles = 10_000U; + + var cpuLimit = jobHandle.GetJobCpuLimitInfo(); + double cpuRatio = 1.0; + if ((cpuLimit.ControlFlags & (uint)JobObjectInfo.JobCpuRateControlLimit.CpuRateControlEnable) != 0 && + (cpuLimit.ControlFlags & (uint)JobObjectInfo.JobCpuRateControlLimit.CpuRateControlHardCap) != 0) + { + // The CpuRate is represented as number of cycles during scheduling interval, where + // a full cpu cycles number would equal 10_000, so for example if the CpuRate is 2_000, + // that means that the application (or container) is assigned 20% of the total CPU available. + // So, here we divide the CpuRate by 10_000 to convert it to a ratio (ex: 0.2 for 20% CPU). + // For more info: https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-jobobject_cpu_rate_control_information?redirectedfrom=MSDN + cpuRatio = cpuLimit.CpuRate / CpuCycles; + } + + SYSTEM_INFO systemInfoValue = systemInfo.GetSystemInfo(); + + // Multiply the cpu ratio by the number of processors to get you the portion + // of processors used from the system. + return cpuRatio * systemInfoValue.NumberOfProcessors; + } + + /// + /// Gets memory limit of the system. + /// + /// Memory limit allocated to the system in bytes. + private ulong GetMemoryLimit(IJobHandle jobHandle) + { + var memoryLimitInBytes = jobHandle.GetExtendedLimitInfo().JobMemoryLimit.ToUInt64(); + + if (memoryLimitInBytes <= 0) + { + MEMORYSTATUSEX memoryStatus = _memoryStatus.Value; + + // Technically, the unconstrained limit is memoryStatus.TotalPageFile. + // Leaving this at physical as it is more understandable to consumers. + memoryLimitInBytes = memoryStatus.TotalPhys; + } + + return memoryLimitInBytes; + } +} + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs index 35c64adeedc..59db4e3b2f9 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.Metrics; -using System.Threading; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -19,8 +18,6 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider private const double Hundred = 100.0d; private const double TicksPerSecondDouble = TimeSpan.TicksPerSecond; - private readonly Lazy _memoryStatus; - /// /// This represents a factory method for creating the JobHandle. /// @@ -32,20 +29,25 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider private readonly TimeProvider _timeProvider; private readonly IProcessInfo _processInfo; private readonly ILogger _logger; - private readonly double _memoryLimit; - private readonly double _cpuLimit; private readonly TimeSpan _cpuRefreshInterval; private readonly TimeSpan _memoryRefreshInterval; private readonly double _metricValueMultiplier; + private double _memoryLimit; + private double _cpuLimit; +#pragma warning disable S1450 // Private fields only used as local variables in methods should become local variables. Those will be used once we bring relevant meters. + private ulong _memoryRequest; + private double _cpuRequest; +#pragma warning restore S1450 // Private fields only used as local variables in methods should become local variables + private long _oldCpuUsageTicks; private long _oldCpuTimeTicks; private DateTimeOffset _refreshAfterCpu; private DateTimeOffset _refreshAfterMemory; private DateTimeOffset _refreshAfterProcessMemory; private double _cpuPercentage = double.NaN; - private ulong _memoryUsage; private double _processMemoryPercentage; + private ulong _memoryUsage; public SystemResources Resources { get; } @@ -55,9 +57,10 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider public WindowsContainerSnapshotProvider( ILogger? logger, IMeterFactory meterFactory, - IOptions options) - : this(new MemoryInfo(), new SystemInfo(), new ProcessInfo(), logger, meterFactory, - static () => new JobHandleWrapper(), TimeProvider.System, options.Value) + IOptions options, + ResourceQuotaProvider resourceQuotaProvider) + : this(new ProcessInfo(), logger, meterFactory, + static () => new JobHandleWrapper(), TimeProvider.System, options.Value, resourceQuotaProvider) { } @@ -66,23 +69,19 @@ public WindowsContainerSnapshotProvider( /// /// This constructor enables the mocking of dependencies for the purpose of Unit Testing only. internal WindowsContainerSnapshotProvider( - IMemoryInfo memoryInfo, - ISystemInfo systemInfo, IProcessInfo processInfo, ILogger? logger, IMeterFactory meterFactory, Func createJobHandleObject, TimeProvider timeProvider, - ResourceMonitoringOptions options) + ResourceMonitoringOptions options, + ResourceQuotaProvider resourceQuotaProvider) { _logger = logger ?? NullLogger.Instance; _logger.RunningInsideJobObject(); _metricValueMultiplier = options.UseZeroToOneRangeForMetrics ? One : Hundred; - _memoryStatus = new Lazy( - memoryInfo.GetMemoryStatus, - LazyThreadSafetyMode.ExecutionAndPublication); _createJobHandleObject = createJobHandleObject; _processInfo = processInfo; @@ -90,16 +89,15 @@ internal WindowsContainerSnapshotProvider( using IJobHandle jobHandle = _createJobHandleObject(); - ulong memoryLimitLong = GetMemoryLimit(jobHandle); - _memoryLimit = memoryLimitLong; - _cpuLimit = GetCpuLimit(jobHandle, systemInfo); + var quota = resourceQuotaProvider.GetResourceQuota(); + _cpuLimit = quota.MaxCpuInCores; + _memoryLimit = quota.MaxMemoryInBytes; + _cpuRequest = quota.GuaranteedCpuInCores; + _memoryRequest = quota.GuaranteedMemoryInBytes; - // CPU request (aka guaranteed CPU units) is not supported on Windows, so we set it to the same value as CPU limit (aka maximum CPU units). - // Memory request (aka guaranteed memory) is not supported on Windows, so we set it to the same value as memory limit (aka maximum memory). - double cpuRequest = _cpuLimit; - ulong memoryRequest = memoryLimitLong; - Resources = new SystemResources(cpuRequest, _cpuLimit, memoryRequest, memoryLimitLong); - _logger.SystemResourcesInfo(_cpuLimit, cpuRequest, memoryLimitLong, memoryRequest); + ulong memoryLimitRounded = (ulong)Math.Round(_memoryLimit); + Resources = new SystemResources(_cpuRequest, _cpuLimit, _memoryRequest, memoryLimitRounded); + _logger.SystemResourcesInfo(_cpuLimit, _cpuRequest, memoryLimitRounded, _memoryRequest); var basicAccountingInfo = jobHandle.GetBasicAccountingInfo(); _oldCpuUsageTicks = basicAccountingInfo.TotalKernelTime + basicAccountingInfo.TotalUserTime; @@ -117,7 +115,6 @@ internal WindowsContainerSnapshotProvider( Meter meter = meterFactory.Create(ResourceUtilizationInstruments.MeterName); #pragma warning restore CA2000 // Dispose objects before losing scope - // Container based metrics: _ = meter.CreateObservableCounter( name: ResourceUtilizationInstruments.ContainerCpuTime, observeValues: GetCpuTime, @@ -162,52 +159,6 @@ public Snapshot GetSnapshot() _processInfo.GetCurrentProcessMemoryUsage()); } - private static double GetCpuLimit(IJobHandle jobHandle, ISystemInfo systemInfo) - { - // Note: This function convert the CpuRate from CPU cycles to CPU units, also it scales - // the CPU units with the number of processors (cores) available in the system. - const double CpuCycles = 10_000U; - - var cpuLimit = jobHandle.GetJobCpuLimitInfo(); - double cpuRatio = 1.0; - if ((cpuLimit.ControlFlags & (uint)JobObjectInfo.JobCpuRateControlLimit.CpuRateControlEnable) != 0 && - (cpuLimit.ControlFlags & (uint)JobObjectInfo.JobCpuRateControlLimit.CpuRateControlHardCap) != 0) - { - // The CpuRate is represented as number of cycles during scheduling interval, where - // a full cpu cycles number would equal 10_000, so for example if the CpuRate is 2_000, - // that means that the application (or container) is assigned 20% of the total CPU available. - // So, here we divide the CpuRate by 10_000 to convert it to a ratio (ex: 0.2 for 20% CPU). - // For more info: https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-jobobject_cpu_rate_control_information?redirectedfrom=MSDN - cpuRatio = cpuLimit.CpuRate / CpuCycles; - } - - SYSTEM_INFO systemInfoValue = systemInfo.GetSystemInfo(); - - // Multiply the cpu ratio by the number of processors to get you the portion - // of processors used from the system. - return cpuRatio * systemInfoValue.NumberOfProcessors; - } - - /// - /// Gets memory limit of the system. - /// - /// Memory limit allocated to the system in bytes. - private ulong GetMemoryLimit(IJobHandle jobHandle) - { - var memoryLimitInBytes = jobHandle.GetExtendedLimitInfo().JobMemoryLimit.ToUInt64(); - - if (memoryLimitInBytes <= 0) - { - MEMORYSTATUSEX memoryStatus = _memoryStatus.Value; - - // Technically, the unconstrained limit is memoryStatus.TotalPageFile. - // Leaving this at physical as it is more understandable to consumers. - memoryLimitInBytes = memoryStatus.TotalPhys; - } - - return memoryLimitInBytes; - } - private double ProcessMemoryPercentage() { DateTimeOffset now = _timeProvider.GetUtcNow(); diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/KubernetesResourceQuotasServiceCollectionExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/KubernetesResourceQuotasServiceCollectionExtensionsTests.cs new file mode 100644 index 00000000000..e073df1c2c2 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/KubernetesResourceQuotasServiceCollectionExtensionsTests.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests; + +public class KubernetesResourceQuotasServiceCollectionExtensionsTests +{ + [Fact] + public void AddKubernetesResourceMonitoring_WithoutConfiguration_RegistersServicesCorrectly() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddKubernetesResourceMonitoring(); + + // Assert + using var serviceProvider = services.BuildServiceProvider(); + + ResourceQuotaProvider? resourceQuotaProvider = serviceProvider.GetService(); + Assert.NotNull(resourceQuotaProvider); + Assert.IsType(resourceQuotaProvider); + Assert.NotNull(serviceProvider.GetService()); + Assert.NotNull(serviceProvider.GetService()); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests.csproj b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests.csproj new file mode 100644 index 00000000000..b2cb0677778 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests.csproj @@ -0,0 +1,17 @@ + + + + Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test + Unit tests for Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes. + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/CpuPercentageCalculatorTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/CpuPercentageCalculatorTests.cs new file mode 100644 index 00000000000..ed7183cf256 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/CpuPercentageCalculatorTests.cs @@ -0,0 +1,130 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Moq; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test; + +public class CpuPercentageCalculatorTests +{ + private const double TicksPerSecondDouble = TimeSpan.TicksPerSecond; // 10,000,000 + private const int MetricValueMultiplier = 100; + + public class TestCpuCalculator + { + private readonly double _cpuLimit; + private ulong _oldCpuUsageTicks; + private long _oldCpuTimeTicks; + + public TestCpuCalculator(double cpuLimit) + { + _cpuLimit = cpuLimit; + } + + public void SetInitialState(ulong initialCpuTicks, long initialTimeTicks) + { + _oldCpuUsageTicks = initialCpuTicks; + _oldCpuTimeTicks = initialTimeTicks; + } + + public double CalculateCpuPercentage(ulong currentCpuTicks, long currentTimeTicks) + { + var usageTickDelta = currentCpuTicks - _oldCpuUsageTicks; + var timeTickDelta = (currentTimeTicks - _oldCpuTimeTicks) * _cpuLimit; + + if (usageTickDelta > 0 && timeTickDelta > 0) + { + return Math.Min(MetricValueMultiplier, usageTickDelta / timeTickDelta * MetricValueMultiplier); + } + return 0; + } + } + + [Theory] + [InlineData(0.3, 4, 1.2, "Current implementation - with core scaling")] + [InlineData(0.3, 1, 0.3, "Fixed implementation - without core scaling")] + public void CpuPercentageCalculation_WithDifferentLimits_ShowsScalingImpact( + double kubernetesLimit, int coreCount, double expectedCpuLimit, string testCase) + { + // Arrange - Simulate GetCpuLimit calculation + double cpuLimit = kubernetesLimit * coreCount; // Your current implementation + var calculator = new TestCpuCalculator(cpuLimit); + + // Initial state - container has been running and consumed some CPU + var initialTime = new DateTime(2025, 1, 1, 12, 0, 0).Ticks; + var initialCpuTicks = (ulong)(1.5 * TicksPerSecondDouble); // 1.5 seconds total CPU consumed + calculator.SetInitialState(initialCpuTicks, initialTime); + + // After 5 seconds, container consumed 0.2 more CPU seconds + var currentTime = initialTime + (5 * TimeSpan.TicksPerSecond); // 5 seconds later + var currentCpuTicks = initialCpuTicks + (ulong)(0.2 * TicksPerSecondDouble); // +0.2 CPU seconds + + // Act + var cpuPercentage = calculator.CalculateCpuPercentage(currentCpuTicks, currentTime); + + // Assert & Debug + var actualCpuUsagePercent = (0.2 / 5.0) * 100; // 4% of wall clock time + var expectedPercentOfLimit = (0.2 / kubernetesLimit) / 5.0 * 100; // % of allocated CPU budget + + Console.WriteLine($"\n=== {testCase} ==="); + Console.WriteLine($"Kubernetes CPU limit: {kubernetesLimit} cores"); + Console.WriteLine($"System cores: {coreCount}"); + Console.WriteLine($"Calculated _cpuLimit: {cpuLimit}"); + Console.WriteLine($"Actual CPU usage: 0.2 cores over 5 seconds = {actualCpuUsagePercent:F1}% of wall clock"); + Console.WriteLine($"Expected % of CPU budget: {expectedPercentOfLimit:F1}%"); + Console.WriteLine($"Your algorithm reports: {cpuPercentage:F1}%"); + Console.WriteLine($"Difference factor: {expectedPercentOfLimit / cpuPercentage:F1}x"); + + // Verify the scaling issue + if (testCase.Contains("Current implementation")) + { + Assert.True(cpuPercentage < 5, $"Current implementation should show low percentage due to scaling issue. Got: {cpuPercentage}%"); + } + else + { + Assert.InRange(cpuPercentage, 10, 15); // Should be around 13.3% with correct scaling + } + } + + [Fact] + public void CpuPercentage_RealisticKubernetesScenario_ShowsProblem() + { + // Arrange - Your exact scenario + // K8s: 0.3 CPU limit, 4-core node + double cpuLimit = 0.3 * 4; // 1.2 (current implementation) + var calculator = new TestCpuCalculator(cpuLimit); + + // Container running for a while, consumed 2 seconds of CPU total + var startTime = DateTime.UtcNow.AddMinutes(-5).Ticks; + calculator.SetInitialState((ulong)(2.0 * TicksPerSecondDouble), startTime); + + // 5 seconds later, consumed 0.15 more CPU seconds (moderate load) + var currentTime = startTime + (5 * TimeSpan.TicksPerSecond); + var currentCpuTicks = (ulong)((2.0 + 0.15) * TicksPerSecondDouble); + + // Act + var result = calculator.CalculateCpuPercentage(currentCpuTicks, currentTime); + + // Assert + Console.WriteLine($"\nRealistic Scenario:"); + Console.WriteLine($"Used 0.15 CPU cores over 5 seconds"); + Console.WriteLine($"That's {(0.15 / 5.0) * 100:F1}% of wall clock time"); + Console.WriteLine($"With 0.3 CPU limit, that's {(0.15 / 0.3 / 5.0) * 100:F1}% of CPU budget per second"); + Console.WriteLine($"Your algorithm reports: {result:F1}%"); + + // This demonstrates the problem - should be around 10%, but will be much lower + Assert.True(result < 5, "Should show deflated percentage due to core scaling"); + + // Shows what it should be without scaling + var correctLimit = 0.3; + var correctTimeTickDelta = (currentTime - startTime) * correctLimit; + var correctPercentage = ((currentCpuTicks - (ulong)(2.0 * TicksPerSecondDouble)) / correctTimeTickDelta) * 100; + Console.WriteLine($"With correct scaling (0.3 limit): {correctPercentage:F1}%"); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/ResourceMonitoringBuilderTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/ResourceMonitoringBuilderTests.cs index d2e3d5d8292..9b4fe5b943b 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/ResourceMonitoringBuilderTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/ResourceMonitoringBuilderTests.cs @@ -1,9 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Diagnostics.Metrics; using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Publishers; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows; using Microsoft.TestUtilities; using Xunit; @@ -53,4 +56,56 @@ public void AddPublisher_CalledMultipleTimes_AddsMultiplePublishersToServiceColl Assert.IsAssignableFrom(publishersArray.First()); Assert.IsAssignableFrom(publishersArray.Last()); } + + [ConditionalFact] + public void AddResourceMonitoring_WithoutConfigureMonitor_CannotResolveSnapshotProviderRequiringOptions() + { + // Arrange - Try to register WindowsSnapshotProvider which requires IOptions + var services = new ServiceCollection() + .AddLogging() + .AddSingleton(sp => new FakeMeterFactory()) + .AddResourceMonitoring() + .AddSingleton(); // ← This will fail to resolve + + using var provider = services.BuildServiceProvider(); + + // Act & Assert - Should throw because IOptions is not registered + var exception = Assert.Throws(() => + provider.GetRequiredService()); + + Assert.Contains("IOptions", exception.Message); + } + + [ConditionalFact] + public void AddResourceMonitoring_WithManualOptionsConfiguration_AllowsSnapshotProviderResolution() + { + // Arrange - Manually register options to fix the issue + var services = new ServiceCollection() + .AddLogging() + .AddSingleton(sp => new FakeMeterFactory()) + .AddResourceMonitoring() + .Configure(options => { }) // ← Manual fix + .AddSingleton(); + + using var provider = services.BuildServiceProvider(); + + // Act & Assert - Should now work + var snapshotProvider = provider.GetRequiredService(); + + Assert.NotNull(snapshotProvider); + Assert.NotNull(snapshotProvider.Resources); + } + + internal sealed class FakeMeterFactory : IMeterFactory + { + public Meter Create(MeterOptions options) + { + return new Meter(options.Name, options.Version); + } + + public void Dispose() + { + // Nothing to dispose + } + } }