GithubHelp home page GithubHelp logo

zombieobjectdetector's Introduction

Zombie Object Detector

Scripts to detect and diagnose zombie objects.

Important Notice

Version 1.1 of the com.unity.memoryprofiler package now includes support for detecting the same types of objects as this tool. See Managed Shell Objects in the package documentation. I expect that this will allow me to deprecate this tool at some point in the future. - @peter-pimley-unity

Quick Start

Package import

In the Unity Package Manager window, select Add package from git URL and add the URL as shown in the green Code button in GitHub. For example https://github.com/Unity-Technologies/ZombieObjectDetector.git.

Scene Configuration

In a scene where you would like to run zombie object detection, place a GameObject with two components; a ZombieObjectDetector and a ZombieObjectDetector_Report_TTY. The two components must be on the same GameObject.

A screenshot of the Unity Inspector window, showing a ZombieObjectDetector and a ZombieObjectDetector_Report_TTY

Detection

To run detection in play mode in the editor, simply click Log Zombies in the Inspector window.

To run detection in a standalone build, call the method RunZombieObjectDetection on the ZombieObjectDetector component. The output will appear in the TTY (wherever Debug.Log goes on your target platform).

Memory

The detection process uses a great deal of managed memory. If your target platform has any sort of developer option to enable extra memory for development, then you should enable it.

Avoiding tests that cause reproduceable crashes

On some platforms (particularly consoles), detection has sometimes crashed reproducably when it tests objects of a particular type. To avoid this, add whatever types are causing problems to the inspector field Do not recurse into these Types during search. The values are all C# regular expressions. See here for reference. After adding a regular expression to the list you can type any string into the Test field, and it will tell you whether that string matches any of the regular expressions above it.

To discover exactly which type(s) are causing the crash in the first place, check the Report Test checkbox on the ZombieObjectDetector_Report_TTY. This will print a Debug.Log line before every single object that is tested. This is incredibly verbose and slow, but it will show you exactly which object (and type) caused the crash.

Background

Many of the user-facing C# classes in Unity act as lightweight handles to data that is actually held internally in native code. The most obvious example is UnityEngine.GameObject. Because C# GameObject instances are C# objects, they are subject to the usual garbage collection rules of the C# language; they will stay alive until they are no longer referenced by anything else, and then some time after that they will be collected by the garbage collector.

However the actual data for these objects is held in natively-allocated memory by the native player executable, and the lifespan of this data and memory is entirely deterministic. The memory holding the data for a GameObject will be deleted when the GameObject is Destroy()-ed manually or when the scene is exited.

This means that there is an opportunity for a C# object to outlive its native counterpart. The C# instance can be "kept alive" by virtue of being referenced by another C# object, even after the native-side object has been destroyed.

For example a developer might keep a List<GameObject> of GameObjects that are interesting to them in some way, such as the enemies in a shoot-em-up. Suppose the developer has a line of code passes the enemy GameObject in to GameObject.Destroy when its health reaches zero. At that point, if the developer fails also to remove it from their List<GameObject>, then the C# instance will remain referenced and alive even after its native counterpart has been destroyed.

The class GameObject has a unique special trick which can make this situation even more difficult to diagnose. A C# GameObject can detect if its native counterpart is no longer alive, and when it is in this special case it will return true if it is compared with null.

Therefore in the code:

GameObject obj = ...
if (obj != null) {

... the GameObject obj will know if its native counterpart has been destroyed, and will not enter the if statement in that case.

There is a good discussion of this situation in this blog post from 2014. As of 2022 nothing has changed in this regard. https://blogs.unity3d.com/2014/05/16/custom-operator-should-we-keep-it/. Specifically the paragraph labelled "purpose two":

When you get a c# object of type "GameObject", it contains almost nothing. this is because Unity is a C/C++ engine. All the actual information about this GameObject (its name, the list of components it has, its HideFlags, etc) lives in the c++ side. The only thing that the c# object has is a pointer to the native object. We call these c# objects "wrapper objects". The lifetime of these c++ objects like GameObject and everything else that derives from UnityEngine.Object is explicitly managed. These objects get destroyed when you load a new scene. Or when you call Object.Destroy(myObject); on them. Lifetime of c# objects gets managed the c# way, with a garbage collector. This means that it's possible to have a c# wrapper object that still exists, that wraps a c++ object that has already been destroyed. If you compare this object to null, our custom == operator will return "true" in this case, even though the actual c# variable is in reality not really null.

Interpreting the output

Output format

For any zombie object encountered, the message in the TTY will be of the form:

Found zombie of type {t}, at {path}

... where t is the type of the zombie object, and path is the period-separated route taken from one class' static field to that object. This is one of possibly-many references keeping that object alive. Once an object is tested via one route, it will not be tested again via other routes.

The first component of path will be a class name. The next component will be a static field of that class, and then each subsequent component will be a field of some object leading up to the zombie object.

Effective interpretation

When a zombie object is discovered, it is possible that that object itself contains references to more objects, causing those objects also to be reported as zombies. This means that a single fix in code can "fix" a huge number of reported objects (hundreds, or even more).

For this reason it is best to treat the tool somewhat like a C compiler. Every time it reports errors, fix just the first one or just the last one. Then re-run the tool to see what effect that single code change produced.

Common Causes

Singleton Pattern

It is common for a singleton class Thing to have a static field private static Thing instance; along with instance = this in Awake. For an object that stays alive for the entire duration of the executable this is fine. However consider an object that is only alive for one run of a level, and dies when going back to the frontend (e.g. some sort of CameraManager in a 3rd person platformer). If you do not also instance = null in OnDestroy, then the object will be kept alive when the player returns to the frontend.

C# Events between objects with different lifetimes.

Suppose you have an instance of a class called something like SaveDataManager that stays alive for the entire duration of the executable. It might have a C# event public event System.Action PrepareToSave which is fired when the game is about to save the player's progress. Separately, suppose you also have a class Enemy for a short-lived NPC (e.g. in a shoot-em-up). Enemy might want to know when a save is about to happen. The code might look like this:

class Enemy : MonoBehaviour
{
  void Awake () {
    SaveDataManager saveMan = ...; // Maybe a global, maybe a [SerializeField], etc.
    saveMan.PrepareToSave += OnPrepareToSave;
  }
  
  void OnPrepareToSave () {
    // ...
  }

  // Code continues below

This is C#'s idiomatic support for the Observer Pattern. It is a very common idiom in C#, and in games generally. The problem is if the observer has a lifetime that is shorter than the subject. In our example it means it is a problem if the Enemy has a shorter lifetime than the SaveDataManager, which is the case here; the SaveDataManager lives forever, and the Enemy lives for just a short while. In this case, if the observer (the Enemy) fails to detach its listener from the subject at the end of its normal lifetime, then it will be "kept alive" by the backing field of the subject's event.

The example above can be fixed by detaching the listener at the end of the Enemy's normal lifetime:

  // Code continues from above

  void OnDestroy()
  {
    saveMan.PrepareToSave -= OnPrepareToSave;
  }
}

A variation of the above is when a C# event is static in its enclosing class. Perhaps there is no instance of that class at all. Again, the observer must detach its listeners from the subject (i.e. from the C# event) when it detects the end of its expected lifetime.

How this tool determines Zombie Objects

The tool attempts to traverse every C# object in existence, and tests each one to see if it is a zombie. The challenge of finding every object is split into two parts. The first part is to find all classes (all Types) and then, for each class, to gather every static field that could point to an object. These are the start points of the search. The second half is to recurse for each object; testing that object itself and then recursing into every one of its (non-static) fields.

In both cases, reflection is used to gather all the fields of the object being tested. Fields that are references to other objects (including arrays and C# events) are recursed into. Fields of basic types such as int are ignored.

An object is considered a zombie if it satisfies all of the following criteria:

  • It is not actually null.
  • It is a subclass of UnityEngine.Object.
  • When you compare it to null using (obj as UnityEngine.Object) == null, it returns true.

Credits

The project was started by Stewart McCready (@StewMcc) during his internship at Unity, with subsequent work by Peter Pimley (@peter-pimley-unity).

zombieobjectdetector's People

Contributors

peter-pimley-unity avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

zombieobjectdetector's Issues

Remove logic from presentation

It would be good if the logic of zombie detection was separated from its presentation to the user.

When a zombie object is detected, rather than printing directly to the log, a callback should be invoked. Then we can make different types of presentation. For example, we could display the results in an editor window.

Change detection logic to be allocation-free.

See README.md.

Currently we perform detection by doing obj.ToString() == "null". This creates a temporary string that must eventually be garbage-collected.

It'd be better (?) to do something along the lines of:

System.Object o = ...; // (current object iteration logic)
UnityEngine.Object uo = o as UnityEngine.Object;
bool zombie = o != null && uo == null;

Also start searching from Scene.GetRootGameObjects.

Currently the search only starts from static fields of classes.

We could also start searching from Scene.GetRootGameObjects.

This would cover cases where a long running "manager" objects has a non-static field that points to zombies.

InvalidCastException: Specified cast is not valid.

InvalidCastException: Specified cast is not valid.
System.Reflection.Assembly.Equals (System.Object o) (at :0)
System.Collections.Generic.ObjectEqualityComparer1[T].Equals (T x, T y) (at <eae584ce26bc40229c1b1aa476bfa589>:0) System.Collections.Generic.HashSet1[T].Contains (T item) (at <351e49e2a5bf4fd6beabb458ce2255f3>:0)
CSharpZombieDetector.ZombieObjectDetector+SearchContext.TestAndRecurse (System.Object obj) (at Library/PackageCache/com.unity.zombieobjectdetector@20b894e8c2/Runtime/ZombieObjectDetector.cs:134)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.