Skip to content

feat: Native AOT frame info #2701

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"omnisharp.useModernNet": true
"omnisharp.useModernNet": true,
"dotnet.defaultSolution": "Sentry.sln"
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
</PropertyGroup>

<PropertyGroup>
Expand Down
9 changes: 2 additions & 7 deletions src/Sentry/AttributeReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@

internal static class AttributeReader
{
public static bool TryGetProjectDirectory(Assembly assembly, out string? projectDirectory)
{
projectDirectory = assembly.GetCustomAttributes<AssemblyMetadataAttribute>()
.FirstOrDefault(x => x.Key == "Sentry.ProjectDirectory")
?.Value;
return projectDirectory != null;
}
public static string? TryGetProjectDirectory(Assembly assembly) =>
assembly.GetCustomAttributes<AssemblyMetadataAttribute>().FirstOrDefault(x => x.Key == "Sentry.ProjectDirectory")?.Value;
}
206 changes: 138 additions & 68 deletions src/Sentry/Internal/DebugStackTrace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ internal class DebugStackTrace : SentryStackTrace
private static readonly Regex RegexAsyncReturn = new(@"^(.+`[0-9]+)\[\[",
RegexOptions.Compiled | RegexOptions.CultureInvariant);

private static readonly Regex RegexNativeAOTInfo = new(@"^(.+)\.([^.]+\(.*\)) ?\+ ?0x",
RegexOptions.Compiled | RegexOptions.CultureInvariant);

