Skip to content

Commit c12d3b6

Browse files
authored
Add Windows disk I/O metrics to ResourceMonitoring (#6181)
1 parent afccabd commit c12d3b6

20 files changed

+926
-3
lines changed

eng/MSBuild/LegacySupport.props

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,8 @@
6666
<ItemGroup Condition="'$(InjectObsoleteAttributeOnLegacy)' == 'true' AND ('$(TargetFramework)' == 'net462' or '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netcoreapp3.1')">
6767
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\LegacySupport\ObsoleteAttribute\*.cs" LinkBase="LegacySupport\ObsoleteAttribute" />
6868
</ItemGroup>
69+
70+
<ItemGroup Condition="'$(InjectPlatformAttributesOnLegacy)' == 'true' AND ('$(TargetFramework)' == 'net462' or '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netcoreapp3.1')">
71+
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\LegacySupport\PlatformAttributes\*.cs" LinkBase="LegacySupport\PlatformAttributes" />
72+
</ItemGroup>
6973
</Project>
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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+
#pragma warning disable S1694
5+
#pragma warning disable S3996
6+
#pragma warning disable SA1128
7+
#pragma warning disable SA1402
8+
#pragma warning disable SA1513
9+
#pragma warning disable SA1649
10+
11+
namespace System.Runtime.Versioning
12+
{
13+
/// <summary>
14+
/// Base type for all platform-specific API attributes.
15+
/// </summary>
16+
#pragma warning disable CS3015 // Type has no accessible constructors which use only CLS-compliant types
17+
internal abstract class OSPlatformAttribute : Attribute
18+
#pragma warning restore CS3015
19+
{
20+
private protected OSPlatformAttribute(string platformName)
21+
{
22+
PlatformName = platformName;
23+
}
24+
public string PlatformName { get; }
25+
}
26+
27+
/// <summary>
28+
/// Records the platform that the project targeted.
29+
/// </summary>
30+
[AttributeUsage(AttributeTargets.Assembly,
31+
AllowMultiple = false, Inherited = false)]
32+
internal sealed class TargetPlatformAttribute : OSPlatformAttribute
33+
{
34+
public TargetPlatformAttribute(string platformName) : base(platformName)
35+
{
36+
}
37+
}
38+
39+
/// <summary>
40+
/// Records the operating system (and minimum version) that supports an API. Multiple attributes can be
41+
/// applied to indicate support on multiple operating systems.
42+
/// </summary>
43+
/// <remarks>
44+
/// Callers can apply a <see cref="SupportedOSPlatformAttribute " />
45+
/// or use guards to prevent calls to APIs on unsupported operating systems.
46+
///
47+
/// A given platform should only be specified once.
48+
/// </remarks>
49+
[AttributeUsage(AttributeTargets.Assembly |
50+
AttributeTargets.Class |
51+
AttributeTargets.Constructor |
52+
AttributeTargets.Enum |
53+
AttributeTargets.Event |
54+
AttributeTargets.Field |
55+
AttributeTargets.Interface |
56+
AttributeTargets.Method |
57+
AttributeTargets.Module |
58+
AttributeTargets.Property |
59+
AttributeTargets.Struct,
60+
AllowMultiple = true, Inherited = false)]
61+
internal sealed class SupportedOSPlatformAttribute : OSPlatformAttribute
62+
{
63+
public SupportedOSPlatformAttribute(string platformName) : base(platformName)
64+
{
65+
}
66+
}
67+
68+
/// <summary>
69+
/// Marks APIs that were removed in a given operating system version.
70+
/// </summary>
71+
/// <remarks>
72+
/// Primarily used by OS bindings to indicate APIs that are only available in
73+
/// earlier versions.
74+
/// </remarks>
75+
[AttributeUsage(AttributeTargets.Assembly |
76+
AttributeTargets.Class |
77+
AttributeTargets.Constructor |
78+
AttributeTargets.Enum |
79+
AttributeTargets.Event |
80+
AttributeTargets.Field |
81+
AttributeTargets.Interface |
82+
AttributeTargets.Method |
83+
AttributeTargets.Module |
84+
AttributeTargets.Property |
85+
AttributeTargets.Struct,
86+
AllowMultiple = true, Inherited = false)]
87+
internal sealed class UnsupportedOSPlatformAttribute : OSPlatformAttribute
88+
{
89+
public UnsupportedOSPlatformAttribute(string platformName) : base(platformName)
90+
{
91+
}
92+
public UnsupportedOSPlatformAttribute(string platformName, string? message) : base(platformName)
93+
{
94+
Message = message;
95+
}
96+
public string? Message { get; }
97+
}
98+
99+
/// <summary>
100+
/// Marks APIs that were obsoleted in a given operating system version.
101+
/// </summary>
102+
/// <remarks>
103+
/// Primarily used by OS bindings to indicate APIs that should not be used anymore.
104+
/// </remarks>
105+
[AttributeUsage(AttributeTargets.Assembly |
106+
AttributeTargets.Class |
107+
AttributeTargets.Constructor |
108+
AttributeTargets.Enum |
109+
AttributeTargets.Event |
110+
AttributeTargets.Field |
111+
AttributeTargets.Interface |
112+
AttributeTargets.Method |
113+
AttributeTargets.Module |
114+
AttributeTargets.Property |
115+
AttributeTargets.Struct,
116+
AllowMultiple = true, Inherited = false)]
117+
internal sealed class ObsoletedOSPlatformAttribute : OSPlatformAttribute
118+
{
119+
public ObsoletedOSPlatformAttribute(string platformName) : base(platformName)
120+
{
121+
}
122+
public ObsoletedOSPlatformAttribute(string platformName, string? message) : base(platformName)
123+
{
124+
Message = message;
125+
}
126+
public string? Message { get; }
127+
public string? Url { get; set; }
128+
}
129+
130+
/// <summary>
131+
/// Annotates a custom guard field, property or method with a supported platform name and optional version.
132+
/// Multiple attributes can be applied to indicate guard for multiple supported platforms.
133+
/// </summary>
134+
/// <remarks>
135+
/// Callers can apply a <see cref="SupportedOSPlatformGuardAttribute " /> to a field, property or method
136+
/// and use that field, property or method in a conditional or assert statements in order to safely call platform specific APIs.
137+
///
138+
/// 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.
139+
/// </remarks>
140+
[AttributeUsage(AttributeTargets.Field |
141+
AttributeTargets.Method |
142+
AttributeTargets.Property,
143+
AllowMultiple = true, Inherited = false)]
144+
internal sealed class SupportedOSPlatformGuardAttribute : OSPlatformAttribute
145+
{
146+
public SupportedOSPlatformGuardAttribute(string platformName) : base(platformName)
147+
{
148+
}
149+
}
150+
151+
/// <summary>
152+
/// Annotates the custom guard field, property or method with an unsupported platform name and optional version.
153+
/// Multiple attributes can be applied to indicate guard for multiple unsupported platforms.
154+
/// </summary>
155+
/// <remarks>
156+
/// Callers can apply a <see cref="UnsupportedOSPlatformGuardAttribute " /> to a field, property or method
157+
/// and use that field, property or method in a conditional or assert statements as a guard to safely call APIs unsupported on those platforms.
158+
///
159+
/// 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.
160+
/// </remarks>
161+
[AttributeUsage(AttributeTargets.Field |
162+
AttributeTargets.Method |
163+
AttributeTargets.Property,
164+
AllowMultiple = true, Inherited = false)]
165+
internal sealed class UnsupportedOSPlatformGuardAttribute : OSPlatformAttribute
166+
{
167+
public UnsupportedOSPlatformGuardAttribute(string platformName) : base(platformName)
168+
{
169+
}
170+
}
171+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Enables use of C# OSPlatform attributes on older frameworks.
2+
3+
To use this source in your project, add the following to your `.csproj` file:
4+
5+
```xml
6+
<PropertyGroup>
7+
<InjectPlatformAttributesOnLegacy>true</InjectPlatformAttributesOnLegacy>
8+
</PropertyGroup>
9+
```

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<InjectSharedRentedSpan>true</InjectSharedRentedSpan>
1515
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
1616
<InjectObsoleteAttributeOnLegacy>true</InjectObsoleteAttributeOnLegacy>
17+
<InjectPlatformAttributesOnLegacy>true</InjectPlatformAttributesOnLegacy>
1718
<InjectSharedBufferWriterPool>true</InjectSharedBufferWriterPool>
1819
<InjectSharedDiagnosticIds>true</InjectSharedDiagnosticIds>
1920
<InjectStringSplitExtensions>true</InjectStringSplitExtensions>
@@ -36,13 +37,14 @@
3637
</ItemGroup>
3738

3839
<ItemGroup>
40+
<PackageReference Include="Microsoft.Bcl.HashCode" Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))" />
3941
<PackageReference Include="Microsoft.Bcl.TimeProvider" />
4042
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
4143
<PackageReference Include="Microsoft.Extensions.Diagnostics" />
4244
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
43-
<PackageReference Include="System.Threading.Tasks.Extensions" Condition="'$(TargetFramework)' == 'net462'" />
44-
<PackageReference Include="Microsoft.Bcl.HashCode" Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))" />
4545
<PackageReference Include="System.Collections.Immutable" Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))" />
46+
<PackageReference Include="System.Diagnostics.PerformanceCounter" />
47+
<PackageReference Include="System.Threading.Tasks.Extensions" Condition="'$(TargetFramework)' == 'net462'" />
4648
</ItemGroup>
4749

