EventCounters de .NET

Este artículo se aplica a: ✔️ SDK de .NET Core 3.0 y versiones posteriores

Los EventCounters son API de .NET que se usan para la recopilación ligera, multiplataforma y casi en tiempo real de métricas de rendimiento. Los EventCounters se agregaron como una alternativa multiplataforma a los "contadores de rendimiento" de .NET Framework en Windows. En este artículo se aprende qué son los EventCounters, cómo se implementan y cómo se usan.

El entorno de ejecución de .NET y algunas bibliotecas de .NET publican información de diagnóstico básica mediante EventCounters a partir de .NET Core 3.0. Además de los EventCounters proporcionados por el entorno de ejecución de .NET, puede implementar sus propios EventCounters. Los EventCounters se pueden usar para realizar el seguimiento de diversas métricas. Obtenga más información en EventCounters conocidos en .NET.

Los EventCounters existen como parte de EventSource y se insertan automáticamente en las herramientas de escucha de forma periódica. Al igual que todos los demás eventos de EventSource, se pueden consumir en proceso y fuera de proceso mediante EventListener y EventPipe. Este artículo se centra en las capacidades multiplataforma de los EventCounters y excluye de forma deliberada a PerfView y ETW (seguimiento de eventos para Windows), aunque ambos se pueden usar con los EventCounters.

Imagen de diagrama de EventCounters en proceso y fuera de proceso

Información general sobre la API EventCounter

Hay dos categorías principales de EventCounters. Algunos contadores son para valores de tipo "tasa", como el número total de excepciones, el número total de GC y el número total de solicitudes. Otros contadores son valores de tipo "instantánea", como el uso del montón, el uso de la CPU y el tamaño del espacio de trabajo. Dentro de cada una de estas categorías de contadores, hay dos tipos de contadores que varían según el modo en que obtienen su valor. Los contadores de sondeo recuperan su valor por medio de una devolución de llamada, mientras que los contadores que no son de sondeo tienen sus valores establecidos directamente en la instancia de contador.

Los contadores se representan mediante las siguientes implementaciones:

Una escucha de eventos especifica la duración de los intervalos de medición. Al final de cada intervalo se transmite un valor a la escucha de cada contador. Las implementaciones de un contador determinan las API y los cálculos que se usan para generar el valor de cada intervalo.

  • EventCounter registra un conjunto de valores. El método EventCounter.WriteMetric agrega un nuevo valor al conjunto. Con cada intervalo se calcula un resumen estadístico del conjunto, como valor mínimo, máximo y medio. La herramienta dotnet-counters siempre muestra el valor medio. EventCounter es útil para describir un conjunto discreto de operaciones. Su uso habitual puede incluir la supervisión del tamaño medio en bytes de operaciones de E/S recientes o el valor monetario medio de un conjunto de transacciones financieras.

  • IncrementingEventCounter registra un total acumulado de cada intervalo de tiempo. El método IncrementingEventCounter.Increment agrega al total. Por ejemplo, si se llama a Increment() tres veces durante un intervalo con valores 1, 2 y 5, el total acumulado de 8 se comunica como valor del contador de este intervalo. La herramienta dotnet-counters muestra la tasa como el total / tiempo registrado. IncrementingEventCounter resulta útil para medir la frecuencia con que se produce una acción, como el número de solicitudes procesadas por segundo.

  • PollingCounter usa una devolución de llamada para determinar el valor que se comunica. Con cada intervalo de tiempo se invoca a la función de devolución de llamada proporcionada por el usuario y se usa el valor devuelto como valor del contador. PollingCounter se puede usar para consultar una métrica de un origen externo, por ejemplo, para obtener los bytes libres actuales en un disco. También se puede usar para notificar estadísticas personalizadas que una aplicación puede calcular a petición. Los ejemplos incluyen los informes del percentil 95 de las latencias de solicitud recientes o la proporción de aciertos o errores actual de una caché.

  • IncrementingPollingCounter usa una devolución de llamada para determinar el valor de incremento comunicado. Con cada intervalo de tiempo se invoca a la devolución de llamada y la diferencia entre la invocación actual y la última es el valor comunicado. La herramienta dotnet-counters siempre muestra la diferencia como una tasa, el valor / tiempo comunicado. Este contador es útil cuando no resulta factible llamar a una API en cada repetición, pero es posible consultar el número total de repeticiones. Por ejemplo, puede notificar el número de bytes escritos en un archivo por segundo, incluso sin una notificación cada vez que se escribe un byte.

Implementación de un EventSource

