diff --git a/eng/MSBuild/LegacySupport.props b/eng/MSBuild/LegacySupport.props
index 2983903a196..15d34725d84 100644
--- a/eng/MSBuild/LegacySupport.props
+++ b/eng/MSBuild/LegacySupport.props
@@ -66,4 +66,8 @@
+
+
+
+
diff --git a/src/LegacySupport/PlatformAttributes/PlatformAttributes.cs b/src/LegacySupport/PlatformAttributes/PlatformAttributes.cs
new file mode 100644
index 00000000000..898fe3d960a
--- /dev/null
+++ b/src/LegacySupport/PlatformAttributes/PlatformAttributes.cs
@@ -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
+{
+ ///
+ /// Base type for all platform-specific API attributes.
+ ///
+#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; }
+ }
+
+ ///
+ /// Records the platform that the project targeted.
+ ///
+ [AttributeUsage(AttributeTargets.Assembly,
+ AllowMultiple = false, Inherited = false)]
+ internal sealed class TargetPlatformAttribute : OSPlatformAttribute
+ {
+ public TargetPlatformAttribute(string platformName) : base(platformName)
+ {
+ }
+ }
+
+ ///
+ /// Records the operating system (and minimum version) that supports an API. Multiple attributes can be
+ /// applied to indicate support on multiple operating systems.
+ ///
+ ///
+ /// Callers can apply a
+ /// or use guards to prevent calls to APIs on unsupported operating systems.
+ ///
+ /// A given platform should only be specified once.
+ ///
+ [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)
+ {
+ }
+ }
+
+ ///
+ /// Marks APIs that were removed in a given operating system version.
+ ///
+ ///
+ /// Primarily used by OS bindings to indicate APIs that are only available in
+ /// earlier versions.
+ ///
+ [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; }
+ }
+
+ ///
+ /// Marks APIs that were obsoleted in a given operating system version.
+ ///
+ ///
+ /// Primarily used by OS bindings to indicate APIs that should not be used anymore.
+ ///
+ [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; }
+ }
+
+ ///
+ /// 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.
+ ///
+ ///
+ /// Callers can apply a 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.
+ ///
+ [AttributeUsage(AttributeTargets.Field |
+ AttributeTargets.Method |
+ AttributeTargets.Property,
+ AllowMultiple = true, Inherited = false)]
+ internal sealed class SupportedOSPlatformGuardAttribute : OSPlatformAttribute
+ {
+ public SupportedOSPlatformGuardAttribute(string platformName) : base(platformName)
+ {
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ ///
+ /// Callers can apply a 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.
+ ///
+ [AttributeUsage(AttributeTargets.Field |
+ AttributeTargets.Method |
+ AttributeTargets.Property,
+ AllowMultiple = true, Inherited = false)]
+ internal sealed class UnsupportedOSPlatformGuardAttribute : OSPlatformAttribute
+ {
+ public UnsupportedOSPlatformGuardAttribute(string platformName) : base(platformName)
+ {
+ }
+ }
+}
diff --git a/src/LegacySupport/PlatformAttributes/README.md b/src/LegacySupport/PlatformAttributes/README.md
new file mode 100644
index 00000000000..1e818310c9b
--- /dev/null
+++ b/src/LegacySupport/PlatformAttributes/README.md
@@ -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
+
+ true
+
+```
\ No newline at end of file
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj
index ea04521c94d..7cdb4b7de49 100644
--- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj
@@ -14,6 +14,7 @@
true
true
true
+ true
true
true
true
@@ -36,13 +37,14 @@
+
-
-
+
+
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.Windows.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.Windows.cs
index 9e8636506c7..f042d892ab1 100644
--- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.Windows.cs
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.Windows.cs
@@ -10,6 +10,12 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring;
public partial class ResourceMonitoringOptions
{
+ ///
+ /// Gets or sets a value indicating whether disk I/O metrics should be enabled.
+ ///
+ [Experimental(diagnosticId: DiagnosticIds.Experiments.ResourceMonitoring, UrlFormat = DiagnosticIds.UrlFormat)]
+ public bool EnableDiskIoMetrics { get; set; }
+
///
/// Gets or sets the list of source IPv4 addresses to track the connections for in telemetry.
///
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs
index c09e4c85b75..f018038c614 100644
--- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs
@@ -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
@@ -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;
@@ -89,6 +91,7 @@ private static IServiceCollection AddResourceMonitoringInternal(
return services;
}
+ [SupportedOSPlatform("windows")]
private static ResourceMonitorBuilder AddWindowsProvider(this ResourceMonitorBuilder builder)
{
builder.PickWindowsSnapshotProvider();
@@ -97,6 +100,12 @@ private static ResourceMonitorBuilder AddWindowsProvider(this ResourceMonitorBui
.AddActivatedSingleton()
.AddActivatedSingleton();
+ builder.Services.TryAddSingleton(TimeProvider.System);
+
+ _ = builder.Services
+ .AddActivatedSingleton()
+ .AddActivatedSingleton();
+
return builder;
}
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Disk/WindowsDiskIoRatePerfCounter.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Disk/WindowsDiskIoRatePerfCounter.cs
new file mode 100644
index 00000000000..5d35efc98ad
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Disk/WindowsDiskIoRatePerfCounter.cs
@@ -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 _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;
+ }
+
+ ///
+ /// Gets the disk I/O measurements.
+ /// Key: Disk name, Value: Total count.
+ ///
+ internal IDictionary TotalCountDict { get; } = new ConcurrentDictionary();
+
+ 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;
+ }
+}
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Disk/WindowsDiskMetrics.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Disk/WindowsDiskMetrics.cs
new file mode 100644
index 00000000000..6c3f3faea34
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Disk/WindowsDiskMetrics.cs
@@ -0,0 +1,148 @@
+// 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.Generic;
+using System.Diagnostics;
+using System.Diagnostics.Metrics;
+using System.Runtime.Versioning;
+using Microsoft.Extensions.Options;
+using Microsoft.Shared.Instruments;
+
+namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Disk;
+
+[SupportedOSPlatform("windows")]
+internal sealed class WindowsDiskMetrics
+{
+ private const string DeviceKey = "system.device";
+ private const string DirectionKey = "disk.io.direction";
+
+ private static readonly KeyValuePair _directionReadTag = new(DirectionKey, "read");
+ private static readonly KeyValuePair _directionWriteTag = new(DirectionKey, "write");
+ private readonly Dictionary _diskIoRateCounters = new();
+
+ public WindowsDiskMetrics(
+ IMeterFactory meterFactory,
+ IPerformanceCounterFactory performanceCounterFactory,
+ TimeProvider timeProvider,
+ IOptions options)
+ {
+ if (!options.Value.EnableDiskIoMetrics)
+ {
+ return;
+ }
+
+#pragma warning disable CA2000 // Dispose objects before losing scope
+ // We don't dispose the meter because IMeterFactory handles that
+ // It's a false-positive, see: https://github.com/dotnet/roslyn-analyzers/issues/6912.
+ // Related documentation: https://github.com/dotnet/docs/pull/37170
+ Meter meter = meterFactory.Create(ResourceUtilizationInstruments.MeterName);
+#pragma warning restore CA2000 // Dispose objects before losing scope
+
+ InitializeDiskCounters(performanceCounterFactory, timeProvider);
+
+ // The metric is aligned with
+ // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/system/system-metrics.md#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
+ _ = meter.CreateObservableCounter(
+ ResourceUtilizationInstruments.SystemDiskOperations,
+ GetDiskOperationMeasurements,
+ unit: "{operation}",
+ description: "Disk operations");
+ }
+
+ private void InitializeDiskCounters(IPerformanceCounterFactory performanceCounterFactory, TimeProvider timeProvider)
+ {
+ const string DiskCategoryName = "LogicalDisk";
+ string[] instanceNames = performanceCounterFactory.GetCategoryInstances(DiskCategoryName);
+ if (instanceNames.Length == 0)
+ {
+ return;
+ }
+
+ List diskIoRatePerformanceCounters =
+ [
+ WindowsDiskPerfCounterNames.DiskWriteBytesCounter,
+ WindowsDiskPerfCounterNames.DiskReadBytesCounter,
+ WindowsDiskPerfCounterNames.DiskWritesCounter,
+ WindowsDiskPerfCounterNames.DiskReadsCounter,
+ ];
+ foreach (string counterName in diskIoRatePerformanceCounters)
+ {
+ try
+ {
+ var ratePerfCounter = new WindowsDiskIoRatePerfCounter(
+ performanceCounterFactory,
+ timeProvider,
+ DiskCategoryName,
+ counterName,
+ instanceNames);
+ ratePerfCounter.InitializeDiskCounters();
+ _diskIoRateCounters.Add(counterName, ratePerfCounter);
+ }
+#pragma warning disable CA1031
+ catch (Exception ex)
+#pragma warning restore CA1031
+ {
+ Debug.WriteLine("Error initializing disk performance counter: " + ex.Message);
+ }
+ }
+ }
+
+ private IEnumerable> GetDiskIoMeasurements()
+ {
+ List> measurements = [];
+
+ if (_diskIoRateCounters.TryGetValue(WindowsDiskPerfCounterNames.DiskWriteBytesCounter, out WindowsDiskIoRatePerfCounter? perSecondWriteCounter))
+ {
+ perSecondWriteCounter.UpdateDiskCounters();
+ foreach (KeyValuePair pair in perSecondWriteCounter.TotalCountDict)
+ {
+ measurements.Add(new Measurement(pair.Value, new TagList { _directionWriteTag, new(DeviceKey, pair.Key) }));
+ }
+ }
+
+ if (_diskIoRateCounters.TryGetValue(WindowsDiskPerfCounterNames.DiskReadBytesCounter, out WindowsDiskIoRatePerfCounter? perSecondReadCounter))
+ {
+ perSecondReadCounter.UpdateDiskCounters();
+ foreach (KeyValuePair pair in perSecondReadCounter.TotalCountDict)
+ {
+ measurements.Add(new Measurement(pair.Value, new TagList { _directionReadTag, new(DeviceKey, pair.Key) }));
+ }
+ }
+
+ return measurements;
+ }
+
+ private IEnumerable> GetDiskOperationMeasurements()
+ {
+ List> measurements = [];
+
+ if (_diskIoRateCounters.TryGetValue(WindowsDiskPerfCounterNames.DiskWritesCounter, out WindowsDiskIoRatePerfCounter? writeCounter))
+ {
+ writeCounter.UpdateDiskCounters();
+ foreach (KeyValuePair pair in writeCounter.TotalCountDict)
+ {
+ measurements.Add(new Measurement(pair.Value, new TagList { _directionWriteTag, new(DeviceKey, pair.Key) }));
+ }
+ }
+
+ if (_diskIoRateCounters.TryGetValue(WindowsDiskPerfCounterNames.DiskReadsCounter, out WindowsDiskIoRatePerfCounter? readCounter))
+ {
+ readCounter.UpdateDiskCounters();
+ foreach (KeyValuePair pair in readCounter.TotalCountDict)
+ {
+ measurements.Add(new Measurement(pair.Value, new TagList { _directionReadTag, new(DeviceKey, pair.Key) }));
+ }
+ }
+
+ return measurements;
+ }
+}
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Disk/WindowsDiskPerfCounterNames.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Disk/WindowsDiskPerfCounterNames.cs
new file mode 100644
index 00000000000..b791bdea3c7
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Disk/WindowsDiskPerfCounterNames.cs
@@ -0,0 +1,12 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Disk;
+
+internal static class WindowsDiskPerfCounterNames
+{
+ internal const string DiskWriteBytesCounter = "Disk Write Bytes/sec";
+ internal const string DiskReadBytesCounter = "Disk Read Bytes/sec";
+ internal const string DiskWritesCounter = "Disk Writes/sec";
+ internal const string DiskReadsCounter = "Disk Reads/sec";
+}
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/IPerformanceCounter.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/IPerformanceCounter.cs
new file mode 100644
index 00000000000..8899296028b
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/IPerformanceCounter.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows;
+
+///
+/// Interface for performance counters.
+///
+internal interface IPerformanceCounter
+{
+ ///
+ /// Gets the name of the performance counter category.
+ ///
+ string InstanceName { get; }
+
+ ///
+ /// Get the next value of the performance counter.
+ ///
+ /// The next value of the performance counter.
+ float NextValue();
+}
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/IPerformanceCounterFactory.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/IPerformanceCounterFactory.cs
new file mode 100644
index 00000000000..ccd05c7218b
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/IPerformanceCounterFactory.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows;
+
+///
+/// Factory interface for creating performance counters.
+///
+internal interface IPerformanceCounterFactory
+{
+ ///
+ /// Creates a performance counter.
+ ///
+ /// The name of the performance counter category.
+ /// The name of the performance counter.
+ /// The name of the instance of the performance counter.
+ /// A new instance of .
+ IPerformanceCounter Create(string categoryName, string counterName, string instanceName);
+
+ ///
+ /// Gets the names of all instances of a performance counter category.
+ ///
+ /// PerformanceCounter category name.
+ /// Array of instance names.
+ string[] GetCategoryInstances(string categoryName);
+}
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Network/WindowsNetworkMetrics.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Network/WindowsNetworkMetrics.cs
index f2efb14e990..1acc8d02edd 100644
--- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Network/WindowsNetworkMetrics.cs
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Network/WindowsNetworkMetrics.cs
@@ -18,7 +18,7 @@ public WindowsNetworkMetrics(IMeterFactory meterFactory, ITcpStateInfoProvider t
#pragma warning disable CA2000 // Dispose objects before losing scope
// We don't dispose the meter because IMeterFactory handles that
- // Is's a false-positive, see: https://github.com/dotnet/roslyn-analyzers/issues/6912.
+ // It's a false-positive, see: https://github.com/dotnet/roslyn-analyzers/issues/6912.
// Related documentation: https://github.com/dotnet/docs/pull/37170
var meter = meterFactory.Create(ResourceUtilizationInstruments.MeterName);
#pragma warning restore CA2000 // Dispose objects before losing scope
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/PerformanceCounterFactory.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/PerformanceCounterFactory.cs
new file mode 100644
index 00000000000..db45b2893de
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/PerformanceCounterFactory.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Runtime.Versioning;
+
+namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows;
+
+[SupportedOSPlatform("windows")]
+internal sealed class PerformanceCounterFactory : IPerformanceCounterFactory
+{
+ public IPerformanceCounter Create(string categoryName, string counterName, string instanceName)
+ => new PerformanceCounterWrapper(categoryName, counterName, instanceName);
+
+ public string[] GetCategoryInstances(string categoryName)
+ {
+ var category = new PerformanceCounterCategory(categoryName);
+ string[] instanceNames = category.GetInstanceNames();
+ return instanceNames == null || instanceNames.Length == 0 ? [] : instanceNames;
+ }
+}
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/PerformanceCounterWrapper.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/PerformanceCounterWrapper.cs
new file mode 100644
index 00000000000..163ca27cda4
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/PerformanceCounterWrapper.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Runtime.Versioning;
+
+namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows;
+
+[SupportedOSPlatform("windows")]
+internal sealed class PerformanceCounterWrapper : IPerformanceCounter
+{
+ private readonly PerformanceCounter _counter;
+
+ internal PerformanceCounterWrapper(string categoryName, string counterName, string instanceName)
+ {
+ _counter = new PerformanceCounter(categoryName, counterName, instanceName, readOnly: true);
+ InstanceName = instanceName;
+ }
+
+ public string InstanceName { get; }
+
+ public float NextValue() => _counter.NextValue();
+}
diff --git a/src/Shared/Instruments/ResourceUtilizationInstruments.cs b/src/Shared/Instruments/ResourceUtilizationInstruments.cs
index 73b33b7b75b..fe18e7ac4fa 100644
--- a/src/Shared/Instruments/ResourceUtilizationInstruments.cs
+++ b/src/Shared/Instruments/ResourceUtilizationInstruments.cs
@@ -58,6 +58,22 @@ internal static class ResourceUtilizationInstruments
///
public const string ProcessMemoryUtilization = "dotnet.process.memory.virtual.utilization";
+ ///
+ /// The name of an instrument to retrieve disk bytes transferred.
+ ///
+ ///
+ /// The type of an instrument is .
+ ///
+ public const string SystemDiskIo = "system.disk.io";
+
+ ///
+ /// The name of an instrument to retrieve disk operations.
+ ///
+ ///
+ /// The type of an instrument is .
+ ///
+ public const string SystemDiskOperations = "system.disk.operations";
+
///
/// The name of an instrument to retrieve network connections information.
///
diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/Disk/WindowsDiskIoRatePerfCounterTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/Disk/WindowsDiskIoRatePerfCounterTests.cs
new file mode 100644
index 00000000000..1e163e6ca44
--- /dev/null
+++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/Disk/WindowsDiskIoRatePerfCounterTests.cs
@@ -0,0 +1,112 @@
+// 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 WindowsDiskIoRatePerfCounterTests
+{
+ private const string CategoryName = "LogicalDisk";
+
+ [ConditionalFact]
+ public void DiskReadsPerfCounter_Per60Seconds()
+ {
+ const string CounterName = WindowsDiskPerfCounterNames.DiskReadsCounter;
+ var performanceCounterFactory = new Mock();
+ var fakeTimeProvider = new FakeTimeProvider { AutoAdvanceAmount = TimeSpan.FromSeconds(60) };
+
+ var ratePerfCounters = new WindowsDiskIoRatePerfCounter(
+ performanceCounterFactory.Object,
+ fakeTimeProvider,
+ CategoryName,
+ CounterName,
+ instanceNames: ["C:", "D:", "_Total"]);
+
+ // Set up
+ var counterC = new FakePerformanceCounter("C:", [0, 1, 1.5f, 2, 2.5f]);
+ var counterD = new FakePerformanceCounter("D:", [0, 2, 2.5f, 3, 3.5f]);
+ performanceCounterFactory.Setup(x => x.Create(CategoryName, CounterName, "C:")).Returns(counterC);
+ performanceCounterFactory.Setup(x => x.Create(CategoryName, CounterName, "D:")).Returns(counterD);
+
+ // Initialize the counters
+ ratePerfCounters.InitializeDiskCounters();
+ Assert.Equal(2, ratePerfCounters.TotalCountDict.Count);
+ Assert.Equal(0, ratePerfCounters.TotalCountDict["C:"]);
+ Assert.Equal(0, ratePerfCounters.TotalCountDict["D:"]);
+
+ // Simulate the first tick
+ ratePerfCounters.UpdateDiskCounters();
+ Assert.Equal(60, ratePerfCounters.TotalCountDict["C:"]); // 1 * 60 = 60
+ Assert.Equal(120, ratePerfCounters.TotalCountDict["D:"]); // 2 * 60 = 120
+
+ // Simulate the second tick
+ ratePerfCounters.UpdateDiskCounters();
+ Assert.Equal(150, ratePerfCounters.TotalCountDict["C:"]); // 60 + 1.5 * 60 = 150
+ Assert.Equal(270, ratePerfCounters.TotalCountDict["D:"]); // 120 + 2.5 * 60 = 270
+
+ // Simulate the third tick
+ ratePerfCounters.UpdateDiskCounters();
+ Assert.Equal(270, ratePerfCounters.TotalCountDict["C:"]); // 150 + 2 * 60 = 270
+ Assert.Equal(450, ratePerfCounters.TotalCountDict["D:"]); // 270 + 3 * 60 = 450
+
+ // Simulate the fourth tick
+ ratePerfCounters.UpdateDiskCounters();
+ Assert.Equal(420, ratePerfCounters.TotalCountDict["C:"]); // 270 + 2.5 * 60 = 420
+ Assert.Equal(660, ratePerfCounters.TotalCountDict["D:"]); // 450 + 3.5 * 60 = 660
+ }
+
+ [ConditionalFact]
+ public void DiskWriteBytesPerfCounter_Per30Seconds()
+ {
+ const string CounterName = WindowsDiskPerfCounterNames.DiskWriteBytesCounter;
+ var performanceCounterFactory = new Mock();
+ var fakeTimeProvider = new FakeTimeProvider { AutoAdvanceAmount = TimeSpan.FromSeconds(30) };
+ var ratePerfCounters = new WindowsDiskIoRatePerfCounter(
+ performanceCounterFactory.Object,
+ fakeTimeProvider,
+ CategoryName,
+ counterName: CounterName,
+ instanceNames: ["C:", "D:", "_Total"]);
+
+ // Set up
+ var counterC = new FakePerformanceCounter("C:", [0, 100, 150.5f, 20, 3.1416f]);
+ var counterD = new FakePerformanceCounter("D:", [0, 2000, 2025, 0, 2.7183f]);
+ performanceCounterFactory.Setup(x => x.Create(CategoryName, CounterName, "C:")).Returns(counterC);
+ performanceCounterFactory.Setup(x => x.Create(CategoryName, CounterName, "D:")).Returns(counterD);
+
+ // Initialize the counters
+ ratePerfCounters.InitializeDiskCounters();
+ Assert.Equal(2, ratePerfCounters.TotalCountDict.Count);
+ Assert.Equal(0, ratePerfCounters.TotalCountDict["C:"]);
+ Assert.Equal(0, ratePerfCounters.TotalCountDict["D:"]);
+
+ // Simulate the first tick
+ ratePerfCounters.UpdateDiskCounters();
+ Assert.Equal(3000, ratePerfCounters.TotalCountDict["C:"]); // 100 * 30 = 3000
+ Assert.Equal(60000, ratePerfCounters.TotalCountDict["D:"]); // 2000 * 30 = 60000
+
+ // Simulate the second tick
+ ratePerfCounters.UpdateDiskCounters();
+ Assert.Equal(7515, ratePerfCounters.TotalCountDict["C:"]); // 3000 + 150.5 * 30 = 7515
+ Assert.Equal(120750, ratePerfCounters.TotalCountDict["D:"]); // 60000 + 2.5 * 30 = 120750
+
+ // Simulate the third tick
+ ratePerfCounters.UpdateDiskCounters();
+ Assert.Equal(8115, ratePerfCounters.TotalCountDict["C:"]); // 7515 + 20 * 30 = 8115
+ Assert.Equal(120750, ratePerfCounters.TotalCountDict["D:"]); // 120750 + 0 * 30 = 120750
+
+ // Simulate the fourth tick
+ ratePerfCounters.UpdateDiskCounters();
+ Assert.Equal(8209, ratePerfCounters.TotalCountDict["C:"]); // 8115 + 3.1416 * 30 = 8209
+ Assert.Equal(120831, ratePerfCounters.TotalCountDict["D:"]); // 120750 + 3.5 * 30 = 120831
+ }
+}
diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/Disk/WindowsDiskMetricsTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/Disk/WindowsDiskMetricsTests.cs
new file mode 100644
index 00000000000..c42ee0b4db7
--- /dev/null
+++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/Disk/WindowsDiskMetricsTests.cs
@@ -0,0 +1,188 @@
+// 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.Generic;
+using System.Diagnostics.Metrics;
+using System.Linq;
+using System.Runtime.Versioning;
+using Microsoft.Extensions.Diagnostics.Metrics.Testing;
+using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Helpers;
+using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Test;
+using Microsoft.Extensions.Time.Testing;
+using Microsoft.Shared.Instruments;
+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 WindowsDiskMetricsTests
+{
+ private const string CategoryName = "LogicalDisk";
+
+ [ConditionalFact]
+ public void Creates_Meter_With_Correct_Name()
+ {
+ using var meterFactory = new TestMeterFactory();
+ var performanceCounterFactoryMock = new Mock();
+ var options = new ResourceMonitoringOptions { EnableDiskIoMetrics = true };
+
+ _ = new WindowsDiskMetrics(
+ meterFactory,
+ performanceCounterFactoryMock.Object,
+ TimeProvider.System,
+ Microsoft.Extensions.Options.Options.Create(options));
+
+ Meter meter = meterFactory.Meters.Single();
+ Assert.Equal(ResourceUtilizationInstruments.MeterName, meter.Name);
+ }
+
+ [ConditionalFact]
+ public void DiskOperationMetricsTest()
+ {
+ using var meterFactory = new TestMeterFactory();
+ var performanceCounterFactory = new Mock();
+ var fakeTimeProvider = new FakeTimeProvider();
+ var options = new ResourceMonitoringOptions { EnableDiskIoMetrics = true };
+
+ // Set up
+ const string ReadCounterName = WindowsDiskPerfCounterNames.DiskReadsCounter;
+ const string WriteCounterName = WindowsDiskPerfCounterNames.DiskWritesCounter;
+ var readCounterC = new FakePerformanceCounter("C:", [0, 1, 1.5f, 2, 2.5f]);
+ var readCounterD = new FakePerformanceCounter("D:", [0, 2, 2.5f, 3, 3.5f]);
+ performanceCounterFactory.Setup(x => x.Create(CategoryName, ReadCounterName, "C:")).Returns(readCounterC);
+ performanceCounterFactory.Setup(x => x.Create(CategoryName, ReadCounterName, "D:")).Returns(readCounterD);
+ var writeCounterC = new FakePerformanceCounter("C:", [0, 10, 15, 20, 25]);
+ var writeCounterD = new FakePerformanceCounter("D:", [0, 20, 25, 30, 35]);
+ performanceCounterFactory.Setup(x => x.Create(CategoryName, WriteCounterName, "C:")).Returns(writeCounterC);
+ performanceCounterFactory.Setup(x => x.Create(CategoryName, WriteCounterName, "D:")).Returns(writeCounterD);
+ performanceCounterFactory.Setup(x => x.GetCategoryInstances(CategoryName)).Returns(["_Total", "C:", "D:"]);
+
+ _ = new WindowsDiskMetrics(
+ meterFactory,
+ performanceCounterFactory.Object,
+ fakeTimeProvider,
+ Options.Options.Create(options));
+ Meter meter = meterFactory.Meters.Single();
+
+ var readTag = new KeyValuePair("disk.io.direction", "read");
+ var writeTag = new KeyValuePair("disk.io.direction", "write");
+ var deviceTagC = new KeyValuePair("system.device", "C:");
+ var deviceTagD = new KeyValuePair("system.device", "D:");
+
+ using var operationCollector = new MetricCollector(meter, ResourceUtilizationInstruments.SystemDiskOperations);
+
+ // 1st measurement
+ fakeTimeProvider.Advance(TimeSpan.FromMinutes(1));
+ operationCollector.RecordObservableInstruments();
+ IReadOnlyList> measurements = operationCollector.GetMeasurementSnapshot();
+ Assert.Equal(4, measurements.Count);
+ Assert.Equal(60, measurements.Last(x => x.MatchesTags(readTag, deviceTagC)).Value); // 1 * 60 = 60
+ Assert.Equal(120, measurements.Last(x => x.MatchesTags(readTag, deviceTagD)).Value); // 2 * 60 = 120
+ Assert.Equal(600, measurements.Last(x => x.MatchesTags(writeTag, deviceTagC)).Value); // 10 * 60 = 600
+ Assert.Equal(1200, measurements.Last(x => x.MatchesTags(writeTag, deviceTagD)).Value); // 20 * 60 = 1200
+
+ // 2nd measurement
+ fakeTimeProvider.Advance(TimeSpan.FromMinutes(1));
+ operationCollector.RecordObservableInstruments();
+ measurements = operationCollector.GetMeasurementSnapshot();
+ Assert.Equal(150, measurements.Last(x => x.MatchesTags(readTag, deviceTagC)).Value); // 60 + 1.5 * 60 = 150
+ Assert.Equal(270, measurements.Last(x => x.MatchesTags(readTag, deviceTagD)).Value); // 120 + 2.5 * 60 = 270
+ Assert.Equal(1500, measurements.Last(x => x.MatchesTags(writeTag, deviceTagC)).Value); // 600 + 15 * 60 = 1500
+ Assert.Equal(2700, measurements.Last(x => x.MatchesTags(writeTag, deviceTagD)).Value); // 1200 + 25 * 60 = 2700
+
+ // 3rd measurement
+ fakeTimeProvider.Advance(TimeSpan.FromSeconds(30));
+ operationCollector.RecordObservableInstruments();
+ measurements = operationCollector.GetMeasurementSnapshot();
+ Assert.Equal(210, measurements.Last(x => x.MatchesTags(readTag, deviceTagC)).Value); // 150 + 2 * 30 = 210
+ Assert.Equal(360, measurements.Last(x => x.MatchesTags(readTag, deviceTagD)).Value); // 270 + 3 * 30 = 360
+ Assert.Equal(2100, measurements.Last(x => x.MatchesTags(writeTag, deviceTagC)).Value); // 1500 + 20 * 60 = 2100
+ Assert.Equal(3600, measurements.Last(x => x.MatchesTags(writeTag, deviceTagD)).Value); // 2700 + 30 * 60 = 3600
+
+ // 4th measurement
+ fakeTimeProvider.Advance(TimeSpan.FromMinutes(1));
+ operationCollector.RecordObservableInstruments();
+ measurements = operationCollector.GetMeasurementSnapshot();
+ Assert.Equal(360, measurements.Last(x => x.MatchesTags(readTag, deviceTagC)).Value); // 210 + 2.5 * 60 = 360
+ Assert.Equal(570, measurements.Last(x => x.MatchesTags(readTag, deviceTagD)).Value); // 360 + 3.5 * 60 = 570
+ Assert.Equal(3600, measurements.Last(x => x.MatchesTags(writeTag, deviceTagC)).Value); // 2100 + 25 * 60 = 3600
+ Assert.Equal(5700, measurements.Last(x => x.MatchesTags(writeTag, deviceTagD)).Value); // 3600 + 35 * 60 = 5700
+ }
+
+ [ConditionalFact]
+ public void DiskIoBytesMetricsTest()
+ {
+ using var meterFactory = new TestMeterFactory();
+ var performanceCounterFactory = new Mock();
+ var fakeTimeProvider = new FakeTimeProvider();
+ var options = new ResourceMonitoringOptions { EnableDiskIoMetrics = true };
+
+ // Set up
+ const string ReadCounterName = WindowsDiskPerfCounterNames.DiskReadBytesCounter;
+ const string WriteCounterName = WindowsDiskPerfCounterNames.DiskWriteBytesCounter;
+ var readCounterC = new FakePerformanceCounter("C:", [0, 10, 15, 20, 25]);
+ var readCounterD = new FakePerformanceCounter("D:", [0, 20, 25, 30, 35]);
+ performanceCounterFactory.Setup(x => x.Create(CategoryName, ReadCounterName, "C:")).Returns(readCounterC);
+ performanceCounterFactory.Setup(x => x.Create(CategoryName, ReadCounterName, "D:")).Returns(readCounterD);
+ var writeCounterC = new FakePerformanceCounter("C:", [0, 100, 150, 200, 250]);
+ var writeCounterD = new FakePerformanceCounter("D:", [0, 200, 250, 300, 350]);
+ performanceCounterFactory.Setup(x => x.Create(CategoryName, WriteCounterName, "C:")).Returns(writeCounterC);
+ performanceCounterFactory.Setup(x => x.Create(CategoryName, WriteCounterName, "D:")).Returns(writeCounterD);
+ performanceCounterFactory.Setup(x => x.GetCategoryInstances(CategoryName)).Returns(["_Total", "C:", "D:"]);
+
+ _ = new WindowsDiskMetrics(
+ meterFactory,
+ performanceCounterFactory.Object,
+ fakeTimeProvider,
+ Options.Options.Create(options));
+ Meter meter = meterFactory.Meters.Single();
+
+ var readTag = new KeyValuePair("disk.io.direction", "read");
+ var writeTag = new KeyValuePair("disk.io.direction", "write");
+ var deviceTagC = new KeyValuePair("system.device", "C:");
+ var deviceTagD = new KeyValuePair("system.device", "D:");
+
+ using var operationCollector = new MetricCollector(meter, ResourceUtilizationInstruments.SystemDiskIo);
+
+ // 1st measurement
+ fakeTimeProvider.Advance(TimeSpan.FromMinutes(1));
+ operationCollector.RecordObservableInstruments();
+ IReadOnlyList> measurements = operationCollector.GetMeasurementSnapshot();
+ Assert.Equal(4, measurements.Count);
+ Assert.Equal(600, measurements.Last(x => x.MatchesTags(readTag, deviceTagC)).Value); // 10 * 60 = 600
+ Assert.Equal(1200, measurements.Last(x => x.MatchesTags(readTag, deviceTagD)).Value); // 20 * 60 = 1200
+ Assert.Equal(6000, measurements.Last(x => x.MatchesTags(writeTag, deviceTagC)).Value); // 100 * 60 = 6000
+ Assert.Equal(12000, measurements.Last(x => x.MatchesTags(writeTag, deviceTagD)).Value); // 200 * 60 = 12000
+
+ // 2nd measurement
+ fakeTimeProvider.Advance(TimeSpan.FromMinutes(1));
+ operationCollector.RecordObservableInstruments();
+ measurements = operationCollector.GetMeasurementSnapshot();
+ Assert.Equal(1500, measurements.Last(x => x.MatchesTags(readTag, deviceTagC)).Value); // 600 + 15 * 60 = 1500
+ Assert.Equal(2700, measurements.Last(x => x.MatchesTags(readTag, deviceTagD)).Value); // 1200 + 25 * 60 = 2700
+ Assert.Equal(15000, measurements.Last(x => x.MatchesTags(writeTag, deviceTagC)).Value); // 6000 + 150 * 60 = 15000
+ Assert.Equal(27000, measurements.Last(x => x.MatchesTags(writeTag, deviceTagD)).Value); // 12000 + 250 * 60 = 27000
+
+ // 3rd measurement
+ fakeTimeProvider.Advance(TimeSpan.FromSeconds(30));
+ operationCollector.RecordObservableInstruments();
+ measurements = operationCollector.GetMeasurementSnapshot();
+ Assert.Equal(2100, measurements.Last(x => x.MatchesTags(readTag, deviceTagC)).Value); // 1500 + 20 * 30 = 210
+ Assert.Equal(3600, measurements.Last(x => x.MatchesTags(readTag, deviceTagD)).Value); // 2700 + 30 * 30 = 360
+ Assert.Equal(21000, measurements.Last(x => x.MatchesTags(writeTag, deviceTagC)).Value); // 15000 + 200 * 60 = 21000
+ Assert.Equal(36000, measurements.Last(x => x.MatchesTags(writeTag, deviceTagD)).Value); // 27000 + 300 * 60 = 36000
+
+ // 4th measurement
+ fakeTimeProvider.Advance(TimeSpan.FromMinutes(1));
+ operationCollector.RecordObservableInstruments();
+ measurements = operationCollector.GetMeasurementSnapshot();
+ Assert.Equal(3600, measurements.Last(x => x.MatchesTags(readTag, deviceTagC)).Value); // 2100 + 25 * 60 = 3600
+ Assert.Equal(5700, measurements.Last(x => x.MatchesTags(readTag, deviceTagD)).Value); // 3600 + 35 * 60 = 5700
+ Assert.Equal(36000, measurements.Last(x => x.MatchesTags(writeTag, deviceTagC)).Value); // 21000 + 250 * 60 = 36000
+ Assert.Equal(57000, measurements.Last(x => x.MatchesTags(writeTag, deviceTagD)).Value); // 36000 + 350 * 60 = 57000
+ }
+}
diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/FakePerformanceCounter.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/FakePerformanceCounter.cs
new file mode 100644
index 00000000000..967399a86cc
--- /dev/null
+++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/FakePerformanceCounter.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Test;
+
+public class FakePerformanceCounter(string instanceName, float[] values) : IPerformanceCounter
+{
+#pragma warning disable S3604 // Member initializer values should not be redundant
+ private readonly object _lock = new();
+#pragma warning restore S3604
+ private int _index;
+
+ public string InstanceName => instanceName;
+
+ public float NextValue()
+ {
+ lock (_lock)
+ {
+ if (_index >= values.Length)
+ {
+ throw new InvalidOperationException("No more values available.");
+ }
+
+ return values[_index++];
+ }
+ }
+}
diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/PerformanceCounterFactoryTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/PerformanceCounterFactoryTests.cs
new file mode 100644
index 00000000000..768fd268175
--- /dev/null
+++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/PerformanceCounterFactoryTests.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Versioning;
+using Microsoft.TestUtilities;
+using Xunit;
+
+namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Test;
+
+[SupportedOSPlatform("windows")]
+[OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "Windows specific.")]
+public class PerformanceCounterFactoryTests
+{
+ [ConditionalFact]
+ public void GetInstanceNameTest()
+ {
+ var performanceCounterFactory = new PerformanceCounterFactory();
+ IPerformanceCounter performanceCounter = performanceCounterFactory.Create("Processor", "% Processor Time", "_Total");
+
+ Assert.IsType(performanceCounter);
+ Assert.Equal("_Total", performanceCounter.InstanceName);
+ }
+}
diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/PerformanceCounterWrapperTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/PerformanceCounterWrapperTests.cs
new file mode 100644
index 00000000000..f27646ae327
--- /dev/null
+++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/PerformanceCounterWrapperTests.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Versioning;
+using Microsoft.TestUtilities;
+using Xunit;
+
+namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Test;
+
+[SupportedOSPlatform("windows")]
+[OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "Windows specific.")]
+public class PerformanceCounterWrapperTests
+{
+ [ConditionalFact]
+ public void GetInstanceNameTest()
+ {
+ var wrapper = new PerformanceCounterWrapper("Processor", "% Processor Time", "_Total");
+ Assert.Equal("_Total", wrapper.InstanceName);
+ }
+}