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