Skip to content

Commit e4f2a07

Browse files
committed
feat: Native AOT frame info (#2701)
1 parent 33a2e8b commit e4f2a07

File tree

8 files changed

+370
-88
lines changed

8 files changed

+370
-88
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
{
2-
"omnisharp.useModernNet": true
2+
"omnisharp.useModernNet": true,
3+
"dotnet.defaultSolution": "Sentry.sln"
34
}

samples/Sentry.Samples.Console.Basic/Sentry.Samples.Console.Basic.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>net6.0</TargetFramework>
5+
<TargetFramework>net7.0</TargetFramework>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
8+
<PublishAot>true</PublishAot>
89
</PropertyGroup>
910

1011
<PropertyGroup>

src/Sentry/AttributeReader.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@
22

33
internal static class AttributeReader
44
{
5-
public static bool TryGetProjectDirectory(Assembly assembly, out string? projectDirectory)
6-
{
7-
projectDirectory = assembly.GetCustomAttributes<AssemblyMetadataAttribute>()
8-
.FirstOrDefault(x => x.Key == "Sentry.ProjectDirectory")
9-
?.Value;
10-
return projectDirectory != null;
11-
}
5+
public static string? TryGetProjectDirectory(Assembly assembly) =>
6+
assembly.GetCustomAttributes<AssemblyMetadataAttribute>().FirstOrDefault(x => x.Key == "Sentry.ProjectDirectory")?.Value;
127
}

src/Sentry/Internal/DebugStackTrace.cs

Lines changed: 138 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ internal class DebugStackTrace : SentryStackTrace
3131
private static readonly Regex RegexAsyncReturn = new(@"^(.+`[0-9]+)\[\[",
3232
RegexOptions.Compiled | RegexOptions.CultureInvariant);
3333

34+
private static readonly Regex RegexNativeAOTInfo = new(@"^(.+)\.([^.]+\(.*\)) ?\+ ?0x",
35+
RegexOptions.Compiled | RegexOptions.CultureInvariant);
36+
3437
internal DebugStackTrace(SentryOptions options)
3538
{
3639
_options = options;
@@ -123,11 +126,14 @@ private IEnumerable<SentryStackFrame> CreateFrames(StackTrace stackTrace, bool i
123126
{
124127
var frames = _options.StackTraceMode switch
125128
{
126-
StackTraceMode.Enhanced => EnhancedStackTrace.GetFrames(stackTrace).Select(p => p as StackFrame),
129+
StackTraceMode.Enhanced => EnhancedStackTrace.GetFrames(stackTrace).Select(p => new RealStackFrame(p)),
127130
_ => stackTrace.GetFrames()
128131
// error CS8619: Nullability of reference types in value of type 'StackFrame?[]' doesn't match target type 'IEnumerable<StackFrame>'.
129132
#if NETCOREAPP3_0
130133
.Where(f => f is not null)
134+
.Select(p => new RealStackFrame(p!))
135+
#else
136+
.Select(p => new RealStackFrame(p))
131137
#endif
132138
};
133139

@@ -153,109 +159,173 @@ private IEnumerable<SentryStackFrame> CreateFrames(StackTrace stackTrace, bool i
153159
#endif
154160

155161
// Remove the frames until the call for capture with the SDK
156-
if (firstFrame
157-
&& isCurrentStackTrace
158-
&& stackFrame.GetMethod() is { } method
159-
&& method.DeclaringType?.AssemblyQualifiedName?.StartsWith("Sentry") == true)
162+
if (firstFrame && isCurrentStackTrace)
160163
{
161-
_options.LogDebug("Skipping initial stack frame '{0}'", method.Name);
162-
continue;
164+
string? frameInfo = null;
165+
if (stackFrame.GetMethod() is { } method)
166+
{
167+
frameInfo = method.DeclaringType?.AssemblyQualifiedName;
168+
}
169+
170+
// Native AOT currently only exposes some method info at runtime via ToString().
171+
// See https://github.com/dotnet/runtime/issues/92869
172+
if (frameInfo is null && stackFrame.HasNativeImage())
173+
{
174+
frameInfo = stackFrame.ToString();
175+
}
176+
177+
if (frameInfo?.StartsWith("Sentry") is true)
178+
{
179+
_options.LogDebug("Skipping initial stack frame '{0}'", frameInfo);
180+
continue;
181+
}
163182
}
164183

165184
firstFrame = false;
166185

167-
yield return CreateFrame(stackFrame);
186+
if (CreateFrame(stackFrame) is { } frame)
187+
{
188+
yield return frame;
189+
}
190+
else
191+
{
192+
_options.LogDebug("Could not resolve stack frame '{0}'", stackFrame.ToString());
193+
}
168194
}
169195
}
170196

171197
/// <summary>
172-
/// Create a <see cref="SentryStackFrame"/> from a <see cref="StackFrame"/>.
198+
/// Native AOT implementation of CreateFrame.
199+
/// Native frames have only limited method information at runtime (and even that can be disabled).
200+
/// We try to parse that and also add addresses for server-side symbolication.
173201
/// </summary>
174-
internal SentryStackFrame CreateFrame(StackFrame stackFrame) => InternalCreateFrame(stackFrame, true);
202+
private SentryStackFrame? TryCreateNativeAOTFrame(IStackFrame stackFrame)
203+
{
204+
if (!stackFrame.HasNativeImage())
205+
{
206+
return null;
207+
}
208+
209+
var frame = ParseNativeAOTToString(stackFrame.ToString());
210+
frame.ImageAddress = stackFrame.GetNativeImageBase();
211+
frame.InstructionAddress = stackFrame.GetNativeIP();
212+
return frame;
213+
}
214+
215+
// Method info is currently only exposed by ToString(), see https://github.com/dotnet/runtime/issues/92869
216+
// We only care about the case where the method is available (`StackTraceSupport` property is the default `true`):
217+
// https://github.com/dotnet/runtime/blob/254230253da143a082f47cfaf8711627c0bf2faf/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/DeveloperExperience/DeveloperExperience.cs#L42
218+
internal static SentryStackFrame ParseNativeAOTToString(string info)
219+
{
220+
var frame = new SentryStackFrame();
221+
var match = RegexNativeAOTInfo.Match(info);
222+
if (match is { Success: true, Groups.Count: 3 })
223+
{
224+
frame.Module = match.Groups[1].Value;
225+
frame.Function = match.Groups[2].Value;
226+
}
227+
return frame;
228+
}
175229

176230
/// <summary>
177231
/// Default the implementation of CreateFrame.
178232
/// </summary>
179-
private SentryStackFrame InternalCreateFrame(StackFrame stackFrame, bool demangle)
233+
private SentryStackFrame? TryCreateManagedFrame(IStackFrame stackFrame)
180234
{
235+
if (stackFrame.GetMethod() is not { } method)
236+
{
237+
return null;
238+
}
239+
181240
const string unknownRequiredField = "(unknown)";
182-
string? projectPath = null;
183-
var frame = new SentryStackFrame();
184-
if (stackFrame.GetMethod() is { } method)
241+
var frame = new SentryStackFrame
185242
{
186-
frame.Module = method.DeclaringType?.FullName ?? unknownRequiredField;
187-
frame.Package = method.DeclaringType?.Assembly.FullName;
243+
Module = method.DeclaringType?.FullName ?? unknownRequiredField,
244+
Package = method.DeclaringType?.Assembly.FullName
245+
};
188246

189-
if (_options.StackTraceMode == StackTraceMode.Enhanced &&
190-
stackFrame is EnhancedStackFrame enhancedStackFrame)
191-
{
192-
var stringBuilder = new StringBuilder();
193-
frame.Function = enhancedStackFrame.MethodInfo.Append(stringBuilder, false).ToString();
247+
if (stackFrame.Frame is EnhancedStackFrame enhancedStackFrame)
248+
{
249+
var stringBuilder = new StringBuilder();
250+
frame.Function = enhancedStackFrame.MethodInfo.Append(stringBuilder, false).ToString();
194251

195-
if (enhancedStackFrame.MethodInfo.DeclaringType is { } declaringType)
196-
{
197-
stringBuilder.Clear();
198-
stringBuilder.AppendTypeDisplayName(declaringType);
199-
200-
// Ben.Demystifier doesn't always include the namespace, even when fullName==true.
201-
// It's important that the module name always be fully qualified, so that in-app frame
202-
// detection works correctly.
203-
var module = stringBuilder.ToString();
204-
frame.Module = declaringType.Namespace is { } ns && !module.StartsWith(ns)
205-
? $"{ns}.{module}"
206-
: module;
207-
}
208-
}
209-
else
252+
if (enhancedStackFrame.MethodInfo.DeclaringType is { } declaringType)
210253
{
211-
frame.Function = method.Name;
254+
stringBuilder.Clear();
255+
stringBuilder.AppendTypeDisplayName(declaringType);
256+
257+
// Ben.Demystifier doesn't always include the namespace, even when fullName==true.
258+
// It's important that the module name always be fully qualified, so that in-app frame
259+
// detection works correctly.
260+
var module = stringBuilder.ToString();
261+
frame.Module = declaringType.Namespace is { } ns && !module.StartsWith(ns)
262+
? $"{ns}.{module}"
263+
: module;
212264
}
265+
}
266+
else
267+
{
268+
frame.Function = method.Name;
269+
}
213270

214-
// Originally we didn't skip methods from dynamic assemblies, so not to break compatibility:
215-
if (_options.StackTraceMode != StackTraceMode.Original && method.Module.Assembly.IsDynamic)
216-
{
217-
frame.InApp = false;
218-
}
271+
// Originally we didn't skip methods from dynamic assemblies, so not to break compatibility:
272+
if (_options.StackTraceMode != StackTraceMode.Original && method.Module.Assembly.IsDynamic)
273+
{
274+
frame.InApp = false;
275+
}
219276

220-
AttributeReader.TryGetProjectDirectory(method.Module.Assembly, out projectPath);
277+
if (AddDebugImage(method.Module) is { } moduleIdx && moduleIdx != DebugImageMissing)
278+
{
279+
frame.AddressMode = GetRelativeAddressMode(moduleIdx);
221280

222-
if (AddDebugImage(method.Module) is { } moduleIdx && moduleIdx != DebugImageMissing)
281+
try
223282
{
224-
frame.AddressMode = GetRelativeAddressMode(moduleIdx);
225-
226-
try
227-
{
228-
var token = method.MetadataToken;
229-
// The top byte is the token type, the lower three bytes are the record id.
230-
// See: https://docs.microsoft.com/en-us/previous-versions/dotnet/netframework-4.0/ms404456(v=vs.100)#metadata-token-structure
231-
var tokenType = token & 0xff000000;
232-
// See https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/metadata/cortokentype-enumeration
233-
if (tokenType == 0x06000000) // CorTokenType.mdtMethodDef
234-
{
235-
frame.FunctionId = token & 0x00ffffff;
236-
}
237-
}
238-
catch (InvalidOperationException)
283+
var token = method.MetadataToken;
284+
// The top byte is the token type, the lower three bytes are the record id.
285+
// See: https://docs.microsoft.com/en-us/previous-versions/dotnet/netframework-4.0/ms404456(v=vs.100)#metadata-token-structure
286+
var tokenType = token & 0xff000000;
287+
// See https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/metadata/cortokentype-enumeration
288+
if (tokenType == 0x06000000) // CorTokenType.mdtMethodDef
239289
{
240-
// method.MetadataToken may throw
241-
// see https://learn.microsoft.com/en-us/dotnet/api/system.reflection.memberinfo.metadatatoken?view=net-6.0
242-
_options.LogDebug("Could not get MetadataToken for stack frame {0} from {1}", frame.Function, method.Module.Name);
290+
frame.FunctionId = token & 0x00ffffff;
243291
}
244292
}
293+
catch (InvalidOperationException)
294+
{
295+
// method.MetadataToken may throw
296+
// see https://learn.microsoft.com/en-us/dotnet/api/system.reflection.memberinfo.metadatatoken?view=net-6.0
297+
_options.LogDebug("Could not get MetadataToken for stack frame {0} from {1}", frame.Function, method.Module.Name);
298+
}
245299
}
246300

247-
frame.ConfigureAppFrame(_options);
248-
249301
if (stackFrame.GetFileName() is { } frameFileName)
250302
{
251-
if (projectPath != null && frameFileName.StartsWith(projectPath, StringComparison.OrdinalIgnoreCase))
303+
if (AttributeReader.TryGetProjectDirectory(method.Module.Assembly) is { } projectPath
304+
&& frameFileName.StartsWith(projectPath, StringComparison.OrdinalIgnoreCase))
252305
{
253306
frame.AbsolutePath = frameFileName;
254307
frameFileName = frameFileName[projectPath.Length..];
255308
}
256309
frame.FileName = frameFileName;
257310
}
258311

312+
return frame;
313+
}
314+
315+
/// <summary>
316+
/// Create a <see cref="SentryStackFrame"/> from a <see cref="StackFrame"/>.
317+
/// </summary>
318+
internal SentryStackFrame? CreateFrame(IStackFrame stackFrame)
319+
{
320+
var frame = TryCreateManagedFrame(stackFrame);
321+
frame ??= TryCreateNativeAOTFrame(stackFrame);
322+
if (frame is null)
323+
{
324+
return null;
325+
}
326+
327+
frame.ConfigureAppFrame(_options);
328+
259329
// stackFrame.HasILOffset() throws NotImplemented on Mono 5.12
260330
var ilOffset = stackFrame.GetILOffset();
261331
if (ilOffset != StackFrame.OFFSET_UNKNOWN)
@@ -270,19 +340,19 @@ private SentryStackFrame InternalCreateFrame(StackFrame stackFrame, bool demangl
270340
}
271341

272342
var colNo = stackFrame.GetFileColumnNumber();
273-
if (lineNo > 0)
343+
if (colNo > 0)
274344
{
275345
frame.ColumnNumber = colNo;
276346
}
277347

278-
if (demangle && _options.StackTraceMode != StackTraceMode.Enhanced)
348+
if (stackFrame.Frame is not EnhancedStackFrame)
279349
{
280350
DemangleAsyncFunctionName(frame);
281351
DemangleAnonymousFunction(frame);
282352
DemangleLambdaReturnType(frame);
283353
}
284354

285-
if (_options.StackTraceMode == StackTraceMode.Enhanced)
355+
if (stackFrame.Frame is EnhancedStackFrame)
286356
{
287357
// In Enhanced mode, Module (which in this case is the Namespace)
288358
// is already prepended to the function, after return type.

0 commit comments

Comments
 (0)