File: NativeRuntimeEventSourceGenerator.cs
Web Access
Project: src\src\libraries\System.Private.CoreLib\gen\System.Private.CoreLib.Generators.csproj (System.Private.CoreLib.Generators)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
 
namespace Generators
{
    [Generator]
    public sealed class NativeRuntimeEventSourceGenerator : IIncrementalGenerator
    {
        private static readonly XNamespace EventNs = "http://schemas.microsoft.com/win/2004/08/events";
 
        public void Initialize(IncrementalGeneratorInitializationContext context)
        {
            IncrementalValuesProvider<AdditionalText> manifestFiles = context.AdditionalTextsProvider.Where(f => f.Path.EndsWith(".man", StringComparison.OrdinalIgnoreCase));
            IncrementalValuesProvider<AdditionalText> inclusionFiles = context.AdditionalTextsProvider.Where(f => f.Path.EndsWith(".lst", StringComparison.OrdinalIgnoreCase));
 
            IncrementalValuesProvider<(AdditionalText Left, System.Collections.Immutable.ImmutableArray<AdditionalText> Right)> combined = manifestFiles.Combine(inclusionFiles.Collect());
 
            context.RegisterSourceOutput(combined, (spc, tuple) =>
            {
                AdditionalText manifestFile = tuple.Left;
                System.Collections.Immutable.ImmutableArray<AdditionalText> inclusionFiles = tuple.Right;
                string manifestText = manifestFile.GetText(spc.CancellationToken)?.ToString();
                if (string.IsNullOrEmpty(manifestText))
                {
                    return;
                }
 
                var manifest = XDocument.Parse(manifestText);
 
                string inclusionText = inclusionFiles.FirstOrDefault()?.GetText(spc.CancellationToken)?.ToString();
 
                Dictionary<string, HashSet<string>> inclusionList = ParseInclusionListFromString(inclusionText);
 
                foreach (KeyValuePair<string, string> kvp in manifestsToGenerate)
                {
                    string providerName = kvp.Key;
                    string className = providerNameToClassNameMap[providerName];
                    XElement? providerNode = manifest
                        .Descendants(EventNs + "provider")
                        .FirstOrDefault(e => (string)e.Attribute("name") == providerName);
 
                    if (providerNode is null)
                    {
                        continue;
                    }
 
                    string source = GenerateEventSourceClass(providerNode, className, inclusionList);
                    spc.AddSource($"{className}.g.cs", SourceText.From(source, System.Text.Encoding.UTF8));
                }
            });
        }
 
        private static Dictionary<string, HashSet<string>> ParseInclusionListFromString(string? inclusionText)
        {
            Dictionary<string, HashSet<string>> inclusionList = [];
            if (string.IsNullOrEmpty(inclusionText))
            {
                return inclusionList;
            }
 
            using var reader = new StringReader(inclusionText);
            string line;
            while ((line = reader.ReadLine()) != null)
            {
                string trimmed = line.Trim();
                if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith("#"))
                {
                    continue;
                }
 
                string[] tokens = trimmed.Split(':');
                if (tokens.Length == 0)
                {
                    continue;
                }
 
                if (tokens.Length > 2)
                {
                    continue;
                }
 
                string providerName, eventName;
                if (tokens.Length == 2)
                {
                    providerName = tokens[0];
                    eventName = tokens[1];
                }
                else
                {
                    providerName = "*";
                    eventName = tokens[0];
                }
                if (!inclusionList.TryGetValue(providerName, out HashSet<string>? value))
                {
                    value = [];
                    inclusionList[providerName] = value;
                }
 
                value.Add(eventName);
            }
            return inclusionList;
        }
 
        private static bool IncludeEvent(Dictionary<string, HashSet<string>> inclusionList, string providerName, string eventName)
        {
            if (inclusionList == null || inclusionList.Count == 0)
            {
                return true;
            }
 
            if (inclusionList.TryGetValue(providerName, out HashSet<string>? events) && events.Contains(eventName))
            {
                return true;
            }
 
            if (inclusionList.TryGetValue("*", out HashSet<string>? wildcardEvents) && wildcardEvents.Contains(eventName))
            {
                return true;
            }
 
            return false;
        }
 
