Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,40 @@ public StackFrameHelper()
internal void InitializeSourceInfo(bool fNeedFileInfo, Exception? exception)
{
StackTrace.GetStackFramesInternal(this, fNeedFileInfo, exception);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be nice to also defer Windows PDB data resolution, but that requires some nontrivial changes in the runtime.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed but totally content if it is out of scope for this PR.

}

public MethodBase? GetMethodBase(int i)
{
// There may be a better way to do this.
// we got RuntimeMethodHandles here and we need to go to MethodBase
// but we don't know whether the reflection info has been initialized
// or not. So we call GetMethods and GetConstructors on the type
// and then we fetch the proper MethodBase!!
IntPtr mh = rgMethodHandle![i];

if (mh == IntPtr.Zero)
return null;

IRuntimeMethodInfo? mhReal = RuntimeMethodHandle.GetTypicalMethodDefinition(new RuntimeMethodInfoStub(new RuntimeMethodHandleInternal(mh), this));

return RuntimeType.GetMethodBase(mhReal);
}

private void InitializeFrameSourceInfo(int index)
{
// rgiMethodToken is null if file info wasn't requested when collecting the stack trace.
if (rgiMethodToken == null)
return;

if (!fNeedFileInfo)
// We use rgiMethodToken[i] to indicate whether we've initialized the info for this frame.
// If the native code set it to zero, then information is already initialized from the native
// symbol reader. If the native code set it to non-zero, then we're supposed to use the managed
// symbol reader to try to resolve debug information.
//
// This is the only purpose the field has, however, so we can assign it to zero AFTER resolving
// symbol information to indicate that it's already been resolved without causing any issues
// elsewhere.
if (Volatile.Read(ref rgiMethodToken[index]) == 0)
return;

// Check if this function is being reentered because of an exception in the code below
Expand All @@ -112,17 +144,16 @@ internal void InitializeSourceInfo(bool fNeedFileInfo, Exception? exception)
Interlocked.CompareExchange(ref s_stackTraceSymbolsCache, CreateStackTraceSymbols(), null);
}

for (int index = 0; index < iFrameCount; index++)
{
// If there was some reason not to try get the symbols from the portable PDB reader like the module was
// ENC or the source/line info was already retrieved, the method token is 0.
if (rgiMethodToken![index] != 0)
{
GetSourceLineInfo(s_stackTraceSymbolsCache!, rgAssembly![index], rgAssemblyPath![index]!, rgLoadedPeAddress![index], rgiLoadedPeSize![index], rgiIsFileLayout![index],
rgInMemoryPdbAddress![index], rgiInMemoryPdbSize![index], rgiMethodToken![index],
rgiILOffset![index], out rgFilename![index], out rgiLineNumber![index], out rgiColumnNumber![index]);
}
}
GetSourceLineInfo(s_stackTraceSymbolsCache!, rgAssembly![index], rgAssemblyPath![index]!, rgLoadedPeAddress![index], rgiLoadedPeSize![index], rgiIsFileLayout![index],
rgInMemoryPdbAddress![index], rgiInMemoryPdbSize![index], rgiMethodToken![index],
rgiILOffset![index], out string? filename, out int lineNumber, out int columnNumber);

rgFilename![index] = filename;
rgiLineNumber![index] = lineNumber;
rgiColumnNumber![index] = columnNumber;

// Make sure we mark down that debug information for this frame was resolved
Volatile.Write(ref rgiMethodToken[index], 0);
}
catch
{
Expand All @@ -133,28 +164,37 @@ internal void InitializeSourceInfo(bool fNeedFileInfo, Exception? exception)
}
}

public MethodBase? GetMethodBase(int i)
{
// There may be a better way to do this.
// we got RuntimeMethodHandles here and we need to go to MethodBase
// but we don't know whether the reflection info has been initialized
// or not. So we call GetMethods and GetConstructors on the type
// and then we fetch the proper MethodBase!!
IntPtr mh = rgMethodHandle![i];

if (mh == IntPtr.Zero)
return null;

IRuntimeMethodInfo? mhReal = RuntimeMethodHandle.GetTypicalMethodDefinition(new RuntimeMethodInfoStub(new RuntimeMethodHandleInternal(mh), this));

return RuntimeType.GetMethodBase(mhReal);
}

public int GetOffset(int i) { return rgiOffset![i]; }
public int GetILOffset(int i) { return rgiILOffset![i]; }
public string? GetFilename(int i) { return rgFilename?[i]; }
public int GetLineNumber(int i) { return rgiLineNumber == null ? 0 : rgiLineNumber[i]; }
public int GetColumnNumber(int i) { return rgiColumnNumber == null ? 0 : rgiColumnNumber[i]; }
public string? GetFilename(int i)
{
InitializeFrameSourceInfo(i);
return rgFilename?[i];
}
public int GetLineNumber(int i)
{
if (rgiLineNumber == null)
{
return 0;
}
else
{
InitializeFrameSourceInfo(i);
return rgiLineNumber[i];
}
}
public int GetColumnNumber(int i)
{
if (rgiColumnNumber == null)
{
return 0;
}
else
{
InitializeFrameSourceInfo(i);
return rgiColumnNumber[i];
}
}

public bool IsLastFrameFromForeignExceptionStackTrace(int i)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;

