Using multiple CallbackProperty: kills performance (drops to Zero FPS) [code repaired]

I am currently visualizing the air pollution of the Earth using Cesium.
On each country of the world, I place a cylinder (or a polygon extrusion of the country’s surrounding).
I have a timeline from 1970 to 2020, and the height of the cylinders (or the polygon extrusions) is set to the
value of the air pollution of each country in the current selected year in the timeline.
This looks fine so far, but if I press “Play” in the lower left “Animate” Widget, I get big performance problems
as the time flows through the years.
The FPS are 7 to 23 FPS, the FPS value is jumping very much around. All entities seem to update their height at random
points in time, in a big chaos, the timeline is stuttering, …

In my first attempt, I was setting the properties directly in all the (195) entities:

Cylinder:
    var viewerEntity = _viewer.entities.getById(ID_OF_COUNTRY);
    viewerEntity.cylinder.length = VALUE_OF_THE_CURRENT_YEAR;
Polygon:
    _dataEntities.forEach(function (entity)
        entity.polygon.extrudedHeight = VALUE_OF_THE_CURRENT_YEAR; 

I already tried out “requestRenderMode”, (see Improving Performance with Explicit Rendering – Cesium)
with no better experience:

_viewer = new Cesium.Viewer('cesiumContainer',
	    requestRenderMode: true,
	    maximumRenderTimeChange: Infinity
	});

I am already using “suspendEvents”, so that all changes are done in ONE step, and one after the other for each single entity:

// Turn "rendering" off:
        _viewer.entities.suspendEvents();
        _dataSourceDisplays.forEach(function (dataSourceDisplay) {
            dataSourceDisplay.dataSources._dataSources[0].entities.suspendEvents();
        });           
// Turn "rendering" on:
        _viewer.entities.resumeEvents();
        _dataSourceDisplays.forEach(function (dataSourceDisplay) {
            dataSourceDisplay.dataSources._dataSources[0].entities.resumeEvents();
        });

OK, then I found a forum entry that said I should use a PropertyCallback for changing entity properties.
Okay, I do not need ONE callback, but 195 (one for each country on the Earth),
but this should be possible with passing an argument to the callback function:
https://groups.google.com/g/cesium-dev/c/b3qcnbzCMxY

But then, using a callback for 195 entites dropped my framerate to 0 FPS!

So,
I now create a very simple example so that you can reproduce it easily:
Please paste the following code into the Cesium sandbox ajnd try it.

If you try it with 1 cylinder (see numberOfEntitiesHori and numberOfEntitiesVert), you will get probably 55 FPS.
Try it with 5 * 5 = 25 cylinders, you will get around 8 FPS.
Try it with 20 * 10 = 200 cylinders (this is my case), you will get 0 FPS.
Looks like the callback function is called too often so that the performance breaks down.

My questions are now:

Am I doing right to switch to CallbackProperty to change the properties of my 195 cylinders / polygon extrusions during runtime?
If yes, how can I make my simplified example (and in the end - my own application) have a good performance?

Can I reduce (or start/stop) the callback function calling frequency of the CallbackProperty?
“requestRenderMode: true” doesn’t seem to affect that, as you can try out in my example.

Thank you
and all the best
Manuel


// Create a viewer
var viewer = new Cesium.Viewer("cesiumContainer", 
  {
     // requestRenderMode tries to speed up performance / getting control over the rendering:
     requestRenderMode: true,
	 maximumRenderTimeChange: Infinity // Sekunden
  }
);

// show FPS
viewer.scene.debugShowFramesPerSecond = true;

// Select here, how many cylinders to display
// In the end, I need e.g. 20 x 10 = 200 cylinders!
var numberOfEntitiesHori = 5;
var numberOfEntitiesVert = 5;

// Create (for example 200) cylinders:

var i, j;
for(i = 0 ; i < numberOfEntitiesHori; i++){
  for(j = 0; j < numberOfEntitiesVert; j++){  
    viewer.entities.add({
      position: Cesium.Cartesian3.fromDegrees(i*2, j*2, 200000.0),
      cylinder: {
        // attach the length to the callback function
        length: new Cesium.CallbackProperty(getSingleCallbackFunction, false),
        topRadius: 60000.0,
        bottomRadius: 60000.0,
        outline: true,
      },
    });
  }
}

viewer.zoomTo(viewer.entities);

