Skip to content

Add Windows disk I/O metrics to ResourceMonitoring #6181

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
555b98e
create an empty file for WindowsDiskMetrics
makazeu Mar 23, 2025
2e8a9be
Implement Windows Disk I/O bytes metric
makazeu Mar 23, 2025
fe0c09b
update
makazeu Mar 23, 2025
664053a
Add SupportedOSPlatform attribute for Windows in disk metrics classes
makazeu Mar 23, 2025
3180f66
update
makazeu Mar 23, 2025
ccb3d27
Fix property name for disk I/O metrics enabling in Windows
makazeu Mar 23, 2025
24e60bd
update
makazeu Mar 23, 2025
f6957fe
Improve error handling in disk performance counter initialization
makazeu Mar 23, 2025
07ceca6
Rename disk performance counter classes
makazeu Mar 23, 2025
9e12912
Implement Disk Operations metric
makazeu Mar 23, 2025
37c9e8a
Style updates
makazeu Mar 24, 2025
8bba5a4
Add PlatformAttributes in LegacySupport
makazeu Mar 24, 2025
878e61b
Add a simple unit test
makazeu Mar 24, 2025
e9985af
Add interfaces for PerformanceCounter
makazeu Mar 24, 2025
6d96e50
update ut
makazeu Mar 24, 2025
6095dd0
Merge branch 'main' into windows-disk-metrics
makazeu Mar 24, 2025
718bc13
Use TimeProvider
makazeu Mar 25, 2025
db2949a
Add TimeProvider as a singleton in ResourceMonitoringServiceCollectio…
makazeu Mar 25, 2025
12edc8a
Add unit tests for PerformanceCounterFactory and PerformanceCounterWr…
makazeu Mar 25, 2025
23a0f3d
update
makazeu Mar 25, 2025
6c108da
Add Unit Tests
makazeu Mar 25, 2025
080ae7d
Refine
makazeu Mar 25, 2025
a3efed7
Refine
makazeu Mar 26, 2025
7053f3f
Refine
makazeu Mar 26, 2025
93ea836
Add WindowsDiskMetricsTests
makazeu Mar 26, 2025
54836ec
Refine
makazeu Mar 26, 2025
a1b620e
Merge branch 'main' into windows-disk-metrics
makazeu Mar 29, 2025
a1de91a
clean
makazeu Mar 29, 2025
a0395c0
Refine
makazeu Mar 29, 2025
c0fbc4d
Merge branch 'main' into windows-disk-metrics
makazeu Apr 24, 2025
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
4 changes: 4 additions & 0 deletions eng/MSBuild/LegacySupport.props
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,8 @@
<ItemGroup Condition="'$(InjectObsoleteAttributeOnLegacy)' == 'true' AND ('$(TargetFramework)' == 'net462' or '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netcoreapp3.1')">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\LegacySupport\ObsoleteAttribute\*.cs" LinkBase="LegacySupport\ObsoleteAttribute" />
</ItemGroup>

<ItemGroup Condition="'$(InjectPlatformAttributesOnLegacy)' == 'true' AND ('$(TargetFramework)' == 'net462' or '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netcoreapp3.1')">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\LegacySupport\PlatformAttributes\*.cs" LinkBase="LegacySupport\PlatformAttributes" />
</ItemGroup>
</Project>
171 changes: 171 additions & 0 deletions src/LegacySupport/PlatformAttributes/PlatformAttributes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#pragma warning disable S1694
#pragma warning disable S3996
#pragma warning disable SA1128
#pragma warning disable SA1402
#pragma warning disable SA1513
#pragma warning disable SA1649

namespace System.Runtime.Versioning
{
/// <summary>
/// Base type for all platform-specific API attributes.
/// </summary>
#pragma warning disable CS3015 // Type has no accessible constructors which use only CLS-compliant types
internal abstract class OSPlatformAttribute : Attribute
#pragma warning restore CS3015
{
private protected OSPlatformAttribute(string platformName)
{
PlatformName = platformName;
}
public string PlatformName { get; }
}

/// <summary>
/// Records the platform that the project targeted.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly,
AllowMultiple = false, Inherited = false)]
internal sealed class TargetPlatformAttribute : OSPlatformAttribute
{
public TargetPlatformAttribute(string platformName) : base(platformName)
{
}
}

