As Jetpack Compose is gaining traction, we plan to offer a specific implementation for our Kotlin Charting Library, Charts.kt.
Before starting to code it, we have to check if it’s a viable solution and if the rendering performances are already good enough.
This small post shows the resulting tests.
What to test?
The graphic needs for data visualization are relatively simple. You can do almost anything you want with rectangles, circles, and paths. Generally, rectangles and circles are directly available in the graphics API. You draw more complex shapes with paths.
So to test the rendering performances, we created a simple application that produces random particles to visualize on a canvas. Using different symbols and counts of particles, we can evaluate the rendering performance of Jetpack Compose Desktop.
We also implemented a JavaFX version for comparison.
Jetpack Compose Desktop
The code is available on GitHub: https://github.com/gzoritchak/compose-rendering
The test implementation uses the 0.4.0 version of JetPack Compose.
We focused our code on the rendering performances and not on the respect of Compose implementation patterns.
In particular, we limit the memory allocations. The particles are mutable classes that contain their position and speed. They are instantiated during startup and updated at each frame.
We then propose 3 different rendering of the particle’s position: a square, a circle and a diamond. The diamond is built through a path.
val particle = particles[id]
val x = xScale(particle.x).toFloat()
val y = yScale(particle.y).toFloat()
val color = Colors.Web.aliceblue.toColor()
when (renderingWith) {
RenderingWith.Square -> drawRect(
color = color,
Offset(x, y),
Size(10f, 10f)
)
RenderingWith.Circle -> drawCircle(
color = color,
radius = 5f,
center = Offset(x, y)
)
RenderingWith.Diamond -> drawPath(Path().apply {
moveTo(x, y + 5)
lineTo(x + 5, y)
lineTo(x, y - 5)
lineTo(x - 5, y)
close()
}, color)
}
See the resulting performances on my MacBook Pro (late 2013 2,3 GHz Intel Core i7).
Up to 10 000 particles, the rendering performs at 60 FPS using squares and circles. It then fell to approximately 30 FPS for 30 000 particles and 8-9 FPS for 100 000 particles. The first decrease with paths (diamonds) appears at 10 000 particles with a not stable 30 FPS. We can also observe some freeze.
Compose | 3000 | 10 000 | 30 000 | 100 000 |
---|---|---|---|---|
Square | 60 FPS | 60 FPS | 28 FPS | 8 FPS |
Circle | 60 FPS | 60 FPS | 31 FPS | 9 FPS |
Diamond | 60 FPS | 30 FPS | 10 FPS | 2 FPS |
Now let’s compare these performances with a settled framework.
JavaFX/TornadoFX implementation
We implemented a JavaFX version of this test application to have a comparison point.
The code is available on this GitHub project.
We render the particles through the Data2viz library, which is the current implementation for our charting library. Under the hood, the Data2viz library uses a JavaFX canvas.
Here is a screencast of the results using the same hardware:
We can observe that the rendering speed is double for squares and circles. The difference is weaker when we use paths.
JavaFX | 3000 | 10 000 | 30 000 | 100 000 |
---|---|---|---|---|
Square | 60 FPS | 60 FPS | 60 FPS | 20 FPS |
Circle | 60 FPS | 60 FPS | 60 FPS | 20 FPS |
Diamond | 60 FPS | 37 FPS | 13 FPS | 3 FPS |
Conclusion
Even if the performances are not as good as their JavaFX counterpart, they are already good enough for many visualization use cases.
Jetbrains builds Jetpack Compose Desktop on solid foundations. The works they are doing on integrating Skia in a Kotlin context is also promising for new usages (Swing, Headless).
Top comments (4)
You may be able to improve the performance of the diamond case in Compose by building a single path that doesn't depend on x and y, and instead position it using a translation transform on the Canvas.
@romainguy Thanks for the proposal.
But applying a translation leads to another drop of FPS.
New implementation:
Results:
Repeating what I mentioned on Slack: my suggestion was not to apply a translation to the Path object itself. Instead create one instance of the Path, and before drawing, translate the Canvas: canvas.withTranslate { drawPath(…) } or equivalent.
Sorry for the mistake; I read your message too fast.
I tried your suggestion with one path and moving the canvas before drawing it. It’s still a little bit under the first implementation performances but more stable (no more freezing).