Optimization tips

From Elite Wiki
Revision as of 16:47, 24 September 2017 by Astrobe (talk | contribs) (Foreword)

Oolite Javascript Optimization tips

This page collects and (attempts to) organize the tips and findings posted in the Performance tips thread

Foreword

Measure before optimizing, focus on the parts of the scripts that are called very often. Optimizing the code that deal with ship spawning is nice, but optimizing the code called by addFrameCallback(), which is executed dozens of times per seconds, is more important.


Just pointing out the obvious here, but I think it needs to be noted that all these optimization techniquess explained here are not themselves the objective, but the means to get performance. These techniques are not there to jump in and start writing complicated code. They should be used when there is need and where they are needed. And to find this out, the biggest weapon we have is profiling. I fully recommend that before you change anything in your code, you profile it and find out where it really needs attention. Cag already did that and this is the right way to go about it.

Oolite Test Release writes something in the log header that may have escaped the attention of many, but if you look more carefully, you'll see it: It says: Build options: [...] JavaScript profiling. So you can use Oolite itself to see where bottlenecks in your OXP might be and then you can apply all these techniques exactly where there is a gain to be obtained.

To use the built-in profiler, you need the Debug Console. You just need to run something like :time worldScripts["snoopers"].calcNewDate() or console.profile(function() { [...] }); to get profiling information about the function you want to examine. [1]


Unclassified

So without any more ado, here is what I learnt:

  • Used time in oxps functions is limited. So, the first limit is a hard one: your functions will crash if they take too much time.
  • Oxp scripts impact framerate. Rather than trying to optimize everything, you can profile to identify which parts need the most improvement. The goal should be to impact the framerate the least possible.
  • Closures are extremely costly. Minimize cost of closures.

A closure is when you access something which is outside your function. No closure when possible. Save the closure result in a variable to avoid doing it again. In oxps, save particularly the native obj-c oolite objects (ie javascript calls provided directly by oolite; oolite is developed in the obj-c language, and provide special javascript objects to use in oxps, like SystemInfo for example, but using them is costly). Functions inside functions (technically closures) generate a new function each time the enclosing function is called: memory leak. Use a prototype if necessary, or declare the inner function outside. Src: https://developers.google.com/speed/art ... javascript

TODO Merge remarks on closures

  • Dereferences are costly. Minimize cost of dereferences. A dereference is (well, at least it's why I call them) when you access a property of an object in this fashion:

Code:

thingie.myProperty or thingie['myProperty']

Save the result in a variable to avoid doing it again. A dereference on this is costly too, especially if it isn't set, as it checks all the prototype chain. So save your this.something in a variable if you're using it more than once. In oxps, save particularly the native obj-c oolite sub-objects.

  • Function calls are costly. Do not split your functions depending on their meaning. Split them depending on what calls them and when. Use comments and correctly named functions and variables to convey meaning.
  • No need to use singletons, as the Script is one. (A Singleton is an object that you only have once. For example, a script in your OXP is a singleton: whatever the location you access it, it will always be the same object. As the script is a singleton, everything you put into it is never created twice, so you don't need a dedicated piece of code to ensure it is unique (another singleton). So... there is no need to implement singletons in Oolite. This advice might be useful only to pro dev willing to code oxps.)
  • Use compiled regexps, initialized only once, rather than regexps created each time.
  • Don't use non-native Objects, if you'll have to (de)serialize them. It will slow as hell your (de)serialization.
  • No foreach loops, no for loops. Use this way:

var z = myArray.length; while (z--) { var myElement = myArray[z]; // my code }

This is the quickest way as it caches the array length and compares to zero only.


  • the following is faster than indexOf when dealing with arrays:

this._index_in_list = function( item, list ) { // for arrays only var k = list.length; while( k-- ) { if( list[ k ] === item ) return k; } return -1; } so, if( targets.indexOf( ship ) ...

becomes if( ws._index_in_list( ship, targets ) ...


  • to speed your functions, rather than filtering your data at the execution, you might store it prefiltered in this way:

{dataType: [data,...], ...}

To speed it more, store separately the filter and the data:

{dataType: [dataId,...], ...} {dataId: data,...}

This way, you avoid the for...in loop, the hasOwnProperty() check, and of course you avoid iterating on the prototypes' properties.

And of course:

  • save everything used twice in a variable,
  • put everything that can be calculated outside of loops, well, outside of loops,
  • put everything that might be useless because only needed after a return, well, after the return.

Doing this, I have sped my code by at least a factor of 40. (Which is not always enough...)

Some micro-optimizations:

  • Replace return $this.myfunction() ? true : false; by return $this.myfunction();
  • Replace return $this.myfunction() === true; by return $this.myfunction();
  • Replace return $this.myfunction() === false; by return !$this.myfunction();

  • never use 'delete' - it's not doing what you'd expect. In shipWillDockWithStation, I see a lot of

if( isValidFrameCallback( this.x) ) { removeFrameCallback( this.x); delete this.x; } where the last line just needs to be this.x= null;

If that's the only reference, garbage collection will do the rest. The delete command removes the property 'x' from the script's object, is an expensive op and not necessary. (esp. if you do 'x= addFrameCallback(...)' in shipWillLaunchFromStation)