Conditions Plugged, Encounters Next

Hey Folks! Memleak work continues today, as conditions get wrapped-up, and we turn to encounters.

For conditions, I finally found out where the extra camp conditions in memory were coming from. AI was cloning hex campsite conditions each time they visited, and never releasing them. As a result, they just piled-up in memory, and for long games with lots of creatures, this could add-up to significant memory. The fix was to just not clone them, as the camp condition should theoretically be the same for all camp visitors. (There is a special case in the code not to destroy a campsite condition when a creature removes it from itself.)

There was also, coincidentally, a bug fix in the way creatures were destroyed. A typo was causing camps and ground to be destroyed when a creature got destroyed, so I've fixed that.

In brief tests, that seemed to solve the remaining condition memory leaks, and cause no other issues. So we'll go with that for now. It's always possible small changes like this have unforeseen effects, though.

Moving on, Encounters are the next big one. After some investigation, it appears each turn, for each creature in an encounter, encounters are getting cloned like crazy. Like 150 encounters in memory after 1-2 turns into the game. (Which are, ostensibly, from either the player or Yezinka.)

When you factor in combat and scavenging (which are also encounters, and in combat's case, involve multiple creatures), this can really get out of hand.

My first idea was to just change the way encounters get requested in the data handler. It's cloned at the source, likely to avoid collisions, and since encounters shouldn't change much from creature-to-creature, I figured it couldn't hurt to not clone them.

But then I remembered Tiago's struggle to get loaded encounters to fit into memory. That's why we load each on-demand from disk.

Plus, battles and scavenge encounters get customized per instance, so that wouldn't work for them the minute more than one creature needed it simultaneously.

So my next idea was to destroy them when the creature is done with them. This turns out to be way harder than expected. Even as the code's author, I can't find a safe (and still reliable) place to do this. Either I delete it too early, or miss my chance before it disappears into memory.

Enter the processed encounter queue. I'm going to see if I can store a reference to each encounter that a creature experiences in a list. And after "some time," that list gets its oldest members destroyed and removed. This way, there's kind of a running destroy queue for each creature. And to avoid figuring out the soonest possible "some time," I just chose 2 turns. In theory, an encounter from two turns ago should be safe to destroy. And when the creature goes bye-bye, the remaining 2 can, too.
I think this approach is sound, if a bit hacky.

The trouble is, it doesn't work. At least, not appreciably. It might've reduced the number of encounters orphaned by the number of encounters creatures have collectively experienced. But for 1-2 turns into the game, this is like 2-5 encounters from a list of ~150. Not good enough.

I have a hunch, though. This may just be the first step. The next step might be to check when each encounter presents its options/choices for the next turn. I bet each outcome is cloning an encounter, and these are just being forgotten each turn. I'll look into those tomorrow.

Whew! Quite a day! And I'm still feeling pretty sick. Almost worse, in fact. Double-whammy? Undead disease? God, I just hope it goes away soon. I'm done with this cough/nose/chills/aches shebang.


ra1's picture

Looks like you built a specialized-heap memory pool with semi-automatic garbage collection. This is something that you could generalize and use in many different cases, if you see the need. Most VMs (like .net and Java) use some form of this -- but your implementation appears more flexible as you control exactly how/when to release allocated objects from the pool.

Sounds like fun :)

dcfedor's picture

There is a certain amount of problem-solving fun to it. And I'll admit bug-fixing in games is challenging and sometimes rewarding detective work.

But I'd much rather be problem-solving game design than memory. That's why I chose a managed language, after all! :)

Which makes me wonder, why isn't the GC collecting these orphans? I mean, the Windows target is .NET, as far as I know. When a local reference goes out of scope, isn't it supposed to be flagged for release? It seems weird that I need to manually destroy/dereference everything.

Dan Fedor - Founder, Blue Bottle Games

ra1's picture

This is just a guess, but another object (that didn't have local scope) is still pointed at the problem-object - so it can't be GCd automatically.