Howto:Canvas Path Benchmarking

From FlightGear wiki
Revision as of 13:09, 30 October 2016 by Hooray (talk | contribs) (→‎Ideas)
Jump to navigation Jump to search
This article is a stub. You can help the wiki by expanding it.
Screenshot showing a Canvas GUI dialog with 3000 random OpenVG paths (line segments) drawn (for benchmarking purposes) [1]

Motivation

1rightarrow.png See Shuttle ADI ball for the main article about this subject.

Assume you have one array (Nasal vector) you have written into a path, now you want to replace the path element by one corresponding to the second array. [2]

Code

###
# Based on, and adapted from: 
# http://wiki.flightgear.org/Canvas_Snippets#Adding_OpenVG_Paths

var (width,height) = (640,480);
var title = 'Canvas.Path.setData() test';

var window = canvas.Window.new([width,height],"dialog").set('title',title);

var myCanvas = window.createCanvas().set("background", canvas.style.getColor("bg_color"));

var root = myCanvas.createGroup();

var graph = root.createChild("group");

var x_axis = graph.createChild("path", "x-axis")
.moveTo(10, height/2)
.lineTo(width-10, height/2)
.setColor(1,0,0)
.setStrokeLineWidth(3);

var y_axis = graph.createChild("path", "y-axis")
.moveTo(10, 10)
.lineTo(10, height-10)
.setColor(0,0,1)
.setStrokeLineWidth(2);


var plot = graph.createChild("path", "plot")
.moveTo(10, height/2)
.setColor(0,1,0)
.setStrokeLineWidth(0.5);

var randomPoints = func(total=3000) {
window.set('title',title~" points="~total);

var cmds = [];
var points = [];

for(var i=0;i<=total;i+=1) {
 append(cmds, canvas.Path.VG_LINE_TO);
 var x = 10+rand() * (width-10) + rand()*5;
 var y = 10+rand() * (height-10) + rand()*8;
 append(points, [x,y]);
 # print("x/y: ", x, y);
}
return {cmds:cmds, points:points};
}


var setData_test = func(data) {
plot.setData( data.cmds, data.points);
}



var updateCallback = func(max_points=5500) {
 var totalPoints = rand() * max_points;
 var myPoints = randomPoints(totalPoints);
 debug.benchmark("setData() test, points:"~totalPoints, func setData_test(myPoints) );
}
# http://wiki.flightgear.org/Built-in_Profiler#Nasal


canvas.InputDialog.getText("Canvas.Path Benchmark", "Max number of points ?", func(btn,value) {
    if (value)  {
    fgcommand("profiler-start");
    var updateTimer = maketimer(0.5, func updateCallback(max_points:int(value)) );
    updateTimer.start();
    settimer(func updateTimer.stop(), 30);

window.del = func()
{
  print("Cleaning up window:",title,"\n");
  updateTimer.stop();
  call(canvas.Window.del, [], me);
  fgcommand("profiler-stop");
};
}
});

Ideas

it's really the amount of property accesses that contribute to the performance issue[3]


api.nas:

 # Set the path data (commands and coordinates)
  setData: func(cmds, coords)
  {
    me.reset();
    me._node.setValues({cmd: cmds, coord: coords});
    me._last_cmd = size(cmds) - 1;
    me._last_coord = size(coords) - 1;
    return me;
  }
Note  This should be really straightofrward to reimplement as part of nasal-props.cxx in C++ (~50 LOC) using the APIs documented at Howto:Extend_Nasal

props.nas:

##                                                                             
# Useful utility.  Sets a whole property tree from a Nasal hash                
# object, such that scalars become leafs in the property tree, hashes          
# become named subnodes, and vectors become indexed subnodes.  This            
# works recursively, so you can define whole property trees with               
# syntax like:                                                                 
#                                                                              
# dialog = {                                                                   
#   name : "exit", width : 180, height : 100, modal : 0,                       
#   text : { x : 10, y : 70, label : "Hello World!" } };                       
#                                                                              
Node.setValues = func(val) {                                                   
    foreach(var k; keys(val)) { me._setChildren(k, val[k]); }                  
}                                                                              
                                                                               