4850
<ItemGroup>

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring;
1010

1111
public partial class ResourceMonitoringOptions
1212
{
13+
/// <summary>
14+
/// Gets or sets a value indicating whether disk I/O metrics should be enabled.
15+
/// </summary>
16+
[Experimental(diagnosticId: DiagnosticIds.Experiments.ResourceMonitoring, UrlFormat = DiagnosticIds.UrlFormat)]
17+
public bool EnableDiskIoMetrics { get; set; }
18+
1319
/// <summary>
1420
/// Gets or sets the list of source IPv4 addresses to track the connections for in telemetry.
1521
/// </summary>

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Diagnostics.CodeAnalysis;
6+
using System.Runtime.Versioning;
67
using Microsoft.Extensions.DependencyInjection.Extensions;
78
using Microsoft.Extensions.Diagnostics.ResourceMonitoring;
89
#if !NETFRAMEWORK
@@ -11,6 +12,7 @@
1112

1213
#endif
1314
using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows;
15+
using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Disk;
1416
using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop;
1517
using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Network;
1618
using Microsoft.Shared.DiagnosticIds;
@@ -89,6 +91,7 @@ private static IServiceCollection AddResourceMonitoringInternal(
8991
return services;
9092
}
9193

94+
[SupportedOSPlatform("windows")]
9295
private static ResourceMonitorBuilder AddWindowsProvider(this ResourceMonitorBuilder builder)
9396
{
9497
builder.PickWindowsSnapshotProvider();
@@ -97,6 +100,12 @@ private static ResourceMonitorBuilder AddWindowsProvider(this ResourceMonitorBui
97100
.AddActivatedSingleton<WindowsNetworkMetrics>()
98101
.AddActivatedSingleton<ITcpStateInfoProvider, WindowsTcpStateInfo>();
99102

103+
builder.Services.TryAddSingleton(TimeProvider.System);
104+
105+
_ = builder.Services
106+
.AddActivatedSingleton<WindowsDiskMetrics>()
107+
.AddActivatedSingleton<IPerformanceCounterFactory, PerformanceCounterFactory>();
108+
100109
return builder;
101110
}
102111

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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+
using System.Runtime.Versioning;
8+
9+
namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Disk;
10+
11+
[SupportedOSPlatform("windows")]
12+
internal sealed class WindowsDiskIoRatePerfCounter
13+
{
14+
private readonly List<IPerformanceCounter> _counters = [];
15+
private readonly IPerformanceCounterFactory _performanceCounterFactory;
16+
private readonly TimeProvider _timeProvider;
17+
private readonly string _categoryName;
18+
private readonly string _counterName;
19+
private readonly string[] _instanceNames;
20+
private long _lastTimestamp;
21+
22+
internal WindowsDiskIoRatePerfCounter(
23+
IPerformanceCounterFactory performanceCounterFactory,
24+
TimeProvider timeProvider,
25+
string categoryName,
26+
string counterName,
27+
string[] instanceNames)
28+
{
29+
_performanceCounterFactory = performanceCounterFactory;
30+
_timeProvider = timeProvider;
31+
_categoryName = categoryName;
32+
_counterName = counterName;
33+
_instanceNames = instanceNames;
34+
}
35+
36+
/// <summary>
37+
/// Gets the disk I/O measurements.
38+
/// Key: Disk name, Value: Total count.
39+
/// </summary>
40+
internal IDictionary<string, long> TotalCountDict { get; } = new ConcurrentDictionary<string, long>();
41+
42+
internal void InitializeDiskCounters()
43+
{
44+
foreach (string instanceName in _instanceNames)
45+
{
46+
// Skip the total instance
47+
if (instanceName.Equals("_Total", StringComparison.OrdinalIgnoreCase))
48+
{
49+
continue;
50+
}
51+
52+
// Create counters for each disk
53+
_counters.Add(_performanceCounterFactory.Create(_categoryName, _counterName, instanceName));
54+
TotalCountDict.Add(instanceName, 0);
55+
}
56+
57+
// Initialize the counters to get the first value
58+
foreach (IPerformanceCounter counter in _counters)
59+
{
60+
_ = counter.NextValue();
61+
}
62+
63+
_lastTimestamp = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds();
64+
}
65+
66+
internal void UpdateDiskCounters()
67+
{
68+
long currentTimestamp = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds();
69+
double elapsedSeconds = (currentTimestamp - _lastTimestamp) / 1000.0; // Convert to seconds
70+
71+
// For the kind of "rate" perf counters, this algorithm calculates the total value over a time interval
72+
// by multiplying the per-second rate (e.g., Disk Bytes/sec) by the time interval between two samples.
73+
// This effectively reverses the per-second rate calculation to a total amount (e.g., total bytes transferred) during that period.
74+
foreach (IPerformanceCounter counter in _counters)
75+
{
76+
// total value = per-second rate * elapsed seconds
77+
double value = counter.NextValue() * elapsedSeconds;
78+
TotalCountDict[counter.InstanceName] += (long)value;
79+
}
80+
81+
_lastTimestamp = currentTimestamp;
82+
}
83+
}

0 commit comments

Comments
 (0)