Skip to content
Draft
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
5 changes: 3 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
projectPath:
Copy link
Collaborator

Choose a reason for hiding this comment

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

pleaes move it to a separated pull request. in the git history no one expects to see cicd pipeline update in the native stack trace parser pull request

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that's the plan!

- test-package
unityVersion:
- 2019.4.40f1
- 2022.3.56f1
targetPlatform:
- StandaloneOSX
- StandaloneWindows
Expand Down Expand Up @@ -48,5 +48,6 @@ jobs:
unityVersion: ${{ matrix.unityVersion }}
targetPlatform: ${{ matrix.targetPlatform }}
projectPath: ${{ matrix.projectPath }}/
allowDirtyBuild: true
allowDirtyBuild: true
Copy link
Collaborator

Choose a reason for hiding this comment

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

broken yaml

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yup allowDirtyBuild & testMode cannot co-exist. also allowDirtyBuild shouldn't have been used here.

testMode: editmode

115 changes: 75 additions & 40 deletions Runtime/Model/BacktraceUnhandledException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,13 @@ private List<BacktraceStackFrame> ConvertStackFrames(IEnumerable<string> frames)

}

//methodname index should be greater than 0 AND '(' should be before ')'
if (methodNameEndIndex < 1 && frameString[methodNameEndIndex - 1] != '(')
// we require a '(' that appears before this ')'
int openParenIndex = frameString.LastIndexOf('(', methodNameEndIndex);
Copy link
Collaborator

Choose a reason for hiding this comment

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

typo paren

if (openParenIndex == -1 || openParenIndex > methodNameEndIndex)
{
result.Add(new BacktraceStackFrame()
{
FunctionName = frame
});
// invalid shape: no matching '(' before ')'
result.Add(new BacktraceStackFrame { FunctionName = frame });
continue;
}

result.Add(ConvertFrame(frameString, methodNameEndIndex));
Expand Down Expand Up @@ -271,63 +271,98 @@ private BacktraceStackFrame SetJITStackTraceInformation(string frameString)
}

/// <summary>
/// Try to convert native stack frame
/// Try to safely convert a native stack frame string into a Backtrace stack frame.
/// Handles frames with or without symbols (e.g. "0xADDR (Module)" or "0xADDR (Module) Symbol").
/// Prevents ArgumentOutOfRangeException by validating index ranges and allowing empty symbols.
Copy link
Collaborator

Choose a reason for hiding this comment

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

whats the point of adding this info to the summary?