En el código siguiente se implementa un ejemplo EventSource expuesto como proveedor "Sample.EventCounter.Minimal" con nombre. Este origen contiene un EventCounter que representa el tiempo de procesamiento de la solicitud. Un contador de este tipo tiene un nombre (es decir, su identificador único en el origen) y un nombre para mostrar, ambos usados por herramientas de escucha como dotnet-counters.

using System.Diagnostics.Tracing;

[EventSource(Name = "Sample.EventCounter.Minimal")]
public sealed class MinimalEventCounterSource : EventSource
{
    public static readonly MinimalEventCounterSource Log = new MinimalEventCounterSource();

    private EventCounter _requestCounter;

    private MinimalEventCounterSource() =>
        _requestCounter = new EventCounter("request-time", this)
        {
            DisplayName = "Request Processing Time",
            DisplayUnits = "ms"
        };

    public void Request(string url, long elapsedMilliseconds)
    {
        WriteEvent(1, url, elapsedMilliseconds);
        _requestCounter?.WriteMetric(elapsedMilliseconds);
    }

    protected override void Dispose(bool disposing)
    {
        _requestCounter?.Dispose();
        _requestCounter = null;

        base.Dispose(disposing);
    }
}

Use dotnet-counters ps para mostrar una lista de los procesos de .NET que se pueden supervisar:

dotnet-counters ps
   1398652 dotnet     C:\Program Files\dotnet\dotnet.exe
   1399072 dotnet     C:\Program Files\dotnet\dotnet.exe
   1399112 dotnet     C:\Program Files\dotnet\dotnet.exe
   1401880 dotnet     C:\Program Files\dotnet\dotnet.exe
   1400180 sample-counters C:\sample-counters\bin\Debug\netcoreapp3.1\sample-counters.exe

Pase el nombre EventSource a la opción --counters para empezar a supervisar el contador:

dotnet-counters monitor --process-id 1400180 --counters Sample.EventCounter.Minimal

En el siguiente ejemplo se muestra la salida de la supervisión:

Press p to pause, r to resume, q to quit.
    Status: Running

[Samples-EventCounterDemos-Minimal]
    Request Processing Time (ms)                            0.445

Presione q para detener el comando de supervisión.

Contadores condicionales

Al implementar un EventSource, se puede crear de forma condicional una instancia de los contadores contenedores al llamar al método EventSource.OnEventCommand con un valor Command de EventCommand.Enable. Para crear una instancia de un contador de forma segura solo si es null, use el operador de asignación de uso combinado de NULL. Además, los métodos personalizados pueden evaluar el método IsEnabled para determinar si el origen del evento actual está habilitado o no.

using System.Diagnostics.Tracing;

[EventSource(Name = "Sample.EventCounter.Conditional")]
public sealed class ConditionalEventCounterSource : EventSource
{
    public static readonly ConditionalEventCounterSource Log = new ConditionalEventCounterSource();

    private EventCounter _requestCounter;

    private ConditionalEventCounterSource() { }

    protected override void OnEventCommand(EventCommandEventArgs args)
    {
        if (args.Command == EventCommand.Enable)
        {
            _requestCounter ??= new EventCounter("request-time", this)
            {
                DisplayName = "Request Processing Time",
                DisplayUnits = "ms"
            };
        }
    }

    public void Request(string url, float elapsedMilliseconds)
    {
        if (IsEnabled())
        {
            _requestCounter?.WriteMetric(elapsedMilliseconds);
        }
    }

    protected override void Dispose(bool disposing)
    {
        _requestCounter?.Dispose();
        _requestCounter = null;

        base.Dispose(disposing);
    }
}

Sugerencia

Los contadores condicionales son contadores de los que se crean instancias de forma condicional, una microoptimización. El entorno de ejecución adopta este patrón para los escenarios en los que normalmente no se usan contadores, para ahorrar una fracción de un milisegundo.

Contadores de ejemplo del entorno de ejecución de .NET Core

Hay muchas implementaciones de ejemplo excelentes del entorno de ejecución de .NET Core. Esta es la implementación del entorno de ejecución del contador que realiza el seguimiento del tamaño del espacio de trabajo de la aplicación.

var workingSetCounter = new PollingCounter(
    "working-set",
    this,
    () => (double)(Environment.WorkingSet / 1_000_000))
{
    DisplayName = "Working Set",
    DisplayUnits = "MB"
};

