Light Style© by Fisana

Jump to content


Photo

[Discussion] Spidermonkey upgrade


  • Please log in to reply
205 replies to this topic

#1 Yves

Yves

    Centurio

  • WFG Programming Team
  • 897 posts

Posted 01 May 2013 - 08:54 PM

I decided to open a thread about the Spidermonkey upgrade.
There are some design decisions to take and it's good to have a place to write down the pros and contras. This is meant for discussions to take place and for work in progress designs and ideas that aren't final yet. I intend to use ticket 1886 for documenting more "final" decisions and information.

Each GUI page in its own compartment
The first major change is having each GUI Page (CGUI Object) in its own compartment using its own ScriptInterface instance. Currently the GUI uses two global objects. One is a page global for each GUI page which should separate global variables between the pages. The other global is used for all pages and contains engine function definitions and such. The new Spidermonkey associates a compartment with each global object and this design doesn't work anymore. At least I didn't find a way to make it work with minor changes. Cross compartment calls and wrapper objects could provide a solution here but I decided the other approach looks more promising.
At the moment the plan is to register all GUI functions for all pages. I don't think registering maybe 100 functions for each GUI page should be a performance problem or memory problem. If it becomes a problem we can still implement something that lets GUI pages include functions they need.
I figured out that the music code passes information across pages by using the global object called "global" (for example "global.curr_music"). A grep for "global\." returned only music related code, so it doesn't seem to be an often used pattern and could be replaced by a state stored in the music manager itself that could be accessed by a function.

Concerning the JS console, the idea is that it should always use the currently active GUI page.


Getting rid of g_ScriptingHost
We are still using the old ScriptingHost that had been created before ScriptInterface was developed. That's unneeded duplication and should be replaced by ScriptInterface. It's sometimes a bit tricky because a lot of code simply accesses this global object and some thoughts are required that this code uses the right context/ScriptInterface/compartment in the future.


Standardizing scripting access
It's possible to access and use the JSAPI in a lot of different ways and we use too many of them in my opinion. That's probably because it grew that way and partially because different code has different requirements.
I don't want to do too much at the moment but after thinking about it a lot and after discussing it with Philip I think we should remove CJSObject and the associated classes. It's currently used by the CRenderer, CGameView and the Soundmanager and I'd try to apply the same new pattern also for the JS console.

The general approach would be providing global functions to scripts instead of custom C++ objects with GetProperty and SetProperty overloads, custom properties and custom methods. That's more or less the same approach Philip has chosen for the Simulation code and it seems to work quite well there.
Object oriented design is good, but I think it's enough to have an object oriented scripting language and an object oriented engine. The interface between JS and the engine benefits more from a clean separation than from OOP. If there are global functions that can be called like "Engine.DoWhatEver()" it's clear that it is an engine function. Properties on the other hand can be script properties or native properties with custom getter- and setter functions that do potentially unexpected things. It's not obvious when using an object if it's a JS object or a C++ object with some custom functionality.

The deprecated interface itself (CJSObject) required some quite hacky bits of code to create a relatively smooth transition between global/static callback functions and the objects. It stores some pointers in private data fields of JS objects and has to take care of some rooting to avoid garbage collection. I have to admit that this is hidden pretty well from the class users but what it does is still quite ugly (necessarily for that approach).
I thought about either extending or removing the CJSObject-approach and my conclusion was that the additional flexibility is not really needed and the simplicity of global functions is the better deal.
Here are some pros and cons I listed if you're interested (+ for pro, - for contra, ? for unknown or to-be-analyzed):
Spoiler


Changing CGameView from the old pattern with CJSObject to the new one with global functions was very straight forward. Here is a reference implementation of CGameView

@stwf
I asked you about changing the Soundmanager code.
What I had in mind was something like the example above plus the required changes in JS-Code and something as a replacement for passing data across GUI pages using "global.". For the moment you could use g_ScriptingHost for calling RegisterFunction instead of passing the ScriptInterface as an argument like in the attached example. I can easily change that in my code without causing merging conflicts later.


Some additional thoughts about a configuration system
Especially the renderer makes a lot of properties available to scripts that are essentially settings.
When an options-screen eventually will be implemented a lot more settings will have to be accessible to scripts. I didn't want to develop such a system for settings because it's actually quite complex and requires some more time thinking about it. That should basically also be possible with global functions or the JSAPI could be used directly (something like the GUI does). In the worst case it would still be possible to reintroduce CJSObject if there are good reasons.

Edit: Leper and I discussed that on IRC and came to the conclusion that functions are probably better anyway for the settings because they can provide a return value if for example the setting can't be enabled because the hardware doesn't support it.



