c# - tener - Contador de rendimiento: System.InvalidOperationException: la categoría no existe
system diagnostics c# example (5)
Tengo la siguiente clase que devuelve el número de solicitud actual por segundo de IIS. Llamo a RefreshCounters cada minuto para mantener actualizado el valor de Solicitudes por segundo (porque es promedio y si lo mantengo demasiado tiempo, el valor anterior influirá demasiado en el resultado) ... y cuando necesito mostrar RequestsPerSecond actual, llamo a esa propiedad.
public class Counters
{
private static PerformanceCounter pcReqsPerSec;
private const string counterKey = "Requests_Sec";
public static object RequestsPerSecond
{
get
{
lock (counterKey)
{
if (pcReqsPerSec != null)
return pcReqsPerSec.NextValue().ToString("N2"); // EXCEPTION
else
return "0";
}
}
}
internal static string RefreshCounters()
{
lock (counterKey)
{
try
{
if (pcReqsPerSec != null)
{
pcReqsPerSec.Dispose();
pcReqsPerSec = null;
}
pcReqsPerSec = new PerformanceCounter("W3SVC_W3WP", "Requests / Sec", "_Total", true);
pcReqsPerSec.NextValue();
PerformanceCounter.CloseSharedResources();
return null;
}
catch (Exception ex)
{
return ex.ToString();
}
}
}
}
El problema es que a veces se lanza una excepción siguiente:
System.InvalidOperationException: Category does not exist.
at System.Diagnostics.PerformanceCounterLib.GetCategorySample(String machine,/ String category)
at System.Diagnostics.PerformanceCounter.NextSample()
at System.Diagnostics.PerformanceCounter.NextValue()
at BidBop.Admin.PerfCounter.Counters.get_RequestsPerSecond() in [[[pcReqsPerSec.NextValue().ToString("N2");]]]
¿No estoy cerrando instancias previas de PerformanceCounter correctamente? ¿Qué estoy haciendo mal para que termine con esa excepción a veces?
EDITAR: Y solo para el registro, estoy organizando esta clase en el sitio web de IIS (es decir, por supuesto, alojado en el grupo de aplicaciones que tiene privilegios administrativos) y los métodos de invocación del servicio ASMX. El sitio que usa los valores de contador (los muestra) llama a RefreshCounters cada 1 minuto y RequestsPerSegundo cada 5 segundos; RequestPerSecond se almacena en caché entre llamadas.
Llamo a RefreshCounters cada 1 minuto porque los valores tienden a volverse "obsoletos", demasiado influenciados por los valores anteriores (que eran reales hace 1 minuto, por ejemplo).
Solo por curiosidad, ¿qué ha establecido para las propiedades en Visual Studio? En VS ve a Project Properties, Build, Platform target y AnyCPU
a AnyCPU
. Lo he visto antes, donde los Contadores de rendimiento no siempre se recuperan cuando está configurado en x86
, y cambiarlo a AnyCPU
podría solucionarlo.
No sé, si esto te pasa ... He leído el artículo Método PerformanceCounter.NextValue
Y hubo un comentario:
// If the category does not exist, create the category and exit.
// Performance counters should not be created and immediately used.
// There is a latency time to enable the counters, they should be created
// prior to executing the application that uses the counters.
// Execute this sample a second time to use the category.
Entonces, tengo una pregunta, que puede llevarme a responder: ¿no es demasiado pronto el llamado a un RequestsPerSecond? Además, te sugiero que pruebes si la categoría no existe y registra la información en alguna parte, para que podamos analizarla y determinar qué condiciones tenemos y con qué frecuencia ocurren.
Antenka te ha guiado en una buena dirección aquí. No debería deshacerse y volver a crear el contador de rendimiento en cada actualización / solicitud de valor. Hay un costo por instanciar los contadores de rendimiento y la primera lectura puede ser inexacta, como se indica en la siguiente cita. Además, tus instrucciones lock() { ... }
son muy amplias (cubren muchas declaraciones) y serán lentas. Es mejor tener tus bloqueos lo más pequeños posible. ¡Le estoy dando un voto a Antenka por la referencia de calidad y buenos consejos!
Sin embargo, creo que puedo ofrecerte una mejor respuesta. Tengo bastante experiencia en la supervisión del rendimiento del servidor y entiendo exactamente lo que necesita. Un problema que su código no tiene en cuenta es que cualquier código que muestre su contador de rendimiento (.aspx, .asmx, aplicación de consola, aplicación winform, etc.) podría solicitar esta estadística a cualquier velocidad; podría solicitarse una vez cada 10 segundos, tal vez 5 veces por segundo, usted no sabe y no debería importarle. Por lo tanto, debe separar el código de recopilación PerformanceCounter de la monitorización del código que realmente informa el valor Requests / Second actual. Y por motivos de rendimiento, también le mostraré cómo configurar el contador de rendimiento en la primera solicitud y luego lo mantendré en funcionamiento hasta que nadie haya hecho ninguna solicitud durante 5 segundos, luego cerrará / desechará el contador de rendimiento correctamente.
public class RequestsPerSecondCollector
{
#region General Declaration
//Static Stuff for the polling timer
private static System.Threading.Timer pollingTimer;
private static int stateCounter = 0;
private static int lockTimerCounter = 0;
//Instance Stuff for our performance counter
private static System.Diagnostics.PerformanceCounter pcReqsPerSec;
private readonly static object threadLock = new object();
private static decimal CurrentRequestsPerSecondValue;
private static int LastRequestTicks;
#endregion
#region Singleton Implementation
/// <summary>
/// Static members are ''eagerly initialized'', that is,
/// immediately when class is loaded for the first time.
/// .NET guarantees thread safety for static initialization.
/// </summary>
private static readonly RequestsPerSecondCollector _instance = new RequestsPerSecondCollector();
#endregion
#region Constructor/Finalizer
/// <summary>
/// Private constructor for static singleton instance construction, you won''t be able to instantiate this class outside of itself.
/// </summary>
private RequestsPerSecondCollector()
{
LastRequestTicks = System.Environment.TickCount;
// Start things up by making the first request.
GetRequestsPerSecond();
}
#endregion
#region Getter for current requests per second measure
public static decimal GetRequestsPerSecond()
{
if (pollingTimer == null)
{
Console.WriteLine("Starting Poll Timer");
// Let''s check the performance counter every 1 second, and don''t do the first time until after 1 second.
pollingTimer = new System.Threading.Timer(OnTimerCallback, null, 1000, 1000);
// The first read from a performance counter is notoriously inaccurate, so
OnTimerCallback(null);
}
LastRequestTicks = System.Environment.TickCount;
lock (threadLock)
{
return CurrentRequestsPerSecondValue;
}
}
#endregion
#region Polling Timer
static void OnTimerCallback(object state)
{
if (System.Threading.Interlocked.CompareExchange(ref lockTimerCounter, 1, 0) == 0)
{
if (pcReqsPerSec == null)
pcReqsPerSec = new System.Diagnostics.PerformanceCounter("W3SVC_W3WP", "Requests / Sec", "_Total", true);
if (pcReqsPerSec != null)
{
try
{
lock (threadLock)
{
CurrentRequestsPerSecondValue = Convert.ToDecimal(pcReqsPerSec.NextValue().ToString("N2"));
}
}
catch (Exception) {
// We had problem, just get rid of the performance counter and we''ll rebuild it next revision
if (pcReqsPerSec != null)
{
pcReqsPerSec.Close();
pcReqsPerSec.Dispose();
pcReqsPerSec = null;
}
}
}
stateCounter++;
//Check every 5 seconds or so if anybody is still monitoring the server PerformanceCounter, if not shut down our PerformanceCounter
if (stateCounter % 5 == 0)
{
if (System.Environment.TickCount - LastRequestTicks > 5000)
{
Console.WriteLine("Stopping Poll Timer");
pollingTimer.Dispose();
pollingTimer = null;
if (pcReqsPerSec != null)
{
pcReqsPerSec.Close();
pcReqsPerSec.Dispose();
pcReqsPerSec = null;
}
}
}
System.Threading.Interlocked.Add(ref lockTimerCounter, -1);
}
}
#endregion
}
Ok, ahora para alguna explicación.
- Primero notará que esta clase está diseñada para ser un singleton estático. No puede cargar múltiples copias de él, tiene un constructor privado y una instancia interna iniciada ansiosamente de sí mismo. Esto garantiza que no cree accidentalmente copias múltiples del mismo
PerformanceCounter
. - A continuación, se dará cuenta en el constructor privado (esto solo se ejecutará una vez cuando se acceda por primera vez a la clase) creamos el contador de
PerformanceCounter
y un temporizador que se utilizará para sondear el contador dePerformanceCounter
. - El método de devolución de llamada del temporizador creará
PerformanceCounter
si es necesario y obtendrá su próximo valor disponible. También cada 5 iteraciones vamos a ver cuánto tiempo ha pasado desde su última solicitud del valor delPerformanceCounter
. Si han pasado más de 5 segundos, apagaremos el temporizador de sondeo como innecesario en este momento. Siempre podemos volver a iniciarlo más tarde si lo necesitamos de nuevo. - Ahora tenemos un método estático llamado
GetRequestsPerSecond()
para llamar, que devolverá el valor actual de RequestsPerSecondPerformanceCounter
.
Los beneficios de esta implementación son que solo crea el contador de rendimiento una vez y luego continúa usando hasta que haya terminado con él. Es fácil de usar porque simplemente llama a RequestsPerSecondCollector.GetRequestsPerSecond()
desde donde lo necesite (.aspx, .asmx, aplicación de consola, aplicación de winforms, etc.). Siempre habrá solo un PerformanceCounter
y siempre se sondeará exactamente 1 vez por segundo, independientemente de la rapidez con la que llame a RequestsPerSecondCollector.GetRequestsPerSecond()
. También cerrará automáticamente y eliminará el PerformanceCounter
si no ha solicitado su valor en más de 5 segundos. Por supuesto, puede ajustar tanto el intervalo de temporizador como los milisegundos de tiempo de espera para satisfacer sus necesidades. Podrías sondear más rápido y agotar el tiempo de espera, digamos 60 segundos en lugar de 5. Elegí 5 segundos, ya que demuestra que funciona muy rápido mientras se depura en Visual Studio. Una vez que lo prueba y sabe que funciona, es posible que desee un tiempo de espera más largo.
Esperamos que esto te ayude no solo a utilizar PerformanceCounters, sino que también te sientas seguro al reutilizar esta clase que está separada de lo que quieras mostrar en las estadísticas. ¡El código reutilizable siempre es una ventaja!
EDITAR: como una pregunta de seguimiento, ¿qué pasa si desea realizar alguna tarea de limpieza o cuidado de niños cada 60 segundos mientras se ejecuta este contador de rendimiento? Bueno, ya tenemos el temporizador ejecutándose cada 1 segundo y una variable que rastrea nuestras iteraciones de bucle llamada stateCounter
que se incrementa en cada devolución de llamada del temporizador. Entonces podrías agregar un código como este:
// Every 60 seconds I want to close/dispose my PerformanceCounter
if (stateCounter % 60 == 0)
{
if (pcReqsPerSec != null)
{
pcReqsPerSec.Close();
pcReqsPerSec.Dispose();
pcReqsPerSec = null;
}
}
Debo señalar que este contador de rendimiento en el ejemplo no debería "ir añejo". Creo que ''Solicitud / Seg.'' Debería ser una estadística promedio y no una estadística de promedio móvil . Pero esta muestra simplemente ilustra una forma en que podría hacer cualquier tipo de limpieza o "cuidado de niños" de su PerformanceCounter
en un intervalo de tiempo regular. En este caso estamos cerrar y eliminar el contador de rendimiento que hará que se vuelva a crear en la próxima devolución de llamada del temporizador. Puede modificar esto para su caso de uso y según el contador de rendimiento específico que esté utilizando. La mayoría de las personas que leen esta pregunta / respuesta no deberían tener que hacer esto. la documentación de su PerformanceCounter deseado para ver si se trata de un conteo continuo, un promedio, una media móvil, etc. ... y ajustar su implementación de manera apropiada.
Acabo de resolver este tipo de error o excepción con:
Utilizando,
new PerformanceCounter("Processor Information", "% Processor Time", "_Total");
En lugar de,
new PerformanceCounter("Processor", "% Processor Time", "_Total");
Tuve un problema al recuperar solicitudes por segundo en IIS usando un código similar al siguiente
var pc = new PerformanceCounter();
pc.CategoryName = @"W3SVC_W3WP";
pc.InstanceName = @"_Total";
pc.CounterName = @"Requests / Sec";
Console.WriteLine(pc.NextValue());
Esto a veces arrojaba InvalidOperationException
y pude reproducir la excepción reiniciando IIS. Si corro con un IIS no calentado, por ejemplo, después de reiniciar un portátil o reiniciar IIS, entonces obtengo esta excepción. Acceda al sitio web primero, realice cualquier solicitud HTTP de antemano, y espere uno o dos segundos y no obtengo la excepción. Esto huele a que los contadores de rendimiento están guardados en la memoria caché, y cuando están inactivos, se vuelcan, y tardan un poco en volver a guardarlos en la memoria caché. (o similar).
Actualización1 : Inicialmente, cuando navego manualmente hacia el sitio web y lo preparo , soluciona el problema. Intenté calentar el servidor mediante programación con el new WebClient().DownloadString(); Thread.Sleep()
new WebClient().DownloadString(); Thread.Sleep()
hasta 3000ms y esto no funcionó? Por lo tanto, mis resultados de calentamiento manual del servidor podrían ser falsos positivos. Dejo mi respuesta aquí, porque podría ser la causa (es decir, el calentamiento manual), y tal vez alguien más pueda dar más detalles.
Actualización2 : Ah, vale, aquí hay algunas pruebas unitarias que resumen algunos aprendizajes de una mayor experimentación que hice ayer. (Por cierto, no hay mucho en google sobre este tema).
Por lo que puedo razonar, las siguientes afirmaciones pueden ser verdaderas; (y presento las pruebas unitarias debajo como evidencia.) Pude haber malinterpretado los resultados, así que por favor vuelva a verificar ;-D
Cree un contador de rendimiento y llame a getValue antes de que exista la categoría, por ejemplo, consultando un contador IIS, mientras IIS está frío y sin proceso en ejecución, lanzará la excepción InvalidOperation "la categoría no existe". (Supongo que esto es cierto para todos los contadores, y no solo para IIS).
Desde una prueba de unidad de Visual Studio, una vez que su contador arroja una excepción, si posteriormente calienta el servidor después de la primera excepción, y crea un nuevo PerformanceCounter y consulta de nuevo, ¡aún arrojará una excepción! (Esta fue una sorpresa, supongo que esto se debe a alguna acción única. Mis disculpas por no haber tenido tiempo suficiente para descompilar las fuentes para investigar más antes de publicar esta respuesta.)
En 2, si marca la prueba de unidad con
[STAThread]
, pude crear un nuevo PerformanceCounter después de que uno haya fallado. (¿Esto podría tener algo que ver con el contador de rendimiento posiblemente siendo singletons? Necesita más pruebas).No se requirió ninguna pausa antes de crear el contador y usarlo, a pesar de algunas advertencias en la misma documentación de MSDN, aparte del tiempo que lleva crear un contador de rendimiento antes de llamar a NextValue (). En mi caso, calentar el contador y poner la "categoría" en existencia, fue para mí disparar un tiro a través de la proa de IIS, es decir, hacer una sola solicitud GET, y viola, ya no obtener "InvalidOperationException", y esto parece ser una solución confiable para mí, para ahora. Al menos al consultar los contadores de rendimiento de IIS.
CreatingPerformanceCounterBeforeWarmingUpServerThrowsException
[Test, Ignore("Run manually AFTER restarting IIS with ''iisreset'' at cmd prompt.")]
public void CreatingPerformanceCounterBeforeWarmingUpServerThrowsException()
{
Console.WriteLine("Given a webserver that is cold");
Console.WriteLine("When I create a performance counter and read next value");
using (var pc1 = new PerformanceCounter())
{
pc1.CategoryName = @"W3SVC_W3WP";
pc1.InstanceName = @"_Total";
pc1.CounterName = @"Requests / Sec";
Action action1 = () => pc1.NextValue();
Console.WriteLine("Then InvalidOperationException will be thrown");
action1.ShouldThrow<InvalidOperationException>();
}
}
[Test, Ignore("Run manually AFTER restarting IIS with ''iisreset'' at cmd prompt.")]
public void CreatingPerformanceCounterAfterWarmingUpServerDoesNotThrowException()
{
Console.WriteLine("Given a webserver that has been Warmed up");
using (var client = new WebClient())
{
client.DownloadString("http://localhost:8082/small1.json");
}
Console.WriteLine("When I create a performance counter and read next value");
using (var pc2 = new PerformanceCounter())
{
pc2.CategoryName = @"W3SVC_W3WP";
pc2.InstanceName = @"_Total";
pc2.CounterName = @"Requests / Sec";
float? result = null;
Action action2 = () => result = pc2.NextValue();
Console.WriteLine("Then InvalidOperationException will not be thrown");
action2.ShouldNotThrow();
Console.WriteLine("And the counter value will be returned");
result.HasValue.Should().BeTrue();
}
}