Alexander Lohnau

Improving the qcolor-from-literal Clazy check

For all of you who don't know, Clazy is a clang compiler plugin that adds checks for Qt semantics. I have it as my default compiler, because it gives me useful hints when writing or working with preexisting code. Recently, I decided to give working on the project a try! One bigger contribution of mine was to the qcolor-from-literal check, which is a performance optimization. A QColor object has different constructors, this check is about the string constructor. It may accept standardized colors like “lightblue”, but also color patterns. Those can have different formats, but all provide an RGB value and optionally transparency. Having Qt parse this as a string causes performance overhead compared to alternatives.

Fixits for RGB/RGBA patterns

When using a color pattern like “#123” or “#112233”, you may simply replace the string parameter with an integer providing the same value. Rather than getting a generic warning about using this other constructor, a more specific warning with a replacement text (called fixit) is emitted.

testf4ile.cpp:92:16: warning: The QColor ctor taking RGB int value is cheaper than one taking string literals [-Wclazy-qcolor-from-literal]
        QColor("#123");
               ^~~~~~
               0x112233
testfile.cpp:93:16: warning: The QColor ctor taking RGB int value is cheaper than one taking string literals [-Wclazy-qcolor-from-lite
        QColor("#112233");
               ^~~~~~~~~
               0x112233

In case a transparency parameter is specified, the fixit and message are adjusted:

testfile.cpp:92:16: warning: The QColor ctor taking ints is cheaper than one taking string literals [-Wclazy-qcolor-from-literal]
        QColor("#9931363b");
               ^~~~~~~~~~~
               0x31, 0x36, 0x3b, 0x99

Warnings for invalid color patterns

Next to providing fixits for more optimized code, the check now verifies that the provided pattern is valid regarding the length and contained characters. Without this addition, an invalid pattern would be silently ignored or cause an improper fixit to be suggested.

.../qcolor-from-literal/main.cpp:21:28: warning: Pattern length does not match any supported one by QColor, check the documentation [-Wclazy-qcolor-from-literal]
    QColor invalidPattern1("#0000011112222");
                           ^
.../qcolor-from-literal/main.cpp:22:28: warning: QColor pattern may only contain hexadecimal digits [-Wclazy-qcolor-from-literal]
    QColor invalidPattern2("#G00011112222");

Fixing a misleading warning for more precise patterns

In case a “#RRRGGGBBB” or “#RRRRGGGGBBBB” pattern is used, the message would previously suggest using the constructor taking ints. This would however result in an invalid QColor, because the range from 0-255 is exceeded. QRgba64 should be used instead, which provides higher precision.


I hope you find this new or rather improved feature of Clazy useful! I utilized the fixits in Kirigami, see https://invent.kde.org/frameworks/kirigami/-/commit/8e4a5fb30cc014cfc7abd9c58bf3b5f27f468168. Doing the change manually in Kirigami would have been way faster, but less fun. Also, we wouldn't have ended up with better tooling :)

Cleaning up KDE's metadata – the little things matter too

Lots of my KDE contributions revolve around plugin code and their metadata, meaning I have a good overview of where and how metadata is used. In this post, I will highlight some recent changes and show you how to utilize them in your Plasma Applets and KRunner plugins!

Applet and Containment metadata

Applets (or Widgets) are one of Plasma's main selling points regarding customizability. Next to user-visible information like the name, description and categories, there is a need for some technical metadata properties. This includes X-Plasma-API-Minimum-Version for the compatible versions, the ID and the package structure, which should always be “Plasma/Applet”.

For integrating with the system tray, applets had to specify the X-Plasma-NotificationArea and X-Plasma-NotificationAreaCategory properties. The first one says that it may be shown in the system tray and the second one says in which category it belongs. But since we don't want any applets without categories in there, the first value is redundant and may be omitted! Also, it was treated like a boolean value, but only "true" or "false" were expected. I stumbled upon this when correcting the types in metadata files.
This was noticed due to preparations for improving the JSON linting we have in KDE. I will resume working on it and might also blog about it :).

What most applets in KDE specify is X-KDE-MainScript, which determines the entrypoint of the applet. This is usually “ui/main.qml”, but in some cases the value differs. When working with applets, it is confusing to first have to look at the metadata in order to find the correct file. This key was removed in Plasma6 and the file is always ui/main.qml. Since this was the default value for the property, you may even omit it for Plasma5.
The same filename convention is also enforced for QML config modules (KCMs).

