I am currently working on a solution whereby I would like to get a meaningful stack trace from a release WebGL build (currently using Unity 2020.1.4). The idea is that I can then post this on a server to be evaluated later on. I created a small demo project with the following code. I tried building 2 builds; build 1.0.5 - development build, build 1.0.6 - non-development build with debug symbols enabled. Attached are 2 screenshots for when I generate a null reference exception.
I don’t want to use FullWithStacktrace (I am using FullWithoutStacktrace) as this will be used for a live environment and such builds have poor performance. So I would like to use the stacktrace from WASM.
My questions are the following;
a…Why isn’t there a function name that includes ‘GenerateNullReferenceException’ (even though mangled it could be helpful). I noticed that when I generate a log error there is an entry with a function name that includes ‘GenerateLogError’ in it however this isn’t the case for null reference exceptions.
b…How can one make use of the debug symbols file? All entries in the 2nd screenshot are from (anonymous) methods. It gets generated and I can see it in build folder but it is never downloaded to the client (should it?). Does the symbols map file get loaded automatically?
Null reference exception stack in build 1.0.5 (development build)
Ok so today I made good improvements, however there are still some pending issues to be addressed.
I made a series of builds to identify if I can get the null reference exception stacktrace from WASM in release build. The build that got me closest to my goal was the one built with the following settings;
Which generates the following stack trace when I press the G (to execute the
GenerateGameObjectNullReferenceException method shown below).
Exception at:
Error
at jsStackTrace (blob:http://localhost:50247/856151ad-578d-4a1a-9d24-39e43f50d98f:2:22520)
at stackTrace (blob:http://localhost:50247/856151ad-578d-4a1a-9d24-39e43f50d98f:2:22691)
at blob:http://localhost:50247/856151ad-578d-4a1a-9d24-39e43f50d98f:2:9961
at <anonymous>:wasm-function[32174]:0x89430f
at <anonymous>:wasm-function[32028]:0x88fd33
at <anonymous>:wasm-function[9307]:0x432a07
at <anonymous>:wasm-function[10278]:0x47e42c
at <anonymous>:wasm-function[24191]:0x70a0d5
at <anonymous>:wasm-function[24189]:0x70a03a
at <anonymous>:wasm-function[31650]:0x881418
at <anonymous>:wasm-function[31646]:0x881144
at <anonymous>:wasm-function[14818]:0x5a196b
at <anonymous>:wasm-function[31778]:0x886504
at <anonymous>:wasm-function[32043]:0x88fe25
at <anonymous>:wasm-function[2951]:0x14b83a
at <anonymous>:wasm-function[2950]:0x14b7a3
at <anonymous>:wasm-function[7339]:0x316059
at <anonymous>:wasm-function[7327]:0x314ba3
at <anonymous>:wasm-function[9635]:0x448a31
at <anonymous>:wasm-function[9634]:0x44875a
at <anonymous>:wasm-function[8080]:0x367d4e
at <anonymous>:wasm-function[7431]:0x31f356
at <anonymous>:wasm-function[7431]:0x31f36d
at <anonymous>:wasm-function[7426]:0x31e583
at <anonymous>:wasm-function[7420]:0x31cdfa
at dynCall_v (<anonymous>:wasm-function[32306]:0x89919e)
at Object.dynCall_v (blob:http://localhost:50247/856151ad-578d-4a1a-9d24-39e43f50d98f:2:469584)
at browserIterationFunc (blob:http://localhost:50247/856151ad-578d-4a1a-9d24-39e43f50d98f:2:131380)
at Object.runIter (blob:http://localhost:50247/856151ad-578d-4a1a-9d24-39e43f50d98f:2:134453)
at Browser_mainLoop_runner (blob:http://localhost:50247/856151ad-578d-4a1a-9d24-39e43f50d98f:2:132915)
This stacktrace is good enough for me as it can be translated to the following using the symbols map;
However when I try to execute the method
GenerateStringNullReferenceException or
GeneratePlayerNullReferenceException no exception is raised and it proceeds as if they were properly initialized. At this point I expect that both will fail on line 58 and line 75 respectively. Why does it crash in GenerateGameObjectNullReferenceException but not the others?
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player {
private int health;
public int Health {
get {
return health;
}
}
public bool IsDead {
get {
return health == 0;
}
}
public void ApplyDamage (int damage) {
health = Mathf.Max (0, health - damage);
}
}
public class Demo : MonoBehaviour {
// Update is called once per frame
void Update () {
if (Input.GetKeyDown (KeyCode.S)) {
GenerateStringNullReferenceException ();
}
if (Input.GetKeyDown (KeyCode.H)) {
HandleStringNullReferenceException ();
}
if (Input.GetKeyDown (KeyCode.P)) {
GeneratePlayerNullReferenceException ();
}
if (Input.GetKeyDown (KeyCode.G)) {
GenerateGameObjectNullReferenceException ();
}
if (Input.GetKeyDown (KeyCode.E)) {
GenerateLogError ();
}
if (Input.GetKeyDown (KeyCode.T)) {
ThrowException ();
}
}
private void GenerateStringNullReferenceException () {
Debug.Log ("Generating null reference exception");
string s = null;
s.Trim ();
Debug.Log (s.Length);
}
private void HandleStringNullReferenceException () {
Debug.Log ("Handling null reference exception");
try {
string s = null;
s.Trim ();
Debug.Log (s.Length);
} catch (Exception ex) {
Debug.LogError (ex.Message);
}
}
private void GeneratePlayerNullReferenceException () {
Player p = null;
p.ApplyDamage (5);
Debug.Log ("Health - " + p.Health);
Debug.Log ("Is dead - " + p.IsDead);
}
private void GenerateGameObjectNullReferenceException () {
var go = GameObject.Find ("NonExistentObject");
Debug.Log (go.name);
}
private void GenerateLogError () {
Debug.LogError ("Computer says no!");
}
private void ThrowException () {
throw new System.Exception ("Kaboom!");
}
}
Another update regarding the code below. Why does the method GenerateStringNullReferenceExceptionOriginal does not generate a null reference exception but GenerateStringNullReferenceExceptionContains does?
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player {
private int health;
public int Health {
get {
return health;
}
}
public bool IsDead {
get {
return health == 0;
}
}
public void ApplyDamage (int damage) {
health = Mathf.Max (0, health - damage);
}
public void PrintName () {
Debug.Log ("Player One");
}
}
public class Demo : MonoBehaviour {
// Update is called once per frame
void Update () {
if (Input.GetKeyDown (KeyCode.Q)) {
GenerateStringNullReferenceExceptionOriginal ();
}
if (Input.GetKeyDown (KeyCode.W)) {
GenerateStringNullReferenceExceptionTrim ();
}
if (Input.GetKeyDown (KeyCode.E)) {
GenerateStringNullReferenceExceptionContains ();
}
if (Input.GetKeyDown (KeyCode.R)) {
HandleStringNullReferenceException ();
}
if (Input.GetKeyDown (KeyCode.T)) {
GeneratePlayerNullReferenceException ();
}
if (Input.GetKeyDown (KeyCode.Y)) {
GenerateGameObjectNullReferenceException ();
}
if (Input.GetKeyDown (KeyCode.U)) {
GenerateLogError ();
}
if (Input.GetKeyDown (KeyCode.I)) {
ThrowException ();
}
}
private void GenerateStringNullReferenceExceptionOriginal () {
Debug.Log ("Generating null reference exception (original)");
string s = null;
s.Trim ();
Debug.Log (s.Length);
}
private void GenerateStringNullReferenceExceptionContains () {
Debug.Log ("Generating null reference exception (contains)");
string s = null;
if (s == null) {
Debug.Log ("String is null!");
s.Contains ("Hello");
if (s != null) {
Debug.Log ("String is not null!");
}
Debug.Log (s.Length);
} else {
s.Trim ();
Debug.Log (s.Length);
}
}
private void GenerateStringNullReferenceExceptionTrim () {
Debug.Log ("Generating null reference exception (trim)");
string s = null;
if (s == null) {
Debug.Log ("String is null!");
s.Trim ();
if (s != null) {
Debug.Log ("String is not null!");
}
Debug.Log (s.Length);
} else {
s.Trim ();
Debug.Log (s.Length);
}
}
private void HandleStringNullReferenceException () {
Debug.Log ("Handling null reference exception");
try {
string s = null;
s.Trim ();
Debug.Log (s.Length);
} catch (Exception ex) {
Debug.LogError (ex.Message);
}
}
private void GeneratePlayerNullReferenceException () {
Player p = null;
if (p == null) {
Debug.Log ("Player is null!");
p.PrintName ();
if (p != null) {
Debug.Log ("Player is not null!");
}
Debug.Log (p.Health);
} else {
p.ApplyDamage (5);
Debug.Log ("Health - " + p.Health);
Debug.Log ("Is dead - " + p.IsDead);
}
}
private void GenerateGameObjectNullReferenceException () {
var go = GameObject.Find ("NonExistentObject");
Debug.Log (go.name);
}
private void GenerateLogError () {
Debug.LogError ("Computer says no!");
}
private void ThrowException () {
throw new System.Exception ("Kaboom!");
}
}
to a build in an Editor script. That will include the function symbol names in the .wasm file. Do notice however that including symbols does increase the size of the .wasm file by some amount.
You can try using the attached buildWebGL.cs build script. (check its contents for documentation)
Oh, forgot to mention - the option Enable Exceptions: None/Explicit/FullWithStacktrace does not relate to the presence of the function symbol names, but rather to the presence of try-catch clauses in the generated code.
I have recovered the required function names using the debugSymbols options (set to true) as explained in post #2 - https://discussions.unity.com/t/810790/2 . This generated a JSON file which includes the ID and associated function name. Using a simple lookup code I was able to convert wasm-function[1234] to the required function name.
The problem I have is as explained at the end of the same post, that certain functions trigger a null exception (for example GenerateGameObjectNullReferenceException) but not others such as GenerateStringNullReferenceException when they should. In fact the same code when executed in development build will generate the null reference (which is correct). I also posted another update with my latest findings as you can see above in post #3.
Could you indicate why this happens and how can we avoid this from happening?
In a nutshell these are the results I am getting with these two builds (using the code from post #3);
Build A
Enable Exceptions - FullWithoutStacktrace
Build Type - Release
Debug Symbols - Enabled
Stack Trace - Full (for all log types)
Press ‘Q’ - Generates exception but no debug symbols in stack trace
Press ‘W’ - Generates exception but no debug symbols in stack trace
Press ‘E’ - Generates exception but no debug symbols in stack trace
Press ‘R’ - Handles exception and outputs error log with debug symbols in stack trace
Press ‘T’ - Generates exception but no debug symbols in stack trace
Press ‘Y’ - Generates exception but no debug symbols in stack trace
Press ‘U’ - Outputs error log with debug symbols in stack trace
Press ‘I’ - Generates exception but no debug symbols in stack trace
Build B
Enable Exceptions - None
Build Type - Release
Debug Symbols - Enabled
Stack Trace - Full (for all log types)
Press ‘Q’ - No error
Press ‘W’ - No error
Press ‘E’ - Generates exception and has debug symbols in stack trace
Press ‘R’ - No error
Press ‘T’ - No error
Press ‘Y’ - Generates exception and has debug symbols in stack trace
Press ‘U’ - Outputs error log with debug symbols in stack trace
Press ‘I’ - Generates exception and has debug symbols in stack trace
As you can see these are totally different outputs. What I want is to have the same exceptions fired as in Build A with debug symbols in the stack trace. Is that possible?
Hmm, yeah, try doing a Release build with Full with stack trace, Debug Symbols - Enabled, Stack Trace - Full, but do the build via the buildWebGL.cs script I posted.
In the build dropdown that appears, try out the option “HTML5 Export/Wasm+Release+Profiling uncompressed…”. Does that give the stack trace information?
So I imported the script attached in the previous post and made two builds as follows;
Build A
Enable Exceptions - FullWithoutStackTrace
Ran the command “HTML5 Export/Wasm+Release+Profiling uncompressed…”
This generates a stack trace that is identical to the output as in my first post. As indicated again below there is no mention of the method that triggered the exception and therefore I would be unable to track the issue down.
Enable Exceptions - FullWithStackTrace
Ran the command “HTML5 Export/Wasm+Release+Profiling uncompressed…”
This generates a partial stack trace in the message (printed in red) as shown below. But as pointed in the documentation “FullWithStackTrace exception support will decrease performance and increase browser memory usage. Only use this for debugging purposes and make sure to test in a 64-bit browser”. As this is a tool meant to run in a release environment, this option is not possible.
As indicated in post #7, the best output I had so far in terms of logging is when I made a build with Enable Exceptions set to None. In this case when I press E, Y or I, a log as shown below is outputted (the one on the left is the actual log given from the WebGL player, the one on the right is how the stacktrace after using the lookup symbols map) and as you can see on line 8 it indicates the origin of the problematic code which is great. The problem is that the code doesn’t always crash when it should as when pressing Q, W, R or T.
So when calling s.Trim() on the null string variable ‘s’ or accessing the method of a non-initialised instance of the Player class, the WebGL player should crash, irrespective of Enable Exceptions value, right?
I am asking if that is the actual bug fix as I am not sure. If it is, it didn’t solve my issue as I tried 2020.1.7f1 already and same results as before.
Currently if null.Function() is called, the WebGL build will:
happily truck through the line to a random crash or no-crash if the page was built with Player->Publishing Settings->Enable Exceptions set to Explicit or None, and
throw an exception if the page was built with Enable Exceptions set to Full (with or without Stacktrace)
The first mode is good for generating smallest code size and performance for the web in the Release scenario where one has already debugged through all kinds of code issues like null references. The second mode is good for debugging, but comes with quite a large penalty on WebAssembly code size and startup load times, and to a smaller degree to runtime performance.
Recently we realized that we are missing a third kind of mode in between, where we would safely abort execution (by raising a WebAssembly trap) if null.Function() is met, but without the penalty on code size, load times or performance. That mode would be good for shipping final projects, while still retaining the ability to get meaningful error reports back to analytics (by e.g. JavaScript window.onerror() based error handlers)
This mode is not listed under the issue tracker since it was observed based on our internal communication, and not due to an error report that was submitted.
Further discussing with our codegen team today, it does look like the function did get inlined. That brings a tricky situation: I understood you would like to get these reports from Release builds. Because inlining is critical for small code size, disabling inlining to get the stack trace would cause quite a large code size penalty that might not be worth the tradeoff. Building in debug mode would limit the amount of inlining, but that would definitely run quite slow. Not sure what to suggest here…
Ok thanks for your detailed explanation, much appreciated.
Unfortunately this won’t allow us to track the origin of the issue and might cause unwanted and misleading side effects. Is there a different approach that we can take to get meaningful error logs from a release WebGL environment (except using the FullWithStacktrace option) with the currently available Unity versions?
In conclusion, what we are truly really after is the 3rd mode whereby an exception is fired for null.Function() without penalty on code size or performance. Do you have an indication when this feature will be available and in which version?
Inlining may be something that happens aggressively in small synthetic test cases like shown above, but in real world code, it may be less common, depending on the sizes of the functions and how many times they are called. Unfortunately I do not have a solution here. Do note that the same inlining will occur on all IL2CPP platforms, not just specifically on WebGL.
[/QUOTE]