PollingCounter comunica la cantidad actual de memoria física asignada al proceso (espacio de trabajo) de la aplicación, ya que captura una métrica en un momento dado. La devolución de llamada del sondeo de un valor es la expresión lambda proporcionada, que es simplemente una llamada a la API System.Environment.WorkingSet. DisplayName y DisplayUnits son propiedades opcionales que se pueden establecer para ayudar al lado del consumidor del contador a mostrar el valor con más claridad. Por ejemplo, dotnet-counters usa estas propiedades para mostrar la versión más descriptiva de los nombres de contador.

Importante

Las propiedades DisplayName no están traducidas.

En el caso de PollingCounter e IncrementingPollingCounter, no es necesario hacer nada más. Ambos sondean los valores por sí mismos en un intervalo solicitado por el consumidor.

Este es un ejemplo de un contador de entorno de ejecución implementado mediante IncrementingPollingCounter.

var monitorContentionCounter = new IncrementingPollingCounter(
    "monitor-lock-contention-count",
    this,
    () => Monitor.LockContentionCount
)
{
    DisplayName = "Monitor Lock Contention Count",
    DisplayRateTimeScale = TimeSpan.FromSeconds(1)
};

IncrementingPollingCounter usa la API Monitor.LockContentionCount para comunicar el incremento del recuento total de contenciones de bloqueo. La propiedad DisplayRateTimeScale es opcional pero, cuando se usa, puede proporcionar una sugerencia sobre el intervalo de tiempo en el que se muestra mejor el contador. Por ejemplo, el recuento de contenciones de bloqueo se muestra mejor como recuento por segundo, por lo que DisplayRateTimeScale se establece en un segundo. La tasa de presentación se puede ajustar a diferentes tipos de contadores de tasa.

Nota:

dotnet-countersno usa DisplayRateTimeScale, y no es necesario que las escuchas de eventos lo usen.

Hay más implementaciones de contador que se pueden usar como referencia en el repositorio del entorno de ejecución de .NET.

Simultaneidad

Sugerencia

La API de EventCounters no garantiza la seguridad para subprocesos. Cuando varios subprocesos llaman a los delegados pasados a instancias PollingCounter o IncrementingPollingCounter, es responsabilidad suya garantizar la seguridad para subprocesos de los delegados.

Por ejemplo, considere el siguiente EventSource para realizar un seguimiento de las solicitudes.

using System;
using System.Diagnostics.Tracing;

public class RequestEventSource : EventSource
{
    public static readonly RequestEventSource Log = new RequestEventSource();

    private IncrementingPollingCounter _requestRateCounter;
    private long _requestCount = 0;

    private RequestEventSource() =>
        _requestRateCounter = new IncrementingPollingCounter("request-rate", this, () => _requestCount)
        {
            DisplayName = "Request Rate",
            DisplayRateTimeScale = TimeSpan.FromSeconds(1)
        };

    public void AddRequest() => ++ _requestCount;

    protected override void Dispose(bool disposing)
    {
        _requestRateCounter?.Dispose();
        _requestRateCounter = null;

        base.Dispose(disposing);
    }
}

Se puede llamar al método AddRequest() desde un controlador de solicitudes y RequestRateCounter sondea el valor en el intervalo especificado por el consumidor del contador. Pero varios subprocesos pueden llamar al método AddRequest() a la vez, con lo que se coloca una condición de carrera sobre _requestCount. Una manera alternativa segura para subprocesos de incrementar _requestCount es usar Interlocked.Increment.

public void AddRequest() => Interlocked.Increment(ref _requestCount);

Para evitar lecturas rasgadas (en arquitecturas de 32 bits) del campo _requestCount de tipo long, use Interlocked.Read.

_requestRateCounter = new IncrementingPollingCounter("request-rate", this, () => Interlocked.Read(ref _requestCount))
{
    DisplayName = "Request Rate",
    DisplayRateTimeScale = TimeSpan.FromSeconds(1)
};

Uso de EventCounters

Hay dos formas principales de usar EventCounters: en proceso o fuera de proceso. El uso de EventCounters se puede clasificar en tres capas de distintas tecnologías de uso.

  • Transporte de eventos en una secuencia sin formato a través de ETW o EventPipe:

    Las API de ETW están incluidas en el sistema operativo Windows y se puede acceder a EventPipe como una API de .NET o el protocolo IPC de diagnóstico.

  • Descodificación del flujo de eventos binario en eventos:

    La biblioteca TraceEvent controla los formatos de flujo de ETW y EventPipe.

  • Herramientas de línea de comandos y GUI:

    Herramientas como PerfView (ETW o EventPipe), dotnet-counters (solo EventPipe) y dotnet-monitor (solo EventPipe).

