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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,9 @@ internal void InitializeDiskCounters()
{
foreach (string instanceName in _instanceNames)
{
// Skip the total instance
if (instanceName.Equals("_Total", StringComparison.OrdinalIgnoreCase))
{
continue;
}

// Create counters for each disk
_counters.Add(_performanceCounterFactory.Create(_categoryName, _counterName, instanceName));
TotalCountDict.Add(instanceName, 0);
TotalCountDict[instanceName] = 0L;
}

// Initialize the counters to get the first value
Expand All @@ -71,6 +65,7 @@ internal void UpdateDiskCounters()
// For the kind of "rate" perf counters, this algorithm calculates the total value over a time interval
// by multiplying the per-second rate (e.g., Disk Bytes/sec) by the time interval between two samples.
// This effectively reverses the per-second rate calculation to a total amount (e.g., total bytes transferred) during that period.
// See https://learn.microsoft.com/zh-cn/archive/blogs/askcore/windows-performance-monitor-disk-counters-explained#windows-performance-monitor-disk-counters-explained
foreach (IPerformanceCounter counter in _counters)
{
// total value = per-second rate * elapsed seconds
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// 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.Concurrent;
using System.Collections.Generic;

namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Disk;

internal sealed class WindowsDiskIoTimePerfCounter
{
private readonly List<IPerformanceCounter> _counters = [];
private readonly IPerformanceCounterFactory _performanceCounterFactory;
private readonly TimeProvider _timeProvider;
private readonly string _categoryName;
private readonly string _counterName;
private readonly string[] _instanceNames;
private long _lastTimestamp;

internal WindowsDiskIoTimePerfCounter(
IPerformanceCounterFactory performanceCounterFactory,
TimeProvider timeProvider,
string categoryName,
string counterName,
string[] instanceNames)
{
_performanceCounterFactory = performanceCounterFactory;
_timeProvider = timeProvider;
_categoryName = categoryName;
_counterName = counterName;
_instanceNames = instanceNames;
}

/// <summary>
/// Gets the disk time measurements.
/// Key: Disk name, Value: Real elapsed time used in busy state.
/// </summary>
internal IDictionary<string, double> TotalSeconds { get; } = new ConcurrentDictionary<string, double>();

internal void InitializeDiskCounters()
{
foreach (string instanceName in _instanceNames)
{
// Create counters for each disk
_counters.Add(_performanceCounterFactory.Create(_categoryName, _counterName, instanceName));
TotalSeconds[instanceName] = 0f;
}

// Initialize the counters to get the first value
foreach (IPerformanceCounter counter in _counters)
{
_ = counter.NextValue();
}

_lastTimestamp = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds();
}

internal void UpdateDiskCounters()
{
long currentTimestamp = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds();
double elapsedSeconds = (currentTimestamp - _lastTimestamp) / 1000.0; // Convert to seconds

// The real elapsed time ("wall clock") used in the I/O path (time from operations running in parallel are not counted).
// Measured as the complement of "Disk\% Idle Time" performance counter: uptime * (100 - "Disk\% Idle Time") / 100
// See https://opentelemetry.io/docs/specs/semconv/system/system-metrics/#metric-systemdiskio_time
foreach (IPerformanceCounter counter in _counters)
{
// io busy time = (1 - (% idle time / 100)) * elapsed seconds
float idleTimePercentage = Math.Min(counter.NextValue(), 100f);
double busyTimeSeconds = (1 - (idleTimePercentage / 100f)) * elapsedSeconds;
TotalSeconds[counter.InstanceName] += busyTimeSeconds;
}

_lastTimestamp = currentTimestamp;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Linq;
using System.Runtime.Versioning;
using Microsoft.Extensions.Options;
using Microsoft.Shared.Instruments;
Expand All @@ -20,6 +21,7 @@ internal sealed class WindowsDiskMetrics
private static readonly KeyValuePair<string, object?> _directionReadTag = new(DirectionKey, "read");
private static readonly KeyValuePair<string, object?> _directionWriteTag = new(DirectionKey, "write");
private readonly Dictionary<string, WindowsDiskIoRatePerfCounter> _diskIoRateCounters = new();
private WindowsDiskIoTimePerfCounter? _diskIoTimePerfCounter;

public WindowsDiskMetrics(
IMeterFactory meterFactory,
Expand All @@ -42,39 +44,69 @@ public WindowsDiskMetrics(
InitializeDiskCounters(performanceCounterFactory, timeProvider);

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

// The metric is aligned with
// https://github.com/open-telemetry/semantic-conventions/blob/main/docs/system/system-metrics.md#metric-systemdiskoperations
// https://opentelemetry.io/docs/specs/semconv/system/system-metrics/#metric-systemdiskoperations
_ = meter.CreateObservableCounter(
ResourceUtilizationInstruments.SystemDiskOperations,
GetDiskOperationMeasurements,
unit: "{operation}",
description: "Disk operations");

// The metric is aligned with
// https://opentelemetry.io/docs/specs/semconv/system/system-metrics/#metric-systemdiskio_time
_ = meter.CreateObservableCounter(
ResourceUtilizationInstruments.SystemDiskIoTime,
GetDiskIoTimeMeasurements,
unit: "s",
description: "Time disk spent activated");
}

private void InitializeDiskCounters(IPerformanceCounterFactory performanceCounterFactory, TimeProvider timeProvider)
{
const string DiskCategoryName = "LogicalDisk";
string[] instanceNames = performanceCounterFactory.GetCategoryInstances(DiskCategoryName);
string[] instanceNames = performanceCounterFactory.GetCategoryInstances(DiskCategoryName)
.Where(instanceName => !instanceName.Equals("_Total", StringComparison.OrdinalIgnoreCase))
.ToArray();
if (instanceNames.Length == 0)
{
return;
}

List<string> diskIoRatePerformanceCounters =
// Initialize disk performance counters for "system.disk.io_time" metric
try
{
var ioTimePerfCounter = new WindowsDiskIoTimePerfCounter(
performanceCounterFactory,
timeProvider,
DiskCategoryName,
WindowsDiskPerfCounterNames.DiskIdleTimeCounter,
instanceNames);
ioTimePerfCounter.InitializeDiskCounters();
_diskIoTimePerfCounter = ioTimePerfCounter;
}
#pragma warning disable CA1031
catch (Exception ex)
#pragma warning restore CA1031
{
Debug.WriteLine("Error initializing disk io time perf counter: " + ex.Message);
}

// Initialize disk performance counters for "system.disk.io" and "system.disk.operations" metrics
List<string> diskIoRatePerfCounterNames =
[
WindowsDiskPerfCounterNames.DiskWriteBytesCounter,
WindowsDiskPerfCounterNames.DiskReadBytesCounter,
WindowsDiskPerfCounterNames.DiskWritesCounter,
WindowsDiskPerfCounterNames.DiskReadsCounter,
];
foreach (string counterName in diskIoRatePerformanceCounters)
foreach (string counterName in diskIoRatePerfCounterNames)
{
try
{
Expand All @@ -91,7 +123,7 @@ private void InitializeDiskCounters(IPerformanceCounterFactory performanceCounte
catch (Exception ex)
#pragma warning restore CA1031
{
Debug.WriteLine("Error initializing disk performance counter: " + ex.Message);
Debug.WriteLine("Error initializing disk io rate perf counter: " + ex.Message);
}
}
}
Expand Down Expand Up @@ -145,4 +177,19 @@ private IEnumerable<Measurement<long>> GetDiskOperationMeasurements()

return measurements;
}

private IEnumerable<Measurement<double>> GetDiskIoTimeMeasurements()
{
List<Measurement<double>> measurements = [];
if (_diskIoTimePerfCounter != null)
{
_diskIoTimePerfCounter.UpdateDiskCounters();
foreach (KeyValuePair<string, double> pair in _diskIoTimePerfCounter.TotalSeconds)
{
measurements.Add(new Measurement<double>(pair.Value, new TagList { new(DeviceKey, pair.Key) }));
}
}

return measurements;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ internal static class WindowsDiskPerfCounterNames
internal const string DiskReadBytesCounter = "Disk Read Bytes/sec";
internal const string DiskWritesCounter = "Disk Writes/sec";
internal const string DiskReadsCounter = "Disk Reads/sec";
internal const string DiskIdleTimeCounter = "% Idle Time";
}
8 changes: 8 additions & 0 deletions src/Shared/Instruments/ResourceUtilizationInstruments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ internal static class ResourceUtilizationInstruments
/// </remarks>
public const string SystemDiskIo = "system.disk.io";

/// <summary>
/// The name of an instrument to retrieve the time disk spent activated.
/// </summary>
/// <remarks>
/// The type of an instrument is <see cref="System.Diagnostics.Metrics.ObservableCounter{T}"/>.
/// </remarks>
public const string SystemDiskIoTime = "system.disk.io_time";

/// <summary>
/// The name of an instrument to retrieve disk operations.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public void DiskReadsPerfCounter_Per60Seconds()
fakeTimeProvider,
CategoryName,
CounterName,
instanceNames: ["C:", "D:", "_Total"]);
instanceNames: ["C:", "D:"]);

// Set up
var counterC = new FakePerformanceCounter("C:", [0, 1, 1.5f, 2, 2.5f]);
Expand Down Expand Up @@ -75,7 +75,7 @@ public void DiskWriteBytesPerfCounter_Per30Seconds()
fakeTimeProvider,
CategoryName,
counterName: CounterName,
instanceNames: ["C:", "D:", "_Total"]);
instanceNames: ["C:", "D:"]);

// Set up
var counterC = new FakePerformanceCounter("C:", [0, 100, 150.5f, 20, 3.1416f]);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// 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.Runtime.Versioning;
using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Test;
using Microsoft.Extensions.Time.Testing;
using Microsoft.TestUtilities;
using Moq;
using Xunit;

namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Disk.Test;

[SupportedOSPlatform("windows")]
[OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "Windows specific.")]
public class WindowsDiskIoTimePerfCounterTests
{
private const string CategoryName = "LogicalDisk";

[ConditionalFact]
public void DiskReadsPerfCounter_Per60Seconds()
{
const string CounterName = WindowsDiskPerfCounterNames.DiskReadsCounter;
var performanceCounterFactory = new Mock<IPerformanceCounterFactory>();
var fakeTimeProvider = new FakeTimeProvider { AutoAdvanceAmount = TimeSpan.FromSeconds(60) };

var perfCounters = new WindowsDiskIoTimePerfCounter(
performanceCounterFactory.Object,
fakeTimeProvider,
CategoryName,
CounterName,
instanceNames: ["C:", "D:"]);

// Set up
var counterC = new FakePerformanceCounter("C:", [100, 100, 0, 99.5f]);
var counterD = new FakePerformanceCounter("D:", [100, 99.9f, 88.8f, 66.6f]);
performanceCounterFactory.Setup(x => x.Create(CategoryName, CounterName, "C:")).Returns(counterC);
performanceCounterFactory.Setup(x => x.Create(CategoryName, CounterName, "D:")).Returns(counterD);

// Initialize the counters
perfCounters.InitializeDiskCounters();
Assert.Equal(2, perfCounters.TotalSeconds.Count);
Assert.Equal(0, perfCounters.TotalSeconds["C:"]);
Assert.Equal(0, perfCounters.TotalSeconds["D:"]);

// Simulate the first tick
perfCounters.UpdateDiskCounters();
Assert.Equal(0, perfCounters.TotalSeconds["C:"], precision: 2); // (100-100)% * 60 = 0
Assert.Equal(0.06, perfCounters.TotalSeconds["D:"], precision: 2); // (100-99.9)% * 60 = 0.06

// Simulate the second tick
perfCounters.UpdateDiskCounters();
Assert.Equal(60, perfCounters.TotalSeconds["C:"], precision: 2); // 0 + (100-0)% * 60 = 60
Assert.Equal(6.78, perfCounters.TotalSeconds["D:"], precision: 2); // 0.06 + (100-88.8)% * 60 = 6.78

// Simulate the third tick
perfCounters.UpdateDiskCounters();
Assert.Equal(60.3, perfCounters.TotalSeconds["C:"], precision: 2); // 60 + (100-99.5)% * 60 = 60.3
Assert.Equal(26.82, perfCounters.TotalSeconds["D:"], precision: 2); // 6.78 + (100-66.6)% * 60 = 6.78 + 20.04 = 26.82
}
}
Loading