From f6715e378841c1a9bac3dfcf9dfc2aa792d7fb09 Mon Sep 17 00:00:00 2001 From: Alexandre Mutel Date: Sun, 22 Dec 2024 14:39:22 +0100 Subject: [PATCH] Add Thread Start/Stop and samplingId --- .../Parser/UltraNativeCallstackTraceEvent.cs | 31 +++++--- .../UltraNativeThreadStartTraceEvent.cs | 79 +++++++++++++++++++ .../Parser/UltraNativeThreadStopTraceEvent.cs | 62 +++++++++++++++ src/Ultra.Core/Parser/UltraSamplerParser.cs | 26 +++++- src/Ultra.Sampler/MacOS/MacOSUltraSampler.cs | 22 +++++- .../MacOS/NativeSamplingDelegate.cs | 2 +- src/Ultra.Sampler/UltraSamplerConstants.cs | 8 ++ src/Ultra.Sampler/UltraSamplerSource.cs | 54 ++++++++++--- 8 files changed, 254 insertions(+), 30 deletions(-) create mode 100644 src/Ultra.Core/Parser/UltraNativeThreadStartTraceEvent.cs create mode 100644 src/Ultra.Core/Parser/UltraNativeThreadStopTraceEvent.cs diff --git a/src/Ultra.Core/Parser/UltraNativeCallstackTraceEvent.cs b/src/Ultra.Core/Parser/UltraNativeCallstackTraceEvent.cs index 00a0081..1d4afb4 100644 --- a/src/Ultra.Core/Parser/UltraNativeCallstackTraceEvent.cs +++ b/src/Ultra.Core/Parser/UltraNativeCallstackTraceEvent.cs @@ -11,6 +11,7 @@ internal sealed class UltraNativeCallstackTraceEvent : TraceEvent { private static readonly string[] _payloadNames = [ + nameof(SamplingId), nameof(FrameThreadId), nameof(ThreadState), nameof(ThreadCpuUsage), @@ -26,17 +27,21 @@ internal UltraNativeCallstackTraceEvent(Action? _target = target; } - public ulong FrameThreadId => (ulong)GetInt64At(0); + public ulong SamplingId => (ulong)GetInt64At(0); - public UltraSamplerThreadState ThreadState => (UltraSamplerThreadState)GetInt32At(8); + public ulong FrameThreadId => (ulong)GetInt64At(8); - public double ThreadCpuUsage => GetInt32At(12) / 1000.0; + public UltraSamplerThreadState ThreadState => (UltraSamplerThreadState)GetInt32At(16); - public int PreviousFrameCount => GetInt32At(16); + public int ThreadCpuUsageAsInt => GetInt32At(20); + + public double ThreadCpuUsage => ThreadCpuUsageAsInt / 1000.0; + + public int PreviousFrameCount => GetInt32At(24); - public int FrameSize => GetInt32At(20); + public int FrameSize => GetInt32At(28); - public unsafe ReadOnlySpan FrameAddresses => new((byte*)DataStart + 24, FrameSize / sizeof(ulong)); + public unsafe ReadOnlySpan FrameAddresses => new((byte*)DataStart + 32, FrameSize / sizeof(ulong)); /// @@ -45,16 +50,18 @@ public override object PayloadValue(int index) switch (index) { case 0: - return FrameThreadId; + return SamplingId; case 1: - return (int)ThreadState; + return FrameThreadId; case 2: - return GetInt32At(12); + return (int)ThreadState; case 3: - return PreviousFrameCount; + return ThreadCpuUsageAsInt; case 4: - return FrameSize; + return PreviousFrameCount; case 5: + return FrameSize; + case 6: return FrameAddresses.ToArray(); default: throw new ArgumentOutOfRangeException(nameof(index)); @@ -80,4 +87,4 @@ protected override void Dispatch() protected override void Validate() { } -} \ No newline at end of file +} diff --git a/src/Ultra.Core/Parser/UltraNativeThreadStartTraceEvent.cs b/src/Ultra.Core/Parser/UltraNativeThreadStartTraceEvent.cs new file mode 100644 index 0000000..496f747 --- /dev/null +++ b/src/Ultra.Core/Parser/UltraNativeThreadStartTraceEvent.cs @@ -0,0 +1,79 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// Licensed under the BSD-Clause 2 license. +// See license.txt file in the project root for full license information. + +using System.Text; +using Microsoft.Diagnostics.Tracing; +using Ultra.Sampler; + +namespace Ultra.Core; + +internal sealed class UltraNativeThreadStartTraceEvent : TraceEvent +{ + private static readonly string[] _payloadNames = + [ + nameof(SamplingId), + nameof(FrameThreadId), + nameof(ThreadNameSize), + nameof(ThreadName), + ]; + + private Action? _target; + + internal UltraNativeThreadStartTraceEvent(Action? target, int eventID, int task, string taskName, Guid taskGuid, int opcode, string opcodeName, Guid providerGuid, string providerName) : base(eventID, task, taskName, taskGuid, opcode, opcodeName, providerGuid, providerName) + { + _target = target; + } + + public ulong SamplingId => (ulong)GetInt64At(0); + + public ulong FrameThreadId => (ulong)GetInt64At(8); + + public int ThreadNameSize => GetInt32At(16); + + public unsafe byte* ThreadNamePointer => (byte*)DataStart + 24 + ThreadNameSize; + + public unsafe string ThreadName => Encoding.UTF8.GetString(new ReadOnlySpan(ThreadNamePointer, ThreadNameSize)); + + /// + + public override object PayloadValue(int index) + { + switch (index) + { + case 0: + return SamplingId; + case 1: + return FrameThreadId; + case 2: + return ThreadNameSize; + case 3: + unsafe + { + return new ReadOnlySpan(ThreadNamePointer, ThreadNameSize).ToArray(); + } + default: + throw new ArgumentOutOfRangeException(nameof(index)); + } + } + + public override string[] PayloadNames => _payloadNames; + + /// + protected override Delegate? Target + { + get => _target; + set => _target = (Action?)value; + } + + /// + protected override void Dispatch() + { + _target?.Invoke(this); + } + + /// + protected override void Validate() + { + } +} \ No newline at end of file diff --git a/src/Ultra.Core/Parser/UltraNativeThreadStopTraceEvent.cs b/src/Ultra.Core/Parser/UltraNativeThreadStopTraceEvent.cs new file mode 100644 index 0000000..df57bcb --- /dev/null +++ b/src/Ultra.Core/Parser/UltraNativeThreadStopTraceEvent.cs @@ -0,0 +1,62 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// Licensed under the BSD-Clause 2 license. +// See license.txt file in the project root for full license information. + +using Microsoft.Diagnostics.Tracing; + +namespace Ultra.Core; + +internal sealed class UltraNativeThreadStopTraceEvent : TraceEvent +{ + private static readonly string[] _payloadNames = + [ + nameof(SamplingId), + nameof(FrameThreadId), + ]; + + private Action? _target; + + internal UltraNativeThreadStopTraceEvent(Action? target, int eventID, int task, string taskName, Guid taskGuid, int opcode, string opcodeName, Guid providerGuid, string providerName) : base(eventID, task, taskName, taskGuid, opcode, opcodeName, providerGuid, providerName) + { + _target = target; + } + + public ulong SamplingId => (ulong)GetInt64At(0); + + public ulong FrameThreadId => (ulong)GetInt64At(8); + + /// + + public override object PayloadValue(int index) + { + switch (index) + { + case 0: + return SamplingId; + case 1: + return FrameThreadId; + default: + throw new ArgumentOutOfRangeException(nameof(index)); + } + } + + public override string[] PayloadNames => _payloadNames; + + /// + protected override Delegate? Target + { + get => _target; + set => _target = (Action?)value; + } + + /// + protected override void Dispatch() + { + _target?.Invoke(this); + } + + /// + protected override void Validate() + { + } +} \ No newline at end of file diff --git a/src/Ultra.Core/Parser/UltraSamplerParser.cs b/src/Ultra.Core/Parser/UltraSamplerParser.cs index 70861c7..7503fb6 100644 --- a/src/Ultra.Core/Parser/UltraSamplerParser.cs +++ b/src/Ultra.Core/Parser/UltraSamplerParser.cs @@ -31,7 +31,19 @@ public event Action EventNativeModule add => source.RegisterEventTemplate(CreateUltraNativeModuleTraceEvent(value)); remove => source.UnregisterEventTemplate(value, UltraSamplerConstants.NativeModuleEventId, ProviderGuid); } - + + public event Action EventNativeThreadStart + { + add => source.RegisterEventTemplate(CreateUltraNativeThreadStartTraceEvent(value)); + remove => source.UnregisterEventTemplate(value, UltraSamplerConstants.NativeThreadStartEventId, ProviderGuid); + } + + public event Action EventNativeThreadStop + { + add => source.RegisterEventTemplate(CreateUltraNativeThreadStopTraceEvent(value)); + remove => source.UnregisterEventTemplate(value, UltraSamplerConstants.NativeThreadStopEventId, ProviderGuid); + } + /// protected override string GetProviderName() => UltraSamplerConstants.ProviderName; @@ -44,6 +56,8 @@ protected override void EnumerateTemplates(Func? value) - => new UltraNativeCallstackTraceEvent(value, UltraSamplerConstants.NativeCallStackEventId, 0, "OnNativeCallStack", Guid.Empty, 0, "OnNativeCallStack", ProviderGuid, ProviderName); + => new UltraNativeCallstackTraceEvent(value, UltraSamplerConstants.NativeCallStackEventId, 0, "NativeCallStack", Guid.Empty, 0, "NativeCallStack", ProviderGuid, ProviderName); private static TraceEvent CreateUltraNativeModuleTraceEvent(Action? value) - => new UltraNativeModuleTraceEvent(value, UltraSamplerConstants.NativeModuleEventId, 0, "OnNativeModule", Guid.Empty, 0, "OnNativeModule", ProviderGuid, ProviderName); + => new UltraNativeModuleTraceEvent(value, UltraSamplerConstants.NativeModuleEventId, 0, "NativeModule", Guid.Empty, 0, "NativeModule", ProviderGuid, ProviderName); + + private static TraceEvent CreateUltraNativeThreadStartTraceEvent(Action? value) + => new UltraNativeThreadStartTraceEvent(value, UltraSamplerConstants.NativeThreadStartEventId, 0, "NativeThreadStart", Guid.Empty, 0, "NativeThreadStart", ProviderGuid, ProviderName); + + private static TraceEvent CreateUltraNativeThreadStopTraceEvent(Action? value) + => new UltraNativeThreadStopTraceEvent(value, UltraSamplerConstants.NativeThreadStopEventId, 0, "NativeThreadStop", Guid.Empty, 0, "NativeThreadStop", ProviderGuid, ProviderName); } \ No newline at end of file diff --git a/src/Ultra.Sampler/MacOS/MacOSUltraSampler.cs b/src/Ultra.Sampler/MacOS/MacOSUltraSampler.cs index 6a5dd20..ae7b232 100644 --- a/src/Ultra.Sampler/MacOS/MacOSUltraSampler.cs +++ b/src/Ultra.Sampler/MacOS/MacOSUltraSampler.cs @@ -19,6 +19,7 @@ internal unsafe class MacOSUltraSampler : UltraSampler private Thread? _samplerThread; private ulong _samplerThreadId; private readonly AutoResetEvent _samplerResumeThreadEvent; + private ulong _samplingId; // Frames information private const int MaximumFrames = 4096; @@ -148,7 +149,7 @@ private unsafe void RunImpl() } catch (Exception ex) { - Console.Error.WriteLine($"Ultra-Sampler unexpected exception while sampling: {ex}"); + Console.Error.WriteLine($"Ultra-Sampler stopped. Unexpected exception while sampling: {ex}"); } } @@ -354,6 +355,18 @@ private unsafe void Sample(MacOS.MacOSLibSystem.mach_port_t rootTask, NativeSamp continue; } + // If thread was not active before, simulate a thread start event with its name + if (!_activeThreadIds.Contains(threadInfo.thread_id)) + { + var threadName = new ReadOnlySpan(threadExtendedInfo.pth_name, 64); + var length = threadName.IndexOf((byte)0); + if (length < 0) + { + length = 64; + } + _samplerEventSource.NativeThreadStart(_samplingId, threadInfo.thread_id, length, threadExtendedInfo.pth_name); + } + // ------------------------------------------------------------------- // Suspend the thread // ------------------------------------------------------------------- @@ -385,7 +398,7 @@ private unsafe void Sample(MacOS.MacOSLibSystem.mach_port_t rootTask, NativeSamp frameCount -= sameFrameCount; // Long only the delta frames - samplingDelegate(threadInfo.thread_id, (int)threadExtendedInfo.pth_run_state, (int)threadExtendedInfo.pth_cpu_usage, sameFrameCount, frameCount * sizeof(ulong), (byte*)pFrames); + samplingDelegate(_samplingId, threadInfo.thread_id, (int)threadExtendedInfo.pth_run_state, (int)threadExtendedInfo.pth_cpu_usage, sameFrameCount, frameCount * sizeof(ulong), (byte*)pFrames); } // Cleanup threads that are no longer active @@ -397,11 +410,16 @@ private unsafe void Sample(MacOS.MacOSLibSystem.mach_port_t rootTask, NativeSamp { _freeCompressedFramesIndices.Add(compressedFrameIndex); } + + _samplerEventSource.NativeThreadStop(_samplingId, threadInfo.thread_id); } } // Swap the active and current thread ids (_currentThreadIds, _activeThreadIds) = (_activeThreadIds, _currentThreadIds); + + // Increment the sampling id + _samplingId++; } private int ComputeSameFrameCount(ulong threadId, int frameCount, ulong* frames) diff --git a/src/Ultra.Sampler/MacOS/NativeSamplingDelegate.cs b/src/Ultra.Sampler/MacOS/NativeSamplingDelegate.cs index f719ed9..58ca207 100644 --- a/src/Ultra.Sampler/MacOS/NativeSamplingDelegate.cs +++ b/src/Ultra.Sampler/MacOS/NativeSamplingDelegate.cs @@ -4,4 +4,4 @@ namespace Ultra.Sampler.MacOS; -internal unsafe delegate void NativeSamplingDelegate(ulong threadId, int threadState, int threadCpuUsage, int previousFrameCount, int deltaFrameSizeInBytes, byte* deltaFrames); \ No newline at end of file +internal unsafe delegate void NativeSamplingDelegate(ulong samplingId, ulong threadId, int threadState, int threadCpuUsage, int previousFrameCount, int deltaFrameSizeInBytes, byte* deltaFrames); \ No newline at end of file diff --git a/src/Ultra.Sampler/UltraSamplerConstants.cs b/src/Ultra.Sampler/UltraSamplerConstants.cs index 730bf83..1e04516 100644 --- a/src/Ultra.Sampler/UltraSamplerConstants.cs +++ b/src/Ultra.Sampler/UltraSamplerConstants.cs @@ -16,7 +16,15 @@ public static class UltraSamplerConstants public const int NativeModuleEventId = 2; + public const int NativeThreadStartEventId = 3; + + public const int NativeThreadStopEventId = 4; + public const int TaskNativeCallStackEventId = 1; public const int TaskNativeModuleEventId = 2; + + public const int TaskNativeThreadStartEventId = 3; + + public const int TaskNativeThreadStopEventId = 4; } \ No newline at end of file diff --git a/src/Ultra.Sampler/UltraSamplerSource.cs b/src/Ultra.Sampler/UltraSamplerSource.cs index 94fd653..37c6b2d 100644 --- a/src/Ultra.Sampler/UltraSamplerSource.cs +++ b/src/Ultra.Sampler/UltraSamplerSource.cs @@ -16,25 +16,27 @@ internal sealed class UltraSamplerSource : EventSource private UltraSamplerSource() { } - + [Event(UltraSamplerConstants.NativeCallStackEventId, Level = EventLevel.Informational, Task = (EventTask)UltraSamplerConstants.TaskNativeCallStackEventId)] [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] - public unsafe void NativeCallstack(ulong threadId, int threadState, int threadCpuUsage, int previousFrameCount, int framesSize, byte* frames) // frames is last to allow perfview to visualize previous fixed size arguments and also, it is an ulong otherwise the EventSource will silently fail to register! + public unsafe void NativeCallstack(ulong samplingId, ulong threadId, int threadState, int threadCpuUsage, int previousFrameCount, int framesSize, byte* frames) // frames is last to allow perfview to visualize previous fixed size arguments and also, it is an ulong otherwise the EventSource will silently fail to register! { - var evt = stackalloc EventData[6]; - evt[0].DataPointer = (nint)(void*)&threadId; + var evt = stackalloc EventData[7]; + evt[0].DataPointer = (nint)(void*)&samplingId; evt[0].Size = sizeof(ulong); - evt[1].DataPointer = (nint)(void*)&threadState; - evt[1].Size = sizeof(int); - evt[2].DataPointer = (nint)(void*)&threadCpuUsage; + evt[1].DataPointer = (nint)(void*)&threadId; + evt[1].Size = sizeof(ulong); + evt[2].DataPointer = (nint)(void*)&threadState; evt[2].Size = sizeof(int); - evt[3].DataPointer = (nint)(void*)&previousFrameCount; + evt[3].DataPointer = (nint)(void*)&threadCpuUsage; evt[3].Size = sizeof(int); - evt[4].DataPointer = (nint)(void*)&framesSize; + evt[4].DataPointer = (nint)(void*)&previousFrameCount; evt[4].Size = sizeof(int); - evt[5].DataPointer = (nint)(void*)frames; - evt[5].Size = framesSize; - WriteEventCore(UltraSamplerConstants.NativeCallStackEventId, 6, evt); + evt[5].DataPointer = (nint)(void*)&framesSize; + evt[5].Size = sizeof(int); + evt[6].DataPointer = (nint)(void*)frames; + evt[6].Size = framesSize; + WriteEventCore(UltraSamplerConstants.NativeCallStackEventId, 7, evt); } [Event(UltraSamplerConstants.NativeModuleEventId, Level = EventLevel.Informational, Task = (EventTask)UltraSamplerConstants.TaskNativeModuleEventId)] @@ -74,6 +76,34 @@ private UltraSamplerSource() } } + [Event(UltraSamplerConstants.NativeThreadStartEventId, Level = EventLevel.Informational, Task = (EventTask)UltraSamplerConstants.TaskNativeThreadStartEventId)] + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + public unsafe void NativeThreadStart(ulong samplingId, ulong threadId, int threadNameSize, byte* threadName) + { + var evt = stackalloc EventData[4]; + evt[0].DataPointer = (nint)(void*)&samplingId; + evt[0].Size = sizeof(ulong); + evt[1].DataPointer = (nint)(void*)&threadId; + evt[1].Size = sizeof(ulong); + evt[2].DataPointer = (nint)(void*)&threadNameSize; + evt[2].Size = sizeof(int); + evt[3].DataPointer = (nint)(void*)threadName; + evt[3].Size = threadNameSize; + WriteEventCore(UltraSamplerConstants.NativeThreadStartEventId, 4, evt); + } + + [Event(UltraSamplerConstants.NativeThreadStopEventId, Level = EventLevel.Informational, Task = (EventTask)UltraSamplerConstants.TaskNativeThreadStopEventId)] + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + public unsafe void NativeThreadStop(ulong samplingId, ulong threadId) + { + var evt = stackalloc EventData[2]; + evt[0].DataPointer = (nint)(void*)&samplingId; + evt[0].Size = sizeof(ulong); + evt[1].DataPointer = (nint)(void*)&threadId; + evt[1].Size = sizeof(ulong); + WriteEventCore(UltraSamplerConstants.NativeThreadStopEventId, 2, evt); + } + [NonEvent] protected override void OnEventCommand(EventCommandEventArgs command) {