We've all experienced that little stutter during flight, ususally at an inconvienent moment. Miners vaporize splinters instead of scooping them, pirates do the same w/ cargo. And escape pods...
Code: Select all
"Pilot dies tragically when escape pod impacts shield, Rescuer claims computer malfunction"
How's that for an epitaph, killed by a computer brain fart!
But seriously, JavaScript was never intended for real time games. It's not just JavaScript, any language that uses garbage collection (GC) has this problem. Thousands of Android game devs have run into this. So what can we do?
Oolite has a generous heap of 32 MB, surely that should be enough, right? How many of you are old enough to remember when Bill Gates said (or didn't say!) that 640K ought to be enough for anybody?
Nature abhors a vacuum and if we had a 1 GB heap, we'd probable fill it. No, it not the size of the heap but how we (ab)use it. Now most oxp authors are not programmers, so they're not to blame. It's up to us who are to lay it out simply and clearly as we can. Our performance wiki definitely needs a section on this.
The single most effective thing is to re-use objects whenever possible. Take arrays:
is fine for initializing one but that is the
only time this statement should be used. If you're doing this to clear an array (& remove references so objects in the list can be GC'd, a laudable goal), what you're really doing is assigning 'mylist' a brand new, empty array, and if 'mylist' is its only reference, you are succeeding in your goal but only as a side-effect. By assigning '[ ]' to your variable, you're tossing the old array (& all its references) on to the garbage heap.
If you want to clear and re-use an array, all you need to do is:
and your array will be as empty as it was when it started. And yes, all the references to objects that were in your list are gone and they'll be GC'd. This seems counterintuitive, and there is a lot of debate as to whether this works across all implementations of JS, but is does work in Oolite, which is all we care about.
Before you go and convert all your arrays to this.$variables, remember Day's trick for assigning function references to properties of a function:
Code: Select all
this._myfunction = function _myfunction() { // NB: the 2nd '_myfunction' names the function, useful in stack dumps and profiling too
var that = _myfunction;
var random = (that.random = that.random || Math.random);
var mylist = (that.mylist = that.mylist || []);
mylist.length = 0;
...
}
Now '_myfunction' will re-use the same array each time it's called, not creating a new one every time with
mylist = [];
This same trick works for dictionary objects, that some functions require as parameters:
Code: Select all
var missionConfig = {titleKey: "oolite-contracts-cargo-none-available-title",
messageKey: "oolite-contracts-cargo-none-available-message",
allowInterrupt: true,
screenID: "oolite-contracts-cargo-none",
exitScreen: "GUI_SCREEN_INTERFACES"};
mission.runScreen(missionConfig);
This will create a new object every time it's invoked. But if we do the same as above:
Code: Select all
var missionConfig = (that.missionConfig = that.missionConfig || {});
...
missionConfig.titleKey = "oolite-contracts-cargo-none-available-title";
missionConfig.messageKey = "oolite-contracts-cargo-none-available-message";
missionConfig.allowInterrupt = true;
missionConfig.screenID = "oolite-contracts-cargo-none";
missionConfig.exitScreen = "GUI_SCREEN_INTERFACES";
mission.runScreen(missionConfig);
we'll only consume a single object here for the entire game.
There is a function in the debug console,
console.writeJSMemoryStats()
, that writes out some handy info:
Code: Select all
JavaScript heap: 19.48 MiB (limit 32.00 MiB, 7 collections to date)
(note to devs: it'd be cool if we could access this data, to isolate offending functions, visualize the 'saw-tooth' heap history, etc.)
I put this on a 5 second timer and discovered that, with no oxp's loaded, the game was chewing thru almost 4 MB every five minutes! All I did was lauch, cut power after a bit and stare at the buoy. It turns out that some of the heap abuse above exists in the game's
Resources\Scripts
files.
Garbage collection appears to occur around the 22.5 MB mark, so this would cause JS to stroke out every 30 minutes, give or take. That w/ no oxp's, mind. [In case you're thinking I'm picking nits, one particular oxp generates 180 MB in 5 minutes, guaranteeing a GC every 38 seconds.]
So I applied the above to those files and was able to get it down to 500 KB every five minutes, so the strokes only occur every 3 hr 45 min. - Long live Oolite!
You can find them here:
https://www.dropbox.com/s/cudwuq8d56epc ... s.zip?dl=0
Please remember to backup the originals first, to a different partition, if possible, if you're using Windows!
I tried to ensure no changes in functionality (ie. introduce any bugs) but Mr. Murphy could have when I wasn't looking. I did make 3 conscious changes:
- oolite-global-prefix.js, near line 120, in "scrambledPseudoRandomNumber", changed 'this.pseudoRandomNumber' to 'system.pseudoRandomNumber', to allow this fn to be cached ('Error: this is undefined' -arrgh)
- oolite-global-prefix.js, near line 190, I commented out "Script.prototype.coordinatesForEscortPosition", as I couldn't figure out why it needed to be added to every script!
- oolite-priorityai.js, near line 1325, I added a prototype function,
_get_infoForSystem
, that caches 'System.infoForSystem' results
Given that Oolite is an inherrently chaotic system, we can't run a test suite against these to find bugs. Back in the Stone Age, when printers were the size of small cars and operators wore hearing protection, we'd do 'code audits'. You'd print your source code out on a single, very long, length of paper (it was perferated every 11 inches and folded itself neatly into a box, most of the time.) You then handed off this 2-3 inch thick block of printout to a work mate, who gleefully ripped into your hard work. The pages were extra wide (14 inches), allowing plenty of room for them to point out where you went wrong, while you sit outside, weeping along with the trees. Those were the days!
I suggest a distributed, more eco-friendly method: pick a file and compare it to its original. Whether or not you find a problem, post that it's been reviewed. Once they've each been looked over a few times, we can consider releasing them to 'volunteers' to run them. A couple notes:
- the 3 contract scripts (cargo, parcels & passengers) are quite similar, so if you do one, skip the other two (and conserve your sanity)
- priorityAI is huge, so you may want to split that up amongst a few of you
I've done the heavy lifting, I need your help to get this puppy safely docked!
Edited: Oct.16/17: new versions of 3 contract scripts