Skip to content

Commit dccb999

Browse files
authored
Add Disk IO time metric for Windows in ResourceMonitoring (#6338)
1 parent 9652f79 commit dccb999

File tree

10 files changed

+226
-24
lines changed

10 files changed

+226
-24
lines changed

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Network/LinuxNetworkUtilizationParser.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ internal sealed class LinuxNetworkUtilizationParser
1818
private static readonly ObjectPool<BufferWriter<char>> _sharedBufferWriterPool = BufferWriterPool.CreateBufferWriterPool<char>();
1919

2020
/// <remarks>
21-
/// File that provide information about currently active TCP_IPv4 connections.
21+
/// File that provides information about currently active TCP_IPv4 connections.
2222
/// </remarks>
2323
private static readonly FileInfo _tcp = new("/proc/net/tcp");
2424

2525
/// <remarks>
26-
/// File that provide information about currently active TCP_IPv6 connections.
26+
/// File that provides information about currently active TCP_IPv6 connections.
2727
/// </remarks>
2828
private static readonly FileInfo _tcp6 = new("/proc/net/tcp6");
2929

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Disk/WindowsDiskIoRatePerfCounter.cs

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ internal sealed class WindowsDiskIoRatePerfCounter
1717
private readonly string _categoryName;
1818
private readonly string _counterName;
1919
private readonly string[] _instanceNames;
20-
private long _lastTimestamp;
20+
private long _lastTimeTicks;
2121

2222
internal WindowsDiskIoRatePerfCounter(
2323
IPerformanceCounterFactory performanceCounterFactory,
@@ -43,15 +43,9 @@ internal void InitializeDiskCounters()
4343
{
4444
foreach (string instanceName in _instanceNames)
4545
{
46-
// Skip the total instance
47-
if (instanceName.Equals("_Total", StringComparison.OrdinalIgnoreCase))
48-
{
49-
continue;
50-
}
51-
5246
// Create counters for each disk
5347
_counters.Add(_performanceCounterFactory.Create(_categoryName, _counterName, instanceName));
54-
TotalCountDict.Add(instanceName, 0);
48+
TotalCountDict[instanceName] = 0L;
5549
}
5650

5751
// Initialize the counters to get the first value
@@ -60,24 +54,25 @@ internal void InitializeDiskCounters()
6054
_ = counter.NextValue();
6155
}
6256

63-
_lastTimestamp = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds();
57+
_lastTimeTicks = _timeProvider.GetUtcNow().Ticks;
6458
}
6559

6660
internal void UpdateDiskCounters()
6761
{
68-
long currentTimestamp = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds();
69-
double elapsedSeconds = (currentTimestamp - _lastTimestamp) / 1000.0; // Convert to seconds
62+
long currentTimeTicks = _timeProvider.GetUtcNow().Ticks;
63+
long elapsedTimeTicks = currentTimeTicks - _lastTimeTicks;
7064

7165
// For the kind of "rate" perf counters, this algorithm calculates the total value over a time interval
7266
// by multiplying the per-second rate (e.g., Disk Bytes/sec) by the time interval between two samples.
7367
// This effectively reverses the per-second rate calculation to a total amount (e.g., total bytes transferred) during that period.
68+
// See https://learn.microsoft.com/zh-cn/archive/blogs/askcore/windows-performance-monitor-disk-counters-explained#windows-performance-monitor-disk-counters-explained
7469
foreach (IPerformanceCounter counter in _counters)
7570
{
7671
// total value = per-second rate * elapsed seconds
77-
double value = counter.NextValue() * elapsedSeconds;
78-
TotalCountDict[counter.InstanceName] += (long)value;
72+
double value = counter.NextValue() * (double)elapsedTimeTicks;
73+
TotalCountDict[counter.InstanceName] += (long)value / TimeSpan.TicksPerSecond;
7974
}
8075

81-
_lastTimestamp = currentTimestamp;
76+
_lastTimeTicks = currentTimeTicks;
8277
}
8378
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Concurrent;
6+
using System.Collections.Generic;
7+
8+
namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Disk;
9+
10+
internal sealed class WindowsDiskIoTimePerfCounter
11+
{
12+
private readonly List<IPerformanceCounter> _counters = [];
13+
private readonly IPerformanceCounterFactory _performanceCounterFactory;
14+
private readonly TimeProvider _timeProvider;
15+
private readonly string _categoryName;
16+
private readonly string _counterName;
17+
private readonly string[] _instanceNames;
18+
private long _lastTimeTicks;
19+
20+
internal WindowsDiskIoTimePerfCounter(
21+
IPerformanceCounterFactory performanceCounterFactory,
22+
TimeProvider timeProvider,
23+
string categoryName,
24+
string counterName,
25+
string[] instanceNames)
26+
{
27+
_performanceCounterFactory = performanceCounterFactory;
28+
_timeProvider = timeProvider;
29+
_categoryName = categoryName;
30+
_counterName = counterName;
31+
_instanceNames = instanceNames;
32+
}
33+
34+
/// <summary>
35+
/// Gets the disk time measurements.
36+
/// Key: Disk name, Value: Real elapsed time used in busy state.
37+
/// </summary>
38+
internal IDictionary<string, double> TotalSeconds { get; } = new ConcurrentDictionary<string, double>();
39+
40+
internal void InitializeDiskCounters()
41+
{
42+
foreach (string instanceName in _instanceNames)
43+
{
44+
// Create counters for each disk
45+
_counters.Add(_performanceCounterFactory.Create(_categoryName, _counterName, instanceName));
46+
TotalSeconds[instanceName] = 0f;
47+
}
48+
49+
// Initialize the counters to get the first value
50+
foreach (IPerformanceCounter counter in _counters)
51+
{
52+
_ = counter.NextValue();
53+
}
54+
55+
_lastTimeTicks = _timeProvider.GetUtcNow().Ticks;
56+
}
57+
58+
internal void UpdateDiskCounters()
59+
{
60+
long currentTimeTicks = _timeProvider.GetUtcNow().Ticks;
61+
long elapsedTimeTicks = currentTimeTicks - _lastTimeTicks;
62+
63+
// The real elapsed time ("wall clock") used in the I/O path (time from operations running in parallel are not counted).
64+
// Measured as the complement of "Disk\% Idle Time" performance counter: uptime * (100 - "Disk\% Idle Time") / 100
65+
// See https://opentelemetry.io/docs/specs/semconv/system/system-metrics/#metric-systemdiskio_time
66+
foreach (IPerformanceCounter counter in _counters)
67+
{
68+
// io busy time = (1 - (% idle time / 100)) * elapsed seconds
69+
float idleTimePercentage = Math.Min(counter.NextValue(), 100f);
70+
double busyTimeTicks = (1 - (idleTimePercentage / 100f)) * (double)elapsedTimeTicks;
71+
TotalSeconds[counter.InstanceName] += busyTimeTicks / TimeSpan.TicksPerSecond;
72+
}
73+
74+
_lastTimeTicks = currentTimeTicks;
75+
}
76+
}

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Disk/WindowsDiskMetrics.cs

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
using System.Collections.Generic;
66
using System.Diagnostics;
77
using System.Diagnostics.Metrics;
8+
using System.Linq;
89
using System.Runtime.Versioning;
10+
using Microsoft.Extensions.Logging;
11+
using Microsoft.Extensions.Logging.Abstractions;
912
using Microsoft.Extensions.Options;
1013
using Microsoft.Shared.Instruments;
1114

@@ -19,14 +22,18 @@ internal sealed class WindowsDiskMetrics
1922

2023
private static readonly KeyValuePair<string, object?> _directionReadTag = new(DirectionKey, "read");
2124
private static readonly KeyValuePair<string, object?> _directionWriteTag = new(DirectionKey, "write");
25+
private readonly ILogger<WindowsDiskMetrics> _logger;
2226
private readonly Dictionary<string, WindowsDiskIoRatePerfCounter> _diskIoRateCounters = new();
27+
private WindowsDiskIoTimePerfCounter? _diskIoTimePerfCounter;
2328

2429
public WindowsDiskMetrics(
30+
ILogger<WindowsDiskMetrics>? logger,
2531
IMeterFactory meterFactory,
2632
IPerformanceCounterFactory performanceCounterFactory,
2733
TimeProvider timeProvider,
2834
IOptions<ResourceMonitoringOptions> options)
2935
{
36+
_logger = logger ?? NullLogger<WindowsDiskMetrics>.Instance;
3037
if (!options.Value.EnableDiskIoMetrics)
3138
{
3239
return;
@@ -42,39 +49,69 @@ public WindowsDiskMetrics(
4249
InitializeDiskCounters(performanceCounterFactory, timeProvider);
4350

4451
// The metric is aligned with
45-
// https://github.com/open-telemetry/semantic-conventions/blob/main/docs/system/system-metrics.md#metric-systemdiskio
52+
// https://opentelemetry.io/docs/specs/semconv/system/system-metrics/#metric-systemdiskio
4653
_ = meter.CreateObservableCounter(
4754
ResourceUtilizationInstruments.SystemDiskIo,
4855
GetDiskIoMeasurements,
4956
unit: "By",
5057
description: "Disk bytes transferred");
5158

5259
// The metric is aligned with
53-
// https://github.com/open-telemetry/semantic-conventions/blob/main/docs/system/system-metrics.md#metric-systemdiskoperations
60+
// https://opentelemetry.io/docs/specs/semconv/system/system-metrics/#metric-systemdiskoperations
5461
_ = meter.CreateObservableCounter(
5562
ResourceUtilizationInstruments.SystemDiskOperations,
5663
GetDiskOperationMeasurements,
5764
unit: "{operation}",
5865
description: "Disk operations");
66+
67+
// The metric is aligned with
68+
// https://opentelemetry.io/docs/specs/semconv/system/system-metrics/#metric-systemdiskio_time
69+
_ = meter.CreateObservableCounter(
70+
ResourceUtilizationInstruments.SystemDiskIoTime,
71+
GetDiskIoTimeMeasurements,
72+
unit: "s",
73+
description: "Time disk spent activated");
5974
}
6075

6176
private void InitializeDiskCounters(IPerformanceCounterFactory performanceCounterFactory, TimeProvider timeProvider)
6277
{
6378
const string DiskCategoryName = "LogicalDisk";
64-
string[] instanceNames = performanceCounterFactory.GetCategoryInstances(DiskCategoryName);
79+
string[] instanceNames = performanceCounterFactory.GetCategoryInstances(DiskCategoryName)
80+
.Where(instanceName => !instanceName.Equals("_Total", StringComparison.OrdinalIgnoreCase))
81+
.ToArray();
6582
if (instanceNames.Length == 0)
6683
{
6784
return;
6885
}
6986

70-
List<string> diskIoRatePerformanceCounters =
87+
// Initialize disk performance counters for "system.disk.io_time" metric
88+
try
89+
{
90+
var ioTimePerfCounter = new WindowsDiskIoTimePerfCounter(
91+
performanceCounterFactory,
92+
timeProvider,
93+
DiskCategoryName,
94+
WindowsDiskPerfCounterNames.DiskIdleTimeCounter,
95+
instanceNames);
96+
ioTimePerfCounter.InitializeDiskCounters();
97+
_diskIoTimePerfCounter = ioTimePerfCounter;
98+
}
99+
#pragma warning disable CA1031
100+
catch (Exception ex)
101+
#pragma warning restore CA1031
102+
{
103+
Log.DiskIoPerfCounterException(_logger, WindowsDiskPerfCounterNames.DiskIdleTimeCounter, ex.Message);
104+
}
105+
106+
// Initialize disk performance counters for "system.disk.io" and "system.disk.operations" metrics
107+
List<string> diskIoRatePerfCounterNames =
71108
[
72109
WindowsDiskPerfCounterNames.DiskWriteBytesCounter,
73110
WindowsDiskPerfCounterNames.DiskReadBytesCounter,
74111
WindowsDiskPerfCounterNames.DiskWritesCounter,
75112
WindowsDiskPerfCounterNames.DiskReadsCounter,
76113
];
77-
foreach (string counterName in diskIoRatePerformanceCounters)
114+
foreach (string counterName in diskIoRatePerfCounterNames)
78115
{
79116
try
80117
{
@@ -91,7 +128,7 @@ private void InitializeDiskCounters(IPerformanceCounterFactory performanceCounte
91128
catch (Exception ex)
92129
#pragma warning restore CA1031
93130
{
94-
Debug.WriteLine("Error initializing disk performance counter: " + ex.Message);
131+
Log.DiskIoPerfCounterException(_logger, counterName, ex.Message);
95132
}
96133
}
97134
}
@@ -145,4 +182,19 @@ private IEnumerable<Measurement<long>> GetDiskOperationMeasurements()
145182

146183
return measurements;
147184
}
185+
186+
private IEnumerable<Measurement<double>> GetDiskIoTimeMeasurements()
187+
{
188+
List<Measurement<double>> measurements = [];
189+
if (_diskIoTimePerfCounter != null)
190+
{
191+
_diskIoTimePerfCounter.UpdateDiskCounters();
192+
foreach (KeyValuePair<string, double> pair in _diskIoTimePerfCounter.TotalSeconds)
193+
{
194+
measurements.Add(new Measurement<double>(pair.Value, new TagList { new(DeviceKey, pair.Key) }));
195+
}
196+
}
197+
198+
return measurements;
199+
}
148200
}

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Disk/WindowsDiskPerfCounterNames.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ internal static class WindowsDiskPerfCounterNames
99
internal const string DiskReadBytesCounter = "Disk Read Bytes/sec";
1010
internal const string DiskWritesCounter = "Disk Writes/sec";
1111
internal const string DiskReadsCounter = "Disk Reads/sec";
12+
internal const string DiskIdleTimeCounter = "% Idle Time";
1213
}

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Log.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,8 @@ public static partial void CpuContainerUsageData(ILogger logger,
4545
[LoggerMessage(6, LogLevel.Debug,
4646
"System resources information: CpuLimit = {cpuLimit}, CpuRequest = {cpuRequest}, MemoryLimit = {memoryLimit}, MemoryRequest = {memoryRequest}.")]
4747
public static partial void SystemResourcesInfo(ILogger logger, double cpuLimit, double cpuRequest, ulong memoryLimit, ulong memoryRequest);
48+
49+
[LoggerMessage(7, LogLevel.Warning,
50+
"Error initializing disk io perf counter: PerfCounter={counterName}, Error={errorMessage}")]
51+
public static partial void DiskIoPerfCounterException(ILogger logger, string counterName, string errorMessage);
4852
}

src/Shared/Instruments/ResourceUtilizationInstruments.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ internal static class ResourceUtilizationInstruments
6666
/// </remarks>
6767
public const string SystemDiskIo = "system.disk.io";
6868

69+
/// <summary>
70+
/// The name of an instrument to retrieve the time disk spent activated.
71+
/// </summary>
72+
/// <remarks>
73+
/// The type of an instrument is <see cref="System.Diagnostics.Metrics.ObservableCounter{T}"/>.
74+
/// </remarks>
75+
public const string SystemDiskIoTime = "system.disk.io_time";
76+
6977
/// <summary>
7078
/// The name of an instrument to retrieve disk operations.
7179
/// </summary>

test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/Disk/WindowsDiskIoRatePerfCounterTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public void DiskReadsPerfCounter_Per60Seconds()
2929
fakeTimeProvider,
3030
CategoryName,
3131
CounterName,
32-
instanceNames: ["C:", "D:", "_Total"]);
32+
instanceNames: ["C:", "D:"]);
3333

