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",