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:
First Thread problems.
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:
privatestaticbool 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);
returntrue;
}
Then we comment out the actual updates of the map and NPCs, just calling ComplexJob on every scheduled Map update:
Ok then.
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).
publicvoid Blah(bool result) {
print("Blah says " + result);
}
// see http://blog.yamanyar.com/2015/05/unity-creating-c-thread-with-callback.html
publicdelegatevoid JobResultHandler(bool result);
publicstaticvoid 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), newobject[] { someParameter, adapter, jobResultHandler });
}
privatestaticvoid ExecuteJob(object state) {
object[] array = state asobject[];
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>
internalstatic ThreadAdapter CreateUnityAdapter() {
GameObject gameObject = new GameObject();
return gameObject.AddComponent<ThreadAdapter>();
}
Does it work?
In this experiment, at least, it completely resolves things!
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:
publicstatic 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):
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;
}
publicstaticvoid 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), newobject[] { param, adapter, jobResultHandler });
}
privatestaticvoid ExecuteUpdateJob(object state) {
object[] array = state asobject[];
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);
});
}
}
publicdelegatevoid 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.