From 85df2e75fe2f9198f280998e5d709b1ec8847d4a Mon Sep 17 00:00:00 2001 From: Noah Falk Date: Thu, 16 Oct 2025 03:54:36 -0700 Subject: [PATCH] Add support for Boolean8 to NetTrace V6. EventSource writes booleans differently in manifest mode vs self-describing mode. Manifest mode uses a 4 byte boolean event field which NetTrace has always supported but Self-describing wrote a 1 byte boolean which NetTrace had no metadata encoding for. Adding support for Boolean8 to NetTrace gives us a path forward to properly describe booleans in metadata regardless of mode. --- src/TraceEvent/EventPipe/EventPipeMetadata.cs | 14 ++- src/TraceEvent/EventPipe/NetTraceFormat.md | 7 +- .../Parsing/EventPipeParsing.cs | 103 +++++++++++++++++- 3 files changed, 114 insertions(+), 10 deletions(-) diff --git a/src/TraceEvent/EventPipe/EventPipeMetadata.cs b/src/TraceEvent/EventPipe/EventPipeMetadata.cs index 9190950e2..1e76757db 100644 --- a/src/TraceEvent/EventPipe/EventPipeMetadata.cs +++ b/src/TraceEvent/EventPipe/EventPipeMetadata.cs @@ -453,7 +453,7 @@ private DynamicTraceEventData.PayloadFetch ParseType( // Fill out the payload fetch object based on the TypeCode. switch (typeCode) { - case EventPipeTypeCode.Boolean: + case EventPipeTypeCode.Boolean32: { payloadFetch.Type = typeof(bool); payloadFetch.Size = 4; // We follow windows conventions and use 4 bytes for bool. @@ -667,6 +667,13 @@ private DynamicTraceEventData.PayloadFetch ParseType( payloadFetch = DynamicTraceEventData.PayloadFetch.DataLocPayloadFetch(offset, elementType); break; } + case EventPipeTypeCode.Boolean8: + { + payloadFetch.Type = typeof(bool); + payloadFetch.Size = 1; + payloadFetch.Offset = offset; + break; + } default: { throw new FormatException($"Field {fieldName}: Typecode {typeCode} is not supported."); @@ -679,7 +686,7 @@ private DynamicTraceEventData.PayloadFetch ParseType( enum EventPipeTypeCode { Object = 1, // Concatenate together all of the encoded fields - Boolean = 3, // A 4-byte LE integer with value 0=false and 1=true. + Boolean32 = 3, // A 4-byte LE integer with value 0=false and 1=true. UTF16CodeUnit = 4, // a 2-byte UTF16 code unit SByte = 5, // 1-byte signed integer Byte = 6, // 1-byte unsigned integer @@ -701,7 +708,8 @@ enum EventPipeTypeCode FixedLengthArray = 22, // New in V6: A fixed-length array of elements. The length is determined by the metadata. UTF8CodeUnit = 23, // New in V6: A single UTF8 code unit (1 byte). RelLoc = 24, // New in V6: An array at a relative location within the payload. - DataLoc = 25 // New in V6: An absolute data location within the payload. + DataLoc = 25, // New in V6: An absolute data location within the payload. + Boolean8 = 26 // New in V6: A 1 byte boolean with value 0=false and 1=true. } private void ParseOptionalMetadataV6OrGreater(ref SpanReader reader) diff --git a/src/TraceEvent/EventPipe/NetTraceFormat.md b/src/TraceEvent/EventPipe/NetTraceFormat.md index 45e7e7130..05c042c87 100644 --- a/src/TraceEvent/EventPipe/NetTraceFormat.md +++ b/src/TraceEvent/EventPipe/NetTraceFormat.md @@ -282,7 +282,7 @@ Type format is: enum TypeCode { Object = 1, // Concatenate together all of the encoded fields - Boolean = 3, // A 4-byte LE integer with value 0=false and 1=true. + Boolean32 = 3, // A 4-byte LE integer with value 0=false and 1=true. UTF16CodeUnit = 4, // a 2-byte UTF16 code unit (Often this is a character, but some characters need more than one code unit to encode) SByte = 5, // 1-byte signed integer Byte = 6, // 1-byte unsigned integer @@ -306,9 +306,10 @@ enum TypeCode RelLoc = 24, // New in V6: An array at a relative location within the payload. // Format: 4 bytes where the high 16 bits are size and low 16 bits are position relative to after this field. // Size is measured in bytes, not elements. The element type must be fixed sized. - DataLoc = 25 // New in V6: An absolute data location within the payload. + DataLoc = 25, // New in V6: An absolute data location within the payload. // Format: 4 bytes where the high 16 bits are size and low 16 bits are position relative to start of the event parameters buffer. // Size is measured in bytes, not elements. The element type must be fixed sized. + Boolean8 = 26 // New in V6: A 1-byte boolean where 0=false and 1=true. } ``` @@ -560,7 +561,7 @@ Last, we are taking the opportunity to simplify the metadata encoding format. Th 1. Metadata rows are no longer encoded with EventHeaders. 2. Most of the metadata fields are now optional and the top-level format of a metadata row was redesigned. 3. The 2nd copy of field information that was added by V2Params in version 5 has been removed. It only existed to support adding array support in a non-breaking way and now arrays are supported in the same FieldDescriptions as all the other types. -4. New payload field types were added: VarInt, VarUInt, FixedLengthArray, UTF8CodeUnit, RelLoc, and DataLoc. +4. New payload field types were added: VarInt, VarUInt, FixedLengthArray, UTF8CodeUnit, RelLoc, DataLoc, and Boolean8. The existing Boolean type was renamed to Boolean32 to avoid ambiguity. 5. Strings in the metadata are now UTF8 rather than UTF16 #### Extended support for event labels diff --git a/src/TraceEvent/TraceEvent.Tests/Parsing/EventPipeParsing.cs b/src/TraceEvent/TraceEvent.Tests/Parsing/EventPipeParsing.cs index 32bdaf70b..e0de40ccc 100644 --- a/src/TraceEvent/TraceEvent.Tests/Parsing/EventPipeParsing.cs +++ b/src/TraceEvent/TraceEvent.Tests/Parsing/EventPipeParsing.cs @@ -980,7 +980,7 @@ public void ParseV6Metadata() writer.WriteHeaders(); writer.WriteMetadataBlock(new EventMetadata(1, "TestProvider", "TestEvent1", 15, new MetadataParameter("Param1", MetadataTypeCode.Int16), - new MetadataParameter("Param2", MetadataTypeCode.Boolean)), + new MetadataParameter("Param2", MetadataTypeCode.Boolean32)), new EventMetadata(2, "TestProvider", "TestEvent2", 16), new EventMetadata(3, "TestProvider", "TestEvent3", 17)); writer.WriteThreadBlock(w => @@ -1383,7 +1383,7 @@ public void ParseV6MetadataObjectParam() new MetadataParameter("Param1", new ObjectMetadataType( new MetadataParameter("NestedParam1", MetadataTypeCode.Int32), new MetadataParameter("NestedParam2", MetadataTypeCode.Byte))), - new MetadataParameter("Param2", MetadataTypeCode.Boolean))); + new MetadataParameter("Param2", MetadataTypeCode.Boolean32))); writer.WriteThreadBlock(w => { w.WriteThreadEntry(999, 0, 0); @@ -1414,6 +1414,100 @@ public void ParseV6MetadataObjectParam() Assert.Equal(1, eventCount); } + [Fact] + public void ParseV6MetadataBoolean8Param() + { + EventPipeWriterV6 writer = new EventPipeWriterV6(); + writer.WriteHeaders(); + writer.WriteMetadataBlock(new EventMetadata(1, "TestProvider", "TestEvent1", 15, + new MetadataParameter("Param1", MetadataTypeCode.Boolean8), + new MetadataParameter("Param2", MetadataTypeCode.Boolean8))); + writer.WriteThreadBlock(w => + { + w.WriteThreadEntry(999, 0, 0); + }); + writer.WriteEventBlock(w => + { + w.WriteEventBlob(1, 999, 1, new byte[] { 1, 0 }); + }); + writer.WriteEndBlock(); + MemoryStream stream = new MemoryStream(writer.ToArray()); + EventPipeEventSource source = new EventPipeEventSource(stream); + int eventCount = 0; + source.Dynamic.All += e => + { + eventCount++; + Assert.Equal($"TestEvent1", e.EventName); + Assert.Equal("TestProvider", e.ProviderName); + Assert.Equal(2, e.PayloadNames.Length); + Assert.Equal("Param1", e.PayloadNames[0]); + Assert.Equal("Param2", e.PayloadNames[1]); + Assert.Equal(true, e.PayloadValue(0)); + Assert.Equal(false, e.PayloadValue(1)); + }; + source.Process(); + Assert.Equal(1, eventCount); + } + + [Fact] + public void ParseV6MetadataBoolean8ArrayAndObjectParam() + { + EventPipeWriterV6 writer = new EventPipeWriterV6(); + writer.WriteHeaders(); + writer.WriteMetadataBlock(new EventMetadata(1, "TestProvider", "TestEvent1", 15, + new MetadataParameter("Param1", new ArrayMetadataType(new MetadataType(MetadataTypeCode.Boolean8))), + new MetadataParameter("Param2", new ObjectMetadataType( + new MetadataParameter("HasValue", MetadataTypeCode.Boolean8), + new MetadataParameter("Value", new MetadataType(MetadataTypeCode.Int32)))))); + writer.WriteThreadBlock(w => + { + w.WriteThreadEntry(999, 0, 0); + }); + writer.WriteEventBlock(w => + { + // Param1 = [true, false, true] + // Param2 = { NestedParam1 = false, NestedParam2 = [false, true] } + w.WriteEventBlob(1, 999, 1, p => + { + // Param1 + p.Write((ushort)3); + p.Write((byte)1); + p.Write((byte)0); + p.Write((byte)1); + // Param2 + p.Write((byte)1); + p.Write((int)184); + }); + }); + writer.WriteEndBlock(); + MemoryStream stream = new MemoryStream(writer.ToArray()); + EventPipeEventSource source = new EventPipeEventSource(stream); + int eventCount = 0; + source.Dynamic.All += e => + { + eventCount++; + Assert.Equal($"TestEvent1", e.EventName); + Assert.Equal("TestProvider", e.ProviderName); + Assert.Equal(2, e.PayloadNames.Length); + Assert.Equal("Param1", e.PayloadNames[0]); + Assert.Equal("Param2", e.PayloadNames[1]); + // Param1 + Assert.Equal(typeof(bool[]), e.PayloadValue(0).GetType()); + bool[] param1 = (bool[])e.PayloadValue(0); + Assert.Equal(3, param1.Length); + Assert.True(param1[0]); + Assert.False(param1[1]); + Assert.True(param1[2]); + // Param2 + var o = (DynamicTraceEventData.StructValue)e.PayloadValue(1); + Assert.Equal(2, o.Count); + Assert.True((bool)o["HasValue"]); + Assert.Equal(184, (int)o["Value"]); + }; + source.Process(); + Assert.Equal(1, eventCount); + } + [Fact] //V6 public void ParseV6OptionalMetadata() { @@ -2696,7 +2790,7 @@ public DataLocMetadataType(MetadataType elementType) : base(MetadataTypeCode.Dat public enum MetadataTypeCode { Object = 1, // Concatenate together all of the encoded fields - Boolean = 3, // A 4-byte LE integer with value 0=false and 1=true. + Boolean32 = 3, // A 4-byte LE integer with value 0=false and 1=true. UTF16CodeUnit = 4, // a 2-byte UTF16 code unit SByte = 5, // 1-byte signed integer Byte = 6, // 1-byte unsigned integer @@ -2717,7 +2811,8 @@ public enum MetadataTypeCode FixedLengthArray = 22, // New in V6: A fixed-length array of elements. The size is determined by the metadata. UTF8CodeUnit = 23, // New in V6: A single UTF8 code unit (1 byte). RelLoc = 24, // New in V6: An array at a relative location within the payload. - DataLoc = 25 // New in V6: An absolute data location within the payload. + DataLoc = 25, // New in V6: An absolute data location within the payload. + Boolean8 = 26 // New in V6: A 1-byte boolean with value 0=false and 1=true. } public class EventPayloadWriter