Is anyone against any of these changes or do you have additional inputs to consider? I've spent a lot of time thinking about it and I'm sure enough to write that post, but I still don't understand everything in detail and could be wrong about some aspects.
  • 1

#2 zoot

zoot

    Primus Pilus

  • Community Members
  • PipPipPipPipPipPip
  • 1,562 posts

Posted 01 May 2013 - 09:10 PM

If you are redesigning scripting access, I recommend taking this issue into consideration: http://trac.wildfire...com/ticket/1589

In short, we need a 'manager' or something similar which can be accessed from all ScriptInterface instances.

Edited by zoot, 01 May 2013 - 09:10 PM.

  • 1

#3 RedFox

RedFox

    Real-time Systems Programmer

  • WFG Programming Team
  • 247 posts

Posted 02 May 2013 - 09:37 AM

This is a huge change, but also something just has to be done. We could follow OOC (Object-Oriented C) approach, where objects are expressed as opaque handles:
// myscript.js
var g_Playlist = [ "music01.ogg", "music02.ogg" ]; // our playlist
var g_Current = 0; // invalid handle
var g_Next = 0; // index for the next sound to play
// ... some event ...
{
	// where sound_* is the global sound interface
	var newsound = sound_load(g_Playlist[g_Next++]); // load the next piece
	if(g_Next >= g_Playlist.length) g_Next = 0;
	sound_stop(g_Current); // stop the current music after we actually loaded newsound
	sound_play(newsound); // start playing the new music
	g_Current = newsound;
}
The C++ code would keep track of the handles and their validity. For example 0 would be an invalid handle. You won't ever get dangling pointers this way, since the handles can be invalidated after it has been deleted. Null pointers are also impossible, since handle 0 is the mark of an invalid handle - in which case the proper interface method should do nothing.

Edited by RedFox, 02 May 2013 - 09:40 AM.

  • 0

"I'm telling you this because you don't get it, you think you get it, which isn't the same as actually getting it. Get it?"


#4 stwf

stwf

    Sesquiplicarius

  • WFG Programming Team
  • 179 posts

Posted 02 May 2013 - 12:05 PM

I'm willing to make whatever changes are needed to move the game forward, but there are a few things that need to be thought through here. What time frame are we talking about here?

I'm not as much in favor with passing handles back to the simulation. It makes me think I would need to keep these references around forever since I wouldn't know when a handle went out of scope or the converse, forcing js developers to dispose of each handle seems like a complication for them. In general I've tried to move the work into the C code like with playlists.

Is the behavior of source/simulation2/CCmpSoundManager along the lines of what you want?
  • 0

#5 stwf

stwf

    Sesquiplicarius

  • WFG Programming Team
  • 179 posts

Posted 06 May 2013 - 05:54 PM

Is anyone against any of these changes or do you have additional inputs to consider? I've spent a lot of time thinking about it and I'm sure enough to write that post, but I still don't understand everything in detail and could be wrong about some aspects.


Hi , they seem doable, so if it helps out keeping the code current I have no problem with it...

Trying to build the latest code with your patch applied brings up the following errors and warnings though:



../../../source/graphics/GameView.cpp:621:10: warning: address of function
'CGameViewImpl::ConstrainCamera' will always evaluate to 'true'
[-Wbool-conversions]
if (!m->ConstrainCamera)
~~~~^~~~~~~~~~~~~~~




../../../source/graphics/GameView.cpp:407:18: error: no matching member function
for call to 'RegisterFunction'
...scriptInterface.RegisterFunction<void, bool, &CGameViewImpl::LockCullCamera>...
~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  • 0

#6 Yves

Yves

    Centurio

  • WFG Programming Team
  • 897 posts

Posted 06 May 2013 - 06:08 PM

The patch wasn't really meant to be applied directly. It was more a general idea how it could work.
I've changed the implementation approach again. Adding a separate file with some global functions in a namespace instead of adding static functions to the class itself seems to be cleaner. When class members become static a lot more in the class may have to become static too, which isn't good.
But again, this patch isn't meant to work directly and without other changes.

New patch here.
  • 0

#7 stwf

stwf

    Sesquiplicarius

  • WFG Programming Team
  • 179 posts

Posted 10 May 2013 - 07:01 PM

Hi,

OK, I have these changes done, although there is a bug or two I'm still working out, the JSObjects have all been replaced by globals.

This is currently in a github branch. Can you get it from there? Would you just like it as a diff file? I suppose I could just check in the changes to svn when its bug free, but that may take a little bit. Would you rather have the changes first?
  • 1