/// </summary>
/// <param name="frameString">Native stack frame</param>
/// <returns>Backtrace stack frame</returns>
/// <param name="frameString">Raw native stack frame line to parse.</param>
/// <returns>Parsed Backtrace stack frame containing address, library, function name, and optional line number.</returns>
private BacktraceStackFrame SetNativeStackTraceInformation(string frameString)
{
var stackFrame = new BacktraceStackFrame
{
StackFrameType = Types.BacktraceStackFrameType.Native
StackFrameType = Types.BacktraceStackFrameType.Native,
FunctionName = string.Empty,
Line = 0
Comment on lines +285 to +286
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are you sure you need to set them?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, needed to guarantee consistent defaults for native frames (especially when the parser exits early). It avoids nulls and keeps the frame type marked as Native even for empty input.

Copy link
Collaborator

Choose a reason for hiding this comment

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

yeah but why we should avoid nulls? I would say null in this case gives us important information - do not set entry in the json field. Are you sure symbolication process later will handle it properly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Solid point, exactly what I needed!
I’ll keep StackFrameType = Native but stop forcing FunctionName = "" and Line = 0.
For frames we can’t parse I’ll leave FunctionName null and only set Line when found.

};
// parse address
var addressSubstringIndex = frameString.IndexOf(' ');
if (addressSubstringIndex == -1)

if (string.IsNullOrEmpty(frameString))
{
stackFrame.FunctionName = frameString;
return stackFrame;
}
stackFrame.Address = frameString.Substring(0, addressSubstringIndex);
var indexPointer = addressSubstringIndex + 1;

// parse library
if (frameString[indexPointer] == '(')
frameString = frameString.Trim();

// Address: starts with "0x" and ends at first space
int index = 0;
if (frameString.StartsWith("0x", StringComparison.Ordinal))
{
indexPointer = indexPointer + 1;
var libraryNameSubstringIndex = frameString.IndexOf(')', indexPointer);
stackFrame.Library = frameString.Substring(indexPointer, libraryNameSubstringIndex - indexPointer);
indexPointer = libraryNameSubstringIndex + 2;
int space = frameString.IndexOf(' ');
if (space > 2)
{
stackFrame.Address = frameString.Substring(0, space);
index = space + 1;
}
else
{
// if Unknown format keep raw text and return
stackFrame.FunctionName = frameString;
return stackFrame;
}
}

// Library: Module
if (index < frameString.Length && frameString[index] == '(')
{
index++;
int close = frameString.IndexOf(')', index);
// if ')' missing leave Library null and continue
if (close > -1)
{
stackFrame.Library = frameString.Substring(index, close - index);
index = close + 1;
if (index < frameString.Length && frameString[index] == ' ')
{
index++;
}
}
}

stackFrame.FunctionName = frameString.Substring(indexPointer);
//cleanup function name
if (stackFrame.FunctionName.StartsWith("(wrapper managed-to-native)"))
// 3) symbol
stackFrame.FunctionName = (index < frameString.Length)
? frameString.Substring(index).Trim()
: string.Empty;

// 4) Normalize known wrappers
if (stackFrame.FunctionName.StartsWith("(wrapper managed-to-native)", StringComparison.Ordinal))
{
stackFrame.FunctionName = stackFrame.FunctionName.Replace("(wrapper managed-to-native)", string.Empty).Trim();
}

if (stackFrame.FunctionName.StartsWith("(wrapper runtime-invoke)"))
if (stackFrame.FunctionName.StartsWith("(wrapper runtime-invoke)", StringComparison.Ordinal))
{
stackFrame.FunctionName = stackFrame.FunctionName.Replace("(wrapper runtime-invoke)", string.Empty).Trim();
}

// try to find source code information
int sourceCodeStartIndex = stackFrame.FunctionName.IndexOf('[');
int sourceCodeEndIndex = stackFrame.FunctionName.IndexOf(']');
if (sourceCodeStartIndex != -1 && sourceCodeEndIndex != -1)
// [file:line] suffix source code information
int srcStart = stackFrame.FunctionName.IndexOf('[');
int srcEnd = stackFrame.FunctionName.IndexOf(']');
if (srcStart != -1 && srcEnd != -1 && srcEnd > srcStart)
{
sourceCodeStartIndex = sourceCodeStartIndex + 1;
var sourceCodeInformation = stackFrame.FunctionName.Substring(
sourceCodeStartIndex,
sourceCodeEndIndex - sourceCodeStartIndex);

var sourceCodeParts = sourceCodeInformation.Split(new char[] { ':' }, 2);
if (sourceCodeParts.Length == 2)
srcStart++;
var src = stackFrame.FunctionName.Substring(srcStart, srcEnd - srcStart);
var parts = src.Split(new char[] { ':' }, 2);
if (parts.Length == 2 && int.TryParse(parts[1], out var line))
{
int.TryParse(sourceCodeParts[1], out stackFrame.Line);
stackFrame.Library = sourceCodeParts[0];
stackFrame.FunctionName = stackFrame.FunctionName.Substring(sourceCodeEndIndex + 2);
stackFrame.Line = line;
stackFrame.Library = parts[0];
// after ']'
int after = srcEnd + 1;
if (after < stackFrame.FunctionName.Length && stackFrame.FunctionName[after] == ' ')
{ after++; }
stackFrame.FunctionName = (after < stackFrame.FunctionName.Length)
? stackFrame.FunctionName.Substring(after)
: string.Empty;
}
}

Expand Down
51 changes: 51 additions & 0 deletions Tests/Runtime/NativeParserTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Backtrace.Unity.Model;
using Backtrace.Unity.Types;
using NUnit.Framework;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Linq;
using System.Text;
using UnityEngine.TestTools;

namespace Backtrace.Unity.Tests.Runtime{
Copy link
Collaborator

Choose a reason for hiding this comment

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

this file is not formatted

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's a draft to review the native parsing logic;
Formatting, var names and other cosmetics will be fixed later.
I only need your thoughts on Parsing for now ~

public class NativeParserTests
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we be more specific? I would call it NativeStackTraceParser instead

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sure

{
private static BacktraceStackFrame Parse(string frame)
{
var instance = new BacktraceUnhandledException("msg", "");
Copy link
Collaborator

Choose a reason for hiding this comment

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

this is not a valid way to test it. Please follow the parser tests that we already have in the tests.

var mi = typeof(BacktraceUnhandledException)
Copy link
Collaborator

Choose a reason for hiding this comment

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

what is mi?

.GetMethod("SetNativeStackTraceInformation",
BindingFlags.NonPublic | BindingFlags.Instance);
Assert.IsNotNull(mi);
Copy link
Collaborator

Choose a reason for hiding this comment

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

what it tests?

return (BacktraceStackFrame)mi.Invoke(instance, new object[] { frame });
}

[Test]
public void SymbolLessModuleLine_ParsesAddressAndLibrary_NoThrow()
{
var f = Parse("0x00007ffad7723088 (UnityPlayer)");
Copy link
Collaborator

Choose a reason for hiding this comment

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

what is f?

Assert.AreEqual("0x00007ffad7723088", f.Address);
Assert.AreEqual("UnityPlayer", f.Library);
Assert.AreEqual(string.Empty, f.FunctionName);
Assert.AreEqual(Types.BacktraceStackFrameType.Native, f.StackFrameType);
}

[Test]
public void WithSymbol_ParsesMethod()
{
var f = Parse("0x00007ffad7ee3c7d (UnityPlayer) UnityMain");
Assert.AreEqual("UnityPlayer", f.Library);
Assert.AreEqual("UnityMain", f.FunctionName);
}

[Test]
public void UnknownShape_ReturnsRawInFunctionName()
{
var f = Parse("nonsense frame with no address");
Assert.AreEqual("nonsense frame with no address", f.FunctionName);
}
}
}
11 changes: 11 additions & 0 deletions Tests/Runtime/NativeParserTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.