What all applets needed to specify was X-Plasma-API. This is typically set to "declarativeappletscript", but we de facto never used its value, but enforced it being present. This was historically needed because in Plasma4, applets could be written in other scripting languages. From Plasma6 onward, you may omit this key.

In the Plasma repositories, this allowed me to clean up over 200 lines of JSON data.

metadata.desktop files of Plasma5 addons

Just in case you were wondering: We have migrated from metadata.desktop files to only metadata.json files in Plasma6. This makes providing metadata more consistent and efficient. In case your projects still use the old format, you can run desktoptojson -i pathto/metadata.desktop and remove the file afterward.
See https://develop.kde.org/docs/plasma/widget/properties/#kpackagestructure for more detailed information. You can even do the conversion when targeting Plasma5 users!

Default object path for KRunner DBus plugins

Another nice addition is that “/runner” will now be the default object path. This means you can omit this one key. Check out the template to get started: https://invent.kde.org/frameworks/krunner/-/tree/master/templates/runner6python. DBus-Runners that worked without deprecations in Plasma5 will continue to do so in Plasma6! For C++ plugins, I will make a porting guide like blog post soonish, because I have started porting my own plugins to work with Plasma6.


Finally, I want to wish you all a happy and productive new year!

Reworking Recent Files search for Plasma 6

For Plasma 6, lots of KRunner plugins and framework functionalities were improved. I took quite a bit of time to work on the recent files search plugin used by KRunner and Plasmas application launchers like Kickoff. This included performance improvements, usability improvements and technical refactorings.

Let's start off with the usability improvements: While the runner was named “Recent Files”, it still provided results for directories. In Dolphin on the other hand, “Recent Locations” are a separate location you may access next to “Recent Files”. Luckily, functionality for only querying files already exists in the KActivities-Stats framework :).
The search for files was also improved. Natalie Clarius added some time ago a patch to avoid false positives when the query is part of the file path. For example, when you type for “myfile” and “/home/user/myfiles/test” was a recent file, this file would be returned by the KActivities-Stats framework. With Natalie's logic, those files would not be shown in KRunner, but still they fill out the limit of recent files we want to query. Of course, one could look for more files in case we discarded too many, but that can cause a performance penalty due to additional SQL queries.
Instead, I have added the ability to filter by the filename in KActivities-Stats. This means false positives are always avoided, and you will for sure get the results you are looking for!

Another useful, but not yet user-facing change is special handling for queries shorter than 3 characters. For those queries, a substring check would cause too many unintended results to be useful. Instead, the filename must start with your given query. Similar to how the Applications-runner handles it. There are plans to make this feature more easily accessible, but for now, it can only be done on the command line using krunner --runner=krunner_recentdocuments.

Example of single-plugin mode
Screenshot of KRunner in single-plugin mode with the recent files plugin

The final issue that everyone will benefit from being fixed is a memory leak. This means for each letter typed, the runner would allocate a bit more RAM for results from the query. This can add up over time in KRunner and Plasmashell. The KActivities-Stats that were “leaking” also caused CPU overhead, because they were notified about changes to the recent files list – even though they were never used again.
Luckily, you don't have to worry about that at all from now on :).

Optimizations

Let us start with the small improvements: In my previous posts, I mentioned that constructing a QueryMatch object before being sure that the current item (application or systemsettings module) matches, causes a performance overhead. The same applied to the recent files plugin, because we discarded some files due to the false positive detection mentioned above.
Getting the filename was also optimized/simplified, because instead of using Qt API to get the filename from the URL, we can just use the resource title from KActivities-Stats.
String comparisons were speed up by reusing the results. For example, when checking if the filename contains the query, we can get the resulting index. If that index is 0, the query is contained and the filename also starts with it. Meaning we'd only do one string comparison for each recent file instead of up to 4.

Getting the appropriate icon is also way faster, because previously a method from KIO was used, which gets the file path, checks some extra cases, but usually gets the icon for the determined mime type. Because the extra cases were not relevant when only allowing files, we can directly get the icon for the respective mimetype. Luckily, we don't even need to determine the mimetype, because the model already contains it.

Reusing previously fetched data