#8 Yves

Yves

    Centurio

  • WFG Programming Team
  • 897 posts

Posted 10 May 2013 - 07:24 PM

Very good, thanks!
I should be able to get the diff from the GIT branch myself and this way I can always get the newest version. Please post the link.

Unfortunately I haven't come as far as I wanted in these two weeks of holidays and the progress will slow down when I start working again next week. But anyway, I'm able to start the game and navigate through the menus with sound disabled. At the moment it crashes when I try starting a match because of some issues with the Javascript context's lifetime. Some more issues could come up with the AI because it uses Javascript intensively.
I would like to have a quick look at your sound code now to make sure it is what I need and then wait with the integration in my Spidermonkey working copy until I get a normal match running.
  • 0

#9 stwf

stwf

    Sesquiplicarius

  • WFG Programming Team
  • 179 posts

Posted 15 May 2013 - 03:13 PM

Hi, sorry for the delay, I missed this somehow...

https://github.com/s...ee/spidermonkey

Is the link to the branch, I think any bugs in it now already existed.....
  • 1

#10 Yves

Yves

    Centurio

  • WFG Programming Team
  • 897 posts

Posted 16 May 2013 - 02:44 PM

Hi, sorry for the delay, I missed this somehow...

No problem, I didn't have time to work on 0ad in the past three days anyway.
It's awesome how you made these changes so quickly to help with the Spidermonkey upgrade! :)

I've applied the changes and it works well. I can hear music, the music continues playing across multiple GUI pages, there are ambient sounds after a game starts and it starts playing another piece of music in game after the first is over. I haven't found any issues yet.

Your implementation has a few minor differences compared to the other scripting implementations I "ported" like for GameView or for the Renderer.
We should try to eliminate these differences just for consistency. These are the main differences I noticed:
  • I'm using a directory called "scripting" for the js stuff, which is the same naming that the simulation and the GUI already use. The file containing the global functions which are regsitered to javascript is called "JSInterface_Renderer" or "JSInterface_GameView" respectively. I suggest you also follow this convention just for consistency. Remember to change Premake4.lua if you change the directory name.
  • I've put all the global scripting functions in a namespace called JSI_Renderer or JSI_GameView respectively. I think this makes sense, you could also add your global functions in a namespace "JSI_Sound" or similar.
  • Currently we use three different ways to make the ScriptingInit/RegisterScriptFunctions function available. I'm using a public non-static class memberfunction for GameView and a public static class memberfunction for the renderer and you use a public non-static global function. Basically I think your approach could be combined with a namespace (point 1) so that we could omit the "Sound" prefix and just name the function "ScriptingInit" or "RegisterScriptFunctions". I prefer the second term because it describes clearer what it actually does. I don't see any advantage of a static class memberfunction at the moment so I'll change that in my code. Another closely related question is...
  • What do we do when functions are only available at certain stages in game? "GameView" for example can only be used when a GameView instance is available which currenty means when a match is running. My hacky workaround is only registring the function in the case of "if(g_Game && g_Game->GetView())". That's not very good because this check runs when the GUI page is initialized and in the future it could be required to access GameView's function from a page that was initialized before the game started. I've noticed that you check for "if ( g_SoundManager )" in each function and simply do nothing if it's false. In the soundmanager's case that's probably good enough because it gets initialized quite early and stays active until the game ends. I'm wondering if I should try to implement a common solution for this problem like a check that can be added when registring the function so that the ScriptInterface itself would react sensible and the C++ code providing the functions doesn't need to worry about that in each individual function. This way I could also use the same approach for registring the functions of GameView and they could be used in all pages when GameView is available no matter when the page was initialized.

  • 0

#11 tuan kuranes

tuan kuranes

    Discens

  • Community Members
  • Pip
  • 31 posts

Posted 16 May 2013 - 04:52 PM

A note/request while you're on js-c++ interface: with current code, cannot make any method with const or const& parameters, and many of them are string or std::vector, thus a lot of unecessary allocation and copies are made on many js/c++ calls.

Edited by tuan kuranes, 16 May 2013 - 05:12 PM.

  • 0

#12 Yves

Yves

    Centurio

  • WFG Programming Team
  • 897 posts

Posted 16 May 2013 - 05:29 PM

A note/request while you're on js-c++ interface: with current code, cannot make any method with const or const& parameters, and many of them are string or std::vector, thus a lot of unecessary allocation and copies are made on many js/c++ calls.

I don't think that's possible with the current approach of converting parameters to native c++ types. That would require direct access to Spidermonkey's memory and knowledge of the internal structure of jsvals (which could change from one Spidermonkey release to another). Also as far as I know spidermonkey's GC could theoretically move the storage location of values.

