Mod Profiler

From Space Engineers Wiki
Jump to navigation Jump to search

The Mod profiler is code injected into all mod scripts for the purpose of measuring their impact. The results of these are summarized in Shift+F1 performance warnings menu to blame specific mods for working slow (often misleading because it also measures during loading time, which when you also consider JIT, it becomes not a very fair assessment).

And while this injected code is very fast, it can still add up if called thousands of times leading to a lot of wasted computation.

To see this injected code you can attach dnSpy to your game and find the mod assembly in the Modules window, there you can see a bunch of try-finally added to everything.

What is injected

Mod profiler is injected into all methods, including:

  • Constructors for both classes and structs.
  • Properties with custom getter and/or setter (public bool Test { get { return _test; } }).
  • Quick-properties (public bool Test => _test;).
  • Anonymous methods and lambdas (list.Sort((a,b) => a.Thing.CompareTo(b.Thing));).

Not injected into:

  • Auto-properties (public bool Test { get; private set; }).
  • Code not declared in mods (game, API or .NET methods).

Ways to avoid mod profiler hits

Don't worry too much about optimizing unless you're dealing with hundreds+ calls per tick or you're trying to make it as fast as possible.

If you do have a need for fast code, here's some tricks to avoid the mod profiler hits:

  • The most obvious one is to reduce the amount of custom methods in hot paths, which unfortunately leads to bad coding practices and hard to read code, try to at least use #region & #endregion to segment code.
  • Avoid declaring constructors for structs (new YourStruct() { FieldA = thing, FieldB = false; }), but try to not mutate it after the fact (remember that structs are copied on pass, that makes mutability very complicated).
  • If you need custom-setter properties, use auto-property with private setter (bool Test { get; private set; }) and have a method to assign the value with the checks you need.
  • Can do sorting without callback by using 2 arrays and Array.Sort(keys, values) which sorts both keys and values based on keys.
    • If you need the behavior of a List<T> then you will also have to manually handle extending the array sizes by creating new ones with double the capacity of the old, and optionally Array.Copy() if it's relevant (most of the time it is, but if you know up front how many things you'll need and you'll overwrite all of them then you can safely skip the copy).
    • Can only use a type that has built-in sorting capability (e.g. int, float, double) or if a comparer exists in the .NET or Space Engineers code. It would defeat the whole effort if you declared your own comparer code.
    • This can be used, for example, to do distance-based sorting for billboards with one array holding the distance to camera and another holding the billboards themselves.
Array sorting code example
int Count = 0;
float[] TempSort = new float[64];
MyBillboard[] Billboards = new MyBillboard[64];

// Alternatively: for less impact per-add, but more memory, can have a List<MyBillboard> that you directly add to and then before sorting you assign the Billboards array with data from the list (and enlarge it if needed).
public void AddBillboard(MyBillboard billboard)
{
    if(Count >= Billboards.Length)
        EnlargeArray(ref Billboards);

    Billboards[Count] = billboard;
    Count++;
}

public void DoSorting()
{
    // because TempSort is only used here and fully rewritten, only needs to be be re-created when it falls behind and no copy required.
    if(TempSort.Length < Billboards.Length)
        TempSort = new float[Billboards.Length];

    for(int i = 0; i < Count; i++)
    {
        TempSort[i] = Billboards[i].DistanceSquared;
    }

    Array.Sort(TempSort, Billboards, 0, Count);
}

// This does not support null arrays! modify it if you need that.
static void EnlargeArray<T>(ref T[] array, int newCapacity = -1)
{
    if(newCapacity == -1)
        newCapacity = MathHelper.GetNearestBiggerPowerOfTwo(array.Length + 1);
    else
        newCapacity = MathHelper.GetNearestBiggerPowerOfTwo(newCapacity);

    T[] oldArray = array;
    T[] newArray = new T[newCapacity];

    Array.Copy(oldArray, newArray, oldArray.Length);

    array = newArray;
}
  • Recursive methods can be converted into a while() loop + a Stack<YourStruct> to hold the scheduled "calls", this still allows you to safely queue things inside the loop.

This also has the benefit of avoiding StackOverflowException if you need a few thousands of these to flow this way.

Recursive while() code example
// only to be used by Example() method.
Stack<Params> RecursiveParams = new Stack<Params>();

struct Params
{
    // no constructor to avoid mod profiler, and because of that these cannot be readonly - but don't use it as a mutable struct!

    public IMyCubeBlock Block;
    public float Num;
}

void Example(IMyCubeBlock startBlock, float startNum)
{
    if(RecursiveParams.Count > 0)
        throw new Exception("Unexpected! RecursiveParams has values!"); // safeguard

    // the initial "call" of the recursion
    RecursiveParams.Push(new Params()
    {
        Block = startBlock,
        Num = startNum,
    });

    // process queued recursion calls
    while(RecursiveParams.Count > 0)
    {
        Params runParams = RecursiveParams.Pop();
        IMyCubeBlock block = runParams.Block;
        float num = runParams.Num;

        // do your thing for this item

        // then if you need to do more recursion, add more stuff to the stack. can be called as many times as you want.
        if(someCondition) // can't really have it without a condition because then it becomes an infinite loop.
        {
            RecursiveParams.Push(new Params()
            {
                Block = someOtherBlock,
                Num = 3,
            });
        }
    }
}

And for context, how a regular call-same-method recursion works with similar names as above:

void ExampleActualRecursion(IMyCubeBlock block, float num)
{
    // do your thing for this item

    if(someCondition) // can't really have it without a condition because then it becomes an infinite loop.
    {
        ExampleActualRecursion(someOtherBlock, 3);
    }
}