Skip to content

Commit 38f648d

Browse files
authored
Background thread support (#67)
* Background thread support * Fixed exception flow * Removed renamed file
1 parent ad3a94c commit 38f648d

File tree

4 files changed

+283
-4
lines changed

4 files changed

+283
-4
lines changed

Runtime/BacktraceClient.cs

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System;
99
using System.Collections;
1010
using System.Collections.Generic;
11+
using System.Threading;
1112
using UnityEngine;
1213

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

23-
public const string VERSION = "3.3.3";
24+
public const string VERSION = "3.3.4";
2425
public bool Enabled { get; private set; }
2526

2627
/// <summary>
2728
/// Client attributes
2829
/// </summary>
2930
private readonly Dictionary<string, string> _clientAttributes = new Dictionary<string, string>();
3031

32+
internal readonly Stack<BacktraceReport> BackgroundExceptions = new Stack<BacktraceReport>();
33+
3134
/// <summary>
3235
/// Attribute object accessor
3336
/// </summary>
@@ -352,7 +355,7 @@ public void Refresh()
352355
}
353356

354357
Enabled = true;
355-
358+
_current = Thread.CurrentThread;
356359
CaptureUnityMessages();
357360
_reportLimitWatcher = new ReportLimitWatcher(Convert.ToUInt32(Configuration.ReportPerMin));
358361

@@ -413,15 +416,28 @@ private void Awake()
413416
/// <summary>
414417
/// Update native client internal ANR timer.
415418
/// </summary>
416-
private void Update()
419+
private void LateUpdate()
417420
{
418421
_nativeClient?.UpdateClientTime(Time.unscaledTime);
422+
423+
if (BackgroundExceptions.Count == 0)
424+
{
425+
return;
426+
}
427+
while (BackgroundExceptions.Count > 0)
428+
{
429+
// use SendReport method isntead of Send method
430+
// because we already applied all watchdog/skipReport rules
431+
// so we don't need to apply them once again
432+
SendReport(BackgroundExceptions.Pop());
433+
}
419434
}
420435

421436
private void OnDestroy()
422437
{
423438
Enabled = false;
424439
Application.logMessageReceived -= HandleUnityMessage;
440+
Application.logMessageReceivedThreaded -= HandleUnityBackgroundException;
425441
#if UNITY_ANDROID || UNITY_IOS
426442
Application.lowMemory -= HandleLowMemory;
427443
_nativeClient?.Disable();
@@ -691,6 +707,8 @@ internal void OnAnrDetected(string stackTrace)
691707
}
692708
#endif
693709

710+
private Thread _current;
711+
694712
/// <summary>
695713
/// Handle Unity unhandled exceptions
696714
/// </summary>
@@ -700,12 +718,24 @@ private void CaptureUnityMessages()
700718
if (Configuration.HandleUnhandledExceptions || Configuration.NumberOfLogs != 0)
701719
{
702720
Application.logMessageReceived += HandleUnityMessage;
721+
Application.logMessageReceivedThreaded += HandleUnityBackgroundException;
703722
#if UNITY_ANDROID || UNITY_IOS
704723
Application.lowMemory += HandleLowMemory;
705724
#endif
706725
}
707726
}
708727

