Skip to content

Commit c847ec9

Browse files
authored
TraceLog streaming/in-memory EventPipe support (#1867)
1 parent 9714907 commit c847ec9

11 files changed

+472
-204
lines changed

.ado.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,4 @@ jobs:
5050
name: Azure Pipelines
5151

5252
steps:
53-
- template: /.pipelines/perfcollect-job.yml
53+
- template: /.pipelines/perfcollect-job.yml

src/Directory.Build.props

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<Project>
2+
<Project>
33

44
<PropertyGroup>
55
<EnableDefaultNoneItems>False</EnableDefaultNoneItems>
@@ -44,6 +44,7 @@
4444
<AzureCoreVersion>1.38.0</AzureCoreVersion>
4545
<AzureIdentityVersion>1.11.4</AzureIdentityVersion>
4646
<MicroBuildCoreVersion>0.3.1</MicroBuildCoreVersion>
47+
<MicrosoftDiagnosticsNETCoreClientVersion>0.2.510501</MicrosoftDiagnosticsNETCoreClientVersion>
4748
<MicrosoftIdentityClientVersion>4.61.3</MicrosoftIdentityClientVersion>
4849
<MicrosoftIdentityClientExtensionsMsalVersion>4.61.3</MicrosoftIdentityClientExtensionsMsalVersion>
4950
<MicrosoftIdentityModelAbstractionsVersion>7.1.2</MicrosoftIdentityModelAbstractionsVersion>
@@ -108,4 +109,4 @@
108109
<AddSyntheticProjectReferencesForSolutionDependencies>false</AddSyntheticProjectReferencesForSolutionDependencies>
109110
</PropertyGroup>
110111

111-
</Project>
112+
</Project>

src/PerfView.TestUtilities/PerfView.TestUtilities.csproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Project Sdk="Microsoft.NET.Sdk">
33

44
<PropertyGroup>
5-
<TargetFramework>net462</TargetFramework>
5+
<TargetFrameworks>net462;net8.0</TargetFrameworks>
66
<Description>Unit test utility library.</Description>
77
<Copyright>Copyright © Microsoft 2017</Copyright>
88

@@ -38,4 +38,4 @@
3838
<AssemblyOriginatorKeyFile>..\MSFT.snk</AssemblyOriginatorKeyFile>
3939
</PropertyGroup>
4040

41-
</Project>
41+
</Project>

src/TraceEvent/EventPipe/EventPipeEventSource.cs

+7
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ public DateTime QPCTimeToTimeStamp(long QPCTime)
125125
/// </summary>
126126
public int MinimumVersionCanRead => 0;
127127

128+
/// <summary>
129+
/// Called after headers are deserialized. This is especially useful in a streaming scenario
130+
/// because the headers are only read after Process() is called.
131+
/// </summary>
132+
internal Action HeadersDeserialized;
133+
128134
protected override void Dispose(bool disposing)
129135
{
130136
if (_deserializer != null)
@@ -144,6 +150,7 @@ public override bool Process()
144150
Debug.Assert(_deserializer != null);
145151
}
146152

153+
HeadersDeserialized?.Invoke();
147154

148155
if (FileFormatVersionNumber >= 3)
149156
{

src/TraceEvent/TraceEvent.Tests/Parsing/EventPipeParsing.cs

+117-54
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using FastSerialization;
2+
using Microsoft.Diagnostics.NETCore.Client;
23
using Microsoft.Diagnostics.Tracing;
34
using Microsoft.Diagnostics.Tracing.Etlx;
45
using Microsoft.Diagnostics.Tracing.EventPipe;
@@ -7,11 +8,14 @@
78
using System;
89
using System.Collections.Generic;
910
using System.Diagnostics;
11+
using System.Diagnostics.Tracing;
1012
using System.IO;
1113
using System.Linq;
1214
using System.Text;
15+
using System.Threading.Tasks;
1316
using Xunit;
1417
using Xunit.Abstractions;
18+
using static Microsoft.Diagnostics.Tracing.Etlx.TraceLog;
1519

1620
namespace TraceEventTests
1721
{
@@ -20,15 +24,54 @@ public class EventPipeParsing : EventPipeTestBase
2024
private class EventRecord
2125
{
2226
public int TotalCount;
23-
public string FirstSeriazliedSample;
27+
public string FirstSerializedSample;
28+
}
29+
30+
private class EventStatistics
31+
{
32+
public SortedDictionary<string, EventRecord> Records = new SortedDictionary<string, EventRecord>(StringComparer.Ordinal);
33+
34+
public void Record(TraceEvent data) => Record(data.ProviderName + "/" + data.EventName, data);
35+
36+
public void Record(string eventName, TraceEvent data)
37+
{
38+
if (Records.ContainsKey(eventName))
39+
{
40+
Records[eventName].TotalCount++;
41+
}
42+
else
43+
{
44+
Records[eventName] = new EventRecord()
45+
{
46+
TotalCount = 1,
47+
FirstSerializedSample = new String(data.ToString().Replace("\n", "\\n").Replace("\r", "\\r").Take(1000).ToArray())
48+
};
49+
}
50+
}
51+
52+
override
53+
public string ToString()
54+
{
55+
StringBuilder sb = new StringBuilder(1024 * 1024);
56+
foreach (var item in Records)
57+
{
58+
sb.AppendLine($"{item.Key}, {item.Value.TotalCount}, {item.Value.FirstSerializedSample}");
59+
}
60+
61+
return sb.ToString();
62+
}
2463
}
2564

2665
public EventPipeParsing(ITestOutputHelper output)
2766
: base(output)
2867
{
2968
}
30-
31-
[Theory()]
69+
70+
#if NETCOREAPP3_0_OR_GREATER
71+
[Theory(Skip = "Snapshot difs due to increased float accuracy on newer .NET versions.")]
72+
#else
73+
[Theory]
74+
#endif
3275
[MemberData(nameof(TestEventPipeFiles))]
3376
public void Basic(string eventPipeFileName)
3477
{
@@ -38,30 +81,13 @@ public void Basic(string eventPipeFileName)
3881
string eventPipeFilePath = Path.Combine(UnZippedDataDir, eventPipeFileName);
3982

4083
Output.WriteLine(string.Format("Processing the file {0}, Making ETLX and scanning.", Path.GetFullPath(eventPipeFilePath)));
41-
42-
var eventStatistics = new SortedDictionary<string, EventRecord>(StringComparer.Ordinal);
84+
var eventStatistics = new EventStatistics();
4385

4486
using (var traceLog = new TraceLog(TraceLog.CreateFromEventPipeDataFile(eventPipeFilePath)))
4587
{
4688
var traceSource = traceLog.Events.GetSource();
4789

48-
traceSource.AllEvents += delegate (TraceEvent data)
49-
{
50-
string eventName = data.ProviderName + "/" + data.EventName;
51-
52-
if (eventStatistics.ContainsKey(eventName))
53-
{
54-
eventStatistics[eventName].TotalCount++;
55-
}
56-
else
57-
{
58-
eventStatistics[eventName] = new EventRecord()
59-
{
60-
TotalCount = 1,
61-
FirstSeriazliedSample = new String(data.ToString().Replace("\n", "\\n").Replace("\r", "\\r").Take(1000).ToArray())
62-
};
63-
}
64-
};
90+
traceSource.AllEvents += eventStatistics.Record;
6591

6692
// Process
6793
traceSource.Process();
@@ -79,7 +105,7 @@ public void Streaming(string eventPipeFileName)
79105

80106
string eventPipeFilePath = Path.Combine(UnZippedDataDir, eventPipeFileName);
81107
Output.WriteLine(string.Format("Processing the file {0}", Path.GetFullPath(eventPipeFilePath)));
82-
var eventStatistics = new SortedDictionary<string, EventRecord>(StringComparer.Ordinal);
108+
var eventStatistics = new EventStatistics();
83109

84110
long curStreamPosition = 0;
85111
using (MockStreamingOnlyStream s = new MockStreamingOnlyStream(new FileStream(eventPipeFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)))
@@ -95,7 +121,6 @@ public void Streaming(string eventPipeFileName)
95121
Assert.InRange(newStreamPosition, curStreamPosition, curStreamPosition + 103_000);
96122
curStreamPosition = newStreamPosition;
97123

98-
99124
string eventName = data.ProviderName + "/" + data.EventName;
100125

101126
// For whatever reason the parse filtering below produces a couple extra events
@@ -108,18 +133,7 @@ public void Streaming(string eventPipeFileName)
108133
eventName == "Microsoft-Windows-DotNETRuntime/Method")
109134
return;
110135

111-
if (eventStatistics.ContainsKey(eventName))
112-
{
113-
eventStatistics[eventName].TotalCount++;
114-
}
115-
else
116-
{
117-
eventStatistics[eventName] = new EventRecord()
118-
{
119-
TotalCount = 1,
120-
FirstSeriazliedSample = new String(data.ToString().Replace("\n", "\\n").Replace("\r", "\\r").Take(1000).ToArray())
121-
};
122-
}
136+
eventStatistics.Record(eventName, data);
123137
};
124138

125139
// this is somewhat arbitrary looking set of parser event callbacks empirically
@@ -148,6 +162,61 @@ public void Streaming(string eventPipeFileName)
148162
ValidateEventStatistics(eventStatistics, eventPipeFileName);
149163
}
150164

165+
#if NETCOREAPP3_0_OR_GREATER
166+
[Theory]
167+
#else
168+
[Theory(Skip = "EventPipeSession connection is only available to target apps on .NET Core 3.0 or later")]
169+
#endif
170+
[InlineData(true)]
171+
[InlineData(false)]
172+
public async Task SessionStreaming(bool initialRundown)
173+
{
174+
var client = new DiagnosticsClient(Process.GetCurrentProcess().Id);
175+
var rundownConfig = initialRundown ? EventPipeRundownConfiguration.Enable(client) : EventPipeRundownConfiguration.None();
176+
var providers = new[]
177+
{
178+
new EventPipeProvider(SampleProfilerTraceEventParser.ProviderName, EventLevel.Informational),
179+
};
180+
using (var session = client.StartEventPipeSession(providers, requestRundown: false))
181+
{
182+
using (var traceSource = CreateFromEventPipeSession(session, rundownConfig))
183+
{
184+
var sampleEventParser = new SampleProfilerTraceEventParser(traceSource);
185+
186+
// Signal that we have received the first event.
187+
var eventCallStackIndex = new TaskCompletionSource<CallStackIndex>();
188+
sampleEventParser.ThreadSample += delegate (ClrThreadSampleTraceData e)
189+
{
190+
eventCallStackIndex.TrySetResult(e.CallStackIndex());
191+
};
192+
193+
// Process in the background (this is blocking).
194+
var processingTask = Task.Run(traceSource.Process);
195+
196+
// Verify the event can be symbolicated on the fly if (initialRundown == true).
197+
var callStackIndex = await eventCallStackIndex.Task;
198+
Assert.NotEqual(CallStackIndex.Invalid, callStackIndex);
199+
var codeAddressIndex = traceSource.TraceLog.CallStacks.CodeAddressIndex(callStackIndex);
200+
Assert.NotEqual(CodeAddressIndex.Invalid, codeAddressIndex);
201+
var methodIndex = traceSource.TraceLog.CodeAddresses.MethodIndex(codeAddressIndex);
202+
if (initialRundown)
203+
{
204+
Assert.NotEqual(MethodIndex.Invalid, methodIndex);
205+
var method = traceSource.TraceLog.CodeAddresses.Methods[methodIndex];
206+
Assert.NotEmpty(method.FullMethodName);
207+
}
208+
else
209+
{
210+
Assert.Equal(MethodIndex.Invalid, methodIndex);
211+
}
212+
213+
// Stop after receiving the first event.
214+
session.Stop();
215+
await processingTask;
216+
}
217+
}
218+
}
219+
151220
[Fact]
152221
public void CanParseHeaderOfV1EventPipeFile()
153222
{
@@ -248,7 +317,7 @@ public void V4EventPipeFileHasProcNumbers()
248317
// Process
249318
traceSource.Process();
250319

251-
for(int i = 0; i < traceSource.NumberOfProcessors; i++)
320+
for (int i = 0; i < traceSource.NumberOfProcessors; i++)
252321
{
253322
Assert.NotEqual(0, counts[i]);
254323
}
@@ -463,7 +532,7 @@ public void WellKnownDiagnosticSourceEventsHavePayloads()
463532
// and ignore the empty parameter metadata provided in the stream, treating the events
464533
// as if the runtime had provided the correct parameter schema.
465534
//
466-
// I am concurrently working on a runtime fix and updated file format revision which can
535+
// I am concurrently working on a runtime fix and updated file format revision which can
467536
// correctly encode these parameter types. However for back-compat with older runtimes we
468537
// need this.
469538

@@ -570,15 +639,9 @@ private void Dynamic_All(TraceEvent obj)
570639
throw new NotImplementedException();
571640
}
572641

573-
private void ValidateEventStatistics(SortedDictionary<string, EventRecord> eventStatistics, string eventPipeFileName)
642+
private void ValidateEventStatistics(EventStatistics eventStatistics, string eventPipeFileName)
574643
{
575-
StringBuilder sb = new StringBuilder(1024 * 1024);
576-
foreach (var item in eventStatistics)
577-
{
578-
sb.AppendLine($"{item.Key}, {item.Value.TotalCount}, {item.Value.FirstSeriazliedSample}");
579-
}
580-
581-
string actual = sb.ToString();
644+
string actual = eventStatistics.ToString();
582645
string baselineFile = Path.Combine(TestDataDir, eventPipeFileName + ".baseline.txt");
583646
string expected = File.ReadAllText(baselineFile);
584647

@@ -603,7 +666,7 @@ public MockStreamingOnlyStream(Stream innerStream)
603666
{
604667
_innerStream = innerStream;
605668
}
606-
public long TestOnlyPosition { get { return _innerStream.Position; } }
669+
public long TestOnlyPosition { get { return _innerStream.Position; } }
607670

608671
public override bool CanRead => true;
609672
public override bool CanSeek => false;
@@ -662,7 +725,7 @@ public void Write(string arg)
662725
public void WriteArray<T>(T[] elements, Action<T> writeElement)
663726
{
664727
WriteArrayLength(elements.Length);
665-
for(int i = 0; i < elements.Length; i++)
728+
for (int i = 0; i < elements.Length; i++)
666729
{
667730
writeElement(elements[i]);
668731
}
@@ -779,7 +842,7 @@ private static void Align(BinaryWriter writer, long previousBytesWritten)
779842
}
780843
}
781844

782-
public static void WriteBlock(BinaryWriter writer, string name, Action<BinaryWriter> writeBlockData,
845+
public static void WriteBlock(BinaryWriter writer, string name, Action<BinaryWriter> writeBlockData,
783846
long previousBytesWritten = 0)
784847
{
785848
Debug.WriteLine($"Starting block {name} position: {writer.BaseStream.Position + previousBytesWritten}");
@@ -931,7 +994,7 @@ MemoryStream GetFirstChunk()
931994
EventPipeWriter.WriteNetTraceHeader(writer);
932995
EventPipeWriter.WriteFastSerializationHeader(writer);
933996
EventPipeWriter.WriteTraceObject(writer);
934-
EventPipeWriter.WriteMetadataBlock(writer,
997+
EventPipeWriter.WriteMetadataBlock(writer,
935998
new EventMetadata(1, "Provider", "Event", 1));
936999
ms.Position = 0;
9371000
return ms;
@@ -950,16 +1013,16 @@ MemoryStream GetNextChunk()
9501013
else
9511014
{
9521015
// 20 blocks, each with 20 events in them
953-
for(int i = 0; i < 20; i++)
1016+
for (int i = 0; i < 20; i++)
9541017
{
955-
EventPipeWriter.WriteEventBlock(writer,
1018+
EventPipeWriter.WriteEventBlock(writer,
9561019
w =>
9571020
{
9581021
for (int j = 0; j < 20; j++)
9591022
{
9601023
EventPipeWriter.WriteEventBlob(w, 1, _sequenceNumber++, payloadSize, WriteEventPayload);
9611024
}
962-
},
1025+
},
9631026
_bytesWritten);
9641027
}
9651028
}
@@ -987,7 +1050,7 @@ public override void Flush()
9871050
public override int Read(byte[] buffer, int offset, int count)
9881051
{
9891052
int ret = _currentChunk.Read(buffer, offset, count);
990-
if(ret == 0)
1053+
if (ret == 0)
9911054
{
9921055
_currentChunk = GetNextChunk();
9931056
_bytesWritten += _currentChunk.Length;

src/TraceEvent/TraceEvent.Tests/Parsing/GeneralParsing.cs

+4
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ public GeneralParsing(ITestOutputHelper output)
2121
/// and scans them (so you should get asserts if there is parsing problem)
2222
/// and ensures that no more than .1% of the events are
2323
/// </summary>
24+
#if NETCOREAPP3_0_OR_GREATER
25+
[Theory(Skip = "Snapshot difs due to increased float accuracy on newer .NET versions.")]
26+
#else
2427
[Theory]
28+
#endif
2529
[MemberData(nameof(TestEtlFiles))]
2630
public void ETW_GeneralParsing_Basic(string etlFileName)
2731
{

0 commit comments

Comments
 (0)