ID attività EventSource
Questo articolo si applica a: ✔️ .NET Core 3.1 e versioni successive ✔️ .NET Framework 4.5 e versioni successive
Questa guida illustra gli ID attività, un identificatore facoltativo che può essere registrato con ogni evento generato tramite System.Diagnostics.Tracing.EventSource. Per un'introduzione, fai riferimento a Introduzione a EventSource.
La sfida di registrare il lavoro simultaneo
Molto tempo fa, una tipica applicazione potrebbe essere stata semplice e a thread singolo, cosa che rende la registrazione semplice. È possibile scrivere ogni passaggio in un file di log in ordine e quindi leggere di nuovo il log esattamente nell'ordine in cui è stato scritto per comprendere cosa è successo. Se l'app ha gestito le richieste, è stata gestita una sola richiesta alla volta. Tutti i messaggi di log per la richiesta A verranno stampati in ordine, quindi tutti i messaggi per B e così via. Quando le app diventano multithread questa strategia non funziona più perché più richieste vengono gestite contemporaneamente. Tuttavia, se ogni richiesta viene assegnata a un singolo thread che la elabora interamente, è possibile risolvere il problema registrando un ID thread per ogni messaggio di log. Ad esempio, un'app multithread potrebbe registrare:
Thread Id Message
--------- -------
12 BeginRequest A
12 Doing some work
190 BeginRequest B
12 uh-oh error happened
190 Doing some work
Leggendo gli ID del thread si sa che il thread 12 stava elaborando la richiesta A e il thread 190 stava elaborando la richiesta B, quindi il messaggio “Oh-oh si è verificato un errore” era correlato alla richiesta A. Tuttavia, la concorrenza delle applicazioni ha continuato a diventare sempre più complessa. È ora comune usare async
e await
in modo che una singola richiesta possa essere gestita parzialmente in molti thread diversi prima del completamento del lavoro. Gli ID thread non sono più sufficienti per correlare tutti i messaggi generati per una richiesta. Gli ID attività risolvono questo problema. Forniscono un identificatore granulare più accurato in grado di tenere traccia delle singole richieste o di parti delle richieste, indipendentemente dal fatto che il lavoro venga distribuito in thread diversi.
Nota
Il concetto di ID attività indicato qui non è uguale a System.Diagnostics.Tracing.Activity, nonostante la denominazione simile.
Rilevamento del lavoro con un ID attività
È possibile eseguire il codice seguente per visualizzare il rilevamento delle attività in azione.
using System.Diagnostics.Tracing;
public static class Program
{
public static async Task Main(string[] args)
{
ConsoleWriterEventListener listener = new ConsoleWriterEventListener();
Task a = ProcessWorkItem("A");
Task b = ProcessWorkItem("B");
await Task.WhenAll(a, b);
}
private static async Task ProcessWorkItem(string requestName)
{
DemoEventSource.Log.WorkStart(requestName);
await HelperA();
await HelperB();
DemoEventSource.Log.WorkStop();
}
private static async Task HelperA()
{
DemoEventSource.Log.DebugMessage("HelperA");
await Task.Delay(100); // pretend to do some work
}
private static async Task HelperB()
{
DemoEventSource.Log.DebugMessage("HelperB");
await Task.Delay(100); // pretend to do some work
}
}
[EventSource(Name ="Demo")]
class DemoEventSource : EventSource
{
public static DemoEventSource Log = new DemoEventSource();
[Event(1)]
public void WorkStart(string requestName) => WriteEvent(1, requestName);
[Event(2)]
public void WorkStop() => WriteEvent(2);
[Event(3)]
public void DebugMessage(string message) => WriteEvent(3, message);
}
class ConsoleWriterEventListener : EventListener
{
protected override void OnEventSourceCreated(EventSource eventSource)
{
if(eventSource.Name == "Demo")
{
Console.WriteLine("{0,-5} {1,-40} {2,-15} {3}", "TID", "Activity ID", "Event", "Arguments");
EnableEvents(eventSource, EventLevel.Verbose);
}
else if(eventSource.Name == "System.Threading.Tasks.TplEventSource")
{
// Activity IDs aren't enabled by default.
// Enabling Keyword 0x80 on the TplEventSource turns them on
EnableEvents(eventSource, EventLevel.LogAlways, (EventKeywords)0x80);
}
}
protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
lock (this)
{
Console.Write("{0,-5} {1,-40} {2,-15} ", eventData.OSThreadId, eventData.ActivityId, eventData.EventName);
if (eventData.Payload.Count == 1)
{
Console.WriteLine(eventData.Payload[0]);
}
else
{
Console.WriteLine();
}
}
}
}
Quando viene eseguito, questo codice stampa l'output:
TID Activity ID Event Arguments
21256 00000011-0000-0000-0000-00006ab99d59 WorkStart A
21256 00000011-0000-0000-0000-00006ab99d59 DebugMessage HelperA
21256 00000012-0000-0000-0000-00006bb99d59 WorkStart B
21256 00000012-0000-0000-0000-00006bb99d59 DebugMessage HelperA
14728 00000011-0000-0000-0000-00006ab99d59 DebugMessage HelperB
11348 00000012-0000-0000-0000-00006bb99d59 DebugMessage HelperB
11348 00000012-0000-0000-0000-00006bb99d59 WorkStop
14728 00000011-0000-0000-0000-00006ab99d59 WorkStop
Nota
Esiste un problema noto per cui il debugger di Visual Studio può causare la generazione di ID attività non validi. Non eseguire questo esempio nel debugger o imposta un punto di interruzione all'inizio di Main e valuta l'espressione 'System.Threading.Tasks.TplEventSource.Log.TasksSetActivityIds = false' nella finestra immediata prima di continuare a risolvere il problema.
Usando gli ID attività è possibile vedere che tutti i messaggi per l'elemento di lavoro A hanno ID 00000011-...
e tutti i messaggi per l'elemento di lavoro B hanno ID 00000012-...
. Entrambi gli elementi di lavoro hanno prima eseguito alcune operazioni sul thread 21256, ma ognuno di essi ha continuato il proprio lavoro su thread di pool di thread separati 11348 e 14728, quindi il tentativo di tenere traccia della richiesta solo con ID thread non avrebbe funzionato.
EventSource ha un'euristica automatica in cui la definizione di un evento denominato _Something_Start seguito immediatamente vicino a un altro evento denominato _Something_Stop viene considerato l'inizio e l'arresto di un'unità di lavoro. La registrazione dell'evento di avvio per una nuova unità di lavoro crea un nuovo ID attività e inizia a registrare tutti gli eventi nello stesso thread con tale ID attività fino a quando non viene registrato l'evento di arresto. L'ID segue automaticamente anche il flusso di controllo asincrono quando si usa async
e await
. Sebbene sia consigliabile usare i suffissi di denominazione Start/Stop, è possibile denominare gli eventi in modo esplicito annotandoli usando la proprietà EventAttribute.Opcode. Impostare il primo evento su EventOpcode.Start
e il secondo su EventOpcode.Stop
.
Richieste di log che eseguono operazioni parallele
A volte una singola richiesta può eseguire parti diverse del lavoro in parallelo ed è possibile raggruppare tutti gli eventi del log e le sottoparti. L'esempio seguente simula una richiesta che esegue due query di database in parallelo e quindi esegue alcune elaborazioni sul risultato di ogni query. Si vuole isolare il lavoro per ogni query, ma anche comprendere quali query appartengono alla stessa richiesta quando è possibile eseguire molte richieste simultanee. Il modello è una struttura ad albero in cui ogni richiesta di primo livello è una radice e quindi le sottoporzioni di lavoro sono rami. Ogni nodo dell'albero ottiene il proprio ID attività e il primo evento registrato con il nuovo ID attività figlio registra un campo aggiuntivo denominato ID attività correlato per descrivere il relativo elemento padre.
Eseguire il codice seguente:
using System.Diagnostics.Tracing;
public static class Program
{
public static async Task Main(string[] args)
{
ConsoleWriterEventListener listener = new ConsoleWriterEventListener();
await ProcessWorkItem("A");
}
private static async Task ProcessWorkItem(string requestName)
{
DemoEventSource.Log.WorkStart(requestName);
Task query1 = Query("SELECT bowls");
Task query2 = Query("SELECT spoons");
await Task.WhenAll(query1, query2);
DemoEventSource.Log.WorkStop();
}
private static async Task Query(string query)
{
DemoEventSource.Log.QueryStart(query);
await Task.Delay(100); // pretend to send a query
DemoEventSource.Log.DebugMessage("processing query");
await Task.Delay(100); // pretend to do some work
DemoEventSource.Log.QueryStop();
}
}
[EventSource(Name = "Demo")]
class DemoEventSource : EventSource
{
public static DemoEventSource Log = new DemoEventSource();
[Event(1)]
public void WorkStart(string requestName) => WriteEvent(1, requestName);
[Event(2)]
public void WorkStop() => WriteEvent(2);
[Event(3)]
public void DebugMessage(string message) => WriteEvent(3, message);
[Event(4)]
public void QueryStart(string query) => WriteEvent(4, query);
[Event(5)]
public void QueryStop() => WriteEvent(5);
}
class ConsoleWriterEventListener : EventListener
{
protected override void OnEventSourceCreated(EventSource eventSource)
{
if (eventSource.Name == "Demo")
{
Console.WriteLine("{0,-5} {1,-40} {2,-40} {3,-15} {4}", "TID", "Activity ID", "Related Activity ID", "Event", "Arguments");
EnableEvents(eventSource, EventLevel.Verbose);
}
else if (eventSource.Name == "System.Threading.Tasks.TplEventSource")
{
// Activity IDs aren't enabled by default.
// Enabling Keyword 0x80 on the TplEventSource turns them on
EnableEvents(eventSource, EventLevel.LogAlways, (EventKeywords)0x80);
}
}
protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
lock (this)
{
Console.Write("{0,-5} {1,-40} {2, -40} {3,-15} ", eventData.OSThreadId, eventData.ActivityId, eventData.RelatedActivityId, eventData.EventName);
if (eventData.Payload.Count == 1)
{
Console.WriteLine(eventData.Payload[0]);
}
else
{
Console.WriteLine();
}
}
}
}
In questo esempio viene stampato l'output, ad esempio:
TID Activity ID Related Activity ID Event Arguments
34276 00000011-0000-0000-0000-000086af9d59 00000000-0000-0000-0000-000000000000 WorkStart A
34276 00001011-0000-0000-0000-0000869f9d59 00000011-0000-0000-0000-000086af9d59 QueryStart SELECT bowls
34276 00002011-0000-0000-0000-0000868f9d59 00000011-0000-0000-0000-000086af9d59 QueryStart SELECT spoons
32684 00002011-0000-0000-0000-0000868f9d59 00000000-0000-0000-0000-000000000000 DebugMessage processing query
18624 00001011-0000-0000-0000-0000869f9d59 00000000-0000-0000-0000-000000000000 DebugMessage processing query
18624 00002011-0000-0000-0000-0000868f9d59 00000000-0000-0000-0000-000000000000 QueryStop
32684 00001011-0000-0000-0000-0000869f9d59 00000000-0000-0000-0000-000000000000 QueryStop
32684 00000011-0000-0000-0000-000086af9d59 00000000-0000-0000-0000-000000000000 WorkStop
In questo esempio è stata eseguita una sola richiesta di primo livello, a cui è stato assegnato l'ID attività 00000011-...
. Quindi ogni evento QueryStart
ha iniziato un nuovo ramo della richiesta rispettivamente con ID attività 00001011-...
e 00002011-...
. È possibile identificare questi ID come elementi figlio della richiesta originale perché entrambi gli eventi di avvio hanno registrato il padre 00000011-...
nel campo ID attività correlata.
Nota
È possibile che i valori numerici degli ID abbiano alcuni modelli chiari tra padre e figlio e non siano casuali. Anche se può aiutare a individuare visivamente la relazione in casi semplici, è preferibile che gli strumenti non si basino su questo e considerino gli ID come identificatori opachi. Man mano che il livello di annidamento va più in profondità, il modello di byte cambia. L'uso del campo ID attività correlata è il modo migliore per garantire che gli strumenti funzionino in modo affidabile indipendentemente dal livello di annidamento.
Poiché le richieste con alberi complessi di elementi del sottolavoro genereranno molti ID attività diversi, questi ID sono in genere meglio analizzati dagli strumenti rispetto a quando si tenta di ricostruire l'albero a mano. PerfView è uno strumento che sa correlare gli eventi annotati con questi ID.