diff --git a/src/Microsoft.Diagnostics.EventFlow.Inputs.EventSource/ActivityPathDecoder.cs b/src/Microsoft.Diagnostics.EventFlow.Inputs.EventSource/ActivityPathDecoder.cs new file mode 100644 index 00000000..e8366c24 --- /dev/null +++ b/src/Microsoft.Diagnostics.EventFlow.Inputs.EventSource/ActivityPathDecoder.cs @@ -0,0 +1,190 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +using System; +using System.Diagnostics; +using System.Text; + +namespace Microsoft.Diagnostics.EventFlow.Inputs +{ + /// + /// A class to decode ETW Activity ID GUIDs into activity paths. + /// + internal static class ActivityPathDecoder + { + /// + /// The encoding for a list of numbers used to make Activity Guids. Basically + /// we operate on nibbles (which are nice because they show up as hex digits). The + /// list is ended with a end nibble (0) and depending on the nibble value (Below) + /// the value is either encoded into nibble itself or it can spill over into the + /// bytes that follow. + /// + private enum NumberListCodes : byte + { + End = 0x0, // ends the list. No valid value has this prefix. + LastImmediateValue = 0xA, + PrefixCode = 0xB, + MultiByte1 = 0xC, // 1 byte follows. If this Nibble is in the high bits, it the high bits of the number are stored in the low nibble. + // commented out because the code does not explicitly reference the names (but they are logically defined). + // MultiByte2 = 0xD, // 2 bytes follow (we don't bother with the nibble optimzation + // MultiByte3 = 0xE, // 3 bytes follow (we don't bother with the nibble optimzation + // MultiByte4 = 0xF, // 4 bytes follow (we don't bother with the nibble optimzation + } + + /// + /// Returns true if 'guid' follow the EventSource style activity ID for the process with ID processID. + /// You can pass a process ID of 0 to this routine and it will do the best it can, but the possibility + /// of error is significantly higher (but still under .1%). + /// + public static unsafe bool IsActivityPath(Guid guid, int processID) + { + uint* uintPtr = (uint*)&guid; + + uint sum = uintPtr[0] + uintPtr[1] + uintPtr[2] + 0x599D99AD; + if (processID == 0) + { + // We guess that the process ID is < 16 bits and because it was xored + // with the lower bits, the upper 16 bits should be independent of the + // particular process, so we can at least confirm that the upper bits + // match. + return (sum & 0xFFFF0000) == (uintPtr[3] & 0xFFFF0000); + } + + if ((sum ^ (uint)processID) == uintPtr[3]) + { + // This is the new style + return true; + } + return sum == uintPtr[3]; // THis is old style where we don't make the ID unique machine wide. + } + + /// + /// Returns a string representation for the activity path. If the GUID is not an activity path then it returns + /// the normal string representation for a GUID. + /// + public static unsafe string GetActivityPathString(Guid guid) + { + if (!IsActivityPath(guid, Process.GetCurrentProcess().Id)) + { + return guid.ToString(); + } + + var processID = ActivityPathProcessID(guid); + StringBuilder sb = StringBuilderCache.Acquire(); + if (processID != 0) + { + sb.Append("/#"); // Use /# to mark the fact that the first number is a process ID. + sb.Append(processID); + } + else + { + sb.Append('/'); // Use // to start to make it easy to anchor + } + byte* bytePtr = (byte*)&guid; + byte* endPtr = bytePtr + 12; + char separator = '/'; + while (bytePtr < endPtr) + { + uint nibble = (uint)(*bytePtr >> 4); + bool secondNibble = false; // are we reading the second nibble (low order bits) of the byte. + NextNibble: + if (nibble == (uint)NumberListCodes.End) + { + break; + } + + if (nibble <= (uint)NumberListCodes.LastImmediateValue) + { + sb.Append('/').Append(nibble); + if (!secondNibble) + { + nibble = (uint)(*bytePtr & 0xF); + secondNibble = true; + goto NextNibble; + } + + // We read the second nibble so we move on to the next byte. + bytePtr++; + continue; + } + else if (nibble == (uint)NumberListCodes.PrefixCode) + { + // This are the prefix codes. If the next nibble is MultiByte, then this is an overflow ID. + // we we denote with a $ instead of a / separator. + + // Read the next nibble. + if (!secondNibble) + { + nibble = (uint)(*bytePtr & 0xF); + } + else + { + bytePtr++; + if (endPtr <= bytePtr) + { + break; + } + nibble = (uint)(*bytePtr >> 4); + } + + if (nibble < (uint)NumberListCodes.MultiByte1) + { + // If the nibble is less than MultiByte we have not defined what that means + // For now we simply give up, and stop parsing. We could add more cases here... + return guid.ToString(); + } + + // If we get here we have a overflow ID, which is just like a normal ID but the separator is $ + separator = '$'; + + // Fall into the Multi-byte decode case. + } + + Debug.Assert((uint)NumberListCodes.MultiByte1 <= nibble, "Single-byte number should be fully handled by the code above"); + + // At this point we are decoding a multi-byte number. + // We are fetching the number as a stream of bytes. + uint numBytes = nibble - (uint)NumberListCodes.MultiByte1; + + uint value = 0; + if (!secondNibble) + { + value = (uint)(*bytePtr & 0xF); + } + bytePtr++; // Adance to the value bytes + + numBytes++; // Now numBytes is 1-4 and reprsents the number of bytes to read. + if (endPtr < bytePtr + numBytes) + { + break; + } + + // Compute the number (little endian) (thus backwards). + for (int i = (int)numBytes - 1; i >= 0; --i) + { + value = (value << 8) + bytePtr[i]; + } + + // Print the value + sb.Append(separator).Append(value); + + bytePtr += numBytes; // Advance past the bytes. + } + + sb.Append('/'); + return StringBuilderCache.GetStringAndRelease(sb); + } + + /// + /// Assuming guid is an Activity Path, extract the process ID from it. + /// + private static unsafe int ActivityPathProcessID(Guid guid) + { + uint* uintPtr = (uint*)&guid; + uint sum = uintPtr[0] + uintPtr[1] + uintPtr[2] + 0x599D99AD; + return (int)(sum ^ uintPtr[3]); + } + } +} diff --git a/src/Microsoft.Diagnostics.EventFlow.Inputs.EventSource/EventDataExtensions.cs b/src/Microsoft.Diagnostics.EventFlow.Inputs.EventSource/EventDataExtensions.cs index e3b0c921..fdd51da7 100644 --- a/src/Microsoft.Diagnostics.EventFlow.Inputs.EventSource/EventDataExtensions.cs +++ b/src/Microsoft.Diagnostics.EventFlow.Inputs.EventSource/EventDataExtensions.cs @@ -28,15 +28,23 @@ public static EventData ToEventData(this EventWrittenEventArgs eventSourceEvent, }; IDictionary payloadData = eventData.Payload; - payloadData.Add("EventId", eventSourceEvent.EventId); - payloadData.Add("EventName", eventSourceEvent.EventName); - payloadData.Add("ActivityID", ActivityPathString(eventSourceEvent.ActivityId)); + payloadData.Add(nameof(eventSourceEvent.EventId), eventSourceEvent.EventId); + payloadData.Add(nameof(eventSourceEvent.EventName), eventSourceEvent.EventName); + if (eventSourceEvent.ActivityId != default(Guid)) + { + payloadData.Add(nameof(EventWrittenEventArgs.ActivityId), ActivityPathDecoder.GetActivityPathString(eventSourceEvent.ActivityId)); + } + if (eventSourceEvent.RelatedActivityId != default(Guid)) + { + payloadData.Add(nameof(EventWrittenEventArgs.RelatedActivityId), eventSourceEvent.RelatedActivityId.ToString()); + } + try { if (eventSourceEvent.Message != null) { // If the event has a badly formatted manifest, the FormattedMessage property getter might throw - payloadData.Add("Message", string.Format(CultureInfo.InvariantCulture, eventSourceEvent.Message, eventSourceEvent.Payload.ToArray())); + payloadData.Add(nameof(eventSourceEvent.Message), string.Format(CultureInfo.InvariantCulture, eventSourceEvent.Message, eventSourceEvent.Payload.ToArray())); } } catch { } @@ -63,125 +71,5 @@ private static void ExtractPayloadData(this EventWrittenEventArgs eventSourceEve eventData.AddPayloadProperty(payloadNamesEnunmerator.Current, payloadEnumerator.Current, healthReporter, context); } } - - /// - /// Returns true if 'guid' follow the EventSouce style activity IDs. - /// - private static unsafe bool IsActivityPath(Guid guid) - { - // We compute a very simple checksum which by adding the first 96 bits as 32 bit numbers. - uint* uintPtr = (uint*)&guid; - return (uintPtr[0] + uintPtr[1] + uintPtr[2] + 0x599D99AD == uintPtr[3]); - } - - /// - /// The encoding for a list of numbers used to make Activity Guids. Basically - /// we operate on nibbles (which are nice because they show up as hex digits). The - /// list is ended with a end nibble (0) and depending on the nibble value (Below) - /// the value is either encoded into nibble itself or it can spill over into the - /// bytes that follow. - /// - private enum NumberListCodes : byte - { - End = 0x0, // ends the list. No valid value has this prefix. - LastImmediateValue = 0xA, - PrefixCode = 0xB, - MultiByte1 = 0xC, // 1 byte follows. If this Nibble is in the high bits, it the high bits of the number are stored in the low nibble. - // commented out because the code does not explicitly reference the names (but they are logically defined). - // MultiByte2 = 0xD, // 2 bytes follow (we don't bother with the nibble optimzation - // MultiByte3 = 0xE, // 3 bytes follow (we don't bother with the nibble optimzation - // MultiByte4 = 0xF, // 4 bytes follow (we don't bother with the nibble optimzation - } - - /// - /// returns a string representation for the activity path. If the GUID is not an activity path then it returns - /// the normal string representation for a GUID. - /// - private static unsafe string ActivityPathString(Guid guid) - { - if (!IsActivityPath(guid)) - return guid.ToString(); - - StringBuilder sb = new StringBuilder(); - sb.Append('/'); // Use // to start to make it easy to anchor - byte* bytePtr = (byte*)&guid; - byte* endPtr = bytePtr + 12; - char separator = '/'; - while (bytePtr < endPtr) - { - uint nibble = (uint)(*bytePtr >> 4); - bool secondNibble = false; // are we reading the second nibble (low order bits) of the byte. - NextNibble: - if (nibble == (uint)NumberListCodes.End) - break; - if (nibble <= (uint)NumberListCodes.LastImmediateValue) - { - sb.Append('/').Append(nibble); - if (!secondNibble) - { - nibble = (uint)(*bytePtr & 0xF); - secondNibble = true; - goto NextNibble; - } - // We read the second nibble so we move on to the next byte. - bytePtr++; - continue; - } - else if (nibble == (uint)NumberListCodes.PrefixCode) - { - // This are the prefix codes. If the next nibble is MultiByte, then this is an overflow ID. - // we we denote with a $ instead of a / separator. - - // Read the next nibble. - if (!secondNibble) - nibble = (uint)(*bytePtr & 0xF); - else - { - bytePtr++; - if (endPtr <= bytePtr) - break; - nibble = (uint)(*bytePtr >> 4); - } - - if (nibble < (uint)NumberListCodes.MultiByte1) - { - // If the nibble is less than MultiByte we have not defined what that means - // For now we simply give up, and stop parsing. We could add more cases here... - return guid.ToString(); - } - // If we get here we have a overflow ID, which is just like a normal ID but the separator is $ - separator = '$'; - // Fall into the Multi-byte decode case. - } - - Debug.Assert((uint)NumberListCodes.MultiByte1 <= nibble); - // At this point we are decoding a multi-byte number, either a normal number or a - // At this point we are byte oriented, we are fetching the number as a stream of bytes. - uint numBytes = nibble - (uint)NumberListCodes.MultiByte1; - - uint value = 0; - if (!secondNibble) - value = (uint)(*bytePtr & 0xF); - bytePtr++; // Adance to the value bytes - - numBytes++; // Now numBytes is 1-4 and reprsents the number of bytes to read. - if (endPtr < bytePtr + numBytes) - break; - - // Compute the number (little endian) (thus backwards). - for (int i = (int)numBytes - 1; 0 <= i; --i) - value = (value << 8) + bytePtr[i]; - - // Print the value - sb.Append(separator).Append(value); - - bytePtr += numBytes; // Advance past the bytes. - } - - if (sb.Length == 0) - sb.Append('/'); - return sb.ToString(); - } - } } \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.EventFlow.Inputs.EventSource/Properties/AssemblyInfo.cs b/src/Microsoft.Diagnostics.EventFlow.Inputs.EventSource/Properties/AssemblyInfo.cs index 19ba0c8b..ad9e1b4e 100644 --- a/src/Microsoft.Diagnostics.EventFlow.Inputs.EventSource/Properties/AssemblyInfo.cs +++ b/src/Microsoft.Diagnostics.EventFlow.Inputs.EventSource/Properties/AssemblyInfo.cs @@ -17,3 +17,6 @@ // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("705eb0a8-8fb0-4f80-8f6a-7bb71a202a2a")] + +[assembly: InternalsVisibleTo("Microsoft.Diagnostics.EventFlow.Inputs.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] + diff --git a/src/Microsoft.Diagnostics.EventFlow.Inputs.EventSource/StringBuilderCache.cs b/src/Microsoft.Diagnostics.EventFlow.Inputs.EventSource/StringBuilderCache.cs new file mode 100644 index 00000000..f56727f9 --- /dev/null +++ b/src/Microsoft.Diagnostics.EventFlow.Inputs.EventSource/StringBuilderCache.cs @@ -0,0 +1,82 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +using System; +using System.Text; + +namespace Microsoft.Diagnostics.EventFlow.Inputs +{ + /// + /// Provides a cached reusable instance of a StringBuilder per thread. It is an optimization that reduces the number of instances constructed and collected. + /// + internal static class StringBuilderCache + { + // The value 360 was chosen in discussion with performance experts as a compromise between using + // as litle memory (per thread) as possible and still covering a large part of short-lived + // StringBuilder creations. + private const int MaxBuilderSize = 360; + + [ThreadStatic] + private static StringBuilder cachedInstance; + + /// + /// Gets a string builder to use of a particular size. + /// + /// Initial capacity of the requested StringBuilder. + /// An instance of a StringBuilder. + /// + /// It can be called any number of times. If a StringBuilder is in the cache then it will be returned and the cache emptied. + /// A StringBuilder instance is cached in Thread Local Storage and so there is one per thread. + /// Subsequent calls will return a new StringBuilder. + /// + public static StringBuilder Acquire(int capacity = 16 /*StringBuilder.DefaultCapacity*/) + { + if (capacity <= MaxBuilderSize) + { + StringBuilder sb = StringBuilderCache.cachedInstance; + if (sb != null) + { + // Avoid stringbuilder block fragmentation by getting a new StringBuilder + // when the requested size is larger than the current capacity + if (capacity <= sb.Capacity) + { + StringBuilderCache.cachedInstance = null; + sb.Clear(); + return sb; + } + } + } + return new StringBuilder(capacity); + } + + /// + /// Place the specified builder in the cache if it is not too big. + /// + /// StringBuilder that is no longer used. + /// + /// The StringBuilder should not be used after it has been released. Unbalanced Releases are perfectly acceptable. + /// It will merely cause the runtime to create a new StringBuilder next time Acquire is called. + /// + public static void Release(StringBuilder sb) + { + if (sb.Capacity <= MaxBuilderSize) + { + StringBuilderCache.cachedInstance = sb; + } + } + + /// + /// Gets the resulting string and releases a StringBuilder instance. + /// + /// StringBuilder to be released. + /// The output of the StringBuilder. + public static string GetStringAndRelease(StringBuilder sb) + { + string result = sb.ToString(); + Release(sb); + return result; + } + } +} diff --git a/src/Microsoft.Diagnostics.EventFlow.Inputs.EventSource/project.json b/src/Microsoft.Diagnostics.EventFlow.Inputs.EventSource/project.json index bdcb62d8..0a01f503 100644 --- a/src/Microsoft.Diagnostics.EventFlow.Inputs.EventSource/project.json +++ b/src/Microsoft.Diagnostics.EventFlow.Inputs.EventSource/project.json @@ -8,7 +8,10 @@ "frameworks": { "netstandard1.6": { - "imports": "dnxcore50" + "imports": "dnxcore50", + "dependencies": { + "System.Diagnostics.Process": "4.1.0" + } }, "net46": {} }, diff --git a/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/EventSourceInputTests.cs b/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/EventSourceInputTests.cs index 5321cd26..4bc8c7bc 100644 --- a/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/EventSourceInputTests.cs +++ b/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/EventSourceInputTests.cs @@ -52,6 +52,30 @@ public void HandlesDuplicatePropertyNames() } } + [Fact] + public void ActivityPathDecoderDecodesHierarchicalActivityId() + { + Guid activityId = new Guid("000000110000000000000000be999d59"); + string activityPath = ActivityPathDecoder.GetActivityPathString(activityId); + Assert.Equal("//1/1/", activityPath); + } + + [Fact] + public void ActivityPathDecoderHandlesNonhierarchicalActivityIds() + { + string guidString = "bf0209f9-bf5e-415e-86ed-0e20b615b406"; + Guid activityId = new Guid(guidString); + string activityPath = ActivityPathDecoder.GetActivityPathString(activityId); + Assert.Equal(guidString, activityPath); + } + + [Fact] + public void ActivityPathDecoderHandlesEmptyActivityId() + { + string activityPath = ActivityPathDecoder.GetActivityPathString(Guid.Empty); + Assert.Equal(Guid.Empty.ToString(), activityPath); + } + [EventSource(Name = "EventSourceInput-TestEventSource")] private class EventSourceInputTestSource : EventSource { diff --git a/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/project.json b/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/project.json index 2cbcea77..4ecb56f7 100644 --- a/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/project.json +++ b/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/project.json @@ -1,7 +1,9 @@ { "version": "1.0.0-*", "buildOptions": { - "debugType": "portable" + "debugType": "portable", + "keyFile": "../../PublicKey.snk", + "delaySign": true }, "dependencies": { "dotnet-test-xunit": "2.2.0-preview2-build1029",