// The callback function:
// Imagine that I assign here the new cylinder height for each cylinder (country) for the currently selected year in the timeline.
// But to show the performance problem, it is already enough to return here one simple value for all cylinders for all years:
function getSingleCallbackFunction() {
        return 800000;
}

My App:

Dear Cesium team,

I guess you all are currently quite busy…
But did you copy and paste my example code into Sandcastle?
Did you reproduce the performance drop to approx. 0 FPS?

Thank you and
all the best
Manuel

Dear Cesium Team,

sorry to bother you again and again with this post, but could you please share your thoughts about any one of my questions?

Thank you and
all the best
Manuel

Dear Cesium team,

Did you copy and paste my example code into Sandcastle and try it out?

Thank you and
all the best
Manuel

Hey Manuel,
I am not part of the Cesium team. I have the same problem. Did you ever find a solution?

There are currently some issues open regarding the requestRenderMode.
Quoting @Gabby_Getz from requestRenderMode property and CallbackProperty function conflicts occurred. · Issue #11315 · CesiumGS/cesium · GitHub :

The API expects non-constant callback properties to be updated every frame, hence request a new frame to render. There would need to be a mechanism in place to determine whether the value has changed or not.

Interestingly, you don’t use the SampledPositionProperty like in Entity primitive continuously re-drawn when position is SampledProperty · Issue #6631 · CesiumGS/cesium · GitHub and still get the issue. This comment (Terrain clamped Entities and Primitives containing polygons defeat RequestRenderMode · Issue #10872 · CesiumGS/cesium · GitHub) suggests that Request render mode was broken in v1.96 · Issue #10756 · CesiumGS/cesium · GitHub might the issue here, but that was apparently fixed, yet I still reproduced your problem in version 1.113 Sandcastle.

I guess, depending on how often you update the cylinder heights, a manual update would be better here, instead of using the CallbackProperty.

Well, I do not remember how, but yes, the performance was OK in the end in my app.

I can just copy-and-paste my function here, where I update the length of my cylnders and polylines, maybe it helps…

I just set:
viewerEntity.cylinder.length = valueScaled;


// Aktualisert den Zustand, welche Höhe die Objekte (Balken, Extrusionen, …) aktuell haben müssen // ------------------------------------------------------------------------------------------------------------------------------------------
function updateDataHeightAndColor() {

var gregorianDate = Cesium.JulianDate.toGregorianDate(_viewer.clock.currentTime);

if (gregorianDate.year == _lastUpdateDate)
    return; // Nichts machen, es ist nicht genug Zeit auf der Timeline vergangen - es ist noch dasselbe Jahr

_lastUpdateDate = gregorianDate.year;
console.log("updateDataHeightAndColor:" + _viewer.clock.currentTime);

var suspendEvents = true;

if (suspendEvents) {
    // Schalte Rendering/Update aus
    _viewer.entities.suspendEvents();
    _dataSourceDisplays.forEach(function (dataSourceDisplay) {
        dataSourceDisplay.dataSources._dataSources[0].entities.suspendEvents();
    });
}

updateDataVisibility();

updateGeometryProperties(_totalDataIndex, true, false, false);
updateGeometryProperties(_selectedDataIndex, false, true, true);

if (suspendEvents) {
    // Schalte Rendering/Update ein
    _viewer.entities.resumeEvents();
    _dataSourceDisplays.forEach(function (dataSourceDisplay) {
        dataSourceDisplay.dataSources._dataSources[0].entities.resumeEvents();
    });
}

// Explicitly render a new frame
//_viewer.scene.requestRender();

};