728+
internal void HandleUnityBackgroundException(string message, string stackTrace, LogType type)
729+
{
730+
// validate if a message is from main thread
731+
// and skip messages from main thread
732+
if (Thread.CurrentThread == _current)
733+
{
734+
return;
735+
}
736+
HandleUnityMessage(message, stackTrace, type);
737+
}
738+
709739
#if UNITY_ANDROID || UNITY_IOS
710740
internal void HandleLowMemory()
711741
{
@@ -824,10 +854,22 @@ private bool ShouldSendReport(Exception exception, List<string> attachmentPaths,
824854
{
825855
return false;
826856
}
857+
827858
//check rate limiting
828859
bool shouldProcess = _reportLimitWatcher.WatchReport(new DateTime().Timestamp());
829860
if (shouldProcess)
830861
{
862+
// This condition checks if we should send exception from current thread
863+
// if comparision result confirm that we're trying to send an exception from different
864+
// thread than main, we should add the exception object to the exception list
865+
// and let update method send data to Backtrace.
866+
if (Thread.CurrentThread.ManagedThreadId != _current.ManagedThreadId)
867+
{
868+
var report = new BacktraceReport(exception, attributes, attachmentPaths);
869+
report.Attributes["exception.thread"] = Thread.CurrentThread.ManagedThreadId.ToString();
870+
BackgroundExceptions.Push(report);
871+
return false;
872+
}
831873
return true;
832874
}
833875
if (OnClientReportLimitReached != null)
@@ -852,6 +894,17 @@ private bool ShouldSendReport(string message, List<string> attachmentPaths, Dict
852894
bool shouldProcess = _reportLimitWatcher.WatchReport(new DateTime().Timestamp());
853895
if (shouldProcess)
854896
{
897+
// This condition checks if we should send exception from current thread
898+
// if comparision result confirm that we're trying to send an exception from different
899+
// thread than main, we should add the exception object to the exception list
900+
// and let update method send data to Backtrace.
901+
if (Thread.CurrentThread.ManagedThreadId != _current.ManagedThreadId)
902+
{
903+
var report = new BacktraceReport(message, attributes, attachmentPaths);
904+
report.Attributes["exception.thread"] = Thread.CurrentThread.ManagedThreadId.ToString();
905+
BackgroundExceptions.Push(report);
906+
return false;
907+
}
855908
return true;
856909
}
857910
if (OnClientReportLimitReached != null)
@@ -880,6 +933,16 @@ private bool ShouldSendReport(BacktraceReport report)
880933
bool shouldProcess = _reportLimitWatcher.WatchReport(new DateTime().Timestamp());
881934
if (shouldProcess)
882935
{
936+
// This condition checks if we should send exception from current thread
937+
// if comparision result confirm that we're trying to send an exception from different
938+
// thread than main, we should add the exception object to the exception list
939+
// and let update method send data to Backtrace.
940+
if (Thread.CurrentThread.ManagedThreadId != _current.ManagedThreadId)
941+
{
942+
report.Attributes["exception.thread"] = Thread.CurrentThread.ManagedThreadId.ToString();
943+
BackgroundExceptions.Push(report);
944+
return false;
945+
}
883946
return true;
884947
}
885948
if (OnClientReportLimitReached != null)
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
using Backtrace.Unity.Model;
2+
using Backtrace.Unity.Types;
3+
using NUnit.Framework;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Threading;
8+
9+
namespace Backtrace.Unity.Tests.Runtime
10+
{
11+
public class BackgroundThreadSupport : BacktraceBaseTest
12+
{
13+
[SetUp]
14+
public void Setup()
15+
{
16+
BeforeSetup();
17+
BacktraceClient.Configuration = GetBasicConfiguration();
18+
AfterSetup();
19+
}
20+
21+
[Test]
22+
public void TestBackgroundThreadSupport_BackgroundExceptionShouldntThrow_ExceptionIsSavedInMainThreadLoop()
23+
{
24+
var client = BacktraceClient;
25+
string exceptionMessage = "foo";
26+
var mainThreadId = Thread.CurrentThread.ManagedThreadId;
27+
var thread = new Thread(() =>
28+
{
29+
Assert.IsTrue(Thread.CurrentThread.ManagedThreadId != mainThreadId);
30+
var exception = new InvalidOperationException(exceptionMessage);
31+
client.Send(exception);
32+
});
33+
thread.Start();
34+
thread.Join();
35+
36+
Assert.IsNotEmpty(BacktraceClient.BackgroundExceptions);
37+
Assert.AreEqual(exceptionMessage, BacktraceClient.BackgroundExceptions.First().Message);
38+
Assert.IsTrue(BacktraceClient.BackgroundExceptions.First().ExceptionTypeReport);
39+
}
40+
41+
42+
[Test]
43+
public void TestBackgroundThreadSupport_BackgroundReportShouldntThrow_ExceptionIsSavedInMainThreadLoop()
44+
{
45+
var client = BacktraceClient;
46+
string exceptionMessage = "foo";
47+
var mainThreadId = Thread.CurrentThread.ManagedThreadId;
48+
var thread = new Thread(() =>
49+
{
50+
Assert.IsTrue(Thread.CurrentThread.ManagedThreadId != mainThreadId);
51+
var exception = new InvalidOperationException(exceptionMessage);
52+
client.Send(new BacktraceReport(exception));
53+
});
54+
thread.Start();
55+
thread.Join();
56+
57+
Assert.IsNotEmpty(BacktraceClient.BackgroundExceptions);
58+
Assert.AreEqual(exceptionMessage, BacktraceClient.BackgroundExceptions.First().Message);
59+
Assert.IsTrue(BacktraceClient.BackgroundExceptions.First().ExceptionTypeReport);
60+
}
61+
62+
[Test]
63+
public void TestBackgroundThreadSupport_BackgroundReportWithAttributesShouldntThrow_ExceptionIsSavedInMainThreadLoop()
64+
{
65+
var client = BacktraceClient;
66+
string exceptionMessage = "foo";
67+
var attributeKey = "attribute-key";
68+
var attributeValue = exceptionMessage;
69+
var mainThreadId = Thread.CurrentThread.ManagedThreadId;
70+
var thread = new Thread(() =>
71+
{
72+
Assert.IsTrue(Thread.CurrentThread.ManagedThreadId != mainThreadId);
73+
var exception = new InvalidOperationException(exceptionMessage);
74+
client.Send(new BacktraceReport(exception, new Dictionary<string, string> { { attributeKey, attributeValue } }));
75+
});
76+
thread.Start();
77+
thread.Join();
78+
79+
Assert.IsNotEmpty(BacktraceClient.BackgroundExceptions);
80+
var storedReport = BacktraceClient.BackgroundExceptions.First();
81+
Assert.AreEqual(exceptionMessage, storedReport.Message);
82+
Assert.IsTrue(storedReport.ExceptionTypeReport);
83+
Assert.IsNotNull(storedReport.Attributes[attributeKey]);
84+
Assert.AreEqual(storedReport.Attributes[attributeKey], attributeValue);
85+
}
86+
87+
88+
[Test]
89+
public void TestBackgroundThreadSupport_BackgroundMessageShouldntThrow_ExceptionIsSavedInMainThreadLoop()
90+
{
91+
var client = BacktraceClient;
92+
string exceptionMessage = "foo";
93+
var mainThreadId = Thread.CurrentThread.ManagedThreadId;
94+
var thread = new Thread(() =>
95+
{
96+
Assert.IsTrue(Thread.CurrentThread.ManagedThreadId != mainThreadId);
97+
client.Send(exceptionMessage);
98+
});
99+
thread.Start();
100+
thread.Join();
101+
102+
Assert.IsNotEmpty(BacktraceClient.BackgroundExceptions);
103+
Assert.AreEqual(exceptionMessage, BacktraceClient.BackgroundExceptions.First().Message);
104+
Assert.IsFalse(BacktraceClient.BackgroundExceptions.First().ExceptionTypeReport);
105+
}
106+
107+
[Test]
108+
public void TestBackgroundThreadSupport_BackgroundUnhandledExceptionShouldntThrow_ExceptionIsSavedInMainThreadLoop()
109+
{
110+
var client = BacktraceClient;
111+
string exceptionMessage = "foo";
112+
var mainThreadId = Thread.CurrentThread.ManagedThreadId;
113+
114+
var thread = new Thread(() =>
115+
{
116+
Assert.IsTrue(Thread.CurrentThread.ManagedThreadId != mainThreadId);
117+
client.HandleUnityBackgroundException(exceptionMessage, string.Empty, UnityEngine.LogType.Exception);
118+
});
119+
thread.Start();
120+
thread.Join();
121+
122+
Assert.IsNotEmpty(BacktraceClient.BackgroundExceptions);
123+
Assert.AreEqual(exceptionMessage, BacktraceClient.BackgroundExceptions.First().Message);
124+
Assert.IsTrue(BacktraceClient.BackgroundExceptions.First().ExceptionTypeReport);
125+
}
126+
127+
[Test]
128+
public void TestBackgroundThreadSupport_UserShouldBeAbleToFilterUnhandledExceptions_ReportShouldntBeAvailableInMainThreadLoop()
129+
{
130+
var client = BacktraceClient;
131+
string exceptionMessage = "foo";
132+
133+
BacktraceClient.SkipReport = (ReportFilterType type, Exception e, string message) =>
134+
{
135+
return true;
136+
};
137+
var mainThreadId = Thread.CurrentThread.ManagedThreadId;
138+
var thread = new Thread(() =>
139+
{
140+
Assert.IsTrue(Thread.CurrentThread.ManagedThreadId != mainThreadId);
141+
client.HandleUnityBackgroundException(exceptionMessage, string.Empty, UnityEngine.LogType.Exception);
142+
});
143+
thread.Start();
144+
thread.Join();
145+
146+
Assert.IsEmpty(BacktraceClient.BackgroundExceptions);
147+
}
148+
149+
[Test]
150+
public void TestBackgroundThreadSupport_UserShouldBeAbleToFilterReports_ReportShouldntBeAvailableInMainThreadLoop()
151+
{
152+
var client = BacktraceClient;
153+
string exceptionMessage = "foo";
154+
155+
BacktraceClient.SkipReport = (ReportFilterType type, Exception e, string message) =>
156+
{
157+
return true;
158+
};
159+
var thread = new Thread(() =>
160+
{
161+
client.Send(exceptionMessage);
162+
var exception = new InvalidOperationException(exceptionMessage);
163+
client.Send(exception);
164+
client.Send(new BacktraceReport(exception));
165+
});
166+
167+
thread.Start();
168+
thread.Join();
169+
170+
Assert.IsEmpty(BacktraceClient.BackgroundExceptions);
171+
}
172+
173+
174+
[Test]
175+
public void TestBackgroundThreadSupport_RateLimitSkipReports_ReportShouldntBeAvailableInMainThreadLoop()
176+
{
177+
var client = BacktraceClient;
178+
string exceptionMessage = "foo";
179+
180+
uint rateLimit = 5;
181+
var expectedNumberOfSkippedReports = 5;
182+
int actualNumberOfSkippedReports = 0;
183+
184+
client.SetClientReportLimit(rateLimit);
185+
client.OnClientReportLimitReached = (BacktraceReport report) =>
186+
{
187+
actualNumberOfSkippedReports++;
188+
};
189+
190+
var thread = new Thread(() =>
191+
{
192+
for (int i = 0; i < rateLimit + expectedNumberOfSkippedReports; i++)
193+
{
194+
client.Send(new InvalidOperationException(exceptionMessage));
195+
}
196+
197+
});
198+
thread.Start();
199+
thread.Join();
200+
Assert.IsNotEmpty(BacktraceClient.BackgroundExceptions);
201+
Assert.AreEqual(rateLimit, BacktraceClient.BackgroundExceptions.Count);
202+
Assert.AreEqual(expectedNumberOfSkippedReports, actualNumberOfSkippedReports);
203+
}
204+
}
205+
}

Tests/Runtime/BackgroundThreadSupportTests.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "io.backtrace.unity",
33
"displayName": "Backtrace",
4-
"version": "3.3.3",
4+
"version": "3.3.4",
55
"unity": "2017.1",
66
"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.",
77
"keywords": [

0 commit comments

Comments
 (0)