This is the most interesting part and was my original idea to improve performance: In KDE Frameworks 6, each runner lives in its own thread. Meaning, one may access member variables in a thread safe way. The data we want to reuse is the KActivities-Stats ResultsModel. This contains paths and additional metadata of recently used files. The number of files is limited to 20, which is the maximum number of entries is KRunner.
Implementing was straightforward: If one previously typed “firef” and after that “firefox”, the previous model can be reused as long as the limit of 20 did not exceed the previously found results. In addition to reusing the data, it is required to check the filenames again, because a file called “firef.test” should not match a query called “firefox”. Due to the preexisting logic to determine the match relevance, this is minimal additional work.
When measuring the real-world impact, it is important to note that fetching results from KActivities SQLite database is the most expensive part. Accessing the model is relatively cheap. Meaning the longer your queries are and if long as the number of results was not exceeded, your performance gains will be significant!

Unoptimized Profiling of the unoptimized version, loading the correct Icon takes a large porting of CPU cycles Optimized Optimized version, CPU cycles are significantly reduced and loading of icons is barely distinguishable from other, minor costs

In benchmarks, one is able to see that the CPU cycles with this patch are roundabout the same among the different measurements, whereas the CPU cycles before this patch correlated with the length of the query. The query was typed in letter by letter, keep in mind that the runner skips queries shorter than 3 characters unless the single runner mode is activated. For benchmarks, I decided against activating this mode, because it is not the normal usecase.

Query CPU Cycles before CPU Cycles with change Reduction
mytextfile 1.73E+10 3.592E+09 79%
user 8.291E+09 3.148E+09 62%
firefox 1.325E+10 3.601E+09 73%

Web-Search-Keywords in KRunner, tales of optimizations

In my last blog post about performance in KRunner I wrote about the applications and system settings runners. I did lots of further investigation and work, some of which is still in progress and not merged yet.
When looking at the benchmark results in the amazing tool Hotspot, I noticed that operations in the “Web Search Keywords” runner were surprisingly heavy in terms of CPU usage.

Looking at the graph, one could see there being two nearly identical callstacks that are responsible for parsing desktop files, which contain the search keywords, texts, and URLs. This of course doesn't really make sense, because they were not changed while benchmarking. The issue was quite simple: When the class where the parsed desktop files are stored is instantiated, the desktop files are loaded. In the method to load configuration, we read some settings like the default shortcut, but also reload the keywords. And due to the config being loaded after the class is created, it is done twice.
The solution is trivial: Suppress the parsing when loading the config for the first time. In case we read the config a second time, it is due to us having changed settings or have added new keywords. Then we of course want to reload all entries.

Profiling excerpt from the match-method, which produces results for your typed query

The other, far more tricky issue was that while we were only supposed to have one central engine for interacting with the web search keywords, we in fact had two of them! Meaning, while we have halved the times the desktop files are loaded, it is still done twice needlessly.

Profiling excerpt from all plugins being loaded (main thread)

After some debugging, I noticed that the class was compiled into two different plugins. Surprising is that the QGLOBALSTATIC macro does not really result in a global singleton, because it is being compiled into different plugins. Once that was clear, the fix was straightforward: Create a small helper-library that includes the necessary files and use this library in the two plugins. This also means that we don't build the containing classes twice, which should decrease build times.
Loading the keywords took ~90 milliseconds on my system. On systems with a weaker CPU and without a SSD, this would take even longer. Because the loading was done twice in the main thread, KRunner startup should be a bit less sluggish.

Hopefully you enjoyed this read, and I am eager to further work on optimizing KRunner & Frameworks.

Profiling & Optimizing KRunner

One central topic of this year's Akademy was energy efficiency and performance of software. I took this occasion to give KRunner another look in regard to profiling, because the multithreading refactor simplified lots of plugin code and allowed for more optimizations.

When I did some benchmarking around two years ago, one of my major performance surprises was the windowed widgets runner. This runner queried all available applets for each letter typed. https://invent.kde.org/plasma/plasma-workspace/-/merge_requests/322/ reused the queried metadata for the duration of the match session.
When I created the “Systemsettings” runner plugin, I took the same approach toward reusing data. But with some better understanding of performance and improved KRunner APIs, I took another stance at improving this and the “Applications” runner. I choose these two, because they are one of the most central plugins for the search experience. Also, the underlying data that is queries (config modules and applications) is quite large.

For the applications runner, I already did an optimization to reuse the loaded apps during the lifetime of the match session around a month ago. All the described improvements are based on top of that work. My approach was to benchmark the runner using hotspot, look at the results and identify unexpectably expensive calculations.