        private static string GenerateEventSourceClass(XElement providerNode, string className, Dictionary<string, HashSet<string>> inclusionList)
        {
            var sw = new StringWriter();
 
            sw.WriteLine($$"""
                // Licensed to the .NET Foundation under one or more agreements.
                // The .NET Foundation licenses this file to you under the MIT license.
                // <auto-generated/>
 
                using System;
 
                namespace System.Diagnostics.Tracing
                {
                    internal sealed partial class {{className}} : EventSource
                    {
                """);
 
            GenerateKeywordsClass(providerNode, sw, inclusionList);
            GenerateEventMethods(providerNode, sw, inclusionList);
 
            sw.WriteLine("""
                    }
                }
                """);
            return sw.ToString();
        }
 
        private static void GenerateKeywordsClass(XElement providerNode, StringWriter writer, Dictionary<string, HashSet<string>> inclusionList)
        {
            string? providerName = providerNode.Attribute("name")?.Value;
 
            if (providerName is null)
            {
                return;
            }
 
            XElement eventsNode = providerNode.Element(EventNs + "events");
            if (eventsNode is null)
            {
                return;
            }
 
            IEnumerable<XElement> eventNodes = eventsNode.Elements(EventNs + "event");
            var usedKeywords = new HashSet<string>();
            foreach (XElement? eventNode in eventNodes)
            {
                string? eventName = eventNode.Attribute("symbol")?.Value;
 
                if (eventName is null
                    || !IncludeEvent(inclusionList, providerName, eventName))
                {
                    continue;
                }
 
                string? keywords = eventNode.Attribute("keywords")?.Value;
                if (!string.IsNullOrEmpty(keywords))
                {
                    foreach (string? kw in keywords.Split([' '], StringSplitOptions.RemoveEmptyEntries))
                    {
                        usedKeywords.Add(kw);
                    }
                }
            }
            XElement? keywordsNode = providerNode.Element(EventNs + "keywords");
            if (keywordsNode is null)
            {
                return;
            }
 
            writer.WriteLine("""
                        public static class Keywords
                        {
                """);
 
            foreach (XElement keywordNode in keywordsNode.Elements(EventNs + "keyword"))
            {
                string? name = keywordNode.Attribute("name")?.Value;
                string? mask = keywordNode.Attribute("mask")?.Value;
                if (name is not null && mask is not null && usedKeywords.Contains(name))
                {
                    writer.WriteLine($"            public const EventKeywords {name} = (EventKeywords){mask};");
                }
            }
 
            writer.WriteLine("""
                        }
 
                """);
        }
 
