Difference between revisions of "Javascript optimization tips"

From Elite Wiki
(Foreword)
(Unclassified)
Line 137: Line 137:
 
----
 
----
  
 +
All Oolite-provided functions which take a vector as an argument may instead be passed an array of three numbers, or an Entity (in which case the entity’s position is used) [emphasis added]
 +
 +
So, use '''ps.position.distanceTo( ent )''' instead of '''ps.position.distanceTo( ent.position )'''
 +
 +
it profiles as 15% faster and you don't have to type positoin as often :) Sometimes it does pay to rtfm
  
  

Revision as of 17:08, 24 September 2017

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]


I wrote a utility that monitors the frame rate in an effort to tailor my code's impact to the player's setup. So on a slower machine, I can do less or do it less often or spread the work out over several frames. It's called fps_monitor and since I had to learn how, it's an oxz:

With it, you can track the fps rate, as well as the median, mode, mean, high & low values. You can set up to 3 time frames and report to the log file and/or the in-game console. You can access the data from an oxp so as to adjust to current conditions.

The readme has several examples of how I'm using it. I'm hoping for some feedback and discussion on using this (or something else) to reclaim our frame rate.

link to the BB post


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...)


  • 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)


  • ship.checkScanner() is "usually considerably quicker than system.filteredEntities()" according to the wiki
  • There are also system.entitiesWithScanClass, system.shipsWithRole and system.shipsWithPrimaryRole which are much faster too.
  • (entitiesWithScanClass is about 10x faster). And if all you need is the #, there are partner fns countEntitiesWithScanClass,
  • countShipsWithRole, and countShipsWithPrimaryRole
  • missionVariables are extremely slow, some say 60 x's (profiling shows their only half as slow as WorldScriptsGetProperty :shock: )
  • x < 0 ? -x : x; instead of Math.abs(x), is over 10 x's faster.
  • x < y ? x : y; instead of Math.min(x, y), is over 12 x's faster (same for max).

All Oolite-provided functions which take a vector as an argument may instead be passed an array of three numbers, or an Entity (in which case the entity’s position is used) [emphasis added]

So, use ps.position.distanceTo( ent ) instead of ps.position.distanceTo( ent.position )

it profiles as 15% faster and you don't have to type positoin as often :) Sometimes it does pay to rtfm



TODO: don't know what to do with the second part of [2]