Skip to content

Commit 829faa8

Browse files
committed
Add basic code sampler for macOS
1 parent b290c6f commit 829faa8

9 files changed

+537
-1
lines changed
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
8+
<IsPackable>false</IsPackable>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<InternalsVisibleTo Include="Ultra.Tests"/>
13+
</ItemGroup>
14+
15+
</Project>

src/Ultra.Sampler/UltraSampler.cs

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
using System.Diagnostics;
2+
using System.Runtime.CompilerServices;
3+
using System.Runtime.InteropServices;
4+
5+
namespace Ultra.Sampler;
6+
7+
using static libSystem;
8+
9+
public static class UltraSampler
10+
{
11+
private static readonly object Lock = new();
12+
private static Thread? _thread;
13+
private static bool _stopped;
14+
private const int MaximumFrames = 65536;
15+
16+
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)], EntryPoint = "ultra_sampler_start")]
17+
internal static void NativeStart() => Start();
18+
19+
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)], EntryPoint = "ultra_sampler_stop")]
20+
internal static void NativeStop() => Stop();
21+
22+
public static void Start()
23+
{
24+
lock (Lock)
25+
{
26+
if (_thread is not null) return;
27+
28+
_thread = new Thread(RunImpl)
29+
{
30+
IsBackground = true,
31+
Name = "Ultra-Sampler",
32+
Priority = ThreadPriority.Highest
33+
};
34+
_thread.Start();
35+
}
36+
}
37+
38+
public static void Stop()
39+
{
40+
lock (Lock)
41+
{
42+
if (_thread is null) return;
43+
44+
_stopped = true;
45+
_thread.Join();
46+
_thread = null;
47+
_stopped = false;
48+
}
49+
}
50+
51+
private static void RunImpl()
52+
{
53+
try
54+
{
55+
task_for_pid(mach_task_self(), Process.GetCurrentProcess().Id, out var rootTask)
56+
.ThrowIfError("task_for_pid");
57+
58+
pthread_threadid_np(0, out var currentThreadId)
59+
.ThrowIfError("pthread_threadid_np");
60+
61+
var frames = GC.AllocateUninitializedArray<ulong>(MaximumFrames, pinned: true);
62+
63+
while (!_stopped)
64+
{
65+
if (UltraSamplerSource.Log.IsEnabled())
66+
{
67+
Sample(rootTask, currentThreadId, frames);
68+
}
69+
70+
// Sleep for 1ms
71+
Thread.Sleep(1);
72+
}
73+
}
74+
catch (Exception ex)
75+
{
76+
Trace.TraceError($"Ultra-Sampler unexpected exception while sampling: {ex}");
77+
}
78+
}
79+
80+
private static unsafe void Sample(mach_port_t rootTask, ulong currentThreadId, Span<ulong> frames)
81+
{
82+
mach_port_t* taskList;
83+
task_threads(rootTask, &taskList, out uint taskCount)
84+
.ThrowIfError("task_threads");
85+
86+
thread_identifier_info threadInfo = new();
87+
88+
ulong* pFrames = (ulong*)Unsafe.AsPointer(ref frames[0]);
89+
90+
for (var i = 0; i < taskCount; i++)
91+
{
92+
var threadPort = taskList[i];
93+
try
94+
{
95+
int threadInfoCount = THREAD_IDENTIFIER_INFO_COUNT;
96+
thread_info(threadPort, THREAD_IDENTIFIER_INFO, out threadInfo, ref threadInfoCount)
97+
.ThrowIfError("thread_info");
98+
99+
//var thread_t = pthread_from_mach_thread_np(threadPort);
100+
101+
//if (thread_t != 0)
102+
//{
103+
// pthread_getname_np(thread_t, nameBuffer, 256)
104+
// .ThrowIfError("pthread_getname_np");
105+
// Console.WriteLine($"Thread ID: {threadInfo.thread_id} Name: {Marshal.PtrToStringAnsi((IntPtr)nameBuffer)}");
106+
//}
107+
//else
108+
//{
109+
// Console.WriteLine($"Thread ID: {threadInfo.thread_id}");
110+
//}
111+
112+
if (threadInfo.thread_id == currentThreadId) continue;
113+
114+
thread_suspend(taskList[i])
115+
.ThrowIfError("thread_suspend");
116+
117+
try
118+
{
119+
arm_thread_state64_t armThreadState = new arm_thread_state64_t();
120+
int armThreadStateCount = ARM_THREAD_STATE64_COUNT;
121+
122+
thread_get_state(threadPort, ARM_THREAD_STATE64, (nint)(void*)&armThreadState, ref armThreadStateCount)
123+
.ThrowIfError("thread_get_state");
124+
125+
//Console.WriteLine($"sp: 0x{armThreadState.__sp:X8}, fp: 0x{armThreadState.__fp:X8}, lr: 0x{armThreadState.__lr:X8}");
126+
WalkCallStack(armThreadState.__sp, armThreadState.__fp, armThreadState.__lr, pFrames);
127+
}
128+
finally
129+
{
130+
thread_resume(threadPort)
131+
.ThrowIfError("thread_resume");
132+
}
133+
}
134+
catch (Exception ex)
135+
{
136+
Trace.TraceError($"Ultra-Sampler unexpected exception while sampling thread #{threadInfo.thread_id}: {ex}");
137+
}
138+
}
139+
}
140+
141+
private static unsafe void WalkCallStack(ulong sp, ulong fp, ulong lr, ulong* frames)
142+
{
143+
int frameIndex = 0;
144+
while (fp != 0 && frameIndex < MaximumFrames)
145+
{
146+
frames[frameIndex++] = lr;
147+
//sp = fp + 16;
148+
lr = *(ulong*)(fp + 8);
149+
fp = *(ulong*)fp;
150+
}
151+
152+
UltraSamplerSource.Log.Callstack(frames, frameIndex);
153+
}
154+
}
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) Alexandre Mutel. All rights reserved.
2+
// Licensed under the BSD-Clause 2 license.
3+
// See license.txt file in the project root for full license information.
4+
5+
namespace Ultra.Sampler;
6+
7+
public static class UltraSamplerParser
8+
{
9+
public const string Name = "Ultra-Sampler";
10+
11+
public const string IdAsString = "04E4DCBF-494F-4A77-B55E-F5C041A92F56";
12+
13+
public static readonly Guid Id = new(IdAsString);
14+
15+
public const int CallStackEvent = 1;
16+
}
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) Alexandre Mutel. All rights reserved.
2+
// Licensed under the BSD-Clause 2 license.
3+
// See license.txt file in the project root for full license information.
4+
5+
using System.Diagnostics.Tracing;
6+
using System.Runtime.CompilerServices;
7+
8+
namespace Ultra.Sampler;
9+
10+
[EventSource(Name = UltraSamplerParser.Name, Guid = UltraSamplerParser.IdAsString)]
11+
public class UltraSamplerSource : EventSource
12+
{
13+
public static readonly UltraSamplerSource Log = new();
14+
15+
private UltraSamplerSource()
16+
{
17+
}
18+
19+
[Event(UltraSamplerParser.CallStackEvent, Level = EventLevel.Verbose)]
20+
[SkipLocalsInit]
21+
[MethodImpl(MethodImplOptions.NoInlining)]
22+
public unsafe void Callstack(ulong* pFrames, int count)
23+
{
24+
if (IsEnabled())
25+
{
26+
Unsafe.SkipInit(out EventSource.EventData evt);
27+
evt.DataPointer = (nint)pFrames;
28+
evt.Size = count * sizeof(ulong);
29+
WriteEventCore(UltraSamplerParser.CallStackEvent, 1, &evt);
30+
}
31+
}
32+
}

0 commit comments

Comments
 (0)