With so much time spent on game development, the last thing any game developer wants to see is a low rating as a result of a buggy game. From our very first Crashlytics NDK release back in 2015, our commitment to game developers remains strong. And recently we released a number of NDK and Unity features that not only increase the stability of games, but also enable developers using Unreal, Cocos2d, Unity or any other native game engine to get to resolutions quickly and with more confidence. Read more about these improvements below.
To provide you with a more accurate view into the stability of your gaming apps, we’ve made significant changes to Crashlytics' native crash capture mechanism by adopting Google's open-source Crashpad library. As a result, you can now get access to several additional classes of errors on newer Android versions as well as get more reliable crash reporting on existing Android versions of your app. Additionally, with Crashpad you no longer have to deal with the inherent complexity of native crash capture and instead you can focus your time on growing your game.
The increased reliability of Crashpad lies in its pursuit of minimizing the amount of work done within the signal handler, which ultimately results in a more reliable crash capturing mechanism. One of Crashpad’s core design goals is to reduce the number of system calls inside the handler to just one. Upon receiving a signal, Crashpad captures the memory space of the crashed application by launching a brand new, healthy process, removing some of the problems of other crash capture approaches such as capturing SIGABRT on Android 10+ devices.
More accurate stack traces, especially within the application frames, lead to faster issue resolutions - there is no doubt about it! That is why we’ve switched our symbol file format to one that is more robust - the Breakpad symbol file. The additional debug information within the Breakpad symbol file, in many circumstances, results in a more accurate stack trace than what you’d see in the logcat.
The key to more accurate stack traces lies within the Call Frame Information that is stored within the debug section of the binaries. This information is what differentiates the Breakpad symbol file from our previous symbol file. Call frame information assists our backend stack unwinding process, minimizing the use of heuristics within application frames that may lead to incorrect stack traces. With this information, our backend more precisely determines how the frames within your application should be unwound and which symbols - inlined or not - correspond to each frame!
Take a look at the Google Games Developer Summit session that explains this in more detail. If you’re already using the Crashlytics NDK SDK, switching to the Breakpad symbol file is a breeze, just add symbolGenerator { breakpad() } to your build.gradle. Check out our docs for more info.
symbolGenerator { breakpad() }
The left image shows a stack that is unwound using our previous symbol format, and the right image shows the same stack unwound using the Breakpad symbol file.
The top image shows a frame symbolicated using our previous symbol format, and the bottom image shows the same frame symbolicated using the Breakpad symbol file.
We heard your feedback around symbol uploading for main applications and stand-alone libraries. And with the latest Crashlytics Gradle plugin, specifying the stripped library directory is no longer necessary so you can get quickly set up with minimal error. We’ve also allowed the unstripped path to point to disparate directories, allowing symbol upload for binaries that are compiled outside of the main application.
We’ve improved grouping for Unity crashes to help you more quickly debug and identify the exact cause of a crash. Our analysis backend now has a more robust set of Unity heuristics, resulting in much better issue fidelity and a more intuitive stack trace visual treatment. No longer will issues highlight a system frame when an application frame is the source of the problem.
Revised Unity Grouping in Crashlytics Console
We’ve enabled automatic capture of various hardware attributes and game-specific values to help solve unique issues related to the model of GPU and screen resolution for example, so you no longer have to capture these yourself and use up valuable key-value pairs.
Unity metadata in Crashlytics console
We hope these improvements help make identifying crashes much easier, and we will continue to improve the games experience by focusing on improving Unity IL2CPP support - support that includes capturing Unity Engine and native extension crashes. Let Crashlytics handle crashes for all of the components that your game depends on, because sometimes, the crash is not your game's fault. There is much more on the horizon, stay tuned!
In the meantime to get started, head to our NDK and Unity onboarding pages. We're excited to help you through your game development journey and can’t wait to hear what you think!
It's amazing to see you using Crashlytics on so many different Apple products – and even wanting to expand that usage! We have always focused on making Crashlytics the best crash reporter on iOS, from our user-friendly onboarding process, to our lightweight SDK. So we’ve made some updates to ensure you can run the Crashlytics SDK seamlessly on all Apple consumer hardware.
In December 2020, we released full support for Apple Silicon Macs running iOS or macOS apps. This means any of the following configurations running on Apple Silicon will work with Crashlytics:
To make it easier to see crashes on these platforms, we added support for filtering in the Firebase console, using the Device filter. We're already working on improving this experience, so stay tuned!
With the introduction of iOS 14, the number of system libraries and libraries used by apps has increased. These libraries can increase startup time as Crashlytics lists the libraries loaded into the app for symbolicating crash reports. In version 7.5.0 of the Crashlytics SDK, we shipped a change to remove this bottleneck, significantly improving the customer experience. For apps with this version of the SDK, we’ve observed a median startup time of about 14 milliseconds, a 75% reduction!
In addition, we’ve improved the speed of our client-side build tools, especially for apps with large binary and dSYM files. This tool processes dSYMs on your build machine before upload to ensure it matches your build environment and Xcode version as closely as possible. We observed the symbol conversion time for a 600 MB dSYM improved from about 12 minutes to 45 seconds. This will help speed up CI builds for your apps and make it possible to rapidly test crashes locally.
We’ve also invested in making sure we fully support App Clips. Crashlytics continues to be a lightweight crash reporter in terms of binary size, so you shouldn’t have to commit much of your App Clip’s 10 MB limit to the Crashlytics installation.
Recently, the community pitched in to get the Crashlytics SDK running on watchOS, adding support for common crashes and non-fatals. Since watchOS is continuing to build out better support for responding to crashes, we’re always happy to accept contributions to improve this!
Open sourcing the Crashlytics SDK as part of the move to Firebase has brought huge benefits. The community has been integral in determining the popularity of various features, and we want to thank folks who have reached out on GitHub, Firebase Support, and other channels to outline their use cases for Crashlytics. We want to especially thank members of the community who have made code changes to improve the SDK directly.
We are continually investing in support for Apple apps, and there’s more coming to help developers diagnose stability issues with their apps. We’re proud of the investments we’ve made here and are excited to continue supporting the Apple app ecosystem.
We graduated the Firebase Crashlytics SDK to General Availability (GA) back in June, and today we are encouraging you to migrate your apps from the Fabric SDK to the Firebase Crashlytics SDK. On November 15th, we’ll be sunsetting the legacy Fabric SDK, meaning any apps that are still using the Fabric SDK will no longer report crashes.
The Firebase Crashlytics SDK can now upload crashes after an app has closed, allowing you to receive crash data in more real time on Android! We've been tracking how the Firebase Crashlytics SDK performs, and we estimate the new SDK captures ~30% more Android crashes, and twice as many on the actual day of the crash.
Additionally, we streamlined our Crashlytics Gradle Plugin, with a new API in the build.gradle for managing and uploading mapping files and native symbol files. The total size of the plugin has also been reduced from 20+ MB to only 100 KB. In order to reduce build times the new plugin supports task configuration avoidance, and has improved up to date checking for gradle tasks. The new plugin will also support all modern Android Gradle Plugin features, such as cleaner support for disabling mapping file uploads of different flavors or buildtypes, and troubleshooting for 4.1+ versions of Android Studio.
We introduced platform support for Catalyst, App Clips for iOS 14, and community support for watchOS!
We also improved our upload-symbols conversion speed. Customers with large dSYMs should see a significant decrease in the time it takes to upload Crashlytics symbols, going from an average of 12 minutes for a 600 MB dSYM down to ~45 seconds!
We also enabled tvOS and macOS apps to share the same Crashlytics installation as an iOS app. You can now configure apps with the same bundle ID to be part of the same project. Meaning crash reports from every OS will be shown in the same dashboard.
In addition to technical improvements, our new SDK has APIs (e.g., package names, initialization code) designed to be consistent with other Firebase products, while also getting rid of references to the now deprecated fabric.io namespace. For example, we changed the Crashlytics initialization statement to use new methods that are more consistent with how other Firebase services are initialized:Fabric.with([Crashlytics.self])
Fabric.with([Crashlytics.self])
is no longer needed. It is now sufficient to simply call:
FirebaseApp.configure()
To continue getting crash reports, please follow our upgrade guide here. As we mentioned during our SDK GA announcement, November 15th will be the last day to upgrade before the legacy SDK is shutdown.
Lastly, we would like to personally thank all of our users who have been part of this journey from Fabric to Firebase with us. We hope to keep providing an amazing crash reporting experience for you. As always, please let us know your thoughts, and tell us how we can improve Crashlytics.
Happy coding!
With our announcement of the Firebase Crashlytics SDK Beta in February, we provided a path to help you remove all Fabric dependencies in your code, and complete your migration to Firebase. Since then, tens of thousands of apps have adopted the Beta SDK, and several hundreds of millions of end-users are using apps that include the SDK. Thank you to the adopters of our Beta SDKs; with your help, we have been able to polish and debug our SDK so we could ship it in its best possible launch state.
We are excited to announce that the Firebase Crashlytics SDK is now publicly available! Whether your app was migrated from Fabric or created in Firebase, we encourage you to upgrade to the official version of the Firebase Crashlytics SDK.
Now that the Firebase Crashlytics SDK is publicly available, we are deprecating the legacy Fabric SDK. The Fabric SDK will continue reporting your app's crashes until November 15, 2020. On this date, the Fabric SDK, and beta versions of the Firebase Crashlytics SDK, will no longer send crashes to your Firebase dashboard. To continue getting crash reports in the Firebase console, make sure your app has the following versions of the Firebase Crashlytics SDK, or newer: 17.0.0+ for Android, 4.0.0+ for iOS, and 6.15.0+ for Unity. Developers new to Crashlytics should begin with our getting started docs.
As always, we’d love to hear your feedback! Let us know what you think through our official support page!
how to deal with asynchronous logic in Unity
The Firebase Unity SDK makes judicious use of asynchronous logic for many of its calls. Unity itself isn’t super resilient to threaded logic, with most of the classes and functions in the UnityEngine namespace just flat out throwing exceptions if invoked off of the main Unity thread. My goal with this post is to provide you the tools you need to not only safely use Firebase’s asynchronous function calls, but to do so in a way that best suits your own programming style and preferences. Ideally even giving you more confidence to thread other parts of your game to provide your players with the smooth and responsive gameplay they expect from a modern video game.
UnityEngine
Let’s get started with a very innocent looking demo script:
using Firebase; using Firebase.Auth; using UnityEngine; using UnityEngine.Assertions; public class FirebaseContinueWith : MonoBehaviour { // Start is called before the first frame update void Start() { Debug.Log("Checking Dependencies"); FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(fixTask => { Assert.IsNull(fixTask.Exception); Debug.Log("Authenticating"); var auth = FirebaseAuth.DefaultInstance; auth.SignInAnonymouslyAsync().ContinueWith(authTask => { Assert.IsNull(authTask.Exception); Debug.Log("Signed in!"); var successes = PlayerPrefs.GetInt("Successes", 0); PlayerPrefs.SetInt("Successes", ++successes); Debug.Log($"Successes: {successes}"); auth.SignOut(); Debug.Log("Signed Out"); }); }); } }
The first thing I do is ensure that Firebase’s dependencies are available on the player’s device with CheckAndFixDependenciesAsync. Note that I’m not really handling any failure cases in this example. This shouldn’t be an issue for this post, but you’ll want to do more than assert in your own games.
CheckAndFixDependenciesAsync
Next I use ContinueWith to create a continuation and I start signing in anonymously with SignInAnonymouslyAsync.
ContinueWith
SignInAnonymouslyAsync
When sign-in completes, I figure out how many times this script has run successfully before by reading PlayerPrefs. Then I increment this value, and write it back out before logging the new number of successes.
This is all super straightforward. I run it and… I just see the log “Signed In!” then nothing. What happened?
Firebase does a lot of work that’s dependent on I/O. This can either be out to disk, or even out to the network. Since you don’t want your game to lock up for potentially many seconds for network latency, Firebase uses Tasks to perform much of this I/O work in the background.
Whenever you continue from this work, you have to be careful to come back into your game in a graceful manner. I’ve done none of that here, and have just charged right into a shared resource managed by the UnityEngine in the form of a call to PlayerPrefs. This most likely raised an exception, but it even got lost in the background thread! What can you do to fix it?
PlayerPrefs
C# has the concept of a TaskScheduler. When you say ContinueWith, rather than just letting it continue on whatever thread the task completed on, you can use a TaskScheduler to force it onto a specific thread. So, I can modify the example to cache the TaskScheduler on which Start() was called. Then I pass that into the ContinueWith statement to be able to safely change the state of objects in my game:
TaskScheduler
Start()
Debug.Log("Checking Dependencies"); var taskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(fixTask => { Assert.IsNull(fixTask.Exception); Debug.Log("Authenticating"); var auth = FirebaseAuth.DefaultInstance; auth.SignInAnonymouslyAsync().ContinueWith(authTask => { Assert.IsNull(authTask.Exception); Debug.Log("Signed in!"); var successes = PlayerPrefs.GetInt("Successes", 0); PlayerPrefs.SetInt("Successes", ++successes); Debug.Log($"Successes: {successes}"); auth.SignOut(); Debug.Log("Signed Out"); }, taskScheduler); });
Since Start executes on the Unity main thread, I grab the scheduler with TaskScheduler.FromCurrentSynchronizationContext(). This way I can get back to the main thread later by passing the scheduler into my second ContinueWith statement. Now whatever work I do in that ContinueWith block will be done in sequence with the game rather than in parallel with it, preventing any threading issues.
TaskScheduler.FromCurrentSynchronizationContext()
When I run the script, I can see that I finally have one success (and that this script hasn’t succeeded before).
This pattern is really common so Firebase provides an extension method named ContinueWithOnMainThread that does all of that hard work for you. If you’re using a newer version of the Firebase Unity SDK, you can write the above as simply:
ContinueWithOnMainThread
Debug.Log("Checking Dependencies"); FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(fixTask => { Assert.IsNull(fixTask.Exception); Debug.Log("Authenticating"); var auth = FirebaseAuth.DefaultInstance; auth.SignInAnonymouslyAsync().ContinueWithOnMainThread(authTask => { Assert.IsNull(authTask.Exception); Debug.Log("Signed in!"); var successes = PlayerPrefs.GetInt("Successes", 0); PlayerPrefs.SetInt("Successes", ++successes); Debug.Log($"Successes: {successes}"); auth.SignOut(); Debug.Log("Signed Out"); }); });
A word of caution as well. In C#, you can safely assume that anything in your ContinueWith block hasn’t been garbage collected. The same doesn’t hold true with Unity’s design. If you were to access any fields of this MonoBehaviour or its encompassing GameObject after OnDestroy is invoked, you would want to check that this hasn’t become null. Due to the way Unity implemented this as well, you cannot do so with the ?? operator.
MonoBehaviour
GameObject
OnDestroy
this
??
Hopefully I’ve shed a little light on what’s happening in these tasks and continuations in Unity. You may also be a little frustrated now. What should be a simple block of code where we fix dependencies, sign on, then do work has become this ugly mess of nested statements that just becomes harder to read as we chain more steps into the logic. If only there were a better way!
Although the goal of tasks is to perform operations in parallel, so much logic in programming is sequential. Since continuations get hard to read, C# provides a mechanism in async/await syntax to represent this sequential logic. To use this mechanism, I’ll rewrite the Start method like this:
Start
async void Start() { Debug.Log("Checking Dependencies"); await FirebaseApp.CheckAndFixDependenciesAsync(); Debug.Log("Authenticating"); var auth = FirebaseAuth.DefaultInstance; await auth.SignInAnonymouslyAsync(); Debug.Log("Signed in!"); var successes = PlayerPrefs.GetInt("Successes", 0); PlayerPrefs.SetInt("Successes", ++successes); Debug.Log($"Successes: {successes}"); auth.SignOut(); Debug.Log("Signed Out"); }
The first thing you’ll notice is that I denote Start as async. This tells the C# compiler “this function will perform work in the background. Do something else whilst it finishes up.”
Then, I replace ContinueWith with the await keyword. If I were doing anything with the Task’s result, I could store the result in a variable.
await
Task
This reads much better, but why doesn’t my code break like the very first sample? It turns out that async functions will always return to the thread they’re awaited on. This way you don’t have to be as careful about thread safety in functions where you do this. In fact, by default, async functions that are awaited will typically execute on the thread that called them unless the developer explicitly did something else.
async
There is one downside compared to the ContinueWith sample though: the code following CheckAndFixDependenciesAsync will execute on the Unity main thread rather than potentially running on a background thread. In practice, this won’t be much of an issue. It could be a behaviour of note if you’re doing some significant amount of work between calls to await. Be aware as well that this code is very similar to the continuation example above. Just like how Unity may clean up your underlying MonoBehaviour before ContinueWith executes, Unity may clean it up when the call to await completes. If you access any member fields after a call to await, you should check to ensure this is not yet null.
Unity has the concept of coroutines, which used to be the preferred method of performing asynchronous work across multiple frames. The interesting bit about Coroutines is that they’re not really asynchronous, behind the scenes they simply generate IEnumerators which are evaluated on the main thread.
Unity has some special yield instructions such as WaitForEndOfFrame and WaitForSeconds, allowing you to jump around to different moments in your game’s time. I choose to implement a new CustomYieldInstruction to wait for a task to complete. I’ve even seen some developers convert something like this into an extension method on the Task class itself!
using System.Threading.Tasks; using UnityEngine; public class YieldTask : CustomYieldInstruction { public YieldTask(Task task) { Task = task; } public override bool keepWaiting => !Task.IsCompleted; public Task Task { get; } }
I can now use yield return on a Task, such as the Tasks typically used for Firebase, to make my asynchronous logic read sequentially. Inside it, I wait for a task to complete with a standard continuation. If I were actually doing something with the result of a Task, I’d have to build this class out a little more. For the time being, this will work to illustrate my basic point.
yield return
I can then reimplement my async/await logic using coroutines like this:
using System.Collections; using System.Collections.Generic; using Firebase; using Firebase.Auth; using UnityEngine; public class FirebaseCoroutine : MonoBehaviour { void Start() { StartCoroutine(DoWork()); } private IEnumerator DoWork() { Debug.Log("Checking Dependencies"); yield return new YieldTask(FirebaseApp.CheckAndFixDependenciesAsync()); Debug.Log("Authenticating"); var auth = FirebaseAuth.DefaultInstance; yield return new YieldTask(auth.SignInAnonymouslyAsync()); Debug.Log("Signed in!"); var successes = PlayerPrefs.GetInt("Successes", 0); PlayerPrefs.SetInt("Successes", ++successes); Debug.Log($"Successes: {successes}"); auth.SignOut(); Debug.Log("Signed Out"); } }
My Start function now just immediately calls a function called DoWork, which is my coroutine.
DoWork
Since I’m not doing anything with the return value of the async function calls, I just allocate temporary YieldTask objects and return them in the DoWork coroutine.
YieldTask
Now there are some important pros and cons to consider if you use this type of logic. This will have a performance hit as not only does the work between each yield return call execute on the main thread, but the property keepWaiting is queried every frame. On the other hand coroutines only exist for as long as a MonoBehaviour hasn’t been destroyed. This means that those caveats I mentioned above with having to check for null after an await or inside a ContinueWith don’t apply to coroutines!
keepWaiting
Sometimes the performance characteristics of coroutines don’t match up exactly to what you want. Remembering that a CustomYieldInstruction is queried every frame, you may end up in a state where Unity is performing many checks against against the keepWaiting property. In this case, it may be beneficial to queue these actions on Unity thread manually by adding work to a queue when it’s ready to be processed. Note that this is effectively how ContinueWithOnMainThread works and you should use that method when possible.
With that in mind, let’s look at an example of how I’ve implemented an action queue:
using System; using System.Collections.Generic; using System.Linq; using Firebase; using Firebase.Auth; using UnityEngine; using UnityEngine.Assertions; public class FirebaseQueue : MonoBehaviour { private Queue<Action> _actionQueue = new Queue<Action>(); void Start() { Debug.Log("Checking Dependencies"); FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(fixTask => { Assert.IsNull(fixTask.Exception); Debug.Log("Authenticating"); var auth = FirebaseAuth.DefaultInstance; auth.SignInAnonymouslyAsync().ContinueWith(authTask => { EnqueueAction(() => { Assert.IsNull(authTask.Exception); Debug.Log("Signed in!"); var successes = PlayerPrefs.GetInt("Successes", 0); PlayerPrefs.SetInt("Successes", ++successes); Debug.Log($"Successes: {successes}"); auth.SignOut(); Debug.Log("Signed Out"); }); }); }); } public void EnqueueAction(Action action) { lock (_actionQueue) { _actionQueue.Enqueue(action); } } void Update() { while (_actionQueue.Any()) { Action action; lock (_actionQueue) { action = _actionQueue.Dequeue(); } action(); } } }
This starts much like all of my continuation based examples. Unlike those, I call EnqueueAction to perform work on the main thread. I would highly recommend breaking this into two different MonBehaviours so you don’t forget to call EnqueueAction, but I’m compressing this for illustrative reasons.
EnqueueAction
MonBehaviour
The EnqueueAction function locks the Queue and adds some nugget of logic in the form of a C# Action into a list of logic to be executed later. If you get really clever, you may be able to just replace all of this with a lockless thread safe queue.
Finally, in Update, I execute every enqueued Action. It is very important to NOT execute the action with the _actionQueue locked. If the Action itself enqueues another Action, you’ll end up in a deadlock.
Update
Action
_actionQueue
Similar to the coroutine, this does involve checking whether or not the queue is empty every frame. Using what you’ve learned above about task schedulers and coroutines, I’m confident that you could reduce this burden with little effort if this becomes an issue.
Finally, there is a hip concept running around many programming circles known as reactive programming. Game developers I talk to tend to either love this or hate it, and there is enough public discourse that I won’t spend this post trying to turn you for or against this paradigm.
Reactive programming tends to favor logic that can come in streams -- that is logic that you would typically register for an event or query something every frame for -- and where you’ll perform functional operations on the streams as they flow through your game. For the purpose of staying consistent with the rest of this post, I’ll use it with the current example with the note that I’m not giving reactive programming its chance to shine.
So, with all that said, first I import UniRx from the Unity Asset Store. Then I have to make sure that there’s a MainThreadDispatcher in my scene:
MainThreadDispatcher
Now I can write my logic in UniRx form:
using Firebase; using Firebase.Auth; using UniRx; using UnityEngine; public class FirebaseRx : MonoBehaviour { // Start is called before the first frame update void Start() { Debug.Log("Checking Dependencies"); FirebaseApp.CheckAndFixDependenciesAsync().ToObservable().Subscribe(status => { Debug.Log("Authenticating"); var auth = FirebaseAuth.DefaultInstance; auth.SignInAnonymouslyAsync().ToObservable().ObserveOnMainThread().Subscribe(user => { Debug.Log("Signed in!"); var successes = PlayerPrefs.GetInt("Successes", 0); PlayerPrefs.SetInt("Successes", ++successes); Debug.Log($"Successes: {successes}"); auth.SignOut(); Debug.Log("Signed Out"); }); }); } }
What’s interesting about UniRx is that I can compose behaviour to form complex interactions. A brief example of this is the call ObserveOnMainThread, which guarantees that the following Subscribe executes on the main thread.
ObserveOnMainThread
Subscribe
For an example like this one, I would not pull in the complexities of UniRx but it’s useful to put it on your radar. If you were instead trying to build game logic around realtime database updates or periodically invoking cloud functions based on streams of events in game, you could do worse than combining UniRx and Zenject to quickly build a robust system around asynchronous logic.
I hope that I’ve not only given you some tools to help understand Firebase’s asynchronous API, but have empowered you to deal with them in a way that best suits your own game and coding style. I would strongly encourage you to create a small project using each of the techniques I’ve outlined here to really get a feel for the shape of each solution, and encourage you to think about ways you might improve your game’s performance using threads elsewhere. I’ve personally gotten some great mileage out of background tasks when dealing with peer to peer communication in games as well as processing and annotating screenshots without halting gameplay. I’ve even found that sometimes things like enemy AI doesn’t actually need to finish processing every frame, and it can sometimes be perfectly fine to let it run for a bit in the background over the course of a few frames.