Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,157 @@ public void WindowsPerformanceCounterReturnsTheExpectedSnapshotWhenARawStrategyI
Assert.AreEqual((double)captures.Last(), snapshot.Value);
}

[Test]
public void WindowsPerformanceCounterDisablesAfterMaxConsecutiveFailures()
{
this.performanceCounter.OnGetCounterValue = () => throw new InvalidOperationException("Instance '7' does not exist in the specified Category.");

for (int i = 0; i < WindowsPerformanceCounter.MaxConsecutiveFailures; i++)
{
Assert.Throws<InvalidOperationException>(() => this.performanceCounter.Capture());
}

Assert.IsTrue(this.performanceCounter.IsDisabled);
Assert.AreEqual(WindowsPerformanceCounter.MaxConsecutiveFailures, this.performanceCounter.ConsecutiveFailures);
}

[Test]
public void WindowsPerformanceCounterSkipsCaptureWhenDisabled()
{
this.performanceCounter.OnGetCounterValue = () => throw new InvalidOperationException("test");

// Disable via consecutive failures
for (int i = 0; i < WindowsPerformanceCounter.MaxConsecutiveFailures; i++)
{
Assert.Throws<InvalidOperationException>(() => this.performanceCounter.Capture());
}

Assert.IsTrue(this.performanceCounter.IsDisabled);

// Subsequent calls should return immediately without throwing
Assert.DoesNotThrow(() => this.performanceCounter.Capture());
}

[Test]
public void WindowsPerformanceCounterResetsFailureCountOnSuccess()
{
int callCount = 0;
this.performanceCounter.OnGetCounterValue = () =>
{
callCount++;
if (callCount <= 3)
{
throw new InvalidOperationException("transient");
}

return 42.0f;
};

// 3 failures — not yet disabled
for (int i = 0; i < 3; i++)
{
Assert.Throws<InvalidOperationException>(() => this.performanceCounter.Capture());
}

Assert.IsFalse(this.performanceCounter.IsDisabled);
Assert.AreEqual(3, this.performanceCounter.ConsecutiveFailures);

// 1 success — resets counter
this.performanceCounter.Capture();
Assert.IsFalse(this.performanceCounter.IsDisabled);
Assert.AreEqual(0, this.performanceCounter.ConsecutiveFailures);
Assert.IsNull(this.performanceCounter.LastError);
}

[Test]
public void WindowsPerformanceCounterResetDisabledStateAllowsRetry()
{
this.performanceCounter.OnGetCounterValue = () => throw new InvalidOperationException("broken");

// Disable the counter
for (int i = 0; i < WindowsPerformanceCounter.MaxConsecutiveFailures; i++)
{
Assert.Throws<InvalidOperationException>(() => this.performanceCounter.Capture());
}

Assert.IsTrue(this.performanceCounter.IsDisabled);

// Reset — should allow retrying
this.performanceCounter.ResetDisabledState();
Assert.IsFalse(this.performanceCounter.IsDisabled);
Assert.AreEqual(0, this.performanceCounter.ConsecutiveFailures);

// Will throw again since the underlying issue persists
Assert.Throws<InvalidOperationException>(() => this.performanceCounter.Capture());
}

[Test]
public void WindowsPerformanceCounterStoresLastError()
{
var expectedException = new InvalidOperationException("Instance '42' does not exist");
this.performanceCounter.OnGetCounterValue = () => throw expectedException;

Assert.Throws<InvalidOperationException>(() => this.performanceCounter.Capture());
Assert.AreEqual(expectedException, this.performanceCounter.LastError);
}

[Test]
[TestCase(CaptureStrategy.Max, 500)]
[TestCase(CaptureStrategy.Min, 100)]
public void WindowsPerformanceCounterSnapshotReturnsExpectedValueForMaxAndMinStrategies(CaptureStrategy strategy, float expected)
{
float[] values = new float[] { 100, 500, 300 };
int captureIndex = 0;
var counter = new TestWindowsPerformanceCounter("Cat", "Cnt", "Inst", strategy);
counter.OnGetCounterValue = () =>
{
float val = values[captureIndex];
captureIndex++;
return val;
};

foreach (var _ in values)
{
counter.Capture();
}

Metric snapshot = counter.Snapshot();
Assert.AreEqual(expected, snapshot.Value);
Assert.AreEqual(@"\Cat(Inst)\Cnt", snapshot.Name);
}