        private static void GenerateEventMethods(XElement providerNode, StringWriter writer, Dictionary<string, HashSet<string>> inclusionList)
        {
            string? providerName = providerNode.Attribute("name")?.Value;
 
            if (providerName is null)
            {
                return;
            }
 
            XElement eventsNode = providerNode.Element(EventNs + "events");
            if (eventsNode == null)
            {
                return;
            }
 
            var eventNodes = eventsNode.Elements(EventNs + "event").ToList();
            XElement templatesNode = providerNode.Element(EventNs + "templates");
            var templateDict = new Dictionary<string, XElement>();
            if (templatesNode != null)
            {
                foreach (XElement? template in templatesNode.Elements(EventNs + "template"))
                {
                    string? name = template.Attribute("tid")?.Value;
                    if (!string.IsNullOrEmpty(name))
                    {
                        templateDict[name] = template;
                    }
                }
            }
 
            // Build a dictionary of eventID -> latest version
            Dictionary<string, string> latestEventVersions = [];
            foreach (XElement? eventNode in eventNodes)
            {
                string? eventName = eventNode.Attribute("symbol")?.Value;
                if (eventName is null
                    || !IncludeEvent(inclusionList, providerName, eventName))
                {
                    continue;
                }
 
                string? eventId = eventNode.Attribute("value")?.Value;
                string? version = eventNode.Attribute("version")?.Value;
                if (eventId is not null && version is not null)
                {
                    if (!latestEventVersions.TryGetValue(eventId, out string? existingVersion) || string.CompareOrdinal(version, existingVersion) > 0)
                    {
                        latestEventVersions[eventId] = version;
                    }
                }
            }
 
            foreach (XElement? eventNode in eventNodes)
            {
                string? eventName = eventNode.Attribute("symbol")?.Value;
                if (eventName is null
                    || !IncludeEvent(inclusionList, providerName, eventName))
                {
                    continue;
                }
 
                if (IsEventManuallyHandled(eventName))
                {
                    continue;
                }
 
                string? eventId = eventNode.Attribute("value")?.Value;
                string? version = eventNode.Attribute("version")?.Value;
                // Only emit the event if it is the latest version for this eventId
                if (eventId is null || version is null || latestEventVersions[eventId] != version)
                {
                    continue;
                }
 
                string? level = eventNode.Attribute("level")?.Value;
                IEnumerable<string>? keywords = eventNode
                    .Attribute("keywords")
                    ?.Value
                    .ToString()
                    .Split([' '], StringSplitOptions.RemoveEmptyEntries)
                    .Select(k => $"Keywords.{k}");
 
                writer.Write($"        [Event({eventId}, Version = {version}, Level = EventLevel.{level?.Replace("win:", "")}");
 
                if (keywords?.Any() == true)
                {
                    writer.Write($", Keywords = {string.Join(" | ", keywords)}");
                }
 
                writer.WriteLine(")]");
 
                // Write the method signature
                writer.Write($"        private void {eventName}(");
 
                string? templateValue = eventNode.Attribute("template")?.Value;
 
                if (!string.IsNullOrEmpty(templateValue)
                    && templateDict.TryGetValue(templateValue, out XElement? template))
                {
                    IEnumerable<XElement> dataNodes = template.Elements(EventNs + "data").ToArray();
                    var paramList = new List<string>();
 
                    // Calculate the number of arguments to emit.
                    // COMPAT: Cut the parameter list at any binary or ansi string arguments,
                    // or if the count attribute is set on any of the parameters.
                    int numArgumentsToEmit = 0;
                    foreach (XElement data in dataNodes)
                    {
                        string? paramType = data.Attribute("inType")?.Value.ToString();
 
                        if (paramType is "win:Binary" or "win:AnsiString")
                        {
                            break;
                        }
 
                        if (!string.IsNullOrEmpty(data.Attribute("count")?.Value))
                        {
                            break;
                        }
 
                        numArgumentsToEmit++;
                    }
 
                    foreach (XElement data in dataNodes)
                    {
                        if (numArgumentsToEmit-- <= 0)
                        {
                            break;
                        }
 
                        string? paramType = data.Attribute("inType")?.Value;
                        string? paramName = data.Attribute("name")?.Value;
                        if (paramType is not null && paramName is not null
                            && manifestTypeToCSharpTypeMap.TryGetValue(paramType, out string? csType))
                        {
                            paramList.Add($"{csType} {paramName}");
                        }
                        else if (paramType is not null && paramName is not null)
                        {
                            paramList.Add($"object {paramName}");
                        }
                    }
                    writer.Write(string.Join(", ", paramList));
                }
 
                writer.WriteLine("""
                    )
                    {
                        // To have this event be emitted from managed side, refer to NativeRuntimeEventSource.cs
                        throw new NotImplementedException();
                    }
 
                    """);
            }
        }
 
        private static bool IsEventManuallyHandled(string eventName)
        {
            foreach (string handledEvent in manuallyHandledEventSymbols)
            {
                if (eventName.StartsWith(handledEvent, StringComparison.Ordinal))
                {
                    return true;
                }
            }
            return false;
        }
 
        private static readonly Dictionary<string, string> manifestsToGenerate = new()
        {
            { "Microsoft-Windows-DotNETRuntime", "NativeRuntimeEventSource.Generated.cs" }
        };
 
        private static readonly Dictionary<string, string> providerNameToClassNameMap = new()
        {
            { "Microsoft-Windows-DotNETRuntime", "NativeRuntimeEventSource" }
        };
 
        private static readonly Dictionary<string, string> manifestTypeToCSharpTypeMap = new()
        {
            { "win:UInt8", "byte" },
            { "win:UInt16", "ushort" },
            { "win:UInt32", "uint" },
            { "win:UInt64", "ulong" },
            { "win:Int32", "int" },
            { "win:Int64", "long" },
            { "win:Pointer", "IntPtr" },
            { "win:UnicodeString", "string" },
            { "win:Binary", "byte[]" },
            { "win:Double", "double" },
            { "win:Boolean", "bool" },
            { "win:GUID", "Guid" },
        };
 
        private static readonly List<string> manuallyHandledEventSymbols =
        [
            // Some threading events are defined manually in NativeRuntimeEventSource.Threading.cs
            "ThreadPool",
            "Contention",
            "WaitHandle"
        ];
    }
}