I think it works without copying memory when registring JSNative functions directly and accessing the argv array using the JSAPI.
But being able to register functions with a fixed number of arguments and native C++ types is worth the additional overhead in most cases IMO.
Also keep in mind that this approach is currently only used for relatively simple argument types like integers or strings and not for passing large objects. I know strings are only "relatively" simple but simplicity of code and the additional automatic validation of the number of arguments and their type is also valuable and not just the performance.

In my opinion we should only worry about this performance loss if we can prove that it's significant enough using the profiler. In this case we can find a specific solution.

[ot]btw, how did you make the graphs in the trac ticket ? is there a wiki page explaining that?[/ot]

Which ones do you mean? This?
It was created using Profiler2. Check source/tools/profiler2 and I've written a short howto here.
Unfortunately there's no wiki article explaining it. It's on my todo-list since quite a while...
  • 0

#13 Yves

Yves

    Centurio

  • WFG Programming Team
  • 897 posts

Posted 16 May 2013 - 09:18 PM

I wanted to test the new Spidermonkey in the profiler a first time. And guess what... it's currently even a little bit slower than the old version. :(
This is from the non-visual replay on Oasis4 with 4 AI players (so no rendering, no GUI etc...). V185 is the old version and the other two lines are both from the new spidermonkey (I ran it twice to be sure).
I'll try to figure out why it's slower this weekend. I have some ideas and I don't think it will stay like that.
profile_spidermonkey.gif
  • 0

#14 stwf

stwf

    Sesquiplicarius

  • WFG Programming Team
  • 179 posts

Posted 17 May 2013 - 02:09 PM

No problem! It actually made my code much simpler, so I'm glad to have them in there...

I'll incorporate all of your suggestions soon, although I may let you work through it a little more on your own so I get all of the fixes at one time ;-)


What do we do when functions are only available at certain stages in game? "GameView" for example can only be used when a GameView instance is available which currenty means when a match is running. My hacky workaround is only registring the function in the case of "if(g_Game && g_Game->GetView())". That's not very good because this check runs when the GUI page is initialized and in the future it could be required to access GameView's function from a page that was initialized before the game started. I've noticed that you check for "if ( g_SoundManager )" in each function and simply do nothing if it's false. In the soundmanager's case that's probably good enough because it gets initialized quite early and stays active until the game ends. I'm wondering if I should try to implement a common solution for this problem like a check that can be added when registring the function so that the ScriptInterface itself would react sensible and the C++ code providing the functions doesn't need to worry about that in each individual function. This way I could also use the same approach for registring the functions of GameView and they could be used in all pages when GameView is available no matter when the page was initialized.



In my case its best since the SoundManager will also be nil if the game was compiled without audio support, or if the user disables music from the settings menu during a game. So its a case I always have to deal with.
  • 0

#15 tuan kuranes

tuan kuranes

    Discens

  • Community Members
  • Pip
  • 31 posts

Posted 22 May 2013 - 06:46 AM

Thanks for the graph link. Definitely needed on wiki. Can I just at least copy/paste it there, even if not exhaustive, that help when searching for it ?

Also keep in mind that this approach is currently only used for relatively simple argument types like integers or strings and not for passing large objects.

In CCmpRangeManager: ExecuteQuery, ResetActiveQuery, GetEntitiesByPlayer, etc.
All those methods do uses std::vector from and to js, and are called very frequently.

In my opinion we should only worry about this performance loss if we can prove that it's significant enough using the profiler.

It does show here with "very sleepy" profiler. Js related string, malloc, free are in the top calls.
Not that it beats the huge "EconomyManager.prototype.buildDropsites" perf gap in aegis bot, but memory fragmentation is the reason of the overall slowdown over time of 0ad.
  • 0

#16 wraitii

wraitii

    Primus Pilus

  • WFG Programming Team
  • 1,497 posts

Posted 22 May 2013 - 06:52 AM

That function should be disregarded when profiling. It does big 2D image manipulations that really shouldn't be done in JS in one turn. I'll try to find a way to even that one out whenever I can.
  • 0
Lancelot de Ferrière le Vayer [ aka Wraitii ]
Wildfire Games Programmer, AI developer, auxiliary map designer, dealing with anything water.
Contact me: wraitii@wildfiregames.com

Also the world's only three-dimensional poodle.

#17 scroogie

scroogie

    Discens

  • Community Members
  • Pip
  • 65 posts

Posted 23 May 2013 - 07:50 AM