3434
// Set up
3535
var counterC = new FakePerformanceCounter("C:", [0, 1, 1.5f, 2, 2.5f]);
@@ -75,7 +75,7 @@ public void DiskWriteBytesPerfCounter_Per30Seconds()
7575
fakeTimeProvider,
7676
CategoryName,
7777
counterName: CounterName,
78-
instanceNames: ["C:", "D:", "_Total"]);
78+
instanceNames: ["C:", "D:"]);
7979

8080
// Set up
8181
var counterC = new FakePerformanceCounter("C:", [0, 100, 150.5f, 20, 3.1416f]);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Runtime.Versioning;
6+
using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Test;
7+
using Microsoft.Extensions.Time.Testing;
8+
using Microsoft.TestUtilities;
9+
using Moq;
10+
using Xunit;
11+
12+
namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Disk.Test;
13+
14+
[SupportedOSPlatform("windows")]
15+
[OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "Windows specific.")]
16+
public class WindowsDiskIoTimePerfCounterTests
17+
{
18+
private const string CategoryName = "LogicalDisk";
19+
20+
[ConditionalFact]
21+
public void DiskReadsPerfCounter_Per60Seconds()
22+
{
23+
const string CounterName = WindowsDiskPerfCounterNames.DiskReadsCounter;
24+
var performanceCounterFactory = new Mock<IPerformanceCounterFactory>();
25+
var fakeTimeProvider = new FakeTimeProvider { AutoAdvanceAmount = TimeSpan.FromSeconds(60) };
26+
27+
var perfCounters = new WindowsDiskIoTimePerfCounter(
28+
performanceCounterFactory.Object,
29+
fakeTimeProvider,
30+
CategoryName,
31+
CounterName,
32+
instanceNames: ["C:", "D:"]);
33+
34+
// Set up
35+
var counterC = new FakePerformanceCounter("C:", [100, 100, 0, 99.5f]);
36+
var counterD = new FakePerformanceCounter("D:", [100, 99.9f, 88.8f, 66.6f]);
37+
performanceCounterFactory.Setup(x => x.Create(CategoryName, CounterName, "C:")).Returns(counterC);
38+
performanceCounterFactory.Setup(x => x.Create(CategoryName, CounterName, "D:")).Returns(counterD);
39+
40+
// Initialize the counters
41+
perfCounters.InitializeDiskCounters();
42+
Assert.Equal(2, perfCounters.TotalSeconds.Count);
43+
Assert.Equal(0, perfCounters.TotalSeconds["C:"]);
44+
Assert.Equal(0, perfCounters.TotalSeconds["D:"]);
45+
46+
// Simulate the first tick
47+
perfCounters.UpdateDiskCounters();
48+
Assert.Equal(0, perfCounters.TotalSeconds["C:"], precision: 2); // (100-100)% * 60 = 0
49+
Assert.Equal(0.06, perfCounters.TotalSeconds["D:"], precision: 2); // (100-99.9)% * 60 = 0.06
50+
51+
// Simulate the second tick
52+
perfCounters.UpdateDiskCounters();
53+
Assert.Equal(60, perfCounters.TotalSeconds["C:"], precision: 2); // 0 + (100-0)% * 60 = 60
54+
Assert.Equal(6.78, perfCounters.TotalSeconds["D:"], precision: 2); // 0.06 + (100-88.8)% * 60 = 6.78
55+
56+
// Simulate the third tick
57+
perfCounters.UpdateDiskCounters();
58+
Assert.Equal(60.3, perfCounters.TotalSeconds["C:"], precision: 2); // 60 + (100-99.5)% * 60 = 60.3
59+
Assert.Equal(26.82, perfCounters.TotalSeconds["D:"], precision: 2); // 6.78 + (100-66.6)% * 60 = 6.78 + 20.04 = 26.82
60+
}
61+
}

0 commit comments

Comments
 (0)