internal DebugStackTrace(SentryOptions options)
{
_options = options;
Expand Down Expand Up @@ -123,11 +126,14 @@ private IEnumerable<SentryStackFrame> CreateFrames(StackTrace stackTrace, bool i
{
var frames = _options.StackTraceMode switch
{
StackTraceMode.Enhanced => EnhancedStackTrace.GetFrames(stackTrace).Select(p => p as StackFrame),
StackTraceMode.Enhanced => EnhancedStackTrace.GetFrames(stackTrace).Select(p => new RealStackFrame(p)),
_ => stackTrace.GetFrames()
// error CS8619: Nullability of reference types in value of type 'StackFrame?[]' doesn't match target type 'IEnumerable<StackFrame>'.
#if NETCOREAPP3_0
.Where(f => f is not null)
.Select(p => new RealStackFrame(p!))
#else
.Select(p => new RealStackFrame(p))
#endif
};

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

// Remove the frames until the call for capture with the SDK
if (firstFrame
&& isCurrentStackTrace
&& stackFrame.GetMethod() is { } method
&& method.DeclaringType?.AssemblyQualifiedName?.StartsWith("Sentry") == true)
if (firstFrame && isCurrentStackTrace)
{
_options.LogDebug("Skipping initial stack frame '{0}'", method.Name);
continue;
string? frameInfo = null;
if (stackFrame.GetMethod() is { } method)
{
frameInfo = method.DeclaringType?.AssemblyQualifiedName;
}

// Native AOT currently only exposes some method info at runtime via ToString().
// See https://github.com/dotnet/runtime/issues/92869
if (frameInfo is null && stackFrame.HasNativeImage())
{
frameInfo = stackFrame.ToString();
}

if (frameInfo?.StartsWith("Sentry") is true)
{
_options.LogDebug("Skipping initial stack frame '{0}'", frameInfo);
continue;
}
}

firstFrame = false;

yield return CreateFrame(stackFrame);
if (CreateFrame(stackFrame) is { } frame)
{
yield return frame;
}
else
{
_options.LogDebug("Could not resolve stack frame '{0}'", stackFrame.ToString());
}
}
}

/// <summary>
/// Create a <see cref="SentryStackFrame"/> from a <see cref="StackFrame"/>.
/// Native AOT implementation of CreateFrame.
/// Native frames have only limited method information at runtime (and even that can be disabled).
/// We try to parse that and also add addresses for server-side symbolication.
/// </summary>
internal SentryStackFrame CreateFrame(StackFrame stackFrame) => InternalCreateFrame(stackFrame, true);
private SentryStackFrame? TryCreateNativeAOTFrame(IStackFrame stackFrame)
{
if (!stackFrame.HasNativeImage())
{
return null;
}

var frame = ParseNativeAOTToString(stackFrame.ToString());
frame.ImageAddress = stackFrame.GetNativeImageBase();
frame.InstructionAddress = stackFrame.GetNativeIP();
return frame;
}

// Method info is currently only exposed by ToString(), see https://github.com/dotnet/runtime/issues/92869
// We only care about the case where the method is available (`StackTraceSupport` property is the default `true`):
// https://github.com/dotnet/runtime/blob/254230253da143a082f47cfaf8711627c0bf2faf/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/DeveloperExperience/DeveloperExperience.cs#L42
internal static SentryStackFrame ParseNativeAOTToString(string info)
{
var frame = new SentryStackFrame();
var match = RegexNativeAOTInfo.Match(info);
if (match is { Success: true, Groups.Count: 3 })
{
frame.Module = match.Groups[1].Value;
frame.Function = match.Groups[2].Value;
}
return frame;
}

/// <summary>
/// Default the implementation of CreateFrame.
/// </summary>
private SentryStackFrame InternalCreateFrame(StackFrame stackFrame, bool demangle)
private SentryStackFrame? TryCreateManagedFrame(IStackFrame stackFrame)
{
if (stackFrame.GetMethod() is not { } method)
{
return null;
}

const string unknownRequiredField = "(unknown)";
string? projectPath = null;
var frame = new SentryStackFrame();
if (stackFrame.GetMethod() is { } method)
var frame = new SentryStackFrame
{
frame.Module = method.DeclaringType?.FullName ?? unknownRequiredField;
frame.Package = method.DeclaringType?.Assembly.FullName;
Module = method.DeclaringType?.FullName ?? unknownRequiredField,
Package = method.DeclaringType?.Assembly.FullName
};

if (_options.StackTraceMode == StackTraceMode.Enhanced &&
stackFrame is EnhancedStackFrame enhancedStackFrame)
{
var stringBuilder = new StringBuilder();
frame.Function = enhancedStackFrame.MethodInfo.Append(stringBuilder, false).ToString();
if (stackFrame.Frame is EnhancedStackFrame enhancedStackFrame)
{
var stringBuilder = new StringBuilder();
frame.Function = enhancedStackFrame.MethodInfo.Append(stringBuilder, false).ToString();

if (enhancedStackFrame.MethodInfo.DeclaringType is { } declaringType)
{
stringBuilder.Clear();
stringBuilder.AppendTypeDisplayName(declaringType);

// Ben.Demystifier doesn't always include the namespace, even when fullName==true.
// It's important that the module name always be fully qualified, so that in-app frame
// detection works correctly.
var module = stringBuilder.ToString();
frame.Module = declaringType.Namespace is { } ns && !module.StartsWith(ns)
? $"{ns}.{module}"
: module;
}
}
else
if (enhancedStackFrame.MethodInfo.DeclaringType is { } declaringType)
{
frame.Function = method.Name;
stringBuilder.Clear();
stringBuilder.AppendTypeDisplayName(declaringType);

// Ben.Demystifier doesn't always include the namespace, even when fullName==true.
// It's important that the module name always be fully qualified, so that in-app frame
// detection works correctly.
var module = stringBuilder.ToString();
frame.Module = declaringType.Namespace is { } ns && !module.StartsWith(ns)
? $"{ns}.{module}"
: module;
}
}
else
{
frame.Function = method.Name;
}

// Originally we didn't skip methods from dynamic assemblies, so not to break compatibility:
if (_options.StackTraceMode != StackTraceMode.Original && method.Module.Assembly.IsDynamic)
{
frame.InApp = false;
}
// Originally we didn't skip methods from dynamic assemblies, so not to break compatibility:
if (_options.StackTraceMode != StackTraceMode.Original && method.Module.Assembly.IsDynamic)
{
frame.InApp = false;
}

AttributeReader.TryGetProjectDirectory(method.Module.Assembly, out projectPath);
if (AddDebugImage(method.Module) is { } moduleIdx && moduleIdx != DebugImageMissing)
{
frame.AddressMode = GetRelativeAddressMode(moduleIdx);

if (AddDebugImage(method.Module) is { } moduleIdx && moduleIdx != DebugImageMissing)
try
{
frame.AddressMode = GetRelativeAddressMode(moduleIdx);

try
{
var token = method.MetadataToken;
// The top byte is the token type, the lower three bytes are the record id.
// See: https://docs.microsoft.com/en-us/previous-versions/dotnet/netframework-4.0/ms404456(v=vs.100)#metadata-token-structure
var tokenType = token & 0xff000000;
// See https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/metadata/cortokentype-enumeration
if (tokenType == 0x06000000) // CorTokenType.mdtMethodDef
{
frame.FunctionId = token & 0x00ffffff;
}
}
catch (InvalidOperationException)
var token = method.MetadataToken;
// The top byte is the token type, the lower three bytes are the record id.
// See: https://docs.microsoft.com/en-us/previous-versions/dotnet/netframework-4.0/ms404456(v=vs.100)#metadata-token-structure
var tokenType = token & 0xff000000;
// See https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/metadata/cortokentype-enumeration
if (tokenType == 0x06000000) // CorTokenType.mdtMethodDef
{
// method.MetadataToken may throw
// see https://learn.microsoft.com/en-us/dotnet/api/system.reflection.memberinfo.metadatatoken?view=net-6.0
_options.LogDebug("Could not get MetadataToken for stack frame {0} from {1}", frame.Function, method.Module.Name);
frame.FunctionId = token & 0x00ffffff;
}
}
catch (InvalidOperationException)
{
// method.MetadataToken may throw
// see https://learn.microsoft.com/en-us/dotnet/api/system.reflection.memberinfo.metadatatoken?view=net-6.0
_options.LogDebug("Could not get MetadataToken for stack frame {0} from {1}", frame.Function, method.Module.Name);
}
}

frame.ConfigureAppFrame(_options);

if (stackFrame.GetFileName() is { } frameFileName)
{
if (projectPath != null && frameFileName.StartsWith(projectPath, StringComparison.OrdinalIgnoreCase))
if (AttributeReader.TryGetProjectDirectory(method.Module.Assembly) is { } projectPath
&& frameFileName.StartsWith(projectPath, StringComparison.OrdinalIgnoreCase))
{
frame.AbsolutePath = frameFileName;
frameFileName = frameFileName[projectPath.Length..];
}
frame.FileName = frameFileName;
}

return frame;
}

/// <summary>
/// Create a <see cref="SentryStackFrame"/> from a <see cref="StackFrame"/>.
/// </summary>
internal SentryStackFrame? CreateFrame(IStackFrame stackFrame)
{
var frame = TryCreateManagedFrame(stackFrame);
frame ??= TryCreateNativeAOTFrame(stackFrame);
if (frame is null)
{
return null;
}

frame.ConfigureAppFrame(_options);

// stackFrame.HasILOffset() throws NotImplemented on Mono 5.12
var ilOffset = stackFrame.GetILOffset();
if (ilOffset != StackFrame.OFFSET_UNKNOWN)
Expand All @@ -270,19 +340,19 @@ private SentryStackFrame InternalCreateFrame(StackFrame stackFrame, bool demangl
}

var colNo = stackFrame.GetFileColumnNumber();
if (lineNo > 0)
if (colNo > 0)
{
frame.ColumnNumber = colNo;
}

if (demangle && _options.StackTraceMode != StackTraceMode.Enhanced)
if (stackFrame.Frame is not EnhancedStackFrame)
{
DemangleAsyncFunctionName(frame);
DemangleAnonymousFunction(frame);
DemangleLambdaReturnType(frame);
}

if (_options.StackTraceMode == StackTraceMode.Enhanced)
if (stackFrame.Frame is EnhancedStackFrame)
{
// In Enhanced mode, Module (which in this case is the Namespace)
// is already prepended to the function, after return type.
Expand Down
Loading