function updateGeometryProperties(dataIndex, updatePolygons, updateBars, updateSphere) {

var valueScaled = 0;

if (_dataEntities[dataIndex] != null) {
    _dataEntities[dataIndex].forEach(function (entity) {
        if (entity.properties.timeseries) {
            var value = interpolateDataValue(_viewer.clock.currentTime, entity.properties.timeseries._value);

            if (_checkboxLogarithmicScaling && _checkboxLogarithmicScaling.checked) {
                // Logarithmische Anzeige
                // valueScaled = Math.log10(value) / Math.log10(_dataEntities[dataIndex].groupMaxValue); // Wert ist nun zwischen 0.0 und 1.0;
                valueScaled = Math.sqrt(value) / Math.sqrt(_dataEntities[dataIndex].groupMaxValue); // Wert ist nun zwischen 0.0 und 1.0;
            }
            else // normale, lineare Anzeige
            {
                var valueScaled = value / _dataEntities[dataIndex].groupMaxValue; // Wert ist nun zwischen 0.0 und 1.0;
            }

            valueScaled = Math.min(Math.max(0, valueScaled), 1); // Clamp zur Sicherheit
            var color = createHeatMapColor(valueScaled); // Wert ist zwischen 0.0 und 1.0;
            valueScaled *= (6371 * 1000 / 2.0); // Maximalwert in Metern, z.B. Erdradius: 6371*1000

            // TODO: negative Werte mit Farbe anzeigen?
            if (valueScaled < 0)
                valueScaled = -valueScaled;

            var alpha = 0.9 * 255; // Transparenzwert der Polygone

            var colorInactive = Cesium.Color.fromBytes(127, 127, 127, alpha);
            var valueInactive = _extrusionsBaseHeight * 1.3;

            if (value == null || value == 0)  // Keine Daten vorhanden 
                valueScaled = valueInactive;

            valueScaled = Math.max(valueScaled, valueInactive); // Die Balken werden nie kleiner als die (sich nicht in der Höhe ändernden) Extrusionen, damit sie anklickbar bleiben

            // setze Höhe der Länder-Extrusion
            if (updatePolygons && entity.polygon != null && entity.polygon.extrudedHeight != null && entity.show) {
                // entity.polygon.extrudedHeight = valueScaled;
                if (value == null || value == 0) { // Keine Daten vorhanden -> inaktiv darstellen
                    if (_useColoring)
                        entity.polygon.material = colorInactive;
                }
                else {
                    if (_useColoring)
                        entity.polygon.material = color;
                }
            }

            // setze Höhe des Balkens / der Kugel

            var viewerEntity = _viewer.entities.getById(entity.properties.code._value);
            if (viewerEntity != null) {
                if (viewerEntity.show) {

                    if (updateBars) {
                        if (viewerEntity.cylinder) {
                            if (value == null || value == 0) { // Keine Daten vorhanden -> inaktiv schalten
                                viewerEntity.cylinder.length = valueScaled;
                                if (_useColoring) {
                                    viewerEntity.cylinder.material = colorInactive;
                                }
                            }
                            else {
                                if (_useColoring)
                                    viewerEntity.cylinder.material = color;
                            }
                        }

                        if (viewerEntity.polyline) {

                            var posFloor = Cesium.Cartesian3.fromDegrees(entity.properties["center-lon"]._value, entity.properties["center-lat"]._value, 0);
                            var posTop = Cesium.Cartesian3.fromDegrees(entity.properties["center-lon"]._value, entity.properties["center-lat"]._value, valueScaled);

                            viewerEntity.polyline.positions = [posFloor, posTop];

                            if (value == null || value == 0) { // Keine Daten vorhanden -> inaktiv schalten
                                if (_useColoring) {
                                    //viewerEntity.polyline.material = colorInactive;
                                }
                            }
                            else {
                                if (_useColoring) {
                                    //viewerEntity.polyline.material = color;
                                }
                            }
                        }

                        if (viewerEntity.label) {
                            var gregorianDate = Cesium.JulianDate.toGregorianDate(_viewer.clock.currentTime);

                            var valueToDisplay;
                            if (value != null)
                                valueToDisplay = value.toFixed(2);
                            else
                                valueToDisplay = "???";

                            viewerEntity.label.text._value = viewerEntity.name + ": " + valueToDisplay + " " + entity.unit + " (" + gregorianDate.year + ")";
                        }

                    }

                    if (updateSphere) {
                        if (viewerEntity.ellipsoid) {
                            var radius = 6371000 + valueScaled; // Erdradius plus Wert
                            _targetSphereRadius = new Cesium.Cartesian3(radius, radius, radius);

                            if (value == null || value == 0) {  // Keine Daten vorhanden -> inaktiv schalten                                   
                                if (_useColoring)
                                    viewerEntity.ellipsoid.material = colorInactive;
                            }
                            else {
                                if (_useColoring)
                                    viewerEntity.ellipsoid.material = color;
                            }
                        }
                    }
                }
            }
            else
                console.log("viewerEntity not found!: " + entity.properties.code._value);
        }
    });
}

}

1 Like