EventSource and Span: take 1

Spread the love

Let’s say you’re living the zero-allocation, universal memory .NET life, and you need to trace some char data. The only problem is that you have a Span<char> and EventSource only supports string. What to do?

We’ll start with a typical EventSource implementation using string:

    [EventSource(Guid = "c451aca3-50ae-4955-9723-38b9ae7d98c0")]
    public sealed class MyEventSource : EventSource
    {
        public static readonly MyEventSource Instance = new MyEventSource();

        private MyEventSource()
        {
        }

        [Event(1, Message = "Hello {0} {1} {2}")]
        public void Hello(int n1, int n2, string s1)
        {
            this.WriteEvent(1, n1, n2, s1);
        }
    }

This generates a manifest like the following:

<instrumentationManifest xmlns="http://schemas.microsoft.com/win/2004/08/events">
 <instrumentation xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:win="http://manifests.microsoft.com/win/2004/08/windows/events">
  <events xmlns="http://schemas.microsoft.com/win/2004/08/events">
<provider name="MyEventSource" guid="{c451aca3-50ae-4955-9723-38b9ae7d98c0}" resourceFileName="x.dll" messageFileName="x.dll" symbol="MyEventSource">
 <tasks>
  <task name="Hello" message="$(string.task_Hello)" value="65533"/>
  <task name="EventSourceMessage" message="$(string.task_EventSourceMessage)" value="65534"/>
 </tasks>
 <opcodes>
 </opcodes>
 <keywords>
  <keyword name="Session3" message="$(string.keyword_Session3)" mask="0x100000000000"/>
  <keyword name="Session2" message="$(string.keyword_Session2)" mask="0x200000000000"/>
  <keyword name="Session1" message="$(string.keyword_Session1)" mask="0x400000000000"/>
  <keyword name="Session0" message="$(string.keyword_Session0)" mask="0x800000000000"/>
 </keywords>
 <events>
  <event value="0" version="0" level="win:LogAlways" symbol="EventSourceMessage" task="EventSourceMessage" template="EventSourceMessageArgs"/>
  <event value="1" version="0" level="win:Informational" symbol="Hello" message="$(string.event_Hello)" task="Hello" template="HelloArgs"/>
 </events>
 <templates>
  <template tid="EventSourceMessageArgs">
   <data name="message" inType="win:UnicodeString"/>
  </template>
  <template tid="HelloArgs">
   <data name="n1" inType="win:Int32"/>
   <data name="n2" inType="win:Int32"/>
   <data name="s1" inType="win:UnicodeString"/>
  </template>
 </templates>
</provider>
</events>
</instrumentation>
<localization>
 <resources culture="en-US">
  <stringTable>
   <string id="event_Hello" value="Hello %1 %2 %3"/>
   <string id="keyword_Session0" value="Session0"/>
   <string id="keyword_Session1" value="Session1"/>
   <string id="keyword_Session2" value="Session2"/>
   <string id="keyword_Session3" value="Session3"/>
   <string id="task_EventSourceMessage" value="EventSourceMessage"/>
   <string id="task_Hello" value="Hello"/>
  </stringTable>
 </resources>
</localization>
</instrumentationManifest>

Finally, a complete self-contained sample to trace and collect the EventSource data:

    internal sealed class Program
    {
        private static void Main()
        {
            const string TraceName = "SampleTrace";
            const string TracePath = "SampleTrace.etl";
            const string ManifestPath = "SampleTrace.man";
            string providerId = EventSource.GetGuid(typeof(MyEventSource)).ToString("b");
            File.WriteAllText(ManifestPath, EventSource.GenerateManifest(typeof(MyEventSource), "x.dll"));
            Logman("create trace", TraceName, "-ow -o", TracePath, "-p", providerId, "0xFFFFFFFFFFFFFFFF 5");
            Logman("start", TraceName);
            
            MyEventSource.Instance.Hello(1, 2, "three");
            
            Logman("stop", TraceName);
            Logman("delete", TraceName);
            FileInfo traceFile = new DirectoryInfo(".").EnumerateFiles("SampleTrace*.etl").Last();
            Exec("tracerpt.exe", "-y -l", traceFile.FullName, "-import", ManifestPath);
        }

        private static void Logman(params string[] args) => Exec("logman.exe", args);

        private static void Exec(string fileName, params string[] args)
        {
            using Process process = Process.Start(fileName, string.Join(' ', args));
            process.WaitForExit();
            if (process.ExitCode != 0)
            {
                throw new InvalidOperationException($"{fileName} failed with code {process.ExitCode}.");
            }
        }
    }