Uso fuera de proceso

El uso de EventCounters fuera de proceso es un método habitual. Puede usar dotnet-counters para consumirlos a modo multiplataforma a través de un EventPipe. La herramienta dotnet-counters es una herramienta global de CLI de dotnet multiplataforma que se puede usar para supervisar los valores de los contadores. Para obtener información sobre cómo usar dotnet-counters para supervisar los contadores, vea dotnet-counters o trabaje con el tutorial Medición del rendimiento mediante EventCounters.

dotnet-trace

La herramienta dotnet-trace se puede usar para consumir los datos del contador a través de EventPipe. Este es un ejemplo de uso de dotnet-trace para recopilar datos del contador.

dotnet-trace collect --process-id <pid> Sample.EventCounter.Minimal:0:0:EventCounterIntervalSec=1

Para obtener más información sobre cómo recopilar valores de un contador a lo largo del tiempo, vea la documentación de dotnet-trace.

Azure Application Insights

Azure Monitor puede usar EventCounters, en concreto Azure Application Insights. Los contadores se pueden agregar y quitar; además, el usuario puede especificar contadores personalizados o contadores conocidos. Para obtener más información, vea Personalización de los contadores que se van a recopilar.

dotnet-monitor

La herramienta dotnet-monitor facilita el acceso a los diagnósticos desde un proceso de .NET de forma remota y automatizada. Además de ofrecer seguimientos, permite supervisar métricas, así como recopilar volcados de memoria y de memoria GC. Se distribuye tanto como herramienta de la CLI como imagen de Docker. Expone una API de REST, y la recopilación de artefactos de diagnóstico se produce a través de llamadas REST.

Para obtener más información, consulte dotnet-monitor.

Uso en proceso

Puede usar los valores de un contador por medio de la API EventListener. EventListener es una forma en proceso de usar los eventos escritos por todas las instancias de un EventSource en la aplicación. Para obtener más información sobre cómo usar la API EventListener, vea EventListener.

En primer lugar, es necesario habilitar el EventSource que genera el valor del contador. Invalide el método EventListener.OnEventSourceCreated para obtener una notificación cuando se cree un EventSource y, si es el EventSource correcto con los EventCounters, puede llamar a EventListener.EnableEvents en él. Esta es una invalidación de ejemplo:

protected override void OnEventSourceCreated(EventSource source)
{
    if (!source.Name.Equals("System.Runtime"))
    {
        return;
    }

    EnableEvents(source, EventLevel.Verbose, EventKeywords.All, new Dictionary<string, string>()
    {
        ["EventCounterIntervalSec"] = "1"
    });
}

Código de ejemplo

Esta es una clase EventListener de ejemplo que imprime todos los nombres y valores de contador de EventSource del entorno de ejecución de .NET para publicar sus contadores internos (System.Runtime) cada segundo.

using System;
using System.Collections.Generic;
using System.Diagnostics.Tracing;

public class SimpleEventListener : EventListener
{
    public SimpleEventListener()
    {
    }

    protected override void OnEventSourceCreated(EventSource source)
    {
        if (!source.Name.Equals("System.Runtime"))
        {
            return;
        }

        EnableEvents(source, EventLevel.Verbose, EventKeywords.All, new Dictionary<string, string>()
        {
            ["EventCounterIntervalSec"] = "1"
        });
    }

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        if (!eventData.EventName.Equals("EventCounters"))
        {
            return;
        }

        for (int i = 0; i < eventData.Payload.Count; ++ i)
        {
            if (eventData.Payload[i] is IDictionary<string, object> eventPayload)
            {
                var (counterName, counterValue) = GetRelevantMetric(eventPayload);
                Console.WriteLine($"{counterName} : {counterValue}");
            }
        }
    }

    private static (string counterName, string counterValue) GetRelevantMetric(
        IDictionary<string, object> eventPayload)
    {
        var counterName = "";
        var counterValue = "";

        if (eventPayload.TryGetValue("DisplayName", out object displayValue))
        {
            counterName = displayValue.ToString();
        }
        if (eventPayload.TryGetValue("Mean", out object value) ||
            eventPayload.TryGetValue("Increment", out value))
        {
            counterValue = value.ToString();
        }

        return (counterName, counterValue);
    }
}

Como se ha mostrado anteriormente, debe asegurarse de que el argumento "EventCounterIntervalSec" esté establecido en el argumento filterPayload al llamar a EnableEvents. De lo contrario, los contadores no pueden vaciar valores, ya que no saben en qué intervalo se debe hacer.

Consulte también