/// <summary>
/// Records the operating system (and minimum version) that supports an API. Multiple attributes can be
/// applied to indicate support on multiple operating systems.
/// </summary>
/// <remarks>
/// Callers can apply a <see cref="SupportedOSPlatformAttribute " />
/// or use guards to prevent calls to APIs on unsupported operating systems.
///
/// A given platform should only be specified once.
/// </remarks>
[AttributeUsage(AttributeTargets.Assembly |
AttributeTargets.Class |
AttributeTargets.Constructor |
AttributeTargets.Enum |
AttributeTargets.Event |
AttributeTargets.Field |
AttributeTargets.Interface |
AttributeTargets.Method |
AttributeTargets.Module |
AttributeTargets.Property |
AttributeTargets.Struct,
AllowMultiple = true, Inherited = false)]
internal sealed class SupportedOSPlatformAttribute : OSPlatformAttribute
{
public SupportedOSPlatformAttribute(string platformName) : base(platformName)
{
}
}

/// <summary>
/// Marks APIs that were removed in a given operating system version.
/// </summary>
/// <remarks>
/// Primarily used by OS bindings to indicate APIs that are only available in
/// earlier versions.
/// </remarks>
[AttributeUsage(AttributeTargets.Assembly |
AttributeTargets.Class |
AttributeTargets.Constructor |
AttributeTargets.Enum |
AttributeTargets.Event |
AttributeTargets.Field |
AttributeTargets.Interface |
AttributeTargets.Method |
AttributeTargets.Module |
AttributeTargets.Property |
AttributeTargets.Struct,
AllowMultiple = true, Inherited = false)]
internal sealed class UnsupportedOSPlatformAttribute : OSPlatformAttribute
{
public UnsupportedOSPlatformAttribute(string platformName) : base(platformName)
{
}
public UnsupportedOSPlatformAttribute(string platformName, string? message) : base(platformName)
{
Message = message;
}
public string? Message { get; }
}

/// <summary>
/// Marks APIs that were obsoleted in a given operating system version.
/// </summary>
/// <remarks>
/// Primarily used by OS bindings to indicate APIs that should not be used anymore.
/// </remarks>
[AttributeUsage(AttributeTargets.Assembly |
AttributeTargets.Class |
AttributeTargets.Constructor |
AttributeTargets.Enum |
AttributeTargets.Event |
AttributeTargets.Field |
AttributeTargets.Interface |
AttributeTargets.Method |
AttributeTargets.Module |
AttributeTargets.Property |
AttributeTargets.Struct,
AllowMultiple = true, Inherited = false)]
internal sealed class ObsoletedOSPlatformAttribute : OSPlatformAttribute
{
public ObsoletedOSPlatformAttribute(string platformName) : base(platformName)
{
}
public ObsoletedOSPlatformAttribute(string platformName, string? message) : base(platformName)
{
Message = message;
}
public string? Message { get; }
public string? Url { get; set; }
}

/// <summary>
/// Annotates a custom guard field, property or method with a supported platform name and optional version.
/// Multiple attributes can be applied to indicate guard for multiple supported platforms.
/// </summary>
/// <remarks>
/// Callers can apply a <see cref="SupportedOSPlatformGuardAttribute " /> to a field, property or method
/// and use that field, property or method in a conditional or assert statements in order to safely call platform specific APIs.
///
/// The type of the field or property should be boolean, the method return type should be boolean in order to be used as platform guard.
/// </remarks>
[AttributeUsage(AttributeTargets.Field |
AttributeTargets.Method |
AttributeTargets.Property,
AllowMultiple = true, Inherited = false)]
internal sealed class SupportedOSPlatformGuardAttribute : OSPlatformAttribute
{
public SupportedOSPlatformGuardAttribute(string platformName) : base(platformName)
{
}
}

/// <summary>
/// Annotates the custom guard field, property or method with an unsupported platform name and optional version.
/// Multiple attributes can be applied to indicate guard for multiple unsupported platforms.
/// </summary>
/// <remarks>
/// Callers can apply a <see cref="UnsupportedOSPlatformGuardAttribute " /> to a field, property or method
/// and use that field, property or method in a conditional or assert statements as a guard to safely call APIs unsupported on those platforms.
///
/// The type of the field or property should be boolean, the method return type should be boolean in order to be used as platform guard.
/// </remarks>
[AttributeUsage(AttributeTargets.Field |
AttributeTargets.Method |
AttributeTargets.Property,
AllowMultiple = true, Inherited = false)]
internal sealed class UnsupportedOSPlatformGuardAttribute : OSPlatformAttribute
{
public UnsupportedOSPlatformGuardAttribute(string platformName) : base(platformName)
{
}
}
}
9 changes: 9 additions & 0 deletions src/LegacySupport/PlatformAttributes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Enables use of C# OSPlatform attributes on older frameworks.