[Test]
public void WindowsPerformanceCounterSnapshotReturnsNoneWhenEmptyAndResetClearsState()
{
// Snapshot with no captures returns Metric.None
Metric noData = this.performanceCounter.Snapshot();
Assert.AreEqual(Metric.None, noData);

// Capture values, verify they exist
this.performanceCounter.OnGetCounterValue = () => 42.0f;
this.performanceCounter.Capture();
this.performanceCounter.Capture();
Assert.AreEqual(2, this.performanceCounter.CounterValues.Count());

// Reset clears all values
this.performanceCounter.Reset();
Assert.IsEmpty(this.performanceCounter.CounterValues);

// After reset, snapshot returns None again
Assert.AreEqual(Metric.None, this.performanceCounter.Snapshot());

// GetCounterName formats correctly with and without instance
Assert.AreEqual(@"\Cat\Cnt", WindowsPerformanceCounter.GetCounterName("Cat", "Cnt"));
Assert.AreEqual(@"\Cat(Inst)\Cnt", WindowsPerformanceCounter.GetCounterName("Cat", "Cnt", "Inst"));

// ToString returns MetricName
Assert.AreEqual(this.performanceCounter.MetricName, this.performanceCounter.ToString());

// MetricRelativity constructor
var relCounter = new WindowsPerformanceCounter("A", "B", CaptureStrategy.Average, MetricRelativity.HigherIsBetter);
Assert.AreEqual(MetricRelativity.HigherIsBetter, relCounter.MetricRelativity);
}

