Delete the QQmlApplicationEngine?2020-08-120
How I tried to delete
QQmlApplicationEngine for less memory usage
Qml manages to separate UI and backend logic, one can adjust c++ code without breaking UI, or develop different front end without implementing same logic twice. As plasma mobile developers, we can utilize the flexibility of Qml and Kirigami framework. Making apps that look equally beautiful on mobile and desktop platform. However, the high memory usage is its major drawback. On a device like pinephone which only has 2 Gigs of ram, low memory consumption is crucial.
KClock is the default clock/alarm app for plasma mobile. It needs to run in background all time otherwise the we can’t ring alarm for you and you may wake up late. The problem is you’ll also want a front end UI to add/remove alarms. And that’s exactly the problem we faced.
On x86_64 platform, KClock consumes 48 MiB private memory and 95 MiB shared memory. While on ARM this would be lower but not too much. We decide that for an app that merely remember when to ring alarm, this is unacceptable. So at first, we skipped
QQmlApplicatoinEngine construction if you pass
--daemon flag on launch. This effectly disables the UI creation and we managed to archieve 6.6 MiB memory usage.
But we’ll have to consruct UI when user decide that he/she/they would like to bring up the front end. And when the last window closed, the memory allocated for UI isn’t freed. You don’t want to lose 40 MiB of ram just to open clock once, do you? Naturally, we want to destroy the
QQmlApplicationEngine once last window closed.
So I connected the QApplication::lastWindowClosed signal to a lambda that calls
delete engine. Now we have problems, not only the memory usage didn’t change, there is also possible crash on second launch. So what’s happening here?
To understand what’s going on, you’ll first learn how Qml works. Qml file is used to describe the parent/child relation between UI objects, when you call
QJSEngine which is a parent class of QQmlApplicationEngine. For some reasons, the JS engine does not free the heap when it’s being destroyed,
and will trys to reuse that JS heap next time it’s re-constructed. So if you destroy - re-create the JS engine, you’re walking on the eggshell. Fortunately, or rather unfortunately, KClock didn’t crash. So we thought our job is done and merged the changes at then. It’s not util I tried to use the same technic on KWeather that I found out the former method was wrong. KWeather crashes everytime I re-constuct the JS engine. And that’s because the KWeather trys to re-use the in
memory js cache.
So how you safely destroy the JS engine? The answer lies in Qt’s documentation. QJSEngine offers
CollectGabarge() which runs JS gabarge collector. In our case, this will collect Qml components that has been destroyed or don’t have a parent. However, we can’t just simply call this function because our components are visual elements and the parent will keeps its children alive. That means we need to destroy the component explicitly.
The Qml follows Qt’s parent/children memory management model, once you destroyed the parent, its children are gone, too. The Qml
Loader comes in handy, set its
source property to empty string and the previously loaded components are destroyed. To sum the things up, in order to reclaim the memory from JS engine, one have to do the following things:
lastWindowClosedsignal to a function that sets Loader’s source to empty string
- once the loader’s source cleaned, call gabarge collector.
- destroy the Engine
- after the engine is destroyed, call
malloc_trim(0)to hand the memory from heap back to system
Due to how Qt’s signal/slot works, transform the above steps to actual code proves complicated and I ended up with some awkward code. But I did manage to delete the JS engine entirely.
The best way
You may wonder, why Qt doesn’t provide a way to destroy JS engine easily? Well, the truth is,
QQmlApplicationEngine is not designed to be deleted. When you find you have to use some “hacky” ways to do things in programming, often it’s your architecture is wrong.
Make each program do one thing well.
Here I’m bundling two programs into one program, one daemon that keeps track of alarm, one that as an interface between user and the daemon. The best solution here is to develop two separate programs. And that’s exactly what I’m doing now. Since system reclaim all the memory from program on its exit, I don’t need to care about deleting the JS engine and all sorts of headache, system will do it for me.
Despite this means re-writing some code and generally more work for me, I’m happy with the learning experience and (hopeful) the result.
And to those of you who don’t bother reading and drag directly to the bottom, the anwser is “don’t delete