In both plugins, I noticed that checks for an application being authorized or not were surprisingly expensive. In systemsettings, we did the check twice, once by appending the old KF5 “.desktop” postfix to the plugin ID. This could simply be removed in KF6. For the applications runner, the check was done inside KService to determine if the application is hidden. With KF6, the concept of ServiceTypes has gone away, and thus we don't need this check anymore (https://invent.kde.org/frameworks/kservice/-/merge_requests/154).
While cleaning up the visibility check in KService provided faster results, it is even better to check the visibility once we load the applications and not when we filter multiple times for each letter typed.

There were also some pretty significant gains in the systemsettings runner by reducing lookups of the KPluginMetaData JSON object. For example, the user-visible name can be read once inside the loop and then stored as a simple variable. Another issue was that we created a QueryMatch object and assigned some properties, before checking if the given config module matches the query. This was a simple fix and avoided lots of unneeded object creations and temporary memory allocations.

Some other smaller improvements were:

  • Systemsettings runner:
    • Reusing calculated values, like the query being split in individual words
    • Only checking the name for short queries, this also avoid results to appear “random”
  • Applications runner:
    • Also avoid creating unneeded QueryMatch, though this affected far fewer cases
    • Reduce double writes/lookups for internal de-duplication check
    • Inline some trivial, internal methods
    • Use temporary variables for KService properties when we need it as a variable in the current scope anyway (avoiding usage of iterator on temporary)
    • Remove unneeded lambdas for simple filtering. This was a leftover for when we used KApplicationTrader for each letter typed
    • Remove unneeded X-KDE-More check, we don't use this in any meaningful way
    • Remove check if app may be shown on current platform. In case this check would return false, the app would be considered hidden.

What it accomplished

Now you know about the technical changes, let's look at the actual performance measurements! In the original merge requests, I have benchmarked using a test application that is part of the KRunner repo. Since that required me manually typing though, I have later on created a tool for automatically running queries.

Systemsettings runner: The total CPU cycles for matching and querying of the config module were reduced by around 30%. If one only looks at the CPU cycles for matching, the reduction was around two thirds. This means that data initialization was slightly improved, but is still the heaviest part. But once you have typed the first letter and the data is loaded, querying KRunner or the application launcher until it is closed is a lot more efficient!

BEFORE:
before.png AFTER:
after.png

Applications runner: The total CPU cycles were reduced by 24% and unlike the other runner, data initialization for the match session took slightly longer. This is due to the check if the app should be shown or not being made during loading and not filtering. But partly due to this tradeoff, querying is almost 60% faster!

The real-world gains depend on the user behavior. For the system settings runner, I have queried “theme” and emulated closing the launcher. For the applications runner, I have used “firefox” instead. In case you have a longer query, the optimizations would come more into play.

BEFORE:
before.png AFTER:
after.png

Besides improving performance, I also made sure to simplify and clean up the code. Compared to the previous state, the changes have removed 40 lines in total. There is for sure room for further optimizations, but I intentionally decided against changes that would cause a complexity vs. performance tradeoff.

I hope you enjoyed this read!

My first in-person Akademy: Thessaloniki 2023

This year, I was finally able to participate in-person at Akademy. Apart from meeting some familiar faces from the Plasma Spring in May this year, I also met lots of new people.

When waiting for the plane in Frankfurt, a group of KDE people formed. Meaning, we had a get-together even before the Akademy had started ;). On the plane to Thessaloniki, I made a merge requests to fix a Kickoff crash due to a KRunner change. Once that was done, everything was in place for the talks!

On Saturday, I talked once again with Nico and also Volker about KF6. This included topics like the remaining challenges, the estimated timeline for KF6 and some practical porting advice. I also gave a talk about KRunner. This was the conference talk of mine that I gave alone, meaning I was a bit nervous 😅. The title was “KRunner: Past, Present, and Future” and it focused on porting, new features and future plans for KF6. Thanks to everyone who was listening to the talk, both in person and online! Some things like the multithreading refactoring are worth their own blog post, which I will do in the next weeks.

The talks from other community members were also quite interesting. Sometimes it was hard to decide to which talk to go :). Multiple talks and BoFs were about energy efficiency and doing measurements. This perfectly aligned with me doing benchmarking of KRunner and the KCoreAddons plugin infrastructure.

hotel view The view of the city from the hotel balcony was also quite nice