namespace System.Diagnostics
{
public partial class StackTrace
{
private StackFrameHelper? _stackFrameHelper;
private bool _fNeedFileInfo;

[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "StackTrace_GetStackFramesInternal")]
private static partial void GetStackFramesInternal(ObjectHandleOnStack sfh, [MarshalAs(UnmanagedType.Bool)] bool fNeedFileInfo, ObjectHandleOnStack e);

Expand Down Expand Up @@ -66,30 +70,47 @@ private void CaptureStackTrace(int skipFrames, bool fNeedFileInfo, Exception? e)
StackF.InitializeSourceInfo(fNeedFileInfo, e);

_numOfFrames = StackF.GetNumberOfFrames();
_stackFrameHelper = StackF;

if (_methodsToSkip > _numOfFrames)
_methodsToSkip = _numOfFrames;

if (_numOfFrames != 0)
_fNeedFileInfo = fNeedFileInfo;

// CalculateFramesToSkip skips all frames in the System.Diagnostics namespace,
// but this is not desired if building a stack trace from an exception.
if (e == null)
_methodsToSkip += CalculateFramesToSkip(_stackFrameHelper, _numOfFrames);

_numOfFrames -= _methodsToSkip;
if (_numOfFrames < 0)
{
_stackFrames = new StackFrame[_numOfFrames];
_numOfFrames = 0;
}
}

for (int i = 0; i < _numOfFrames; i++)
{
_stackFrames[i] = new StackFrame(StackF, i, fNeedFileInfo);
}
/// <summary>
/// Gets the <see cref="StackFrame"/> array for this StackTrace, possibly creating if needed.
/// </summary>
/// <returns></returns>
private StackFrame[] GetFramesCore()
{
if (_numOfFrames != 0 && _stackFrames == null)
{
Debug.Assert(_stackFrameHelper != null);

// CalculateFramesToSkip skips all frames in the System.Diagnostics namespace,
// but this is not desired if building a stack trace from an exception.
if (e == null)
_methodsToSkip += CalculateFramesToSkip(StackF, _numOfFrames);
// the frame array should contain all frames (even skipped ones!)
StackFrame[] stackFrames = new StackFrame[_stackFrameHelper.GetNumberOfFrames()];

_numOfFrames -= _methodsToSkip;
if (_numOfFrames < 0)
for (int i = 0; i < stackFrames.Length; i++)
{
_numOfFrames = 0;
stackFrames[i] = new StackFrame(_stackFrameHelper, i, _fNeedFileInfo);
}

Interlocked.CompareExchange(ref _stackFrames, stackFrames, null);
}

return _stackFrames ?? [];
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,10 @@ internal void ToString(TraceFormat traceFormat, StringBuilder builder)
builder.AppendLine();
}
#endif

private StackFrame[] GetFramesCore()
{
return _stackFrames ?? [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should NativeAOT get the same optimization? We do not want NativeAOT performance to be lagging behind.

Copy link
Contributor Author

@nike4613 nike4613 May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if debug info resolution is as significant on NAOT; I haven't benched it. My driving scenario is JIT-only for entirely separate reasons anyway. It shouldn't be too hard to do there, though I'd want to profile it first.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency I think doing the same defered StackFrame creation would be nice. I suspect the optimization won't make a substantial difference unless it is run together with a DeveloperExperience implementation that does more appreciable work:

internal StackFrame(IntPtr ipAddress, bool needFileInfo)
{
InitializeForIpAddress(ipAddress, needFileInfo);
}
/// <summary>
/// Internal stack frame initialization based on IP address.
/// </summary>
private void InitializeForIpAddress(IntPtr ipAddress, bool needFileInfo)
{
_ipAddress = ipAddress;
_needFileInfo = needFileInfo;
if (_ipAddress == Exception.EdiSeparator)
{
_isLastFrameFromForeignExceptionStackTrace = true;
}
else if (_ipAddress != IntPtr.Zero)
{
IntPtr methodStartAddress = RuntimeImports.RhFindMethodStartAddress(ipAddress);
_nativeOffset = (int)((nint)_ipAddress - (nint)methodStartAddress);
DeveloperExperience.Default.TryGetILOffsetWithinMethod(_ipAddress, out _ilOffset);
if (needFileInfo)
{
DeveloperExperience.Default.TryGetSourceLineInfo(
_ipAddress,
out _fileName,
out _lineNumber,
out _columnNumber);
}
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if debug info resolution is as significant on NAOT; I haven't benched it.

I would be good to see some numbers.

run together with a DeveloperExperience

DeveloperExperience is on by default in NativeAOT.

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,9 @@ public StackTrace(IEnumerable<StackFrame> frames)
/// </summary>
public virtual StackFrame? GetFrame(int index)
{
if (_stackFrames != null && index < _numOfFrames && index >= 0)
return _stackFrames[index + _methodsToSkip];
StackFrame[] stackFrames = GetFramesCore();
if (stackFrames != null && index < _numOfFrames && index >= 0)
return stackFrames[index + _methodsToSkip];

return null;
}
Expand All @@ -170,13 +171,14 @@ public StackTrace(IEnumerable<StackFrame> frames)
/// </summary>
public virtual StackFrame[] GetFrames()
{
if (_stackFrames == null || _numOfFrames <= 0)
StackFrame[] stackFrames = GetFramesCore();
if (stackFrames == null || _numOfFrames <= 0)
return Array.Empty<StackFrame>();

// We have to return a subset of the array. Unfortunately this
// means we have to allocate a new array and copy over.
StackFrame[] array = new StackFrame[_numOfFrames];
Array.Copy(_stackFrames, _methodsToSkip, array, 0, _numOfFrames);
Array.Copy(stackFrames, _methodsToSkip, array, 0, _numOfFrames);
return array;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,10 @@ private void InitializeForException(Exception e, int skipFrames, bool needFileIn
_stackFrames[foreignFrames + i] = new StackFrame(frames[i], needFileInfo);
}
}

private StackFrame[] GetFramesCore()
{
return _stackFrames ?? [];
}
}
}
Loading