Skip to content
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
69 changes: 66 additions & 3 deletions Runtime/BacktraceClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;

[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Backtrace.Unity.Tests.Runtime")]
Expand All @@ -20,14 +21,16 @@ public class BacktraceClient : MonoBehaviour, IBacktraceClient
{
public BacktraceConfiguration Configuration;

public const string VERSION = "3.3.3";
public const string VERSION = "3.3.4";
public bool Enabled { get; private set; }

/// <summary>
/// Client attributes
/// </summary>
private readonly Dictionary<string, string> _clientAttributes = new Dictionary<string, string>();

internal readonly Stack<BacktraceReport> BackgroundExceptions = new Stack<BacktraceReport>();

Choose a reason for hiding this comment

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

Why is this Stack instead of Queue?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm yes, you're right. Queue makes more sense here


/// <summary>
/// Attribute object accessor
/// </summary>
Expand Down Expand Up @@ -352,7 +355,7 @@ public void Refresh()
}

Enabled = true;

_current = Thread.CurrentThread;
CaptureUnityMessages();
_reportLimitWatcher = new ReportLimitWatcher(Convert.ToUInt32(Configuration.ReportPerMin));

Expand Down Expand Up @@ -413,15 +416,28 @@ private void Awake()
/// <summary>
/// Update native client internal ANR timer.
/// </summary>
private void Update()
private void LateUpdate()

Choose a reason for hiding this comment

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

Why is this LateUpdate now? What happens in Update?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

To give main thread to update before an anr thread

Choose a reason for hiding this comment

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

Ah okay that makes sense

{
_nativeClient?.UpdateClientTime(Time.unscaledTime);

if (BackgroundExceptions.Count == 0)
{
return;
}
while (BackgroundExceptions.Count > 0)
{
// use SendReport method isntead of Send method
// because we already applied all watchdog/skipReport rules
// so we don't need to apply them once again
SendReport(BackgroundExceptions.Pop());
}
}

private void OnDestroy()
{
Enabled = false;
Application.logMessageReceived -= HandleUnityMessage;
Application.logMessageReceivedThreaded -= HandleUnityBackgroundException;
#if UNITY_ANDROID || UNITY_IOS
Application.lowMemory -= HandleLowMemory;
_nativeClient?.Disable();
Expand Down Expand Up @@ -691,6 +707,8 @@ internal void OnAnrDetected(string stackTrace)
}
#endif

private Thread _current;

/// <summary>
/// Handle Unity unhandled exceptions
/// </summary>
Expand All @@ -700,12 +718,24 @@ private void CaptureUnityMessages()
if (Configuration.HandleUnhandledExceptions || Configuration.NumberOfLogs != 0)
{
Application.logMessageReceived += HandleUnityMessage;
Application.logMessageReceivedThreaded += HandleUnityBackgroundException;
#if UNITY_ANDROID || UNITY_IOS
Application.lowMemory += HandleLowMemory;
#endif
}
}

internal void HandleUnityBackgroundException(string message, string stackTrace, LogType type)
{
// validate if a message is from main thread
// and skip messages from main thread
if (Thread.CurrentThread == _current)
{
return;
}
HandleUnityMessage(message, stackTrace, type);
}

