From dfdf2280d7efe3d2be5ea0810881aaef06a97f31 Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Mon, 20 Jan 2025 12:25:35 +0100 Subject: [PATCH 1/5] Add container.cpu.time metric --- .../Linux/LinuxUtilizationParserCgroupV1.cs | 2 +- .../Linux/LinuxUtilizationParserCgroupV2.cs | 2 +- .../Linux/LinuxUtilizationProvider.cs | 12 ++++ .../WindowsContainerSnapshotProvider.cs | 12 ++++ .../ResourceUtilizationInstruments.cs | 8 +++ .../Linux/AcceptanceTest.cs | 40 ++++++++++--- .../Linux/LinuxUtilizationProviderTests.cs | 10 +++- .../WindowsContainerSnapshotProviderTests.cs | 60 +++++++++++++++++++ 8 files changed, 135 insertions(+), 11 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV1.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV1.cs index 07d39e70178..486ede1ddd5 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV1.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV1.cs @@ -145,7 +145,7 @@ public long GetHostCpuUsageInNanoseconds() $"'{_procStat}' should contain whitespace separated values according to POSIX. We've failed trying to get {i}th value. File content: '{new string(stat)}'."); } - stat = stat.Slice(next, stat.Length - next); + stat = stat.Slice(next); } return (long)(total / (double)_userHz * NanosecondsInSecond); diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs index 113b6b3ad6a..970f462c357 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs @@ -163,7 +163,7 @@ public long GetHostCpuUsageInNanoseconds() $"'{_procStat}' should contain whitespace separated values according to POSIX. We've failed trying to get {i}th value. File content: '{new string(stat)}'."); } - stat = stat.Slice(next, stat.Length - next); + stat = stat.Slice(next); } return (long)(total / (double)_userHz * NanosecondsInSecond); diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs index 7fc5947aa36..f3bf95281c4 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.Diagnostics.Metrics; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -14,6 +15,7 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider { private const double One = 1.0; private const long Hundred = 100L; + private const double NanosecondsInSecond = 1_000_000_000; private readonly object _cpuLocker = new(); private readonly object _memoryLocker = new(); @@ -66,6 +68,7 @@ public LinuxUtilizationProvider(IOptions options, ILi var meter = meterFactory.Create(ResourceUtilizationInstruments.MeterName); #pragma warning restore CA2000 // Dispose objects before losing scope + _ = meter.CreateObservableCounter(name: ResourceUtilizationInstruments.ContainerCpuTime, observeValues: GetCpuTime, unit: "s", description: "CPU time used by the container."); _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, observeValue: () => CpuUtilization() * _scaleRelativeToCpuLimit, unit: "1"); _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, observeValue: MemoryUtilization, unit: "1"); _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerCpuRequestUtilization, observeValue: () => CpuUtilization() * _scaleRelativeToCpuRequest, unit: "1"); @@ -167,4 +170,13 @@ public Snapshot GetSnapshot() userTimeSinceStart: TimeSpan.FromTicks((long)(cgroupTime / Hundred * _scaleRelativeToCpuRequestForTrackerApi)), memoryUsageInBytes: memoryUsed); } + + private IEnumerable> GetCpuTime() + { + long hostCpuTime = _parser.GetHostCpuUsageInNanoseconds(); + long cgroupCpuTime = _parser.GetCgroupCpuUsageInNanoseconds(); + + yield return new(cgroupCpuTime / NanosecondsInSecond, [new KeyValuePair("cpu.mode", "user")]); + yield return new(hostCpuTime / NanosecondsInSecond, [new KeyValuePair("cpu.mode", "system")]); + } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs index 56d8bc2e578..f22a1595bfb 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using System.Threading; @@ -17,6 +18,7 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider { private const double One = 1.0d; private const double Hundred = 100.0d; + private const double TicksPerSecoundDouble = TimeSpan.TicksPerSecond; private readonly Lazy _memoryStatus; @@ -114,6 +116,7 @@ internal WindowsContainerSnapshotProvider( #pragma warning restore CA2000 // Dispose objects before losing scope // Container based metrics: + _ = meter.CreateObservableCounter(name: ResourceUtilizationInstruments.ContainerCpuTime, observeValues: GetCpuTime, unit: "s", description: "CPU time used by the container."); _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, observeValue: CpuPercentage); _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, observeValue: () => MemoryPercentage(() => _processInfo.GetMemoryUsage())); @@ -211,6 +214,15 @@ private double MemoryPercentage(Func getMemoryUsage) } } + private IEnumerable> GetCpuTime() + { + using var jobHandle = _createJobHandleObject(); + var basicAccountingInfo = jobHandle.GetBasicAccountingInfo(); + + yield return new(basicAccountingInfo.TotalUserTime / TicksPerSecoundDouble, [new KeyValuePair("cpu.mode", "user")]); + yield return new(basicAccountingInfo.TotalKernelTime / TicksPerSecoundDouble, [new KeyValuePair("cpu.mode", "system")]); + } + private double CpuPercentage() { var now = _timeProvider.GetUtcNow(); diff --git a/src/Shared/Instruments/ResourceUtilizationInstruments.cs b/src/Shared/Instruments/ResourceUtilizationInstruments.cs index a5a95a254c5..c3d0adf81b5 100644 --- a/src/Shared/Instruments/ResourceUtilizationInstruments.cs +++ b/src/Shared/Instruments/ResourceUtilizationInstruments.cs @@ -18,6 +18,14 @@ internal static class ResourceUtilizationInstruments /// public const string MeterName = "Microsoft.Extensions.Diagnostics.ResourceMonitoring"; + /// + /// The name of an instrument to retrieve CPU time consumed by the specific container on all available CPU cores, measured in seconds. + /// + /// + /// The type of an instrument is . + /// + public const string ContainerCpuTime = "container.cpu.time"; + /// /// The name of an instrument to retrieve CPU limit consumption of all processes running inside a container or control group in range [0, 1]. /// 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 c786ec416a0..173697164e8 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; @@ -209,6 +210,8 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou using var listener = new MeterListener(); var clock = new FakeTimeProvider(DateTimeOffset.UtcNow); + var cpuUserTime = 0.0d; + var cpuKernelTime = 0.0d; var cpuFromGauge = 0.0d; var cpuLimitFromGauge = 0.0d; var cpuRequestFromGauge = 0.0d; @@ -219,8 +222,8 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou object? meterScope = null; listener.InstrumentPublished = (Instrument instrument, MeterListener meterListener) => OnInstrumentPublished(instrument, meterListener, meterScope); - listener.SetMeasurementEventCallback((m, f, _, _) - => OnMeasurementReceived(m, f, ref cpuFromGauge, ref cpuLimitFromGauge, ref cpuRequestFromGauge, ref memoryFromGauge, ref memoryLimitFromGauge)); + listener.SetMeasurementEventCallback((m, f, tags, _) + => OnMeasurementReceived(m, f, tags, ref cpuUserTime, ref cpuKernelTime, ref cpuFromGauge, ref cpuLimitFromGauge, ref cpuRequestFromGauge, ref memoryFromGauge, ref memoryLimitFromGauge)); listener.Start(); using var host = FakeHost.CreateBuilder() @@ -246,6 +249,8 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou Assert.Equal(0, utilization.CpuUsedPercentage); Assert.Equal(100, utilization.MemoryUsedPercentage); Assert.True(double.IsNaN(cpuFromGauge)); + Assert.Equal(0.000102312, cpuUserTime); + Assert.Equal(0.8, cpuKernelTime); // gauge multiplied by 100 because gauges are in range [0, 1], and utilization is in range [0, 100] Assert.Equal(utilization.MemoryUsedPercentage, memoryFromGauge * 100); @@ -264,6 +269,8 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou Assert.Equal(1, utilization.CpuUsedPercentage); Assert.Equal(50, utilization.MemoryUsedPercentage); Assert.Equal(0.5, cpuLimitFromGauge * 100); + Assert.Equal(0.000112312, cpuUserTime); + Assert.Equal(0.81, cpuKernelTime); Assert.Equal(utilization.CpuUsedPercentage, cpuRequestFromGauge * 100); Assert.Equal(utilization.MemoryUsedPercentage, memoryLimitFromGauge * 100); Assert.Equal(utilization.CpuUsedPercentage, cpuFromGauge * 100); @@ -292,6 +299,8 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou using var listener = new MeterListener(); var clock = new FakeTimeProvider(DateTimeOffset.UtcNow); + var cpuUserTime = 0.0d; + var cpuKernelTime = 0.0d; var cpuFromGauge = 0.0d; var cpuLimitFromGauge = 0.0d; var cpuRequestFromGauge = 0.0d; @@ -302,8 +311,8 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou object? meterScope = null; listener.InstrumentPublished = (Instrument instrument, MeterListener meterListener) => OnInstrumentPublished(instrument, meterListener, meterScope); - listener.SetMeasurementEventCallback((m, f, _, _) - => OnMeasurementReceived(m, f, ref cpuFromGauge, ref cpuLimitFromGauge, ref cpuRequestFromGauge, ref memoryFromGauge, ref memoryLimitFromGauge)); + listener.SetMeasurementEventCallback((m, f, tags, _) + => OnMeasurementReceived(m, f, tags, ref cpuUserTime, ref cpuKernelTime, ref cpuFromGauge, ref cpuLimitFromGauge, ref cpuRequestFromGauge, ref memoryFromGauge, ref memoryLimitFromGauge)); listener.Start(); using var host = FakeHost.CreateBuilder() @@ -351,6 +360,8 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou Assert.Equal(1, roundedCpuUsedPercentage); Assert.Equal(50, utilization.MemoryUsedPercentage); Assert.Equal(0.5, cpuLimitFromGauge * 100); + Assert.Equal(0.000112, cpuUserTime); + Assert.Equal(0.81, cpuKernelTime); Assert.Equal(roundedCpuUsedPercentage, Math.Round(cpuRequestFromGauge * 100)); Assert.Equal(utilization.MemoryUsedPercentage, memoryLimitFromGauge * 100); Assert.Equal(roundedCpuUsedPercentage, Math.Round(cpuFromGauge * 100)); @@ -369,6 +380,7 @@ private static void OnInstrumentPublished(Instrument instrument, MeterListener m #pragma warning disable S1067 // Expressions should not be too complex if (instrument.Name == ResourceUtilizationInstruments.ProcessCpuUtilization || instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization || + instrument.Name == ResourceUtilizationInstruments.ContainerCpuTime || instrument.Name == ResourceUtilizationInstruments.ContainerCpuRequestUtilization || instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization || instrument.Name == ResourceUtilizationInstruments.ContainerMemoryLimitUtilization) @@ -378,10 +390,12 @@ private static void OnInstrumentPublished(Instrument instrument, MeterListener m #pragma warning restore S1067 // Expressions should not be too complex } +#pragma warning disable S107 // Methods should not have too many parameters private static void OnMeasurementReceived( - Instrument instrument, double value, - ref double cpuFromGauge, ref double cpuLimitFromGauge, ref double cpuRequestFromGauge, - ref double memoryFromGauge, ref double memoryLimitFromGauge) + Instrument instrument, double value, ReadOnlySpan> tags, + ref double cpuUserTime, ref double cpuKernelTime, ref double cpuFromGauge, ref double cpuLimitFromGauge, + ref double cpuRequestFromGauge, ref double memoryFromGauge, ref double memoryLimitFromGauge) +#pragma warning restore S107 // Methods should not have too many parameters { if (instrument.Name == ResourceUtilizationInstruments.ProcessCpuUtilization) { @@ -391,6 +405,18 @@ private static void OnMeasurementReceived( { memoryFromGauge = value; } + else if (instrument.Name == ResourceUtilizationInstruments.ContainerCpuTime) + { + var tagsArray = tags.ToArray(); + if (tagsArray.Contains(new KeyValuePair("cpu.mode", "user"))) + { + cpuUserTime = value; + } + else if (tagsArray.Contains(new KeyValuePair("cpu.mode", "system"))) + { + cpuKernelTime = value; + } + } else if (instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization) { cpuLimitFromGauge = value; diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs index 1eb7a0af712..e2f2d00e2c6 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs @@ -75,7 +75,7 @@ public void Provider_Registers_Instruments() listener.Start(); listener.RecordObservableInstruments(); - Assert.Equal(5, samples.Count); + Assert.Equal(7, samples.Count); Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization); Assert.True(double.IsNaN(samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization).value)); @@ -91,6 +91,9 @@ public void Provider_Registers_Instruments() Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization); Assert.Equal(0.5, samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization).value); + + Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuTime && Math.Abs(x.value - (50.0 / 1_000_000_000)) < 0.00001); + Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuTime && Math.Abs(x.value - 0.8) < 0.00001); } [ConditionalFact] @@ -144,7 +147,7 @@ public void Provider_Registers_Instruments_CgroupV2() listener.Start(); listener.RecordObservableInstruments(); - Assert.Equal(5, samples.Count); + Assert.Equal(7, samples.Count); Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization); Assert.True(double.IsNaN(samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization).value)); @@ -160,6 +163,9 @@ public void Provider_Registers_Instruments_CgroupV2() Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization); Assert.Equal(1, samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization).value); + + Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuTime && Math.Abs(x.value - (102312.0 / 1_000_000)) < 0.00001); + Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuTime && Math.Abs(x.value - 0.8) < 0.00001); } [Fact] diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs index 103fdc988d5..ff3b5ad0c57 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs @@ -191,6 +191,66 @@ public void GetSnapshot_With_JobMemoryLimit_Set_To_Zero_ProducesCorrectSnapshot( Assert.True(data.MemoryUsageInBytes > 0); } + [Fact] + public void SnapshotProvider_EmitsCpuTimeMetric() + { + // Simulating 10% CPU usage (2 CPUs, 2000 ticks initially, 4000 ticks after 1 ms): + JOBOBJECT_BASIC_ACCOUNTING_INFORMATION updatedAccountingInfo = default; + updatedAccountingInfo.TotalKernelTime = 2500; + updatedAccountingInfo.TotalUserTime = 1500; + + _jobHandleMock.SetupSequence(j => j.GetBasicAccountingInfo()) + .Returns(_accountingInfo) + .Returns(_accountingInfo) + .Returns(updatedAccountingInfo) + .Returns(updatedAccountingInfo) + .Throws(new InvalidOperationException("We shouldn't hit here...")); + + _sysInfo.NumberOfProcessors = 2; + + var fakeClock = new FakeTimeProvider(); + using var meter = new Meter(nameof(SnapshotProvider_EmitsCpuMetrics)); + var meterFactoryMock = new Mock(); + meterFactoryMock.Setup(x => x.Create(It.IsAny())) + .Returns(meter); + using var metricCollector = new MetricCollector(meter, ResourceUtilizationInstruments.ContainerCpuTime, fakeClock); + + var options = new ResourceMonitoringOptions { CpuConsumptionRefreshInterval = TimeSpan.FromMilliseconds(2) }; + + var snapshotProvider = new WindowsContainerSnapshotProvider( + _memoryInfoMock.Object, + _systemInfoMock.Object, + _processInfoMock.Object, + _logger, + meterFactoryMock.Object, + () => _jobHandleMock.Object, + fakeClock, + options); + + // Step #0 - state in the beginning: + metricCollector.RecordObservableInstruments(); + var snapshot = metricCollector.GetMeasurementSnapshot(); + Assert.Equal(2, snapshot.Count); + Assert.Contains(_accountingInfo.TotalKernelTime / (double)TimeSpan.TicksPerSecond, snapshot.Select(m => m.Value)); + Assert.Contains(_accountingInfo.TotalKernelTime / (double)TimeSpan.TicksPerSecond, snapshot.Select(m => m.Value)); + + // Step #1 - simulate 1 millisecond passing and collect metrics again: + fakeClock.Advance(TimeSpan.FromMilliseconds(1)); + metricCollector.RecordObservableInstruments(); + snapshot = metricCollector.GetMeasurementSnapshot(); + Assert.Contains(updatedAccountingInfo.TotalKernelTime / (double)TimeSpan.TicksPerSecond, snapshot.Select(m => m.Value)); + Assert.Contains(updatedAccountingInfo.TotalKernelTime / (double)TimeSpan.TicksPerSecond, snapshot.Select(m => m.Value)); + + // Step #2 - simulate 1 millisecond passing and collect metrics again: + fakeClock.Advance(TimeSpan.FromMilliseconds(1)); + metricCollector.RecordObservableInstruments(); + snapshot = metricCollector.GetMeasurementSnapshot(); + + // CPU time should be the same as before, as we're not simulating any CPU usage: + Assert.Contains(updatedAccountingInfo.TotalKernelTime / (double)TimeSpan.TicksPerSecond, snapshot.Select(m => m.Value)); + Assert.Contains(updatedAccountingInfo.TotalKernelTime / (double)TimeSpan.TicksPerSecond, snapshot.Select(m => m.Value)); + } + [Theory] [InlineData(ResourceUtilizationInstruments.ProcessCpuUtilization, true)] [InlineData(ResourceUtilizationInstruments.ProcessCpuUtilization, false)] From 132b73ebf88ad8fb9042a1a95bf1c10e47891cdf Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 Date: Thu, 6 Feb 2025 10:55:27 +0100 Subject: [PATCH 2/5] PR Comments --- .../Windows/WindowsContainerSnapshotProvider.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs index f22a1595bfb..0e4c83636ab 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs @@ -18,7 +18,7 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider { private const double One = 1.0d; private const double Hundred = 100.0d; - private const double TicksPerSecoundDouble = TimeSpan.TicksPerSecond; + private const double TicksPerSecondDouble = TimeSpan.TicksPerSecond; private readonly Lazy _memoryStatus; @@ -219,8 +219,8 @@ private IEnumerable> GetCpuTime() using var jobHandle = _createJobHandleObject(); var basicAccountingInfo = jobHandle.GetBasicAccountingInfo(); - yield return new(basicAccountingInfo.TotalUserTime / TicksPerSecoundDouble, [new KeyValuePair("cpu.mode", "user")]); - yield return new(basicAccountingInfo.TotalKernelTime / TicksPerSecoundDouble, [new KeyValuePair("cpu.mode", "system")]); + yield return new(basicAccountingInfo.TotalUserTime / TicksPerSecondDouble, [new KeyValuePair("cpu.mode", "user")]); + yield return new(basicAccountingInfo.TotalKernelTime / TicksPerSecondDouble, [new KeyValuePair("cpu.mode", "system")]); } private double CpuPercentage() From aaffffca1cbf5b9269626ba9710404f99ade6af9 Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 Date: Mon, 30 Jun 2025 22:20:50 +0200 Subject: [PATCH 3/5] Update --- .../Linux/LinuxUtilizationParserCgroupV2.cs | 2 +- .../Linux/LinuxUtilizationProvider.cs | 7 +++-- .../WindowsContainerSnapshotProvider.cs | 26 ++++++++++--------- .../Linux/AcceptanceTest.cs | 16 ++++++------ 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs index 335fcc4d082..0e367891a96 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs @@ -131,7 +131,7 @@ public string GetCgroupPath(string filename) } // Extract the part after the last colon and cache it for future use - ReadOnlySpan trimmedPath = fileContent.Slice(colonIndex + 1); + ReadOnlySpan trimmedPath = fileContent[(colonIndex + 1)..]; _cachedCgroupPath = "/sys/fs/cgroup" + trimmedPath.ToString().TrimEnd('/') + "/"; return $"{_cachedCgroupPath}{filename}"; diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs index c066e15fd20..b9b98b68b96 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs @@ -105,7 +105,6 @@ public LinuxUtilizationProvider(IOptions options, ILi _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, observeValue: () => CpuUtilization() * _scaleRelativeToCpuLimit, unit: "1"); _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerCpuRequestUtilization, observeValue: () => CpuUtilization() * _scaleRelativeToCpuRequest, unit: "1"); _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ProcessCpuUtilization, observeValue: () => CpuUtilization() * _scaleRelativeToCpuRequest, unit: "1"); - _ = meter.CreateObservableCounter(name: ResourceUtilizationInstruments.ContainerCpuTime, observeValues: GetCpuTime, unit: "s", description: "CPU time used by the container."); } _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, observeValue: MemoryUtilization, unit: "1"); @@ -296,9 +295,9 @@ public Snapshot GetSnapshot() private IEnumerable> GetCpuTime() { long hostCpuTime = _parser.GetHostCpuUsageInNanoseconds(); - long cgroupCpuTime = _parser.GetCgroupCpuUsageInNanoseconds(); + double cgroupCpuTime = CpuUtilizationWithoutHostDelta(); - yield return new(cgroupCpuTime / NanosecondsInSecond, [new KeyValuePair("cpu.mode", "user")]); - yield return new(hostCpuTime / NanosecondsInSecond, [new KeyValuePair("cpu.mode", "system")]); + yield return new Measurement(cgroupCpuTime / NanosecondsInSecond, [new KeyValuePair("cpu.mode", "user")]); + yield return new Measurement(hostCpuTime / NanosecondsInSecond, [new KeyValuePair("cpu.mode", "system")]); } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs index ad380bde10c..ca6ceaff8bd 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs @@ -87,16 +87,16 @@ internal WindowsContainerSnapshotProvider( _timeProvider = timeProvider; - using var jobHandle = _createJobHandleObject(); + using IJobHandle jobHandle = _createJobHandleObject(); - var memoryLimitLong = GetMemoryLimit(jobHandle); + ulong memoryLimitLong = GetMemoryLimit(jobHandle); _memoryLimit = memoryLimitLong; _cpuLimit = GetCpuLimit(jobHandle, systemInfo); // 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). - var cpuRequest = _cpuLimit; - var memoryRequest = memoryLimitLong; + double cpuRequest = _cpuLimit; + ulong memoryRequest = memoryLimitLong; Resources = new SystemResources(cpuRequest, _cpuLimit, memoryRequest, memoryLimitLong); _logger.SystemResourcesInfo(_cpuLimit, cpuRequest, memoryLimitLong, memoryRequest); @@ -112,7 +112,7 @@ internal WindowsContainerSnapshotProvider( // We don't dispose the meter because IMeterFactory handles that // An issue on analyzer side: https://github.com/dotnet/roslyn-analyzers/issues/6912 // Related documentation: https://github.com/dotnet/docs/pull/37170 - var meter = meterFactory.Create(ResourceUtilizationInstruments.MeterName); + Meter meter = meterFactory.Create(ResourceUtilizationInstruments.MeterName); #pragma warning restore CA2000 // Dispose objects before losing scope // Container based metrics: @@ -158,7 +158,7 @@ private static double GetCpuLimit(IJobHandle jobHandle, ISystemInfo systemInfo) cpuRatio = cpuLimit.CpuRate / CpuCycles; } - var systemInfoValue = systemInfo.GetSystemInfo(); + 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. @@ -175,7 +175,7 @@ private ulong GetMemoryLimit(IJobHandle jobHandle) if (memoryLimitInBytes <= 0) { - var memoryStatus = _memoryStatus.Value; + MEMORYSTATUSEX memoryStatus = _memoryStatus.Value; // Technically, the unconstrained limit is memoryStatus.TotalPageFile. // Leaving this at physical as it is more understandable to consumers. @@ -187,7 +187,7 @@ private ulong GetMemoryLimit(IJobHandle jobHandle) private double MemoryPercentage(Func getMemoryUsage) { - var now = _timeProvider.GetUtcNow(); + DateTimeOffset now = _timeProvider.GetUtcNow(); lock (_memoryLocker) { @@ -197,7 +197,7 @@ private double MemoryPercentage(Func getMemoryUsage) } } - var memoryUsage = getMemoryUsage(); + ulong memoryUsage = getMemoryUsage(); lock (_memoryLocker) { @@ -216,11 +216,13 @@ private double MemoryPercentage(Func getMemoryUsage) private IEnumerable> GetCpuTime() { - using var jobHandle = _createJobHandleObject(); + using IJobHandle jobHandle = _createJobHandleObject(); var basicAccountingInfo = jobHandle.GetBasicAccountingInfo(); - yield return new(basicAccountingInfo.TotalUserTime / TicksPerSecondDouble, [new KeyValuePair("cpu.mode", "user")]); - yield return new(basicAccountingInfo.TotalKernelTime / TicksPerSecondDouble, [new KeyValuePair("cpu.mode", "system")]); + yield return new Measurement(basicAccountingInfo.TotalUserTime / TicksPerSecondDouble, + [new KeyValuePair("cpu.mode", "user")]); + yield return new Measurement(basicAccountingInfo.TotalKernelTime / TicksPerSecondDouble, + [new KeyValuePair("cpu.mode", "system")]); } private double CpuPercentage() 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 cae2a1ac39c..349a877cbad 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs @@ -375,8 +375,6 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")] public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgroupsv2_v2() { - var cpuRefresh = TimeSpan.FromMinutes(13); - var memoryRefresh = TimeSpan.FromMinutes(14); var fileSystem = new HardcodedValueFileSystem(new Dictionary { { new FileInfo("/proc/self/cgroup"), "0::/fakeslice"}, @@ -392,6 +390,8 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou using var listener = new MeterListener(); var clock = new FakeTimeProvider(DateTimeOffset.UtcNow); var cpuFromGauge = 0.0d; + var cpuUserTime = 0.0d; + var cpuKernelTime = 0.0d; var cpuLimitFromGauge = 0.0d; var cpuRequestFromGauge = 0.0d; var memoryFromGauge = 0.0d; @@ -401,8 +401,8 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou object? meterScope = null; listener.InstrumentPublished = (Instrument instrument, MeterListener meterListener) => OnInstrumentPublished(instrument, meterListener, meterScope); - listener.SetMeasurementEventCallback((m, f, _, _) - => OnMeasurementReceived(m, f, ref cpuFromGauge, ref cpuLimitFromGauge, ref cpuRequestFromGauge, ref memoryFromGauge, ref memoryLimitFromGauge)); + listener.SetMeasurementEventCallback((m, f, tags, _) + => OnMeasurementReceived(m, f, tags, ref cpuUserTime, ref cpuKernelTime, ref cpuFromGauge, ref cpuLimitFromGauge, ref cpuRequestFromGauge, ref memoryFromGauge, ref memoryLimitFromGauge)); listener.Start(); using var host = FakeHost.CreateBuilder() @@ -451,8 +451,6 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")] public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgroupsv2_v2_Using_NrPeriods() { - var cpuRefresh = TimeSpan.FromMinutes(13); - var memoryRefresh = TimeSpan.FromMinutes(14); var fileSystem = new HardcodedValueFileSystem(new Dictionary { { new FileInfo("/proc/self/cgroup"), "0::/fakeslice"}, @@ -469,6 +467,8 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou var clock = new FakeTimeProvider(DateTimeOffset.UtcNow); var cpuFromGauge = 0.0d; var cpuLimitFromGauge = 0.0d; + var cpuUserTime = 0.0d; + var cpuKernelTime = 0.0d; var cpuRequestFromGauge = 0.0d; var memoryFromGauge = 0.0d; var memoryLimitFromGauge = 0.0d; @@ -477,8 +477,8 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou object? meterScope = null; listener.InstrumentPublished = (Instrument instrument, MeterListener meterListener) => OnInstrumentPublished(instrument, meterListener, meterScope); - listener.SetMeasurementEventCallback((m, f, _, _) - => OnMeasurementReceived(m, f, ref cpuFromGauge, ref cpuLimitFromGauge, ref cpuRequestFromGauge, ref memoryFromGauge, ref memoryLimitFromGauge)); + listener.SetMeasurementEventCallback((m, f, tags, _) + => OnMeasurementReceived(m, f, tags, ref cpuUserTime, ref cpuKernelTime, ref cpuFromGauge, ref cpuLimitFromGauge, ref cpuRequestFromGauge, ref memoryFromGauge, ref memoryLimitFromGauge)); listener.Start(); using var host = FakeHost.CreateBuilder() From 47ee0002561c7141f5f018d95fc15707d1241d0f Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:09:05 +0200 Subject: [PATCH 4/5] Update --- .../Linux/LinuxUtilizationProvider.cs | 56 ++++++++++++------- .../ResourceHealthCheckExtensionsTests.cs | 1 + .../Linux/AcceptanceTest.cs | 24 ++------ .../Linux/LinuxUtilizationProviderTests.cs | 15 ++--- 4 files changed, 48 insertions(+), 48 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs index fa7398b1d0b..0c3d4124e17 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.Metrics; -using System.Linq; using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -17,6 +16,7 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider { private const double One = 1.0; private const long Hundred = 100L; + private const double NanosecondsInSecond = 1_000_000_000; private readonly object _cpuLocker = new(); private readonly object _memoryLocker = new(); @@ -82,14 +82,19 @@ public LinuxUtilizationProvider(IOptions options, ILi (_previousCgroupCpuTime, _previousCgroupCpuPeriodCounter) = _parser.GetCgroupCpuUsageInNanosecondsAndCpuPeriodsV2(); _ = meter.CreateObservableGauge( - ResourceUtilizationInstruments.ContainerCpuLimitUtilization, - () => GetMeasurementWithRetry(() => CpuUtilizationLimit(cpuLimit)), - "1"); + name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, + observeValues: () => GetMeasurementWithRetry(() => CpuUtilizationLimit(cpuLimit)), + unit: "1"); _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ContainerCpuRequestUtilization, observeValues: () => GetMeasurementWithRetry(() => CpuUtilizationRequest(cpuRequest)), unit: "1"); + + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ContainerCpuTime, + observeValues: GetCpuTime, + unit: "1"); } else { @@ -111,12 +116,12 @@ public LinuxUtilizationProvider(IOptions options, ILi _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, - observeValues: () => GetMeasurementWithRetry(() => MemoryUtilization()), + observeValues: () => GetMeasurementWithRetry(MemoryUtilization), unit: "1"); _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ProcessMemoryUtilization, - observeValues: () => GetMeasurementWithRetry(() => MemoryUtilization()), + observeValues: () => GetMeasurementWithRetry(MemoryUtilization), unit: "1"); // cpuRequest is a CPU request (aka guaranteed number of CPU units) for pod, for host its 1 core @@ -259,23 +264,32 @@ public Snapshot GetSnapshot() memoryUsageInBytes: memoryUsed); } - private IEnumerable> GetMeasurementWithRetry(Func func) + private Measurement[] GetMeasurementWithRetry(Func func) + { + if (!TryGetValueWithRetry(func, out double value)) + { + return Array.Empty>(); + } + + return new[] { new Measurement(value) }; + } + + private bool TryGetValueWithRetry(Func func, out T value) + where T : struct { + value = default; if (Volatile.Read(ref _measurementsUnavailable) == 1 && _timeProvider.GetUtcNow() - _lastFailure < _retryInterval) { - return Enumerable.Empty>(); + return false; } try { - double result = func(); - if (Volatile.Read(ref _measurementsUnavailable) == 1) - { - _ = Interlocked.Exchange(ref _measurementsUnavailable, 0); - } + value = func(); + _ = Interlocked.CompareExchange(ref _measurementsUnavailable, 0, 1); - return new[] { new Measurement(result) }; + return true; } catch (Exception ex) when ( ex is System.IO.FileNotFoundException || @@ -285,7 +299,7 @@ ex is System.IO.DirectoryNotFoundException || _lastFailure = _timeProvider.GetUtcNow(); _ = Interlocked.Exchange(ref _measurementsUnavailable, 1); - return Enumerable.Empty>(); + return false; } } @@ -296,10 +310,14 @@ ex is System.IO.DirectoryNotFoundException || private IEnumerable> GetCpuTime() { - long hostCpuTime = _parser.GetHostCpuUsageInNanoseconds(); - double cgroupCpuTime = CpuUtilizationWithoutHostDelta(); + if (TryGetValueWithRetry(_parser.GetHostCpuUsageInNanoseconds, out long systemCpuTime)) + { + yield return new Measurement(systemCpuTime / NanosecondsInSecond, [new KeyValuePair("cpu.mode", "system")]); + } - yield return new Measurement(cgroupCpuTime / NanosecondsInSecond, [new KeyValuePair("cpu.mode", "user")]); - yield return new Measurement(hostCpuTime / NanosecondsInSecond, [new KeyValuePair("cpu.mode", "system")]); + if (TryGetValueWithRetry(CpuUtilizationV2, out double userCpuTime)) + { + yield return new Measurement(userCpuTime, [new KeyValuePair("cpu.mode", "user")]); + } } } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckExtensionsTests.cs index 16ec64fa003..7d9b347a59d 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckExtensionsTests.cs @@ -500,6 +500,7 @@ public async Task TestCpuAndMemoryChecks_WithMetrics( accountingInfoAfter1Ms.TotalUserTime = (long)(utilization * 100); jobHandleMock.SetupSequence(j => j.GetBasicAccountingInfo()) .Returns(() => initialAccountingInfo) // this is called from the WindowsContainerSnapshotProvider's constructor + .Returns(() => initialAccountingInfo) // this is called from the WindowsContainerSnapshotProvider's GetCpuTime method .Returns(() => accountingInfoAfter1Ms); // this is called from the WindowsContainerSnapshotProvider's CpuPercentage method using var meter = new Meter("Microsoft.Extensions.Diagnostics.ResourceMonitoring"); diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs index 6274980946d..4f273f179aa 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs @@ -249,8 +249,6 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou Assert.Equal(0, utilization.CpuUsedPercentage); Assert.Equal(100, utilization.MemoryUsedPercentage); Assert.True(double.IsNaN(cpuFromGauge)); - Assert.Equal(0.000102312, cpuUserTime); - Assert.Equal(0.8, cpuKernelTime); // gauge multiplied by 100 because gauges are in range [0, 1], and utilization is in range [0, 100] Assert.Equal(utilization.MemoryUsedPercentage, memoryFromGauge * 100); @@ -269,8 +267,6 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou Assert.Equal(1, utilization.CpuUsedPercentage); Assert.Equal(50, utilization.MemoryUsedPercentage); Assert.Equal(0.5, cpuLimitFromGauge * 100); - Assert.Equal(0.000112312, cpuUserTime); - Assert.Equal(0.81, cpuKernelTime); Assert.Equal(utilization.CpuUsedPercentage, cpuRequestFromGauge * 100); Assert.Equal(utilization.MemoryUsedPercentage, memoryLimitFromGauge * 100); Assert.Equal(utilization.CpuUsedPercentage, cpuFromGauge * 100); @@ -360,8 +356,6 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou Assert.Equal(1, roundedCpuUsedPercentage); Assert.Equal(50, utilization.MemoryUsedPercentage); Assert.Equal(0.5, cpuLimitFromGauge * 100); - Assert.Equal(0.000112, cpuUserTime); - Assert.Equal(0.81, cpuKernelTime); Assert.Equal(roundedCpuUsedPercentage, Math.Round(cpuRequestFromGauge * 100)); Assert.Equal(utilization.MemoryUsedPercentage, memoryLimitFromGauge * 100); Assert.Equal(roundedCpuUsedPercentage, Math.Round(cpuFromGauge * 100)); @@ -373,7 +367,7 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou [ConditionalFact] [CombinatorialData] [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")] - public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgroupsv2_v2_Using_NrPeriods() + public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgroupsv2_Using_LinuxCalculationV2() { var fileSystem = new HardcodedValueFileSystem(new Dictionary { @@ -396,7 +390,6 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou var cpuRequestFromGauge = 0.0d; var memoryFromGauge = 0.0d; var memoryLimitFromGauge = 0.0d; - using var e = new ManualResetEventSlim(); object? meterScope = null; listener.InstrumentPublished = (Instrument instrument, MeterListener meterListener) @@ -405,13 +398,12 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou => OnMeasurementReceived(m, f, tags, ref cpuUserTime, ref cpuKernelTime, ref cpuFromGauge, ref cpuLimitFromGauge, ref cpuRequestFromGauge, ref memoryFromGauge, ref memoryLimitFromGauge)); listener.Start(); - using var host = FakeHost.CreateBuilder() + using IHost host = FakeHost.CreateBuilder() .ConfigureServices(x => x.AddLogging() .AddSingleton(clock) .AddSingleton(new FakeUserHz(100)) .AddSingleton(fileSystem) - .AddSingleton(new GenericPublisher(_ => e.Set())) .AddResourceMonitoring(x => x.ConfigureMonitor(options => { options.UseLinuxCalculationV2 = true; @@ -420,15 +412,11 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou .Build(); meterScope = host.Services.GetRequiredService(); - var tracker = host.Services.GetService(); - Assert.NotNull(tracker); _ = host.RunAsync(); listener.RecordObservableInstruments(); - var utilization = tracker.GetUtilization(TimeSpan.FromSeconds(5)); - fileSystem.ReplaceFileContent(new FileInfo("/proc/stat"), "cpu 11 10 10 10 10 10 10 10 10 10"); fileSystem.ReplaceFileContent(new FileInfo("/sys/fs/cgroup/fakeslice/cpu.stat"), "usage_usec 1120000\nnr_periods 56"); fileSystem.ReplaceFileContent(new FileInfo("/sys/fs/cgroup/memory.current"), "524298"); @@ -437,14 +425,10 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou clock.Advance(TimeSpan.FromSeconds(6)); listener.RecordObservableInstruments(); - e.Wait(); - - utilization = tracker.GetUtilization(TimeSpan.FromSeconds(5)); - - var roundedCpuUsedPercentage = Math.Round(utilization.CpuUsedPercentage, 1); - Assert.Equal(42, Math.Round(cpuLimitFromGauge * 100)); Assert.Equal(83, Math.Round(cpuRequestFromGauge * 100)); + Assert.Equal(167, Math.Round(cpuUserTime * 100)); + Assert.Equal(81, Math.Round(cpuKernelTime * 100)); return Task.CompletedTask; } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs index eb0bd9ba6f2..b34dfb1c258 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs @@ -75,7 +75,7 @@ public void Provider_Registers_Instruments() listener.Start(); listener.RecordObservableInstruments(); - Assert.Equal(7, samples.Count); + Assert.Equal(5, samples.Count); Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization); Assert.True(double.IsNaN(samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization).value)); @@ -91,9 +91,6 @@ public void Provider_Registers_Instruments() Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization); Assert.Equal(0.5, samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization).value); - - Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuTime && Math.Abs(x.value - (50.0 / 1_000_000_000)) < 0.00001); - Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuTime && Math.Abs(x.value - 0.8) < 0.00001); } [ConditionalFact] @@ -147,7 +144,7 @@ public void Provider_Registers_Instruments_CgroupV2() listener.Start(); listener.RecordObservableInstruments(); - Assert.Equal(7, samples.Count); + Assert.Equal(5, samples.Count); Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization); Assert.True(double.IsNaN(samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization).value)); @@ -163,9 +160,6 @@ public void Provider_Registers_Instruments_CgroupV2() Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization); Assert.Equal(1, samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization).value); - - Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuTime && Math.Abs(x.value - (102312.0 / 1_000_000)) < 0.00001); - Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuTime && Math.Abs(x.value - 0.8) < 0.00001); } [Fact] @@ -265,7 +259,7 @@ public void Provider_Registers_Instruments_CgroupV2_WithoutHostCpu() listener.Start(); listener.RecordObservableInstruments(); - Assert.Equal(4, samples.Count); + Assert.Equal(6, samples.Count); Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization); Assert.True(double.IsNaN(samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization).value)); @@ -273,6 +267,9 @@ public void Provider_Registers_Instruments_CgroupV2_WithoutHostCpu() Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuRequestUtilization); Assert.True(double.IsNaN(samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerCpuRequestUtilization).value)); + Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuTime); + Assert.All(samples.Where(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerCpuTime), item => double.IsNaN(item.value)); + Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerMemoryLimitUtilization); Assert.Equal(1, samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerMemoryLimitUtilization).value); From 0d93a6c201b745dcadfef5f0ead77a269dfe6478 Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 Date: Fri, 4 Jul 2025 13:44:08 +0200 Subject: [PATCH 5/5] PR Comments --- .../Linux/AcceptanceTest.cs | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs index 4f273f179aa..0221efc27c9 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs @@ -395,7 +395,16 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou listener.InstrumentPublished = (Instrument instrument, MeterListener meterListener) => OnInstrumentPublished(instrument, meterListener, meterScope); listener.SetMeasurementEventCallback((m, f, tags, _) - => OnMeasurementReceived(m, f, tags, ref cpuUserTime, ref cpuKernelTime, ref cpuFromGauge, ref cpuLimitFromGauge, ref cpuRequestFromGauge, ref memoryFromGauge, ref memoryLimitFromGauge)); + => OnMeasurementReceived(m, + f, + tags, + ref cpuUserTime, + ref cpuKernelTime, + ref cpuFromGauge, + ref cpuLimitFromGauge, + ref cpuRequestFromGauge, + ref memoryFromGauge, + ref memoryLimitFromGauge)); listener.Start(); using IHost host = FakeHost.CreateBuilder() @@ -454,10 +463,16 @@ private static void OnInstrumentPublished(Instrument instrument, MeterListener m } #pragma warning disable S107 // Methods should not have too many parameters - private static void OnMeasurementReceived( - Instrument instrument, double value, ReadOnlySpan> tags, - ref double cpuUserTime, ref double cpuKernelTime, ref double cpuFromGauge, ref double cpuLimitFromGauge, - ref double cpuRequestFromGauge, ref double memoryFromGauge, ref double memoryLimitFromGauge) + private static void OnMeasurementReceived(Instrument instrument, + double value, + ReadOnlySpan> tags, + ref double cpuUserTime, + ref double cpuKernelTime, + ref double cpuFromGauge, + ref double cpuLimitFromGauge, + ref double cpuRequestFromGauge, + ref double memoryFromGauge, + ref double memoryLimitFromGauge) #pragma warning restore S107 // Methods should not have too many parameters { if (instrument.Name == ResourceUtilizationInstruments.ProcessCpuUtilization)