Thursday, 27 August 2026
This is the second half of what changed in Dolphin 26.08. The first half is what you can see: the features, the behaviour changes, the bugs. This one is what you can measure.
Dolphin does very little file work itself. It asks KIO, so the numbers below are mostly KIO numbers: 6.25 is the version that shipped alongside Dolphin 26.04, 6.29 is the one that ships alongside 26.08, four framework releases and 294 commits apart. The last section is 6.30, the release still to come, since several of its changes are large enough to be worth showing early.
Every figure here was measured on the same machine, with the two versions built the same way and run one after another, round by round. Where a number is not solid I say so rather than rounding it up.
Memory and object lifetimes
The 26.04 post ended by saying the next thing was to point LeakSanitizer at Dolphin. It turned into a lot of fixes, and into a CI job: KIO now runs its tests under LeakSanitizer on every merge request, which is what stops the list below from growing back.
Sebastian Englbrecht was the busiest contributor to Dolphin this cycle by commit count, and
most of that work was lifetime and ownership: killing in-flight KIO
jobs
in destructors, deleting the Konsole
part,
destroying owned objects before the KIO cache tears
down
and fixing unowned
allocations.
He also added a smoke
test,
eight places-panel unit
tests,
and a rule forbidding bare
QTest::qWait()
with the existing waits replaced by signal-based
ones,
which is the sort of thing that makes the rest of the CI trustworthy. Wendi Gan fixed an
occasional use-after-free crash
in KConfig::sync() during exit, closing two reports,
516481 and
518433.
My own share was in both projects. In Dolphin: aborting the folder stat job when the
Information Panel is destroyed,
a use-after-free of the current version-control plugin,
a leaked submenu in the folder-icon action
and a leak in the trash settings page on exit.
In KIO, besides turning the sanitiser on in CI and teaching the tests to wait for what they
allocate to be deleted,
three fixes are ones a user could have met: the "Move Into New Folder" drop action had done
nothing at all since 6.25, because the menu was parented to a plugin destroyed the moment the
action fires
while the folder creation it starts runs asynchronously; FilePreviewJob armed a repeating
timeout timer it never stopped,
so with a context menu held open the orphaned timer could fire on a finished job and re-enter
emitResult(); and KDirModel dereferenced the node of a directory that had already left the
model
when an earlier listing completed late.
Performance improvements in KIO between 6.25 and 6.29
The big one is the in-process worker transport. For local files KIO has for some time run the
file worker in a thread of the application rather than a separate process. Even in
that thread it still talked to the application over a socket, serialising every command and every
block of file content through a QLocalSocket pair. In 6.29 that channel is a new
ThreadConnectionBackend, an in-memory queue, and kio_file hands over owned byte arrays instead of copying them, which makes in-process reads zero-copy. This is the
first half of the improvements described in
the copy post from July.
Second, CopyJob no longer re-probes the destination filesystem type for every file. It called KFileSystemType::fileSystemType() per file for the FAT and NTFS checks, and on libmount builds each of those calls parses
the entire mount table. Copying N files parsed /proc/self/mountinfo N times. It is
determined once now. Each parse is about 57 microseconds on a host with 39 mounts,
and it scales with the number of mounts.
Third, and this is the memory one, the directory lister cache got much smaller.
A directory that no lister was showing any more went into a cache of ten and stayed
there until nine others had displaced it. For a picture folder of 50000 files that
is a lot of KFileItem and UDSEntry retained for nothing. The cache now holds
three directories, which is what going back a level or two actually needs, and drops
anything no lister has asked for in three minutes.
Fourth, UDSEntry got smaller, which saves memory. An
entry now keeps its numbers and its strings in two separate vectors,
which buys a byte for every field an entry holds. And loading now sizes each
vector from what the entry actually
holds:
both vectors used to be sized from the count of fields alone, a third of it for the
strings and two thirds for the numbers. A stat of a local file gives one string, the name,
and eight numbers, so the numbers grew past their room while the strings kept more than
they needed. That sizing change on its own takes a listing of 200000 local files from 528 bytes
an entry to 400, which is 105.6 MB down to 80 MB, 25.6 MB saved. Loading also stopped hunting for a shared
value on
the fields where values cannot repeat: no two entries of a listing carry the same name, url or
local path, so comparing them with the entry before never found anything to share. Reading one
entry of a local folder off the wire went from 530 to 488 nanoseconds.
Smaller ones worth naming: KFileItem no longer reads .directory on slow filesystems when working out an icon name
(6.28, closing bug 519189), Sebastian
Englbrecht fixed QPluginLoader, QLibraryPrivate and thread lifecycle leaks in the worker machinery (6.28).
Nineteen reports were closed by fixes in KIO across those four releases. Most of them are filed against the applications rather than against KIO, since that is where a user meets the problem: Dolphin, Plasma, Konsole, KWin.
Benchmarks
Methodology
I built KIO 6.25.0 and 6.29.0 from their tags, Release on gcc 16.2.1 and Qt 6.11.1, and ran the same harness against each. The arms are interleaved one round at a time, so a CPU frequency or load excursion hits both equally, and the best of N rounds is reported.
Everything ran on one 13th Gen Core i7-1365U with 30 GB of RAM, kernel 7.1.4, on ext4, with the benchmark processes pinned to the same two cores.
Copying
KIO::copy() of N files into an empty directory, timed from job start to the
result signal. Source files are created untimed, the destination is cleared
untimed. cp -r is there as a raw-tool floor, not as a target.
Copying many small files is more than twice as fast as it was in April. The gain falls off as files get larger, which is what you would expect: the fix is to the per-file overhead, and once each file carries a megabyte of actual I/O the overhead stops being what you are waiting for.
There is still a gap with cp, discussed at length in
the July post. KIO is doing more than cp does, but not
five times more, and the batching work that closes most of the rest of that gap is still in
progress.
Deleting
KIO::del() over a freshly created tree, best of three rounds. rm -rf is the raw-tool
floor here, as cp -r is above, and it is handed the same list of paths the job is given
rather than the folder that holds them.
Deletion improves less than copying, as expected: a delete carries no file content, so the zero-copy half of the transport work does nothing for it. What is left is the cheaper per-command round trip, 1.23x on the smaller tree and 1.25x on the larger one.
rm -rf is six times quicker than 6.29 on both trees, a wider gap than copying shows
against cp. That fits: a copy at least spends real time moving bytes, where a delete is
almost nothing but the per-file round trip, so what KIO adds is most of what there is to
measure.
How the job is asked matters as much as the count. These runs hand KIO::del() the 5000
files one by one, the way a select-all in a file manager does. Handing it the one folder
instead takes about a fifth of that on the same filesystem, because the worker then walks
the tree itself instead of taking a command per file. rm given the folder rather
than the 5000 paths goes from 69.4 ms to 66.8, which is the difference the filesystem
charges for the two shapes. The rest of it is KIO's. That second path is the one the 6.30
section below measures.
Listing
One KCoreDirLister over an existing directory. The cold number is the first
listing in a fresh process. The warm number is a later listing of the same
directory, which KCoreDirListerCache answers without going to disk. The second chart is the resident growth
while the lister is still showing the folder, which is what a file manager displaying that
folder actually costs.
Listing is about 9 percent quicker on the 5000-file folder and 15 percent on the 50000-file
one, and holds about 14 percent less memory in both cases. The memory part is the UDSEntry
work described above. The warm listing is unchanged either way, 2.6 ms against 2.8 ms on the
5000-file folder and 37 ms on the 50000-file one, which is the cache answering rather than
the disk.
Memory kept for folders you have left
Of everything measured here, this is what a long-running Dolphin notices most. Walk twelve directories one after another, the way you do going down and back up a tree, and then look at how much memory the process is still holding. Nothing is on screen in either case, so all of it is held for folders nobody is looking at any more.
In 6.25 a directory nothing was showing any more sat in a cache of ten and stayed until nine others displaced it. Browse through a few large folders and you are holding all of them. The cache now keeps three, and drops anything no lister has wanted for three minutes. Repeat listings do not suffer for it, which is what the warm listing above shows: re-opening a folder you just left still finds it. What the eleventh slot bought was going eleven folders back, and every user of a large folder paid for it.
6.30 improvements
None of this has shipped, so treat it as a preview rather than a result. 6.30 has several commits that touch exactly what this post measures, so I built it as a third arm and ran the same harness.
Most of what 6.30 gains comes from one change: handing over what a message carries as it is
when the worker runs in the application's own
process.
kio_file runs in a thread of the application, but used to write its data down with
QDataStream the way a process-based worker has to. Except it does not need to. Avoiding that
serialisation is where the fewer allocations come from, and the entries cost less memory
besides, since one no longer reserves room for fields most files never carry. Nothing changes
for a worker in another process, which still has a socket between it and the application.
The other major improvement was a dormant issue. It had been there for years, and benchmarking made it visible: 50000 files listed in 1233 ms while 5000 listed in 36, 10 times the files taking 34 times as long, where listing should be close to linear.
A profile of the large case revealed 42 percent of the whole listing is KFileItem copy
construction and destruction, all of it under KCoreDirListerCache::DirItem::insertSortedItems
calling QList::reserve once per batch of entries. Except reserve has no relocatable
fast path: on the growth path it allocates exactly the size asked for and copy-appends
every element already there, even for a type declared Q_RELOCATABLE_TYPE as KFileItem. Since the
previous call had sized the buffer exactly, every batch reallocated and copied the whole
list. Heaptrack counted 239 such calls for 50000 files, so about six million copy
constructions for fifty thousand items.
Deleting the reserve call and
letting the list grow geometrically takes that listing from 1233 ms to 531 ms, over 2x
quicker. What cost the time was inside those reallocations, copy-constructing every
KFileItem already in the list, and a KFileItem copy is a refcount bump that allocates
nothing at all. So none of it showed up in an allocation count, which is why it took a
cycles profile to find.
Deleting a folder gained from
af3d9e0e7, which removes a tree with the
system's own calls instead of walking it, and how much depends on the shape of
the tree. openat and unlinkat work from a descriptor for the directory an entry was
read from, so they stop resolving a whole path for every entry, and the deeper the tree
the more that is worth. Fifteen thousand files on tmpfs, deleted as one folder, with
rm -rf over the same tree as the floor:
6.29 costs more the deeper the tree, four times as much at 800 levels as at ten. 6.30 does
not care how deep it is, and stays within a third of rm -rf at every depth, which is about
as close as a job that reports progress and can be cancelled is going to get to a tool that
does neither.
Closing
The copy path still has more in it than 6.29 ships: the batching that closes most of the
remaining gap to cp is written and measured in Making KIO copy many
files fast and is not finished yet. The listing
path has an open question of its own, which is that an entry could carry its fields more
cheaply again; a branch that does it trades about two percent of listing time for eleven
percent of the memory an entry holds, and which of the two is the better trade depends on
how many entries you are holding.
Enjoy the speed.








".
).
.




