301 Days

A year of gamedev experiments.

Day 24 - Performance Enhancement 1

| Comments

We shouldn’t be having performance issues already; let’s take care of that before moving on.


Is there a way to get rid of the spike?

There are noticable stutters in the visuals (especially noticable when moving the camera), and the Unity Profiler makes it clear why:

Drilling down shows that it’s all in the updates that call the SQL database. We just can’t keep Unity waiting for Update to finish while we query the big relational monster and sort through the response. The obvious solution is to do it all in a different thread, but what’s the Unity-appropriate way of doing that? This blog post seems to have the answer, but as usual we’ll do some experimenting with it first.

Here’s a nice fake task to take up some time and CPU, calculating the largest prime under 50000 in a most inefficient way:

Assets/Managers/MapManager.cslink
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
private static bool ComplexJob(string x) {
    int a = 50000;
    print("Complex Job begins " + a + " " + x);
    int highestPrime = 2;
    for (int i = 1; i <= a; i++) {
        bool isPrime = true;
        for (int j = 2; j <= i; j++) {
            if (i != j && i % j == 0) {
                isPrime = false;
                break;
            }
        }
        if (isPrime) highestPrime = i;
    }
    print("Complex Job ends " + highestPrime + " " + x);
    return true;
}

Then we comment out the actual updates of the map and NPCs, just calling ComplexJob on every scheduled Map update: That’s pretty clear, the spikes taking nearly a second for a single frame. Interestingly, when I doubled a to 100k, I stopped seeing the spikes on the Profiler; everything pauses, but the Profiler just doesn’t show it.

Now let’s implement the code for pushing that call onto a separate thread (a lot of this is verbatim from the blog mentioned above).

Assets/ThreadAdapter.cslink
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
internal class ThreadAdapter : MonoBehaviour {

    private volatile bool waitCall = true;

    public static int x = 0;

    //this will hold the reference to delegate which will be
    //executed on ui thread
    private volatile Action theAction;

    public void Awake() {
        DontDestroyOnLoad(gameObject);
        this.name = "ThreadAdapter-" + (x++);
    }

    public IEnumerator Start() {
        while (waitCall) {
            yield return new WaitForSeconds(.05f);
        }
        theAction();
        Destroy(gameObject);
    }

    public void ExecuteOnUi(Action action) {
        this.theAction = action;
        waitCall = false;
    }
}
Assets/Managers/MapManager.cslink
244
245
246
247
248
249
void UpdateCellContents() {
    // ourMap.UpdateCells();
    JobResultHandler jrh = new JobResultHandler(Blah);
    DoJobAsync("Happy", jrh);
    // ComplexJob("Unhappy");
}
Assets/Managers/MapManager.cslink
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
    public void Blah(bool result) {
        print("Blah says " + result);
    }

    // see http://blog.yamanyar.com/2015/05/unity-creating-c-thread-with-callback.html

    public delegate void JobResultHandler(bool result);

    public static void DoJobAsync(string someParameter, JobResultHandler jobResultHandler = null) {
        //If callback is null; we do not need unity adapter, otherwise we need to create it in ui thread.
        ThreadAdapter adapter = jobResultHandler == null ? null : CreateUnityAdapter();

        System.Threading.ThreadPool.QueueUserWorkItem(
            //jobResultHandler is a function reference; which will be called by UIThread with result data
         new System.Threading.WaitCallback(ExecuteJob), new object[] { someParameter, adapter, jobResultHandler });
    }

    private static void ExecuteJob(object state) {
        object[] array = state as object[];

        string someParameter = (string)array[0];
        ThreadAdapter adapter = array[1] as ThreadAdapter;
        JobResultHandler callback = array[2] as JobResultHandler;

        //... time consuming job is performed here...
        bool result = ComplexJob(someParameter);

        // if adapter is not null; callback is also not null.
        if (adapter != null) {
            adapter.ExecuteOnUi(delegate {
                callback(result);
            });
        }
    }

    /// <summary>
    /// Must be called from an ui thread
    /// </summary>
    /// <returns>The unity adapter.</returns>
    internal static ThreadAdapter CreateUnityAdapter() {
        GameObject gameObject = new GameObject();
        return gameObject.AddComponent<ThreadAdapter>();
    }

Does it work?


Now on a real update spike

So let’s put this method to practice on the cell updates in MapManager. First, we need to make a static method we can call to do the update:

Assets/DragonsSpine/World/Map.cslink
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
public static Dictionary<String,Cell> UpdateCells(Dictionary<String,Cell> oldCells, string mapName) {
    // load the cell information from the Cell table
    List<Cell> cellsList = DAL.DBWorld.GetCellList(mapName);
    if (cellsList == null) {
        Utils.Log("Failed to get cell list from DB?", Utils.LogType.SystemFailure);
        return oldCells;
    }

    var newCells = oldCells;
    foreach (string tempKey in newCells.Keys) {
        newCells[tempKey].IsMapPortal = false;
    }

    string key = "";
    foreach (Cell iCell in cellsList) {
        key = iCell.X.ToString() + "," + iCell.Y.ToString() + "," + iCell.Z.ToString();
        if (newCells.ContainsKey(key)) {
            newCells[key].Segue = iCell.Segue;
            newCells[key].Description.Trim();
            newCells[key].Description = iCell.Description + " " + newCells[key].Description;
            newCells[key].Description.Trim();
            newCells[key].cellLock = iCell.cellLock;
            newCells[key].IsMapPortal = iCell.IsMapPortal;
            if (iCell.IsMapPortal) Utils.Log("Cell " + key + " is a map portal.", Utils.LogType.SystemFailure);
            newCells[key].IsTeleport = iCell.IsTeleport;
            newCells[key].IsSingleCustomer = iCell.IsSingleCustomer;
        }
    }
    return newCells;
}

I’ve clearly been doing too much functional programming lately, since I felt the need to return a new dictionary, even when the one coming in was passed by reference. Another refactoring to-do.

We’ll use a little flag to know when the other thread is done (another to-do: find out the C# concurrency idiom for this sort of thing):

Assets/Managers/MapManager.cslink
173
174
175
176
177
178
179
180
void Update() {
    if (updateComplete && Time.time > nextDBUpdate) {
        print("Time to read from the DB! " + Time.time);
        nextDBUpdate = Time.time + dbUpdateDelta;
        updateComplete = false;
        UpdateCellContents();
    }
}

And now the meat of the implementation:

Assets/Managers/MapManager.cslink
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
void UpdateIsComplete(Dictionary<String,Cell> newCells) {
    foreach (string cellkey in newCells.Keys) {
        // so we can make sure changes are actually seen, since we currently do nothing with them.
        if (newCells[cellkey].IsMapPortal) print("newCell " + cellkey + " is a map portal.");
    }
    ourMap.cells = newCells;
    updateComplete = true;
}

public static void DoUpdateAsync(Dictionary<String,Cell> param, UpdateResultHandler jobResultHandler = null) {
    //If callback is null; we do not need unity adapter, otherwise we need to create it in ui thread.
    ThreadAdapter adapter = jobResultHandler == null ? null : CreateUnityAdapter();

    System.Threading.ThreadPool.QueueUserWorkItem(
        //jobResultHandler is a function reference; which will be called by UIThread with result data
     new System.Threading.WaitCallback(ExecuteUpdateJob), new object[] { param, adapter, jobResultHandler });
}

private static void ExecuteUpdateJob(object state) {
    object[] array = state as object[];

    var param = (Dictionary<String,Cell>) array[0];
    ThreadAdapter adapter = array[1] as ThreadAdapter;
    UpdateResultHandler callback = array[2] as UpdateResultHandler;

    //... time consuming job is performed here...
    Dictionary<String,Cell> result = Map.UpdateCells(param, "Island of Kesmai");

    // if adapter is not null; callback is also not null.
    if (adapter != null) {
        adapter.ExecuteOnUi(delegate {
            callback(result);
        });
    }
}

public delegate void UpdateResultHandler(Dictionary<String,Cell> result);

void UpdateCellContents() {
    //   old way:
    // ourMap.UpdateCells();
    // updateComplete = true;
    //   new way:
    UpdateResultHandler jrh = new UpdateResultHandler(UpdateIsComplete);
    DoUpdateAsync(ourMap.cells, jrh);
}

Since we don’t currently have code for relocating portals or anything, we have to just do some DB updates and watch the log to see that they are reflected. The profiler now shows the foreach loop in UpdateIsComplete being the only expensive thing happening on the Unity UI thread. Yay!

Tomorrow we make NPC updates cheap, and then think about doing some substantive Cell updates (and why we need to).


A dreamy aside

I’m an unapologetic Media Molecule fan. Of all the studios I admire, they’re doing some of the most interesting stuff. I ran a fansite during the early days of LittleBigPlanet, and have had the opportunity to meet with a number of the Molecules over the years. I’m occasionally tempted to work on a fansite for their latest project, Dreams, but know I don’t have the time. Luckily others are doing more/better than I would, anyway. This analysis by MrStampy83 covers just about everything I have noticed, including many of the same hopes and concerns that I have. I was really happy to see some confirmation that he’s on the right track.

Update: Exciting! Looks like while I was typing this up, Media Molecule announced a live Dev Diary event on Twitch on Friday.


Day 24 code - visualizer

Comments