mardi 10 octobre 2023

Debugging Enumerators and Coroutines: Use Reflection to get the current line of an iterator block

I am building a Coroutine wrapper system. I would like users to be able to jump to the Coroutine's last yield statement, or "state".

The wrapper is implemented as an Enumerator wrapper, that is passed to StartCoroutine. I have access to the iterator object, its current value and the source MonoBehaviour.

Here's the gist of it

    public static CoroutineWrapper StartCoroutineWrapped(this MonoBehaviour component, IEnumerator coroutineEnumerator)
    {
      CoroutineWrapper wrapper = new(component, coroutineEnumerator);
      component.StartCoroutine(wrapper);
      return wrapper;
    }

public class CoroutineWrapper: IEnumerator
  {
    public readonly MonoBehaviour Source;

    public readonly IEnumerator Child;

    internal CoroutineWrapper(MonoBehaviour source, IEnumerator coroutine)
    {
      Source = source;
      Child = coroutine;
    }

    public bool MoveNext() => Child.MoveNext();

    public object Current => Child.Current;
  }

Currently, the only ways I have found to be able to do this:

  1. force the user to wrap their return value inside an object. In Debug mode, that object stores a stack trace in its constructor, which can be recovered.
yield return new YieldWrapper(null);
yield return new YieldWrapper(new WaitForEndOfFrame());
//etc

In Current or MoveNext, I check for the type of the yielded value, and store the frame if it is a YieldWrapper.

object Current {
  get {
    object current = Child.Current
    if (current is not YieldWrapper wrapper) return current;
    Trace = wrapper.Trace;
    return wrapper.Value;
  }
}
  1. allow the user to set a flag on the coroutine that will break on the next MoveNext call. Users can then move into the MoveNext call to see the current state.

  2. display the hidden "<>1__state" value of the iterator, which gives a rough idea of the state of the coroutine, but cannot be mapped to any line code.

Obviously they all have drawbacks

  1. Any yield statement that's not wrapped cannot be jumped to. Lots of needless allocations even in Release mode. Otherwise the best hope for a solution.

  2. Cannot see current state, only next one. Useless if a "WaitWhile" instruction is blocked and you want to find which one.

  3. Cannot actually be used as is, only by comparing it to others. Can tell you if a coroutine is actually changing state, or staying in the same one.

Potential ideas:

  • Find some way to get the stacktrace of a method before or after calling the method. If that were possible, I could easily do this with MoveNext and get a file name and line.

  • Find some hidden reflection magic to turn that "<>1__state" into a file position. Current research tells me "<>1__state" is not documented, not guaranteed to exist and might kill my dog if I rely on it in any way shape or form.

  • Instead of displaying said state, store it over multiple iterations to build a map of the iterator. But since I can never get a guaranteed stack trace inside the user coroutine, not actually that helpful. Could be used to infer that some states yield to more stalling maybe.

  • Simply use UniRx instead





Aucun commentaire:

Enregistrer un commentaire