##                                                                             
# Private function to do the work of setValues().                              
# The first argument is a child name, the second a nasal scalar,               
# vector, or hash.                                                             
#                                                                              
Node._setChildren = func(name, val) {                                          
    var subnode = me.getNode(name, 1);                                         
    if(typeof(val) == "scalar") { subnode.setValue(val); }                     
    elsif(typeof(val) == "hash") { subnode.setValues(val); }                   
    elsif(typeof(val) == "vector") {                                           
        for(var i=0; i<size(val); i+=1) {                                      
            var iname = name ~ "[" ~ i ~ "]";                                  
            me._setChildren(iname, val[i]);                                    
        }                                                                      
    }                                                                          
}

props.Node is pretty bad, and passing separate arguments to getprop() is as good (if not better) than passing one argument (without concatenating it, i.e. scalar constant)[4]

The point was just that there's TONS of data in the property tree in cases like KNUQ (1200+ paths).

And I used props.copy() as a test case to see how long it takes in Nasal space to write so many properties to the tree, because that's exactly what the code is doing to: Setting 1200+ paths in the canvas.

So I wanted to see how long this operation takes in and of itself.[5]


Extend nasal-props.cxx to implement props.setValues() and props.setChildren() in C++ (or directly use Nasal/CppBind).

cppbind used for Nasal/C++ bindings, but probably it'll be better to port the whole file at once). One thing I don't like is the signature of getValue and similar methods. I'd keep it similar to the core SGPropertyNode and don't allow creating new nodes with eg. getValue but instead return a default value (which can be passed as default argument). Otherwise, having relative paths is definitely a useful addition.[6]

getprop/setprop are known for not just being "quick & dirty", but also for outperforming the props.nas magic performance-wise, including the Nasal ghosts, which is plausible - because of the API and stack machine overhead. Andy himself repeatedly mentioned that several times in the past, e.g. see: http://www.mail-archive.com/flightgear-devel@lists.sourceforge.net/msg12222.html Also, Thorsten has previously run benchmarks to compare props.nas vs. setprop/getprop.

Like Tom mentioned previously, it would probably make sense to eventually port the whole props.nas stuff to use cppbind and then proceed from there, that should leave little Nasal-space overhead, and once everything is in C++ space, it will be straightforward to see how to optimie things there using the google perftools fgcommands. Performance-wise, many fgcommands and Nasal extension functions show up - even without necessarily invoking the GC. And the constant string building is a known issue, but there are certainly workarounds or special-case optimizations possible, even if it's just a simple space/time tradeoff in the form of a cached property tree. [7]

we should not provide the wrappers in props.nas (which are naCodes) and instead directly port the C++ extensions (naCCodes) to live directly in the object? That could be done using the naRef me and a hash lookup for "_g" (which should be interned and/or cached just like arg and parents are). I'm not sure I understand your last paragraph though...[8]

I think we've been tossing with the idea of using Tom's cppbind framework to update/modernie the props.nas/props.cxx stuff - and it would obviously also help with Canvas related stuff where lots of property tree I/O is a factor (i.e. taxiways layer) - so given that 3.0 is now out, this could be a good opportunity to re-implement props.nas on top of cppbind - indeed, it would be interest to benchmark a test case first - in theory - once implemented in native code, a cached property.setValue() call should be faster than plain setprop("foo", value). [9]

Related

References

References
  1. https://forum.flightgear.org/viewtopic.php?f=71&t=30864&hilit=removeAllChildren
  2. Thorsten  (Oct 30th, 2016).  Re: Canvas ADI ball (shuttle) / circular clipping .
  3. Hooray  (Sep 20th, 2012).  Re: Using a canvas map in the GUI .
  4. Philosopher  (Feb 20th, 2014).  getprop() and .
  5. Hooray  (Sep 20th, 2012).  Re: .
  6. TheTom  (Apr 10th, 2013).  Re: Relative paths for Nasal .
  7. Hooray  (Apr 15th, 2013).  Re: Relative paths for Nasal .
  8. Philosopher  (Apr 17th, 2013).  Re: Relative paths for Nasal .
  9. Hooray  (Feb 20th, 2014).  Re: getprop() and .