To use this source in your project, add the following to your `.csproj` file:

```xml
<PropertyGroup>
<InjectPlatformAttributesOnLegacy>true</InjectPlatformAttributesOnLegacy>
</PropertyGroup>
```
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<InjectSharedRentedSpan>true</InjectSharedRentedSpan>
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
<InjectObsoleteAttributeOnLegacy>true</InjectObsoleteAttributeOnLegacy>
<InjectPlatformAttributesOnLegacy>true</InjectPlatformAttributesOnLegacy>
<InjectSharedBufferWriterPool>true</InjectSharedBufferWriterPool>
<InjectSharedDiagnosticIds>true</InjectSharedDiagnosticIds>
<InjectStringSplitExtensions>true</InjectStringSplitExtensions>
Expand All @@ -36,13 +37,14 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Bcl.HashCode" Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))" />
<PackageReference Include="Microsoft.Bcl.TimeProvider" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
<PackageReference Include="Microsoft.Extensions.Diagnostics" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
<PackageReference Include="System.Threading.Tasks.Extensions" Condition="'$(TargetFramework)' == 'net462'" />
<PackageReference Include="Microsoft.Bcl.HashCode" Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))" />
<PackageReference Include="System.Collections.Immutable" Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))" />
<PackageReference Include="System.Diagnostics.PerformanceCounter" />
<PackageReference Include="System.Threading.Tasks.Extensions" Condition="'$(TargetFramework)' == 'net462'" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring;

public partial class ResourceMonitoringOptions
{
/// <summary>
/// Gets or sets a value indicating whether disk I/O metrics should be enabled.
/// </summary>
[Experimental(diagnosticId: DiagnosticIds.Experiments.ResourceMonitoring, UrlFormat = DiagnosticIds.UrlFormat)]
public bool EnableDiskIoMetrics { get; set; }

/// <summary>
/// Gets or sets the list of source IPv4 addresses to track the connections for in telemetry.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.Versioning;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Diagnostics.ResourceMonitoring;
#if !NETFRAMEWORK
Expand All @@ -11,6 +12,7 @@

#endif
using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows;
using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Disk;
using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop;
using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Network;
using Microsoft.Shared.DiagnosticIds;
Expand Down Expand Up @@ -89,6 +91,7 @@ private static IServiceCollection AddResourceMonitoringInternal(
return services;
}

[SupportedOSPlatform("windows")]
private static ResourceMonitorBuilder AddWindowsProvider(this ResourceMonitorBuilder builder)
{
builder.PickWindowsSnapshotProvider();
Expand All @@ -97,6 +100,12 @@ private static ResourceMonitorBuilder AddWindowsProvider(this ResourceMonitorBui
.AddActivatedSingleton<WindowsNetworkMetrics>()
.AddActivatedSingleton<ITcpStateInfoProvider, WindowsTcpStateInfo>();

builder.Services.TryAddSingleton(TimeProvider.System);

_ = builder.Services
.AddActivatedSingleton<WindowsDiskMetrics>()
.AddActivatedSingleton<IPerformanceCounterFactory, PerformanceCounterFactory>();

return builder;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// 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;
using System.Runtime.Versioning;

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

[SupportedOSPlatform("windows")]
internal sealed class WindowsDiskIoRatePerfCounter
{
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 WindowsDiskIoRatePerfCounter(
IPerformanceCounterFactory performanceCounterFactory,
TimeProvider timeProvider,
string categoryName,
string counterName,
string[] instanceNames)
{
_performanceCounterFactory = performanceCounterFactory;
_timeProvider = timeProvider;
_categoryName = categoryName;
_counterName = counterName;
_instanceNames = instanceNames;
}

/// <summary>
/// Gets the disk I/O measurements.
/// Key: Disk name, Value: Total count.
/// </summary>
internal IDictionary<string, long> TotalCountDict { get; } = new ConcurrentDictionary<string, long>();

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);
}

// 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

// 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.
foreach (IPerformanceCounter counter in _counters)
{
// total value = per-second rate * elapsed seconds
double value = counter.NextValue() * elapsedSeconds;
TotalCountDict[counter.InstanceName] += (long)value;
}

_lastTimestamp = currentTimestamp;
}
}
Loading
Loading