We’re using logman.exe to create the trace collector and tracerpt.exe to decode the ETL data. The resulting dumpfile.xml shows the expected event data:

	<EventData>
		<Data Name="n1">1</Data>
		<Data Name="n2">2</Data>
		<Data Name="s1">three</Data>
	</EventData>

Obviously, this EventSource does not meet our requirements. It uses a string and also the slow WriteEvent overload. Let’s fix the latter problem:

    [EventSource(Guid = "c451aca3-50ae-4955-9723-38b9ae7d98c0")]
    public sealed class MyEventSource : EventSource
    {
        public static readonly MyEventSource Instance = new MyEventSource();

        private MyEventSource()
        {
        }

        [Event(1, Message = "Hello {0} {1} {2}")]
        public unsafe void Hello(int n1, int n2, string s1)
        {
            if (this.IsEnabled())
            {
                s1 ??= string.Empty;
                fixed (char* _s1 = s1)
                {
                    EventData* descrs = stackalloc EventData[3];
                    descrs[0].DataPointer = (IntPtr)(&n1);
                    descrs[0].Size = sizeof(int);
                    descrs[1].DataPointer = (IntPtr)(&n2);
                    descrs[1].Size = sizeof(int);
                    descrs[2].DataPointer = (IntPtr)_s1;
                    descrs[2].Size = (s1.Length + 1) * 2;
                    this.WriteEventWithRelatedActivityIdCore(1, null, 3, descrs);
                }
            }
        }
    }

It’s not pretty to look at but it has the same outcome as the simple version without the overhead. Still, we haven’t solved the Span problem. How can we get EventSource to accept a nonstandard event signature while still generating a proper manifest?

It turns out that event methods do not have to be public, so we can write the undesirable string version as a private method. Then we can use [NonEvent] methods to implement the Span logic that we need:

    [EventSource(Guid = "c451aca3-50ae-4955-9723-38b9ae7d98c0")]
    public sealed class MyEventSource3 : EventSource
    {
        public static readonly MyEventSource3 Instance = new MyEventSource3();

        private MyEventSource3()
        {
        }

        [NonEvent]
        public void Hello(int n1, int n2, ReadOnlySpan<char> s1)
        {
            this.HelloCore(n1, n2, s1);
        }

        [Event(1, Message = "Hello {0} {1} {2}")]
        private void Hello(int n1, int n2, string s1)
        {
            this.HelloCore(n1, n2, s1);
        }

        [NonEvent]
        private unsafe void HelloCore(int n1, int n2, ReadOnlySpan<char> s1)
        {
            if (this.IsEnabled())
            {
                fixed (char* _s1 = s1)
                {
                    EventData* descrs = stackalloc EventData[3];
                    descrs[0].DataPointer = (IntPtr)(&n1);
                    descrs[0].Size = sizeof(int);
                    descrs[1].DataPointer = (IntPtr)(&n2);
                    descrs[1].Size = sizeof(int);
                    descrs[2].DataPointer = (IntPtr)_s1;
                    descrs[2].Size = (s1.Length + 1) * 2;
                    this.WriteEventWithRelatedActivityIdCore(1, null, 3, descrs);
                }
            }
        }
    }

(Note that since string is implicitly convertible to Span<char>, we can call the “core” method in both situations.)

We need one modification of the main program to demonstrate the Span case:

            const string Str = "threeeeeeeeeeee";
            ReadOnlySpan<char> s = Str.AsSpan(0, 5);
            MyEventSource.Instance.Hello(1, 2, s);

The generated manifest is identical, so that’s a win. Unfortunately, the data is not what we expected:

	<EventData>
		<Data Name="n1">1</Data>
		<Data Name="n2">2</Data>
		<Data Name="s1">threee</Data>
	</EventData>

We expected “three” but got “threee”. The problem is here:

                    descrs[2].Size = (s1.Length + 1) * 2;

ETW expects all strings to be null-terminated, but we are using a span which is a five character prefix from a larger string, no more and no less. What we have here is an out of bounds error that is not reported by the runtime. When we said unsafe we meant it!

All we wanted was allocation-free string data tracing, but it would seem that we’re out of luck here with Span. Is there any way around this limitation? We’ll find out, when we pick up this investigation later…

One thought on “EventSource and Span: take 1

  1. Pingback: EventSource and Span: take 2 – WriteAsync .NET

Leave a Reply

Your email address will not be published. Required fields are marked *