diff --git a/src/Ultra.Core/DiagnosticPortSession.cs b/src/Ultra.Core/DiagnosticPortSession.cs index 57e124c..588f4b6 100644 --- a/src/Ultra.Core/DiagnosticPortSession.cs +++ b/src/Ultra.Core/DiagnosticPortSession.cs @@ -7,6 +7,7 @@ using System.Diagnostics.Tracing; using Microsoft.Diagnostics.NETCore.Client; using Microsoft.Diagnostics.Tracing.Parsers; +using Ultra.Sampler; namespace Ultra.Core; diff --git a/src/Ultra.Sampler/MacOS/NativeModuleEventKind.cs b/src/Ultra.Core/Parser/UltraSamplerNativeModuleEventKind.cs similarity index 76% rename from src/Ultra.Sampler/MacOS/NativeModuleEventKind.cs rename to src/Ultra.Core/Parser/UltraSamplerNativeModuleEventKind.cs index 269e570..fc53b77 100644 --- a/src/Ultra.Sampler/MacOS/NativeModuleEventKind.cs +++ b/src/Ultra.Core/Parser/UltraSamplerNativeModuleEventKind.cs @@ -2,11 +2,13 @@ // Licensed under the BSD-Clause 2 license. // See license.txt file in the project root for full license information. -namespace Ultra.Sampler.MacOS; +namespace Ultra.Core; -internal enum NativeModuleEventKind +public enum UltraSamplerNativeModuleEventKind { AlreadyLoaded = 0, + Loaded = 1, + Unloaded = 2 } \ No newline at end of file diff --git a/src/Ultra.Core/Parser/UltraSamplerParser.cs b/src/Ultra.Core/Parser/UltraSamplerParser.cs index 79e506b..1851b4c 100644 --- a/src/Ultra.Core/Parser/UltraSamplerParser.cs +++ b/src/Ultra.Core/Parser/UltraSamplerParser.cs @@ -4,6 +4,7 @@ using System.Text; using Microsoft.Diagnostics.Tracing; +using Ultra.Sampler; namespace Ultra.Core; @@ -142,7 +143,7 @@ internal UltraNativeModuleTraceEvent(Action? target _target = target; } - public UltraNativeModuleEventKind NativeModuleEventKind => (UltraNativeModuleEventKind)GetInt32At(0); + public UltraSamplerNativeModuleEventKind NativeModuleEventKind => (UltraSamplerNativeModuleEventKind)GetInt32At(0); public ulong LoadAddress => (ulong)GetInt64At(4); @@ -198,13 +199,4 @@ protected override void Dispatch() protected override void Validate() { } -} - -public enum UltraNativeModuleEventKind -{ - AlreadyLoaded = 0, - - Loaded = 1, - - Unloaded = 2 -} +} \ No newline at end of file diff --git a/src/Ultra.Sampler/MacOS/MacOSLibSystem.cs b/src/Ultra.Sampler/MacOS/MacOSLibSystem.cs index b2fc3e4..321a076 100644 --- a/src/Ultra.Sampler/MacOS/MacOSLibSystem.cs +++ b/src/Ultra.Sampler/MacOS/MacOSLibSystem.cs @@ -2,6 +2,7 @@ // Licensed under the BSD-Clause 2 license. // See license.txt file in the project root for full license information. +using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -30,6 +31,7 @@ internal static partial class MacOSLibSystem public const int TASK_DYLD_INFO = 17; public const int THREAD_IDENTIFIER_INFO = 4; + public const int THREAD_EXTENDED_INFO = 5; public const int x86_THREAD_STATE64 = 4; public const int ARM_THREAD_STATE64 = 6; public const int VM_REGION_BASIC_INFO_64 = 9; @@ -37,6 +39,8 @@ internal static partial class MacOSLibSystem public static readonly unsafe int TASK_DYLD_INFO_COUNT = sizeof(task_dyld_info) / sizeof(uint); public static readonly unsafe int THREAD_IDENTIFIER_INFO_COUNT = sizeof(thread_identifier_info) / sizeof(uint); + public static readonly unsafe int THREAD_EXTENDED_INFO_COUNT = sizeof(thread_extended_info) / sizeof(uint); + public static readonly unsafe int x86_THREAD_STATE64_COUNT = sizeof(x86_thread_state64_t) / sizeof(uint); public static readonly unsafe int ARM_THREAD_STATE64_COUNT = sizeof(arm_thread_state64_t) / sizeof(uint); @@ -64,15 +68,19 @@ internal static partial class MacOSLibSystem public static unsafe partial return_t task_resume(task_inspect_t target_task); [LibraryImport(LibSystem)] + [SuppressGCTransition] // We don't want to transition to preemptive mode as we are dealing high-performance profiling public static partial return_t thread_suspend(thread_act_t target_act); [LibraryImport(LibSystem)] + [SuppressGCTransition] // We don't want to transition to preemptive mode as we are dealing high-performance profiling public static partial return_t thread_resume(thread_act_t target_act); [LibraryImport(LibSystem)] - public static partial return_t thread_info(thread_act_t target_act, uint flavor, out /*int*/thread_identifier_info thread_info, ref /*uint*/int thread_info_count); + [SuppressGCTransition] // We don't want to transition to preemptive mode as we are dealing high-performance profiling + public static unsafe partial return_t thread_info(thread_act_t target_act, uint flavor, void* thread_info, ref /*uint*/int thread_info_count); [LibraryImport(LibSystem)] + [SuppressGCTransition] // We don't want to transition to preemptive mode as we are dealing high-performance profiling public static partial return_t thread_get_state(thread_inspect_t target_act, uint flavor, /*uint**/nint old_state, ref /*uint*/int old_state_count); [LibraryImport(LibSystem)] @@ -376,6 +384,63 @@ public struct uuid_command { public uint reserved3; /* reserved */ } + public const int THREAD_BASIC_INFO = 3; + + public struct thread_basic_info_data_t + { + public uint user_time; + public uint system_time; + public int cpu_usage; + public int policy; + public TH_STATE run_state; + public TH_FLAGS flags; + public int suspend_count; + public int sleep_time; + } + + public unsafe struct thread_extended_info + { // same as proc_threadinfo (from proc_info.h) & proc_threadinfo_internal (from bsd_taskinfo.h) + public ulong pth_user_time; /* user run time */ + public ulong pth_system_time; /* system run time */ + public int pth_cpu_usage; /* scaled cpu usage percentage */ + public int pth_policy; /* scheduling policy in effect */ + public TH_STATE pth_run_state; /* run state (see below) */ + public TH_FLAGS pth_flags; /* various flags (see below) */ + public int pth_sleep_time; /* number of seconds that thread */ + public int pth_curpri; /* cur priority*/ + public int pth_priority; /* priority*/ + public int pth_maxpriority; /* max priority*/ + public fixed byte pth_name[64]; /* thread name, if any */ + }; + public enum TH_STATE + { + TH_STATE_RUNNING = 1, /* thread is running normally */ + TH_STATE_STOPPED = 2, /* thread is stopped */ + TH_STATE_WAITING = 3, /* thread is waiting normally */ + TH_STATE_UNINTERRUPTIBLE = 4, /* thread is in an uninterruptible wait */ + TH_STATE_HALTED = 5 /* thread is halted at a clean point */ + } + + public const TH_STATE TH_STATE_RUNNING = TH_STATE.TH_STATE_RUNNING; + public const TH_STATE TH_STATE_STOPPED = TH_STATE.TH_STATE_STOPPED; + public const TH_STATE TH_STATE_WAITING = TH_STATE.TH_STATE_WAITING; + public const TH_STATE TH_STATE_UNINTERRUPTIBLE = TH_STATE.TH_STATE_UNINTERRUPTIBLE; + public const TH_STATE TH_STATE_HALTED = TH_STATE.TH_STATE_HALTED; + + public const int TH_USAGE_SCALE = 1000; // Scale factor for usage values + + [Flags] + public enum TH_FLAGS + { + TH_FLAGS_SWAPPED = 0x1, /* thread is swapped out */ + TH_FLAGS_IDLE = 0x2, /* thread is an idle thread */ + TH_FLAGS_GLOBAL_FORCED_IDLE = 0x4 /* thread performs global forced idle */ + } + + public const TH_FLAGS TH_FLAGS_SWAPPED = TH_FLAGS.TH_FLAGS_SWAPPED; + public const TH_FLAGS TH_FLAGS_IDLE = TH_FLAGS.TH_FLAGS_IDLE; + public const TH_FLAGS TH_FLAGS_GLOBAL_FORCED_IDLE = TH_FLAGS.TH_FLAGS_GLOBAL_FORCED_IDLE; + public const uint LC_UUID = 0x1b; public const uint LC_SEGMENT_64 = 0x19; diff --git a/src/Ultra.Sampler/MacOS/MacOSUltraSampler.cs b/src/Ultra.Sampler/MacOS/MacOSUltraSampler.cs index 8cc353b..48ba412 100644 --- a/src/Ultra.Sampler/MacOS/MacOSUltraSampler.cs +++ b/src/Ultra.Sampler/MacOS/MacOSUltraSampler.cs @@ -13,76 +13,99 @@ namespace Ultra.Sampler.MacOS; internal unsafe class MacOSUltraSampler : UltraSampler { - private bool _stopped; - private Thread? _thread; - private bool _captureEnabled; - private readonly AutoResetEvent _resumeCaptureThread; - + // Sampler General info/state + private bool _samplerStopped; + private bool _samplerEnabled; + private Thread? _samplerThread; + private ulong _samplerThreadId; + private readonly AutoResetEvent _samplerResumeThreadEvent; + + // Frames information private const int MaximumFrames = 4096; - private readonly ulong[] _frames; - + private readonly ulong[] _frames; // 32 KB + + private const int MaximumCompressedFrameTotalCount = 64; + private const int MaximumCompressedFrameCount = MaximumCompressedFrameTotalCount - 1; + private const int MaximumThreadCountForCompressedFrames = 512; + private readonly ulong[] _allCompressedFrames; // 256 KB + private UnsafeList _freeCompressedFramesIndices = new(MaximumThreadCountForCompressedFrames); + private UnsafeDictionary _threadIdToCompressedFrameIndex = new(MaximumThreadCountForCompressedFrames); + private UnsafeHashSet _activeThreadIds = new(MaximumThreadCountForCompressedFrames); + private UnsafeHashSet _currentThreadIds = new(MaximumThreadCountForCompressedFrames); + private UnsafeList _tempThreadIds = new(MaximumThreadCountForCompressedFrames); + + // Modules private const int DefaultImageCount = 1024; private UnsafeList _moduleEvents = new(DefaultImageCount); - private bool _initializingModules; + private readonly bool _initializingModules; private readonly object _moduleEventLock = new(); private int _nextModuleEventIndexToLog; private readonly UltraSamplerSource _samplerEventSource; - - private readonly MacOSLibSystem.dyld_register_callback _callbackDyldAdded; - private readonly MacOSLibSystem.dyld_register_callback _callbackDyldRemoved; + // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable + private readonly MacOSLibSystem.dyld_register_callback _callbackDyldAdded; // Keep a reference to avoid the GC to collect it + // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable + private readonly MacOSLibSystem.dyld_register_callback _callbackDyldRemoved; // Keep a reference to avoid the GC to collect it public MacOSUltraSampler() { _frames = GC.AllocateArray(4096, true); - _resumeCaptureThread = new AutoResetEvent(false); + _allCompressedFrames = GC.AllocateArray(MaximumCompressedFrameTotalCount * MaximumThreadCountForCompressedFrames, true); + + // Initialize _freeCompressedFramesIndices (order from last to first to pickup the index 0 first) + for (int i = MaximumThreadCountForCompressedFrames - 1; i >= 0; --i) + { + _freeCompressedFramesIndices.Add(i); + } + + _samplerResumeThreadEvent = new AutoResetEvent(false); _callbackDyldAdded = new MacOSLibSystem.dyld_register_callback(CallbackDyldAdded); _callbackDyldRemoved = new MacOSLibSystem.dyld_register_callback(CallbackDyldRemoved); + // Make sure to use the instance to trigger the constructor of the EventSource so that it is registered in the runtime! + _samplerEventSource = UltraSamplerSource.Log; + // Register dyld callbacks _initializingModules = true; MacOSLibSystem._dyld_register_func_for_add_image(Marshal.GetFunctionPointerForDelegate(_callbackDyldAdded)); _initializingModules = false; MacOSLibSystem._dyld_register_func_for_remove_image(Marshal.GetFunctionPointerForDelegate(_callbackDyldRemoved)); - - // Make sure to use the instance to trigger the constructor of the EventSource so that it is registered in the runtime! - _samplerEventSource = UltraSamplerSource.Log; } protected override void StartImpl() { - if (_thread is not null) return; + if (_samplerThread is not null) return; - _thread = new Thread(RunImpl) + _samplerThread = new Thread(RunImpl) { IsBackground = true, Name = "Ultra-Sampler", Priority = ThreadPriority.Highest }; - _thread.Start(); + _samplerThread.Start(); } protected override void StopImpl() { - if (_thread is null) return; + if (_samplerThread is null) return; - _resumeCaptureThread.Set(); - _thread.Join(); - _thread = null; - _stopped = false; + _samplerResumeThreadEvent.Set(); + _samplerThread.Join(); + _samplerThread = null; + _samplerStopped = false; } protected override void EnableImpl() { _nextModuleEventIndexToLog = 0; - _captureEnabled = true; - _resumeCaptureThread.Set(); + _samplerEnabled = true; + _samplerResumeThreadEvent.Set(); } protected override void DisableImpl() { _nextModuleEventIndexToLog = 0; - _captureEnabled = false; + _samplerEnabled = false; } private unsafe void RunImpl() @@ -92,14 +115,14 @@ private unsafe void RunImpl() MacOS.MacOSLibSystem.task_for_pid(MacOS.MacOSLibSystem.mach_task_self(), Process.GetCurrentProcess().Id, out var rootTask) .ThrowIfError("task_for_pid"); - MacOS.MacOSLibSystem.pthread_threadid_np(0, out var currentThreadId) + MacOS.MacOSLibSystem.pthread_threadid_np(0, out _samplerThreadId) .ThrowIfError("pthread_threadid_np"); bool sendManifest = true; - while (!_stopped) + while (!_samplerStopped) { - if (_captureEnabled) + if (_samplerEnabled) { if (sendManifest) { @@ -111,21 +134,21 @@ private unsafe void RunImpl() NotifyPendingNativeModuleEvents(); // Sample the callstacks - Sample(rootTask, currentThreadId, _frames, UltraSamplerSource.Log.NativeCallstack); + Sample(rootTask, UltraSamplerSource.Log.NativeCallstack); // Sleep for 1ms Thread.Sleep(1); } else { - _resumeCaptureThread.WaitOne(); + _samplerResumeThreadEvent.WaitOne(); sendManifest = true; } } } catch (Exception ex) { - Trace.TraceError($"Ultra-Sampler unexpected exception while sampling: {ex}"); + Console.Error.WriteLine($"Ultra-Sampler unexpected exception while sampling: {ex}"); } } @@ -145,17 +168,17 @@ private static void SendManifest() private void CallbackDyldAdded(MacOSLibSystem.mach_header* header, nint slideVmAddr) { - AddModuleEvent(_initializingModules ? NativeModuleEventKind.AlreadyLoaded : NativeModuleEventKind.Loaded, (nint)header); + AddModuleEvent(_initializingModules ? UltraSamplerNativeModuleEventKind.AlreadyLoaded : UltraSamplerNativeModuleEventKind.Loaded, (nint)header); } private void CallbackDyldRemoved(MacOSLibSystem.mach_header* header, IntPtr vmaddr_slide) { - AddModuleEvent(NativeModuleEventKind.Unloaded, (nint)header); + AddModuleEvent(UltraSamplerNativeModuleEventKind.Unloaded, (nint)header); } [SkipLocalsInit] [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddModuleEvent(NativeModuleEventKind kind, nint loadAddress) + private void AddModuleEvent(UltraSamplerNativeModuleEventKind kind, nint loadAddress) { var result = MacOSLibSystem.dladdr(loadAddress, out var info); if (result == 0) @@ -264,80 +287,162 @@ private static ulong GetDyldCodeSize(nint headerPtr) return size; } - public void Sample(NativeCallstackDelegate nativeCallstack) - { - MacOS.MacOSLibSystem.task_for_pid(MacOS.MacOSLibSystem.mach_task_self(), Process.GetCurrentProcess().Id, out var rootTask) - .ThrowIfError("task_for_pid"); + //public void Sample(NativeCallstackDelegate nativeCallstack) + //{ + // MacOS.MacOSLibSystem.task_for_pid(MacOS.MacOSLibSystem.mach_task_self(), Process.GetCurrentProcess().Id, out var rootTask) + // .ThrowIfError("task_for_pid"); - MacOS.MacOSLibSystem.pthread_threadid_np(0, out var currentThreadId) - .ThrowIfError("pthread_threadid_np"); + // MacOS.MacOSLibSystem.pthread_threadid_np(0, out var currentThreadId) + // .ThrowIfError("pthread_threadid_np"); - Sample(rootTask, currentThreadId, _frames, nativeCallstack); - } + // Sample(rootTask, currentThreadId, _frames, nativeCallstack); + //} - private static unsafe void Sample(MacOS.MacOSLibSystem.mach_port_t rootTask, ulong currentThreadId, Span frames, NativeCallstackDelegate nativeCallstack) + private unsafe void Sample(MacOS.MacOSLibSystem.mach_port_t rootTask, NativeSamplingDelegate samplingDelegate) { // We support only ARM64 for the sampler if (RuntimeInformation.ProcessArchitecture != Architecture.Arm64) return; + MacOS.MacOSLibSystem.thread_identifier_info threadInfo = default; + MacOSLibSystem.thread_extended_info threadExtendedInfo = default; + MacOS.MacOSLibSystem.arm_thread_state64_t armThreadState = new MacOS.MacOSLibSystem.arm_thread_state64_t(); + int armThreadStateCount = MacOS.MacOSLibSystem.ARM_THREAD_STATE64_COUNT; + MacOS.MacOSLibSystem.mach_port_t* taskList; MacOS.MacOSLibSystem.task_threads(rootTask, &taskList, out uint taskCount) .ThrowIfError("task_threads"); - MacOS.MacOSLibSystem.thread_identifier_info threadInfo = new(); - - ulong* pFrames = (ulong*)Unsafe.AsPointer(ref frames[0]); + ulong* pFrames = (ulong*)Unsafe.AsPointer(ref _frames[0]); + _currentThreadIds.Clear(); for (var i = 0; i < taskCount; i++) { var threadPort = taskList[i]; - try + + int infoCount = MacOS.MacOSLibSystem.THREAD_IDENTIFIER_INFO_COUNT; + var status = MacOS.MacOSLibSystem.thread_info(threadPort, MacOS.MacOSLibSystem.THREAD_IDENTIFIER_INFO, &threadInfo, ref infoCount); + if (status.IsError) { - int threadInfoCount = MacOS.MacOSLibSystem.THREAD_IDENTIFIER_INFO_COUNT; - MacOS.MacOSLibSystem.thread_info(threadPort, MacOS.MacOSLibSystem.THREAD_IDENTIFIER_INFO, out threadInfo, ref threadInfoCount) - .ThrowIfError("thread_info"); + continue; + } + + if (threadInfo.thread_id == _samplerThreadId) continue; - //var thread_t = pthread_from_mach_thread_np(threadPort); + _currentThreadIds.Add(threadInfo.thread_id); // Record which thread was seen/active - //if (thread_t != 0) - //{ - // pthread_getname_np(thread_t, nameBuffer, 256) - // .ThrowIfError("pthread_getname_np"); - // Console.WriteLine($"Thread ID: {threadInfo.thread_id} Name: {Marshal.PtrToStringAnsi((IntPtr)nameBuffer)}"); - //} - //else - //{ - // Console.WriteLine($"Thread ID: {threadInfo.thread_id}"); - //} + infoCount = MacOS.MacOSLibSystem.THREAD_EXTENDED_INFO_COUNT; + status = MacOS.MacOSLibSystem.thread_info(threadPort, MacOS.MacOSLibSystem.THREAD_EXTENDED_INFO, &threadExtendedInfo, ref infoCount); + if (status.IsError || (threadExtendedInfo.pth_flags & MacOSLibSystem.TH_FLAGS_IDLE) != 0) // If the thread is idle, we skip it + { + continue; + } - if (threadInfo.thread_id == currentThreadId) continue; + // ------------------------------------------------------------------- + // Suspend the thread + // ------------------------------------------------------------------- + status = MacOS.MacOSLibSystem.thread_suspend(taskList[i]); + if (status.IsError) // Don't throw if we can't suspend a thread + { + continue; + } - MacOS.MacOSLibSystem.thread_suspend(taskList[i]) - .ThrowIfError("thread_suspend"); + int frameCount = 0; - try - { - MacOS.MacOSLibSystem.arm_thread_state64_t armThreadState = new MacOS.MacOSLibSystem.arm_thread_state64_t(); - int armThreadStateCount = MacOS.MacOSLibSystem.ARM_THREAD_STATE64_COUNT; + // ------------------------------------------------------------------- + // Resume the thread + // ------------------------------------------------------------------- + status = MacOS.MacOSLibSystem.thread_get_state(threadPort, MacOS.MacOSLibSystem.ARM_THREAD_STATE64, (nint)(void*)&armThreadState, ref armThreadStateCount); + if (status.IsSuccess) + { + //Console.WriteLine($"sp: 0x{armThreadState.__sp:X8}, fp: 0x{armThreadState.__fp:X8}, lr: 0x{armThreadState.__lr:X8}"); + frameCount = WalkNativeCallStack(armThreadState.__sp, armThreadState.__fp, armThreadState.__lr, pFrames); + } - MacOS.MacOSLibSystem.thread_get_state(threadPort, MacOS.MacOSLibSystem.ARM_THREAD_STATE64, (nint)(void*)&armThreadState, ref armThreadStateCount) - .ThrowIfError("thread_get_state"); + // ------------------------------------------------------------------- + // Resume the thread + // ------------------------------------------------------------------- + MacOS.MacOSLibSystem.thread_resume(threadPort); - //Console.WriteLine($"sp: 0x{armThreadState.__sp:X8}, fp: 0x{armThreadState.__fp:X8}, lr: 0x{armThreadState.__lr:X8}"); - int frameCount = WalkNativeCallStack(armThreadState.__sp, armThreadState.__fp, armThreadState.__lr, pFrames); - nativeCallstack(threadInfo.thread_id, frameCount * sizeof(ulong), (byte*)pFrames); - } - finally + // Compute the same frame count + var sameFrameCount = ComputeSameFrameCount(threadInfo.thread_id, frameCount, pFrames); + frameCount -= sameFrameCount; + pFrames += 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); + } + + // Cleanup threads that are no longer active + foreach (var previousActiveThreadId in _activeThreadIds) + { + if (!_currentThreadIds.Contains(previousActiveThreadId)) + { + if (_threadIdToCompressedFrameIndex.Remove(previousActiveThreadId, out var compressedFrameIndex)) { - MacOS.MacOSLibSystem.thread_resume(threadPort) - .ThrowIfError("thread_resume"); + _freeCompressedFramesIndices.Add(compressedFrameIndex); } } - catch (Exception ex) + } + + // Swap the active and current thread ids + (_currentThreadIds, _activeThreadIds) = (_activeThreadIds, _currentThreadIds); + } + + private int ComputeSameFrameCount(ulong threadId, int frameCount, ulong* frames) + { + // We limit the frame recording to MaximumCompressedFrameCount + frameCount = Math.Min(frameCount, MaximumCompressedFrameCount); + + bool hasCompressedFrames = true; + if (!_threadIdToCompressedFrameIndex.TryGetValue(threadId, out var index)) + { + if (_freeCompressedFramesIndices.Count > 0) + { + index = _freeCompressedFramesIndices.RemoveLast(); + hasCompressedFrames = false; + } + else + { + // We are full, no compressed frame + return 0; + } + + _threadIdToCompressedFrameIndex.Add(threadId, index); + } + + int sameFrameCount = 0; + + ref var allCompressedFrames = ref _allCompressedFrames[0]; + var indexInCompressedFrames = index * MaximumCompressedFrameTotalCount; + ref ulong previousFrame = ref Unsafe.Add(ref allCompressedFrames, indexInCompressedFrames); + + if (hasCompressedFrames) + { + int previousFrameCount = (int)previousFrame; + previousFrame = ref Unsafe.Add(ref previousFrame, 1); + + var minFrameCount = Math.Min(previousFrameCount, frameCount); + for (; sameFrameCount < minFrameCount; sameFrameCount++) { - Trace.TraceError($"Ultra-Sampler unexpected exception while sampling thread #{threadInfo.thread_id}: {ex}"); + if (frames[sameFrameCount] != previousFrame) + { + break; + } + + previousFrame = ref Unsafe.Add(ref previousFrame, 1); } } + + // Copy the new frames to the current frames + previousFrame = ref Unsafe.Add(ref allCompressedFrames, indexInCompressedFrames); + previousFrame = (ulong)frameCount; + previousFrame = ref Unsafe.Add(ref previousFrame, 1); + for (int i = 0; i < frameCount; i++) + { + Unsafe.Add(ref previousFrame, i) = frames[i]; + } + + return sameFrameCount; } private static unsafe int WalkNativeCallStack(ulong sp, ulong fp, ulong lr, ulong* frames) diff --git a/src/Ultra.Sampler/MacOS/NativeModuleEvent.cs b/src/Ultra.Sampler/MacOS/NativeModuleEvent.cs index 8bd1ffb..875fd49 100644 --- a/src/Ultra.Sampler/MacOS/NativeModuleEvent.cs +++ b/src/Ultra.Sampler/MacOS/NativeModuleEvent.cs @@ -8,7 +8,7 @@ namespace Ultra.Sampler.MacOS; internal struct NativeModuleEvent { - public NativeModuleEventKind Kind; + public UltraSamplerNativeModuleEventKind Kind; public ulong LoadAddress; public ulong Size; public byte[]? Path; diff --git a/src/Ultra.Sampler/MacOS/NativeSamplingDelegate.cs b/src/Ultra.Sampler/MacOS/NativeSamplingDelegate.cs new file mode 100644 index 0000000..f719ed9 --- /dev/null +++ b/src/Ultra.Sampler/MacOS/NativeSamplingDelegate.cs @@ -0,0 +1,7 @@ +// 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. + +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 diff --git a/src/Ultra.Sampler/UltraSampler.cs b/src/Ultra.Sampler/UltraSampler.cs index dbcc7f6..9efa985 100644 --- a/src/Ultra.Sampler/UltraSampler.cs +++ b/src/Ultra.Sampler/UltraSampler.cs @@ -10,7 +10,7 @@ internal abstract class UltraSampler public bool IsEnabled { get; private set; } - public static UltraSampler Instance { get; } = OperatingSystem.IsMacOS() ? new MacOSUltraSampler() : throw new PlatformNotSupportedException(); + public static UltraSampler Instance { get; } = OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? new MacOSUltraSampler() : new NopSampler(); [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)], EntryPoint = "ultra_sampler_start")] internal static void NativeStart() => Instance.Start(); @@ -59,4 +59,24 @@ public void Disable() protected abstract void EnableImpl(); protected abstract void DisableImpl(); + + + private sealed class NopSampler : UltraSampler + { + protected override void StartImpl() + { + } + + protected override void StopImpl() + { + } + + protected override void EnableImpl() + { + } + + protected override void DisableImpl() + { + } + } } \ No newline at end of file diff --git a/src/Ultra.Sampler/UltraSamplerConstants.cs b/src/Ultra.Sampler/UltraSamplerConstants.cs index 7d5b4e7..730bf83 100644 --- a/src/Ultra.Sampler/UltraSamplerConstants.cs +++ b/src/Ultra.Sampler/UltraSamplerConstants.cs @@ -2,7 +2,7 @@ // Licensed under the BSD-Clause 2 license. // See license.txt file in the project root for full license information. -namespace Ultra.Core; +namespace Ultra.Sampler; public static class UltraSamplerConstants { diff --git a/src/Ultra.Sampler/MacOS/NativeCallstackDelegate.cs b/src/Ultra.Sampler/UltraSamplerNativeModuleEventKind.cs similarity index 57% rename from src/Ultra.Sampler/MacOS/NativeCallstackDelegate.cs rename to src/Ultra.Sampler/UltraSamplerNativeModuleEventKind.cs index 917980c..c63eb1e 100644 --- a/src/Ultra.Sampler/MacOS/NativeCallstackDelegate.cs +++ b/src/Ultra.Sampler/UltraSamplerNativeModuleEventKind.cs @@ -2,6 +2,13 @@ // Licensed under the BSD-Clause 2 license. // See license.txt file in the project root for full license information. -namespace Ultra.Sampler.MacOS; +namespace Ultra.Sampler; -internal unsafe delegate void NativeCallstackDelegate(ulong threadId, int frameCount, byte* pFrames); \ No newline at end of file +public enum UltraSamplerNativeModuleEventKind +{ + AlreadyLoaded = 0, + + Loaded = 1, + + Unloaded = 2 +} \ No newline at end of file diff --git a/src/Ultra.Sampler/UltraSamplerSource.cs b/src/Ultra.Sampler/UltraSamplerSource.cs index b0bdc3b..31236e5 100644 --- a/src/Ultra.Sampler/UltraSamplerSource.cs +++ b/src/Ultra.Sampler/UltraSamplerSource.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Tracing; -using Ultra.Core; namespace Ultra.Sampler; @@ -20,7 +19,7 @@ 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 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 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[3]; evt[0].DataPointer = (nint)(void*)&threadId; diff --git a/src/Ultra.Sampler/UltraSamplerThreadState.cs b/src/Ultra.Sampler/UltraSamplerThreadState.cs new file mode 100644 index 0000000..1b5b770 --- /dev/null +++ b/src/Ultra.Sampler/UltraSamplerThreadState.cs @@ -0,0 +1,16 @@ +// 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. + +namespace Ultra.Sampler; + +public enum UltraSamplerThreadState +{ + // Matching TH_STATE from mach/thread_info.h + + Running = 1, /* thread is running normally */ + Stopped = 2, /* thread is stopped */ + Waiting = 3, /* thread is waiting normally */ + Uninterruptible = 4, /* thread is in an uninterruptible wait */ + Halted = 5 /* thread is halted at a clean point */ +} \ No newline at end of file