Continuing off with our Unity WEBGL Optimization Techniques and approaches, here is Part 4 in the series. Again, we hope you found the previous article helpful and are looking for more ways to boost the performance of your project. With this article we will be focusing on the main tool for optimizing your project, The “Unity Profiler”.
What is the “Unity Profiler”?
The best definition of what the profiler is comes from the Unity Docs themselves: “The Unity Profiler Window helps you to optimize your game. It reports for you how much time is spent in the various areas of your game. For example, it can report the percentage of time spent rendering, animating or in your game logic.”
The profiler will show you the areas that you should focus your attention on.
Using the Unity Profiler
When and how to begin thinking about how to go about evaluating the performance of your project it can seem like a daunting task. It is actually quite common to fall into the trap of believing the the complexity of the project’s scene effects performance. For instance, it’s often thought that the total amount of objects you have within a scene will impact performance. This is a generalized statement and really lacks meaningful information.
Performance is actually impacted by the actual processing of the objects in the scene. When you follow this line of thought, it becomes easy to understand that when your processing a lot of objects verses a small number objects, the scene with the most objects will always take longer.
Another way to consider this is having 1 scene with 100 objects with no effects verses another scene with 50 objects that have extra effects, the latter can cost more in the performance department then the first one due to the way the objects are being processed.
One of the first things to do to start narrowing this down and start making an impact for performance recovery is using a more fine-tuned approach, which is to consider your target audience. In doing so, you will be able to start building the project with an idea of the types of devices that the end users of your product will most likely be using. Once your armed with this basic information you can then begin to nail down performance issues by using the Unity Profiler.
When your developing on mobile verses a desktop the hardware is uniquely different and the approaches while they can still be used, optimizing things on a desktop that you normally do for mobile can make little difference as the hardware is more powerful and therefore the impact of processing is lower.
Accessing the Unity Profiler
To access The Unity Profiler:
- To Open the Profiler window from Unity Editor Toolbar: Select -> Window -> Profiler.
- Then press the Record button. (It must be enabled to capture game data)
- Then just press the Play button and then pause or disable the Record button
- You will now have a bunch of collected data to analyze and look for issues.
Note: For an interactive lesson on using the profile watch: Introduction to the Profiler.
General Tips for Using the Profiler
- Always best to focus on the specific areas of your project that have the highest amount of time to process, this is your main area of opportunity.
- Remember to save the profiler data each time to allow a comparison between the previous results verses new changes and improvements.
- Consider that changes that are made to improve performance may result in indirectly producing a negative effect..
Note: It is recommended that you do a remote profile for your project on the actual target platform rather then using the Unity Editor. This is done by remote profiling. It is not uncommon to experience unexpected consequences of your optimization approach. For obvious reasons, you would not want profiling data from a desktop for a project that will be used on mobile devices.
Where to Start with the Profiler
The “Overview” section of the Profiler will show information about your project’s script performance. It is important to know that the information is per frame. What this means is that when you click anywhere on the graph you are selecting a specific frame. While it is possible to expand some method calls, its is only available to 1 level in depth. When using the profiler you will see categories on the left side that you can investigate the different areas of your project.
The “Big 3” categories that developers should focus on at the start are: CPU Usage, Rendering, & Memory.
- The “CPU Usage” category shows the total time needed to compute and reader a frame. Times are categorized to Rendering, Scripts, Physics, etc.
- The “Rendering” category shows information about Batches, SetPass Calls (Draw Calls), Triangles, & Vertices count.
- The “Memory” category shows the project’s memory usage statistics.
Note: You can enable Deep Profiling by clicking the button at the bottom to provide are more deeper look at your scripts however the trade off being it will require a lot more memory and will make your overall project run a lot slower, and it is quite possible that Unity will stall and run out of memory.
Profiling without Deep Profiling
As you become more familiar with the Profiler you will quickly learn that it does not profile all the method calls of your scripts. The default is set at a depth of 1 level.
When you want to profile individual code on scripts without going through the whole deep profiling aspect you can simply add some code. To profile a function or block of code add in the namespace:
using UnityEngine.Profiling;and then add this to the top: Profiler.BeginSample then add Profiler.EndSample at the bottom.
Then you use this script results and the Profiler to record the execution time and have it all displayed in the Profiler window. All this without the need to use Deep Profiling. The main advantage of this approach is that it allows you to look at specific selections of code without going though building a development build which is uncompressed and can take a long time depending on the actual size of the project.
You can add as many Profiler.Samples as you need/want as they come with zero overhead when they are being used in a non-development build.
Note: The Unity profiler will disable some asynchronous rendering resulting in about half of the normal allowed time for CPU and GPU work to maintain framerate. This will appear in the profiler as Device.Present taking a long time. Additionally, not all CPU work is shown in the profile such as WorldAnchor update calculations.
Remote profiling is useful as you will be able to see how your project runs on the actual target hardware. That alone is a huge advantage as you can experience the results first hand and tackle the problems quickly. Often you can find many posts about the project running fine in the UnityEditor but having dramatically different results on the target platform device or different device. A great way to avoid your users having problems like this by using the Remote Profiling feature.
This can be enabled by first, building a Development Build and by selecting Autoconnect Profiler in the build window. This will automatically start the remote profiling when the project is launched. More on remote profiling here: Unity Docs: Remote Profiling.
Spikes are literally spike looking objects in the profiler window. What this means is that there is something happing in this frame that is causing excessive processing and requiring a long time to handle. These should definitely be looked into.
When looking into the issue remember:
- FPS: “Frames Per Second” (FPS) is is determined by both the CPU and GPU usage.
- CPU : Physics, Code, Skinning (when not done in GPU), Particles, Raycasting, (Flares)
- GPU : Fillrate, Shaders, Drawcalls, Image Effects.
The following are some of the major things that are known to cause spikes.
To better understand Garbage Collection, consider that all the memory allocated by project at the runtime must be freed at some point, and this is also done at the runtime. This process is called “Garbage Collection” and it can and sometimes will freeze your project until it has finished.
Under most circumstance this usually only takes a fraction of a second, but this is more than enough to make it feel like your project is lagging or bottlenecking. Unfortunately, the only way to deal with this issue directly is to prevent it from happening at all. This is when ObjectPooling comes into play to prevent extra garbage.
Summary of Tips to Avoid Creating Excess Garbage Collection
As quoted from Microsoft Doc’s:
- Avoid the use of LINQ since it causes heavy allocations.
- Avoid the use of lambdas, as they cause excessive allocations.
- Beware of string boxing! A common case for that is passing structs to a method that takes an interface as a parameter. Instead, make the method take the concrete type (by ref) so that it can be passed without allocation.
- Always use structs over classes whenever you can.
- Default implementations for value equality and GetHashcode use reflection. This is not only slow but also performs a lot of allocations.
- Avoid foreach loops on everything except raw arrays and List. Each call potentially allocates an Enumerator. Prefer regular for loops whenever possible. (More Info)
- Optimize for cache coherency. Cache misses are orders of magnitude more expensive than most CPU instructions, so avoiding random jumping through memory can have a huge impact on performance. Flat arrays of compact structs that are processed in order is preferred..
- Avoid interfaces and virtual methods such as inner loops. Interfaces in particular are much slower than a direct call. Virtual functions are not as bad, but still significantly slower than a direct call.
- Looping over a native array with direct indexing is by far the fastest way to process a list of items. List is significantly slower, even worse if you use foreach, and even worse if you cast to IList first.
- Disable idle animations. Avoid design patterns where an animator sits in a loop setting a value to the same thing. There is considerable overhead for this technique, with no effect on the application. Instead, terminate the animation and restart when appropriate.
- Avoid deep object hierarchies for moving objects. When moving a transform, all of the parent and child transforms also get recomputed. If content moves in the scene in each frame, this cost will add up.
- Avoid the foreach construct (except for arrays and List). This will sometimes allocate an IEnumerable, and just generally introduce iteration overhead. It’s usually much faster to explicitly iterate over a concrete collection type.
- Consider caching often-used components. For example, if you often need to access the Rigid Body of an object, just grab it once and reference it with a private variable rather than looking it up each time.
- Avoid any synchronous loading code or other long running operations. You should make sure that any long running operation is asynchronous.
- Do NOT use FixedUpdate unless absolutely necessary, as FixedUpdate can be called multiple times per frame. Use either Update or your own update manager instead.
- When you have a lot of objects in a scene and/or scripts that do heavy processing, avoid using an Update function on every object. In this case it is recommended to have a few higher level “manager” objects with Update functions that calls into any other objects that need attention. The specifics of using this approach is highly application-dependent, but you can often skip large numbers of objects at once using this approach.
Note: Garbage collections become significantly costlier the less free space in memory you have available.
More detailed information on GarbageCollection.
Instantiating objects during the game is one of the most expensive operations. When you’re instantiating an object, you are instantiating all its children including all the components. All script components requires to be properly initialized. This is easy way to generate quite a large spike.
To prevent this from happening, learn about object pooling. Simply talking you need to instantiate all possible needed objects at the beginning and hide them until needed. There are some pool scripts on the asset store that will help you handling that issue.
Scripts and expensive algorithms
Sometimes spikes will be generated by your own scripts. Maybe you’re doing too expensive operation that could be optimized or should be done in separate thread.
This kind of spikes are usually the easiest to fix, but most probably you will need to enable Deep Profile mode to make the Profiler to generate more information about your scripts performance.
Many algorithms may be optimized. Those that cannot may be executed in coroutines or separate threads.
Be aware that if you will decide to move your code to a separate thread, you shouldn’t call Unity API from within that thread. Instead you should use components like Dispatcher from UnityToolbag that will dispatch Unity API calls to the main thread.
Background processes and operating system
From time to time you may experience spikes that are no fault of your project. These spikes are displayed within the profiler with a large amount of time assigned to Other category.
This can especially be seen on operating systems that have too many apps running in the background (desktop operating systems or Android). This is not something that you can directly and effectively handle. The only real solution is to kill as many background applications that running as is possible, keeping your Profiler window open and than observing if the spikes will go away. You can also try profiling the project on a different device.
This completes are 4 Part Series on Unity Optimization Techniques. We hope you were able to have some valuable takeaways and found something beneficial within our articles.