Our KF6 and Qt6 porting BoFs were also quite productive. On Tuesday, we had our traditional KF6 weekly. Having this in person was definitely a nice refreshment! Apart from some general questions about documentation and KF6 Phabricator tasks, we discussed the release schedule. The main takeaway is that we want to improve the release automation and have created a small team to handle the KDE Frameworks releases. This includes Harald Sitter, Nicolas Fella, David Edmundson and me. Feel free to join the weeklies in our Big Blue Button room https://meet.kde.org/b/ada-mi8-aem at 17:00 CEST each Tuesday.

Since we had so many talented KDE people in one place, I decided to have a KRunner BoF on Tuesday morning. Subject of discussion was for example the sorting of KRunner, how to better organize the categories and the revival of the so-called “single runner mode”. This mode allows you to query only one specific plugin, instead of all available ones. This was previously only available from the D-Bus interface, but I have added a command line option to KRunner. To better visualize this special mode being in use, a tool-button was added as an indicator. This can also be used to go back to querying all runners. Kai will implement clickable categories that allow you to enable this mode without any command line options being necessary!

Finally, I would like to thank everyone who made this awesome experience possible! I am already looking forward to the next Akademy and the next sprints.

In Plasma5, we had different sorting implementations for KRunner and Kicker. This had historical reasons, because Kicker only used a subset of the available KRunner plugins. Due to the increased reliability, we decided to allow all available plugins to be loaded. However, the model still hard-coded the order in which the categories are displayed. This was reported in this bug which received numerous duplicates.

To address this concern, I focused on refactoring and cleaning up KRunner as part of KDE Frameworks 6. Among the significant architectural changes was the integration of KRunner's model responsible for sorting into the KRunner framework itself. This integration enabled easier code sharing and simplified code maintenance. Consequently, the custom sorting logic previously present in Kicker could be removed.

Further plans

Now you know some of the improvements that have been done, but more interesting might be the future plans! While the sorting in KRunner was in lots of regards better than the one from Kicker, it still has some flaws. For instance, tweaking the order of results from a plugin developer's perspective proved challenging, since rearranging categories could occur unintentionally. Also, KRunner implements logic to prioritize often launched results. In practice, this did not work quite well, because it only changed one of two sorting factors that are basically the same (sounds messy, I know :D).

The plan is to have two separate sorting values: One for the categories and one for the results within a category. This allows KRunner to more intelligently learn which categories you use more the most and prioritize them for further queries.

Another feature request to configure the sorting of plugins. With the described change, this is far easier to implement. Some of the visuals were already discussed at the Plasma sprint last month.

Stay tuned for updates!

Reviving and reworking the KRunner help

Currently KRunner does not provide any usage information itself, there is only an bit of documentation on the user base wiki. Back in the KDE4 days KRunner had help button, but fundamental changes in the architecture meant that the code could not simply be ported to the current version. KDE4 Krunner usage help Since the help system required reimplementation, it was decided to implement it as a plugin for KRunner's powerful plugin infrastructure. This plugin produces results when one types ? or help. To make this more discoverable a button is added, which puts the text ? in the search field. new help funtionality By having it as a plugin it is reusable in every case KRunner is used, for example the KWin overview effect, the Application Launcher or the Plasma-Mobile search.

But it is not only under the hood different from the KDE4 version – there are quite a few changes to improve the usability:

For example only one usage example is displayed for each runner. This way one can get a better overview over the different runners.

Also, runners can display their description instead of the first possible usage. For example the sessions-runner can log out, suspend, switch user or reboot the PC. This would be too much information for the simple overview. Which is why the description is shown instead.

If the plugin has a configuration module, you can launch it as an action. configure action Otherwise you would have needed to click the configure button on the left, search for the runner and then click the configure button for the specific runner.

When one click on of a match or selects it and presses enter, a detailed page of all the available usages of this runner is displayed: detailed help info

Here you get all the available usage information displayed. For getting a better overview, the queries are marked in bold. Internally, this uses the styled text from Qt and every runner which has multiline text can utilize this feature.

When one runs one of the matches, the suggested query gets put in the KRunner search field. The placeholder text is selected so that you can immediately overwrite it with your query, still get a little hint what the runner expects. autocompleted text

Hopefully you like this feature and can be even more productive with KRunner :–)

In case you have developed a KRunner plugin yourself, check out the docs: for DBus runners or for C++ plugins.