Idling around with Cesium renderer – part 2

As mentioned in part 1, Labels and Billboards in Cesium are not as efficient as the rest of the Graphics arsenal.  We saw how scripting takes too much of our thread just by showing a few thousand billboards and labels. In part 1, we mentioned that the issue might arise from looping over a few thousands of billboard and label Graphics.  We looked at the BillboardVisualizer.update method and the homework was to think about a solution.

Now I will show a proof of concept for a solution.  We will start small and enhance it.  Ready? Here it goes.

The problem with the loop is that it is wasteful. It runs over the billboards and labels – even those that were not updated. So what if we could tell Cesium to skip billboards and labels that we didn’t update?

Let’s see one way to do it. In our app’s code, we will add something to the entity:

function randomCoordinateJitter(degree, margin) {
    return degree + margin * (Math.random() - 0.5) / 0.5;
}

var viewer = new Cesium.Viewer('cesiumContainer');
for (var i = 0; i <= 15000; i++) {
    var entity = viewer.entities.add({
        position : Cesium.Cartesian3.fromDegrees(
            randomCoordinateJitter(-77.009003, .5),
            randomCoordinateJitter(38.889931, .5)
        ),
        label : {
            text : 'CUTE #' + i
        },
        billboard : {
            height : 24,
            width : 36,
            image : 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRhFpaxDXCS5O9hx90F3ufJI2VnC_wW0lPnrr6BIb18P4V5JXxBCg'
        },
        ellipse : {
            semiMajorAxis : 100,
            semiMinorAxis : 80
        }
    });
    entity.updateOnce = 1;
}

Note the line: entity.updateOnce = 1;

So far so good… but – how can Cesium know about this parameter?  Now we are going to do something scary – get into Cesium’s code.  Fork, clone, create a new branch “optimized labels and billboards” and let’s open the Entity.js, BillboardVisualizer.js and LabelVisualizer.js files for edit.

In Entity.js, we would like to define the updateOnce property. Take a look at the code here:

https://github.com/YonatanKra/cesium/blob/labels-and-billboards-optimization/Source/DataSources/Entity.js

Look at the commit in order to see the differences. This is a Cesium technical issue. Note the tests I’ve added are definitely not complete…

The main take home message here is this – when we change the entity’s updateOnce property, we also change the Label and Billboard updateOnce property.  This means we need to define the Label and Billboard Graphics properties:

Click here to see the commit

While we could have used a regular updateOnce property, it seems that Cesium convention is to use setters and getters for properties – hence the usage of getters and setters.

Another good point might be the suggestion to use prototypical inheritance (PI).  There’s a lot of boilerplate code in the Cesium “classes” that can be improved with PI.

Next – we would like our Visualizers to use this new updateOnce property in the Visualizer update loop. Ready? Let’s go! Here are the changes:

In LabelVisualizer.js, I’ve added these lines to the code of the update method, just below the init of labelGraphics inside the loop.

if (labelGraphics.updateOnce === 2) {
    continue;
}

if (labelGraphics.updateOnce === 1) {
    labelGraphics.updateOnce = 2;
}

The lines inside the loop actually tell Cesium – if I say something has changed (updateOnce === 1), change the flag to 2 and update.  Next time the renderer comes back here, it would not update this graphic, because its value would be 2 (unless the user changed it along the way).

This way, if nothing happens – do nothing!

Click here to see the full commit

In your Cesium clone, run the combine command (gulp combine or npm run combine) in order to generate a new Cesium build to test in your code. Take it from the Build/CesiumUnminified folder.

Now let’s run the performance recording with the new code:

Figure 1: Summary of recording after optimization. Recording took 20.43 seconds, and idle time is more than 19 seconds.
Figure 2: The summary before the optimization.

 

 

 

 

 

 

See the difference above? While before optimization we had roughly 50% of the time as idle time, now we have around 19/20 of the time as idle time… that’s a big improvement.  Let’s deep dive and compare the methods we’ve just changed:

Figure 3: Deep dive after optimization. Notice that the visualizers we took care of took 85.6ms and 63.9md out of the 20 seconds of recording.

If we look at the results from part 1, we can definitely see the difference:

Figure 4: deep dive into functions time consumption before the optimization. Note that our updaters took roughly 4.5 and 3.2 seconds… that’s a lot of time out of 19 seconds recording.

In figure 4, we see the results from part 1.  The updaters we optimized took a very long time. In figure 3, we see that the time for these functions dropped dramatically and freed our browser to do its work.

Now this implementation gives us some more power than mentioned here.  It allows us to update only a certain graphic in an entity, if the others haven’t changed (for instance – if a label changed, but the billboard didn’t).  Here’s an example:

// inside some loop
entity.label.text = 'New text';
entity.label.updateOnce = 1;

The code above would make sure that the label will be updated, but the other graphics would remain untouched, since they were not changed in any way.

Summary and discussion

In part 1 we saw how to measure performance and pin down key points in our performance problem.  We tracked the problem till we found the culprits – the billboard and label visualizers.  In part 2, we saw an idea of how to optimize the visualizers in order to free up scripting time and turn it into idle time.  We actually allowed a developer to tell Cesium when something has changed, and prevented Cesium from going over entities that did not need to change.

Note: As mentioned above, this solution is a POC only.  This is not a PR or a recommnded way to change Cesium. Some more work needs to be done in order to make this a generic solution.

Some ideas how to make this change PR worthy:

  1. Testing – complete testing and make sure old tests work (some of the old tests might need to be changed).
  2. Make sure dynamic properties change as well (for instance, just by reading the code, it seems that time dependent or callback properties would not update…).
  3. Generalize the solution for all Graphics (would probably add only a minor improvement).
  4. The current implementation requires the user to notify Cesium about changes.  This mechanism could be inside the Cesium graphics set mechanism itself to make things more transparent for the user.

Some ideas for future work:

  1. Optimize the scene.render method
  2. Consider using WebWorker in order to render the scene and build the shaders etc.

In conclusion – Cesium is a very big library, and is very generic.  The Cesium team did and is doing a lot of good work adding features and taking care of performance optimization.  Even though – due to the size of Cesium, it is hard to get to all edge cases.  What can you do to help?

  1. You could use the tools given here to track down optimization issues – you don’t need to fix them, just report them in the forum or github.
  2. You can try to implement some of the ideas above or your own as a POC – and publish your results in Github/Forum in order to get help or reviews implementing a full solution (just create your own branch, and people would be able to play with it and make their own experiments).

If you do something interesting in that field, or have an idea you’d like to share or get help implementing, feel free to comment below.

Leave a Reply