#if UNITY_ANDROID || UNITY_IOS
internal void HandleLowMemory()
{
Expand Down Expand Up @@ -824,10 +854,22 @@ private bool ShouldSendReport(Exception exception, List<string> attachmentPaths,
{
return false;
}

//check rate limiting
bool shouldProcess = _reportLimitWatcher.WatchReport(new DateTime().Timestamp());
if (shouldProcess)
{
// This condition checks if we should send exception from current thread
// if comparision result confirm that we're trying to send an exception from different
// thread than main, we should add the exception object to the exception list
// and let update method send data to Backtrace.
if (Thread.CurrentThread.ManagedThreadId != _current.ManagedThreadId)
{
var report = new BacktraceReport(exception, attributes, attachmentPaths);

Choose a reason for hiding this comment

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

This seems to be repeated 3 times. Can we pull this into a function?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not really because of different parameters that this function require and cost of BacktraceReport creation

Choose a reason for hiding this comment

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

Okay I see. Since this gets called in Update method we need to consider cost of BacktraceReport creation.

report.Attributes["exception.thread"] = Thread.CurrentThread.ManagedThreadId.ToString();
BackgroundExceptions.Push(report);
return false;
}
return true;
}
if (OnClientReportLimitReached != null)
Expand All @@ -852,6 +894,17 @@ private bool ShouldSendReport(string message, List<string> attachmentPaths, Dict
bool shouldProcess = _reportLimitWatcher.WatchReport(new DateTime().Timestamp());
if (shouldProcess)
{
// This condition checks if we should send exception from current thread
// if comparision result confirm that we're trying to send an exception from different
// thread than main, we should add the exception object to the exception list
// and let update method send data to Backtrace.
if (Thread.CurrentThread.ManagedThreadId != _current.ManagedThreadId)
{
var report = new BacktraceReport(message, attributes, attachmentPaths);
report.Attributes["exception.thread"] = Thread.CurrentThread.ManagedThreadId.ToString();
BackgroundExceptions.Push(report);
return false;
}
return true;
}
if (OnClientReportLimitReached != null)
Expand Down Expand Up @@ -880,6 +933,16 @@ private bool ShouldSendReport(BacktraceReport report)
bool shouldProcess = _reportLimitWatcher.WatchReport(new DateTime().Timestamp());
if (shouldProcess)
{
// This condition checks if we should send exception from current thread
// if comparision result confirm that we're trying to send an exception from different
// thread than main, we should add the exception object to the exception list
// and let update method send data to Backtrace.
if (Thread.CurrentThread.ManagedThreadId != _current.ManagedThreadId)
{
report.Attributes["exception.thread"] = Thread.CurrentThread.ManagedThreadId.ToString();
BackgroundExceptions.Push(report);
return false;
}
return true;
}
if (OnClientReportLimitReached != null)
Expand Down
205 changes: 205 additions & 0 deletions Tests/Runtime/BackgroundThreadSupportTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
using Backtrace.Unity.Model;
using Backtrace.Unity.Types;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace Backtrace.Unity.Tests.Runtime
{
public class BackgroundThreadSupport : BacktraceBaseTest
{
[SetUp]
public void Setup()
{
BeforeSetup();
BacktraceClient.Configuration = GetBasicConfiguration();
AfterSetup();
}

[Test]
public void TestBackgroundThreadSupport_BackgroundExceptionShouldntThrow_ExceptionIsSavedInMainThreadLoop()
{
var client = BacktraceClient;
string exceptionMessage = "foo";
var mainThreadId = Thread.CurrentThread.ManagedThreadId;
var thread = new Thread(() =>
{
Assert.IsTrue(Thread.CurrentThread.ManagedThreadId != mainThreadId);
var exception = new InvalidOperationException(exceptionMessage);
client.Send(exception);
});
thread.Start();
thread.Join();

Assert.IsNotEmpty(BacktraceClient.BackgroundExceptions);
Assert.AreEqual(exceptionMessage, BacktraceClient.BackgroundExceptions.First().Message);
Assert.IsTrue(BacktraceClient.BackgroundExceptions.First().ExceptionTypeReport);
}


[Test]
public void TestBackgroundThreadSupport_BackgroundReportShouldntThrow_ExceptionIsSavedInMainThreadLoop()
{
var client = BacktraceClient;
string exceptionMessage = "foo";
var mainThreadId = Thread.CurrentThread.ManagedThreadId;
var thread = new Thread(() =>
{
Assert.IsTrue(Thread.CurrentThread.ManagedThreadId != mainThreadId);
var exception = new InvalidOperationException(exceptionMessage);
client.Send(new BacktraceReport(exception));
});
thread.Start();
thread.Join();

Assert.IsNotEmpty(BacktraceClient.BackgroundExceptions);
Assert.AreEqual(exceptionMessage, BacktraceClient.BackgroundExceptions.First().Message);
Assert.IsTrue(BacktraceClient.BackgroundExceptions.First().ExceptionTypeReport);
}

[Test]
public void TestBackgroundThreadSupport_BackgroundReportWithAttributesShouldntThrow_ExceptionIsSavedInMainThreadLoop()
{
var client = BacktraceClient;
string exceptionMessage = "foo";
var attributeKey = "attribute-key";
var attributeValue = exceptionMessage;
var mainThreadId = Thread.CurrentThread.ManagedThreadId;
var thread = new Thread(() =>
{
Assert.IsTrue(Thread.CurrentThread.ManagedThreadId != mainThreadId);
var exception = new InvalidOperationException(exceptionMessage);
client.Send(new BacktraceReport(exception, new Dictionary<string, string> { { attributeKey, attributeValue } }));
});
thread.Start();
thread.Join();

Assert.IsNotEmpty(BacktraceClient.BackgroundExceptions);
var storedReport = BacktraceClient.BackgroundExceptions.First();
Assert.AreEqual(exceptionMessage, storedReport.Message);
Assert.IsTrue(storedReport.ExceptionTypeReport);
Assert.IsNotNull(storedReport.Attributes[attributeKey]);
Assert.AreEqual(storedReport.Attributes[attributeKey], attributeValue);
}


[Test]
public void TestBackgroundThreadSupport_BackgroundMessageShouldntThrow_ExceptionIsSavedInMainThreadLoop()
{
var client = BacktraceClient;
string exceptionMessage = "foo";
var mainThreadId = Thread.CurrentThread.ManagedThreadId;
var thread = new Thread(() =>
{
Assert.IsTrue(Thread.CurrentThread.ManagedThreadId != mainThreadId);
client.Send(exceptionMessage);
});
thread.Start();
thread.Join();

Assert.IsNotEmpty(BacktraceClient.BackgroundExceptions);
Assert.AreEqual(exceptionMessage, BacktraceClient.BackgroundExceptions.First().Message);
Assert.IsFalse(BacktraceClient.BackgroundExceptions.First().ExceptionTypeReport);
}

[Test]
public void TestBackgroundThreadSupport_BackgroundUnhandledExceptionShouldntThrow_ExceptionIsSavedInMainThreadLoop()
{
var client = BacktraceClient;
string exceptionMessage = "foo";
var mainThreadId = Thread.CurrentThread.ManagedThreadId;

var thread = new Thread(() =>
{
Assert.IsTrue(Thread.CurrentThread.ManagedThreadId != mainThreadId);
client.HandleUnityBackgroundException(exceptionMessage, string.Empty, UnityEngine.LogType.Exception);
});
thread.Start();
thread.Join();

Assert.IsNotEmpty(BacktraceClient.BackgroundExceptions);
Assert.AreEqual(exceptionMessage, BacktraceClient.BackgroundExceptions.First().Message);
Assert.IsTrue(BacktraceClient.BackgroundExceptions.First().ExceptionTypeReport);
}

[Test]
public void TestBackgroundThreadSupport_UserShouldBeAbleToFilterUnhandledExceptions_ReportShouldntBeAvailableInMainThreadLoop()
{
var client = BacktraceClient;
string exceptionMessage = "foo";

BacktraceClient.SkipReport = (ReportFilterType type, Exception e, string message) =>
{
return true;
};
var mainThreadId = Thread.CurrentThread.ManagedThreadId;
var thread = new Thread(() =>
{
Assert.IsTrue(Thread.CurrentThread.ManagedThreadId != mainThreadId);
client.HandleUnityBackgroundException(exceptionMessage, string.Empty, UnityEngine.LogType.Exception);
});
thread.Start();
thread.Join();

Assert.IsEmpty(BacktraceClient.BackgroundExceptions);
}

[Test]
public void TestBackgroundThreadSupport_UserShouldBeAbleToFilterReports_ReportShouldntBeAvailableInMainThreadLoop()
{
var client = BacktraceClient;
string exceptionMessage = "foo";

BacktraceClient.SkipReport = (ReportFilterType type, Exception e, string message) =>
{
return true;
};
var thread = new Thread(() =>
{
client.Send(exceptionMessage);
var exception = new InvalidOperationException(exceptionMessage);
client.Send(exception);
client.Send(new BacktraceReport(exception));
});

thread.Start();
thread.Join();

Assert.IsEmpty(BacktraceClient.BackgroundExceptions);
}


[Test]
public void TestBackgroundThreadSupport_RateLimitSkipReports_ReportShouldntBeAvailableInMainThreadLoop()
{
var client = BacktraceClient;
string exceptionMessage = "foo";

uint rateLimit = 5;
var expectedNumberOfSkippedReports = 5;
int actualNumberOfSkippedReports = 0;

client.SetClientReportLimit(rateLimit);
client.OnClientReportLimitReached = (BacktraceReport report) =>
{
actualNumberOfSkippedReports++;
};

var thread = new Thread(() =>
{
for (int i = 0; i < rateLimit + expectedNumberOfSkippedReports; i++)
{
client.Send(new InvalidOperationException(exceptionMessage));
}

});
thread.Start();
thread.Join();
Assert.IsNotEmpty(BacktraceClient.BackgroundExceptions);
Assert.AreEqual(rateLimit, BacktraceClient.BackgroundExceptions.Count);
Assert.AreEqual(expectedNumberOfSkippedReports, actualNumberOfSkippedReports);
}
}
}
11 changes: 11 additions & 0 deletions Tests/Runtime/BackgroundThreadSupportTests.cs.meta

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "io.backtrace.unity",
"displayName": "Backtrace",
"version": "3.3.3",
"version": "3.3.4",
"unity": "2017.1",
"description": "Backtrace's integration with Unity games allows customers to capture and report handled and unhandled Unity exceptions to their Backtrace instance, instantly offering the ability to prioritize and debug software errors.",
"keywords": [
Expand Down