private class TestWindowsPerformanceCounter : WindowsPerformanceCounter
{
public TestWindowsPerformanceCounter(string category, string name, string instance, CaptureStrategy captureStrategy)
Expand All @@ -147,7 +298,7 @@ protected override bool TryGetCounterValue(out float? value)
if (this.OnGetCounterValue != null)
{
value = this.OnGetCounterValue.Invoke();
return true;
return value != null;
}
else
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace VirtualClient
{
using System.Collections.Generic;
using NUnit.Framework;
using VirtualClient.Contracts;

[TestFixture]
[Category("Unit")]
public class WmiPerformanceCounterProviderTests
{
[Test]
public void WmiPerformanceCounterProviderStaticMappingMethodsMapCorrectly()
{
// Forward mapping — known counters
Assert.AreEqual("PercentProcessorTime", WmiPerformanceCounterProvider.MapCounterNameToWmiProperty("% Processor Time"));
Assert.AreEqual("PercentUserTime", WmiPerformanceCounterProvider.MapCounterNameToWmiProperty("% User Time"));
Assert.AreEqual("PercentPrivilegedTime", WmiPerformanceCounterProvider.MapCounterNameToWmiProperty("% Privileged Time"));
Assert.AreEqual("PercentIdleTime", WmiPerformanceCounterProvider.MapCounterNameToWmiProperty("% Idle Time"));
Assert.AreEqual("PercentInterruptTime", WmiPerformanceCounterProvider.MapCounterNameToWmiProperty("% Interrupt Time"));
Assert.AreEqual("PercentDPCTime", WmiPerformanceCounterProvider.MapCounterNameToWmiProperty("% DPC Time"));
Assert.AreEqual("PercentC1Time", WmiPerformanceCounterProvider.MapCounterNameToWmiProperty("% C1 Time"));
Assert.AreEqual("PercentC2Time", WmiPerformanceCounterProvider.MapCounterNameToWmiProperty("% C2 Time"));
Assert.AreEqual("PercentC3Time", WmiPerformanceCounterProvider.MapCounterNameToWmiProperty("% C3 Time"));
Assert.AreEqual("InterruptsPersec", WmiPerformanceCounterProvider.MapCounterNameToWmiProperty("Interrupts/sec"));
Assert.AreEqual("DPCsQueuedPersec", WmiPerformanceCounterProvider.MapCounterNameToWmiProperty("DPCs Queued/sec"));
Assert.AreEqual("DPCRate", WmiPerformanceCounterProvider.MapCounterNameToWmiProperty("DPC Rate"));
Assert.AreEqual("C1TransitionsPersec", WmiPerformanceCounterProvider.MapCounterNameToWmiProperty("C1 Transitions/sec"));
Assert.AreEqual("C2TransitionsPersec", WmiPerformanceCounterProvider.MapCounterNameToWmiProperty("C2 Transitions/sec"));
Assert.AreEqual("C3TransitionsPersec", WmiPerformanceCounterProvider.MapCounterNameToWmiProperty("C3 Transitions/sec"));
Assert.AreEqual("PercentProcessorPerformance", WmiPerformanceCounterProvider.MapCounterNameToWmiProperty("% Processor Performance"));
Assert.AreEqual("PercentProcessorUtility", WmiPerformanceCounterProvider.MapCounterNameToWmiProperty("% Processor Utility"));
Assert.AreEqual("ProcessorFrequency", WmiPerformanceCounterProvider.MapCounterNameToWmiProperty("Processor Frequency"));
Assert.AreEqual("PercentofMaximumFrequency", WmiPerformanceCounterProvider.MapCounterNameToWmiProperty("% of Maximum Frequency"));

// Forward mapping — default fallback (spaces/special chars replaced)
Assert.AreEqual("SomeCustomCounter", WmiPerformanceCounterProvider.MapCounterNameToWmiProperty("Some Custom Counter"));

// Reverse mapping — known properties
Assert.AreEqual("% Processor Time", WmiPerformanceCounterProvider.MapWmiPropertyToCounterName("PercentProcessorTime"));
Assert.AreEqual("% User Time", WmiPerformanceCounterProvider.MapWmiPropertyToCounterName("PercentUserTime"));
Assert.AreEqual("% Privileged Time", WmiPerformanceCounterProvider.MapWmiPropertyToCounterName("PercentPrivilegedTime"));
Assert.AreEqual("% Idle Time", WmiPerformanceCounterProvider.MapWmiPropertyToCounterName("PercentIdleTime"));
Assert.AreEqual("% Interrupt Time", WmiPerformanceCounterProvider.MapWmiPropertyToCounterName("PercentInterruptTime"));
Assert.AreEqual("% DPC Time", WmiPerformanceCounterProvider.MapWmiPropertyToCounterName("PercentDPCTime"));
Assert.AreEqual("% C1 Time", WmiPerformanceCounterProvider.MapWmiPropertyToCounterName("PercentC1Time"));
Assert.AreEqual("% C2 Time", WmiPerformanceCounterProvider.MapWmiPropertyToCounterName("PercentC2Time"));
Assert.AreEqual("% C3 Time", WmiPerformanceCounterProvider.MapWmiPropertyToCounterName("PercentC3Time"));
Assert.AreEqual("Interrupts/sec", WmiPerformanceCounterProvider.MapWmiPropertyToCounterName("InterruptsPersec"));
Assert.AreEqual("DPCs Queued/sec", WmiPerformanceCounterProvider.MapWmiPropertyToCounterName("DPCsQueuedPersec"));
Assert.AreEqual("DPC Rate", WmiPerformanceCounterProvider.MapWmiPropertyToCounterName("DPCRate"));
Assert.AreEqual("C1 Transitions/sec", WmiPerformanceCounterProvider.MapWmiPropertyToCounterName("C1TransitionsPersec"));
Assert.AreEqual("C2 Transitions/sec", WmiPerformanceCounterProvider.MapWmiPropertyToCounterName("C2TransitionsPersec"));
Assert.AreEqual("C3 Transitions/sec", WmiPerformanceCounterProvider.MapWmiPropertyToCounterName("C3TransitionsPersec"));
Assert.AreEqual("% Processor Performance", WmiPerformanceCounterProvider.MapWmiPropertyToCounterName("PercentProcessorPerformance"));
Assert.AreEqual("% Processor Utility", WmiPerformanceCounterProvider.MapWmiPropertyToCounterName("PercentProcessorUtility"));
Assert.AreEqual("Processor Frequency", WmiPerformanceCounterProvider.MapWmiPropertyToCounterName("ProcessorFrequency"));
Assert.AreEqual("% of Maximum Frequency", WmiPerformanceCounterProvider.MapWmiPropertyToCounterName("PercentofMaximumFrequency"));

// Reverse mapping — default passthrough for unknown properties
Assert.AreEqual("UnknownProp", WmiPerformanceCounterProvider.MapWmiPropertyToCounterName("UnknownProp"));

// GetWmiClassName — supported categories
Assert.AreEqual("Win32_PerfFormattedData_Counters_ProcessorInformation", WmiPerformanceCounterProvider.GetWmiClassName("Processor"));
Assert.AreEqual("Win32_PerfFormattedData_Counters_ProcessorInformation", WmiPerformanceCounterProvider.GetWmiClassName("Processor Information"));

// GetWmiClassName — unsupported categories return null
Assert.IsNull(WmiPerformanceCounterProvider.GetWmiClassName("Memory"));
Assert.IsNull(WmiPerformanceCounterProvider.GetWmiClassName("PhysicalDisk"));
}

[Test]
public void WmiPerformanceCounterProviderConstructorAndSnapshotWorkCorrectly()
{
using (var provider = new WmiPerformanceCounterProvider("Processor", "% Processor Time", "_Total", CaptureStrategy.Average))
{
// Constructor sets all properties
Assert.AreEqual("Processor", provider.Category);
Assert.AreEqual("% Processor Time", provider.Name);
Assert.AreEqual("_Total", provider.InstanceName);
Assert.AreEqual(CaptureStrategy.Average, provider.Strategy);
Assert.AreEqual(@"\Processor(_Total)\% Processor Time", provider.MetricName);
Assert.AreEqual(MetricRelativity.Undefined, provider.MetricRelativity);
Assert.IsFalse(provider.IsDisabled);

// Snapshot with no captures returns Metric.None
Metric noData = provider.Snapshot();
Assert.AreEqual(Metric.None, noData);

// Reset does not throw
Assert.DoesNotThrow(() => provider.Reset());

// QueryAllInstances with unsupported category returns empty dictionary
Dictionary<string, Dictionary<string, float>> empty = WmiPerformanceCounterProvider.QueryAllInstances("Memory");
Assert.IsEmpty(empty);
}
}
}
}
51 changes: 51 additions & 0 deletions src/VirtualClient/VirtualClient.Core/WindowsPerformanceCounter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,19 @@ namespace VirtualClient
/// </summary>
public class WindowsPerformanceCounter : IPerformanceMetric, IDisposable
{
/// <summary>
/// The maximum number of consecutive capture failures before the counter is disabled.
/// </summary>
public const int MaxConsecutiveFailures = 5;

private PerformanceCounter counter;
private ConcurrentBag<float> counterValues;
private SemaphoreSlim semaphore;
private DateTime? captureStartTime;
private DateTime nextCounterVerificationTime;
private bool disposed;
private int consecutiveFailures;
private Exception lastError;

/// <summary>
/// Intialize a new instance of the <see cref="WindowsPerformanceCounter"/> class.
Expand Down Expand Up @@ -156,6 +163,22 @@ public WindowsPerformanceCounter(string counterCategory, string counterName, str
/// </summary>
public CaptureStrategy Strategy { get; }

/// <summary>
/// Gets a value indicating whether this counter has been disabled due to
/// repeated consecutive capture failures.
/// </summary>
public bool IsDisabled { get; private set; }

/// <summary>
/// Gets the number of consecutive capture failures.
/// </summary>
public int ConsecutiveFailures => this.consecutiveFailures;

/// <summary>
/// Gets the last error that occurred during capture.
/// </summary>
public Exception LastError => this.lastError;

/// <summary>
/// The set of counter values that have been captured during the current
/// interval.
Expand Down Expand Up @@ -191,26 +214,54 @@ public static string GetCounterName(string category, string counterName, string
/// <inheritdoc />
public void Capture()
{
if (this.IsDisabled)
{
return;
}

try
{
this.semaphore.Wait(CancellationToken.None);

if (this.TryGetCounterValue(out float? counterValue))
{
this.counterValues.Add(counterValue.Value);
Interlocked.Exchange(ref this.consecutiveFailures, 0);
this.lastError = null;

if (this.captureStartTime == null)
{
this.captureStartTime = DateTime.UtcNow;
}
}
}
catch (Exception exc)
{
this.lastError = exc;
int failures = Interlocked.Increment(ref this.consecutiveFailures);
if (failures >= WindowsPerformanceCounter.MaxConsecutiveFailures)
{
this.IsDisabled = true;
}

throw;
}
finally
{
this.semaphore.Release();
}
}

/// <summary>
/// Resets the disabled state so the counter can be retried.
/// </summary>
public void ResetDisabledState()
{
this.IsDisabled = false;
Interlocked.Exchange(ref this.consecutiveFailures, 0);
this.lastError = null;
}

/// <summary>
/// Disposes of resources used by the instance.
/// </summary>
Expand Down
Loading