wraitii: do you mean EconomyManager.prototype.getBestResourceBuildSpot()? Maybe that could even be held globally on the C++ side?
  • 0

#18 wraitii

wraitii

    Primus Pilus

  • WFG Programming Team
  • 1,497 posts

Posted 23 May 2013 - 10:42 AM

Exactly. Basically it handles a lot of maps in one go, and JS is not exactly perfect to do that. I'm considering switching some stuffs to c++ as part of improving the AI. I want however to be sure that the added cost of memory transfers are not bigger than the increase in computation speed.
  • 0
Lancelot de Ferrière le Vayer [ aka Wraitii ]
Wildfire Games Programmer, AI developer, auxiliary map designer, dealing with anything water.
Contact me: wraitii@wildfiregames.com

Also the world's only three-dimensional poodle.

#19 Yves

Yves

    Centurio

  • WFG Programming Team
  • 897 posts

Posted 23 May 2013 - 05:45 PM

I haven't found the problem yet.

First I thought it could be caused by JIT code being thrown away by the garbage collector and then being regenerated over and over again. I found a switch that apparently turns off gargabe collection of JITed code completely. It increased performance a bit, but that's barely visible on the graph (total_keepjit/bright blue).

Another Idea I had was that the wrapper values for sharedAI and gamestate passed to the AI cause overhead each time they are accessed. The new Spidermonkey doesn't support multiple global objects per compartment or per runtime anymore, so the gamestate and the sharedAI object can't be passed directly to each AIPlayer as we did with 1.85. This means we have to pass wrapper objects that allow them to be accessed through compartment boundaries.
For testing I changed the code to copy the gamestate to each AIPlayer's compartment at the beginning of the AI computations. My theory was that this would cause overhead at the beginning of the simulation turn but If the wrapper acces made a significant difference, it should speed up those turns that contain a lot of wrapper access and therefore the pieks on graph should become smaller.
As you can see (total_copy_state/red) the performance decrease is relatively even accross all turns. My conclusion is that wrapping doesn't have a significant impact on performance.

I also tested how fast it is with methodJIT completely disabled (total_nojit/yellow).
If you compare it to the previous graph (total_new/green), you see that methodJIT improves the overall simulation performance by about 20-35% in this case (I just picked 3 points in the graph). This at least proves that JS execution performance can make a difference and motivates me to try V18 with IonMonkey. :)

I also played a bit with valgrind but didn't really come to a conclusion there.
The next test will be with qbot because it doesn't use the shared API and therefore needs less compartment switching/wrapping.

graph2.gif

Thanks for the graph link. Definitely needed on wiki. Can I just at least copy/paste it there, even if not exhaustive, that help when searching for it ?

Yes you can copy it to the wiki if you want. There's already an outdated profiling wiki page.

In CCmpRangeManager: ExecuteQuery, ResetActiveQuery, GetEntitiesByPlayer, etc.
All those methods do uses std::vector from and to js, and are called very frequently.

Thanks for the hint. For the moment I focus on my changes that could have caused the measurable performance difference. Most of what I changed is in the GUI where it doesn't affect the graph I posted and I didn't touch the simulation. But that's something I will also check when this problem is solved.

Exactly. Basically it handles a lot of maps in one go, and JS is not exactly perfect to do that. I'm considering switching some stuffs to c++ as part of improving the AI. I want however to be sure that the added cost of memory transfers are not bigger than the increase in computation speed.

A guy on the Mozilla #jsapi channel posted this recently:
https://gist.github....idereit/5616961
He also describes some situations where moving code to JS actually improves performance.
The only way to figure it out is probably testing.
  • 0

#20 Yves

Yves

    Centurio

  • WFG Programming Team
  • 897 posts

Posted 23 May 2013 - 08:49 PM

Non-Visual replay with qbot:
./pyrogenesis -quickstart -autostart="Oasis 04" -autostart-ai=1:qbot -autostart-ai=2:qbot -autostart-ai=3:qbot -autostart-ai=4:qbot
graph3_qbot.gif

Random map generation:
./pyrogenesis -autostart=alpine_lakes -autostart-random=123 -autostart-size=256

Spidermonkey 1.85 (3 runs)
TIMER| LoadRMS: 35.2256 s
TIMER| LoadRMS: 35.5012 s
TIMER| LoadRMS: 35.4555 s

Spidermonkey 17: (3 runs)
TIMER| Load RMS: 36.8751 s
TIMER| Load RMS: 37.1415 s
TIMER| Load RMS: 36.8051 s

... I start considering that it really simply is slower :(
Maybe I should just try V18.
  • 0