Difference between revisions of "Javascript optimization tips"

From Elite Wiki
m (Tagged as Help page)
(Oolite Javascript Optimization tips: Added link)
Line 25: Line 25:
 
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.
 
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.
  
[http://www.aegidian.org/bb/viewtopic.php?f=4&t=18837&start=45#p258473| link to the BB post]
+
*[http://www.aegidian.org/bb/viewtopic.php?f=4&t=18837&start=45#p258473| link to the BB post]
 +
*[http://www.aegidian.org/bb/viewtopic.php?f=2&t=16990 Thread on tracking FPS] (2014-16)
 
----
 
----
  

Revision as of 06:28, 4 October 2022

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.


Quick and easy performance boosts

  • ship.checkScanner() is "usually considerably quicker than system.filteredEntities()" according to the wiki
  • There are also system.entitiesWithScanClass (about 10x faster), system.shipsWithRole and system.shipsWithPrimaryRole which are much faster too.
  • If all you need is the count, there are partner functions countEntitiesWithScanClass, countShipsWithRole, and countShipsWithPrimaryRole
  • missionVariables are extremely slow, some say 60 x's (profiling shows their only half as slow as WorldScriptsGetProperty)
  • 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).

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

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.


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


The properties .heading and .vectorForward are always identical (verified in the source code). And they are always unit vectors, so there's no need to use .direction() on them directly. If your callback uses both, just pick one. .heading is easier to type but .vectorForward is nearer (in Ship vs Entity) and that's one less get in your profile.

Memory managment

Javascript is a garbage collected language. This means that it automatically removes from memory the objects that are no longer used. However, this automatic memory management requires computations to determine which objects are "dead" and which are not. This is done in "garbage collection cycles" that may require significant time and cause a stutter in-game. The more objects a script generates, the more work for the garbage collector. There are some techniques to mitigate this.

The single most effective thing is to re-use objects whenever possible. Take arrays:

mylist = [];

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

mylist.length = 0;

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:

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:

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:

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:

JavaScript heap: 19.48 MiB (limit 32.00 MiB, 7 collections to date)

Unsorted

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)


  • In oxps, save particularly the native obj-c oolite sub-objects.

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