Sunday, November 28, 2010

Faster cocos sprites

versión en castellano

In the last blog entry we seen that cocos 0.4.0 sprites were more slow than the pyglet 1.1.4 ones. Lets see if cocos code can be improved to close the performance gap.

Approaching the problem

Python includes the cProfile module which allows to measure how much time is spent in each callable, so I added a few lines in the scripts to generate profile data.

Running the scripts produces a lot of statistics; to explore these I used RunSnakeRun, a visualizer for cProfile stats. It represents the call tree as nested boxes, with box area proportional to time spent in the callable.

Playing a bit with the visualization options, I got two images that seems interesting; combined and annotated they look as:



What grabs a little my attention is the area covered by lambdas and _set_position in cocosnode.py

From where the lambdas come from ?

CocosNode objects have a number of python properties, like x, y, position, rotation and scale. Using w as a generic property name , the lambda appareance corresponds to the usage pattern
  • define methods _get_w and _set_w
  • define the property by
The reason behind that usage relates to inheritance:
  • without the lambda, if a subclass redefines _set_w, the property will not use the new version except if rebinded explicitly by a w = property(...)
  • with the lambda, if a subclass redefines _set_w, the property will use the new definition
Well, can it be that the overhead in lambdas account for the slowness ?
Time to change the code and see what happens.

First try

After branching the repo I begun to eliminate one or other lambdas; some changes gave slightly better performance, some others bigger gains but the balls stop moving.
The quantity of code is not too big, but the indirections are a bit too much to follow.

To understand better what should be happening, I rewrote the cocos Sprite methods eliminating the indirections and code duplications.
The test script works fine, and performance is par with pyglet sprites.

Second try

The first solution is fast but dirty: with the code consolidation we lose the separation of concerns between classes; any change in the base classes should be manually copied to the Sprite class.

From the different stages in the previous experiment, seems that the performance loss coming from lambda use is small, and that the problem can come from a double call to some not so light method.
What is needed here is to know exactly what methods are called.

sys.settrace to the rescue

Calling sys.settrace(my_spy_function) in a script will instruct the python interpreter to call my_spy_function whenever some events happen, like 'call', 'ret', 'line'; it will also pass a frame object with info about the current execution state.

Making my_spy_function filter events and synthesize some useful information we can get a clean picture of the call tree, restricted to the calls of interest.

This article helped a lot to write my custom spy function.

Spying with settrace, a pair of suspicious lines are confirmed to double call; when corrected the balls stops moving, but the trace tells which method was missing a call. Now it is easy to fix.

Measuring the performance gives:


The clean solution has 90% of pyglet performance, fairly better than the 60% of cocos 0.4.0

No comments:

Post a Comment