[frameworks/kio] /: kdirlister: hold three directories in the lister cache, and not for long

Méven Car <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 5e36acccb3ec5ed5e1d049e697c6639a7f54bc65 by Méven Car.
Committed on 30/07/2026 at 09:20.
Pushed by meven into branch 'master'.

kdirlister: hold three directories in the lister cache, and not for long

A directory that no lister holds any more went into a cache of ten, whatever it held, and stayed there
until nine others had come and gone. An application that shows a listing of 50000 files, as a picture
folder is, held the items of ten of them: near 500 MB of KFileItem and UDSEntry that nothing on screen
was showing any more.

The cache now holds three directories, which is what going back a level or two asks of it, and drops one
that no lister has wanted for three minutes. What is held for the way back is held for the time a way
back takes.

The lifetime is the one thing a test cannot wait for, so it says how long to keep a directory while
BUILD_TESTING is set, and the cache follows it.

M  +28   -2    autotests/kdirlistertest.cpp
M  +1    -0    autotests/kdirlistertest.h
M  +43   -3    src/core/kcoredirlister.cpp
M  +11   -0    src/core/kcoredirlister_p.h

https://invent.kde.org/frameworks/kio/-/commit/5e36acccb3ec5ed5e1d049e697c6639a7f54bc65

diff --git a/autotests/kdirlistertest.cpp b/autotests/kdirlistertest.cpp
index 40d7853f93..d45472757a 100644
--- a/autotests/kdirlistertest.cpp
+++ b/autotests/kdirlistertest.cpp
@@ -1722,10 +1722,36 @@ void KDirListerTest::testCacheEviction()
         QVERIFY(KDirWatch::self()->contains(newDirPath));
     }
 
-    // watches were removed as the dirItem were evicted from cache
+    // watches were removed as the dirItem were evicted from cache, which keeps three directories
     QVERIFY(!KDirWatch::self()->contains(newDir.path()));
     QVERIFY(!KDirWatch::self()->contains(newDir.path() + QString("dir_0")));
-    QVERIFY(KDirWatch::self()->contains(newDir.path() + QString("dir_1")));
+    QVERIFY(!KDirWatch::self()->contains(newDir.path() + QString("dir_1")));
+    QVERIFY(KDirWatch::self()->contains(newDir.path() + QString("dir_10")));
+}
+
+void KDirListerTest::testCacheExpiry()
+{
+    qputenv("KIO_DIRLISTER_CACHE_LIFETIME_MS", "300");
+    auto restore = qScopeGuard([]() {
+        qunsetenv("KIO_DIRLISTER_CACHE_LIFETIME_MS");
+    });
+
+    QTemporaryDir newDir(homeTmpDir());
+    const QString cachedDirPath = newDir.path() + QString("cached");
+    QVERIFY(QDir().mkdir(cachedDirPath));
+
+    MyDirLister dirLister;
+    dirLister.openUrl(QUrl::fromLocalFile(cachedDirPath));
+    QVERIFY(dirLister.spyCompleted.wait(500));
+    QVERIFY(KDirWatch::self()->contains(cachedDirPath));
+
+    // Leaving the directory puts it in the cache, where it is kept for as long as the lifetime this
+    // test asked for when it set KIO_DIRLISTER_CACHE_LIFETIME_MS.
+    dirLister.openUrl(QUrl::fromLocalFile(newDir.path()));
+    QVERIFY(dirLister.spyCompleted.wait(500));
+    QVERIFY(KDirWatch::self()->contains(cachedDirPath));
+
+    QTRY_VERIFY_WITH_TIMEOUT(!KDirWatch::self()->contains(cachedDirPath), 5000);
 }
 
 void KDirListerTest::testUnreadableParentDirectory()
diff --git a/autotests/kdirlistertest.h b/autotests/kdirlistertest.h
index 5813126205..5edcb485c0 100644
--- a/autotests/kdirlistertest.h
+++ b/autotests/kdirlistertest.h
@@ -112,6 +112,7 @@ private Q_SLOTS:
     void testMimeFilter();
     void testBug386763();
     void testCacheEviction();
+    void testCacheExpiry();
     void testUnreadableParentDirectory();
     void testPathWithSquareBrackets();
     void testSFTPRedirect();
diff --git a/src/core/kcoredirlister.cpp b/src/core/kcoredirlister.cpp
index ab0de89973..b9f7c8ba92 100644
--- a/src/core/kcoredirlister.cpp
+++ b/src/core/kcoredirlister.cpp
@@ -42,13 +42,25 @@ Q_LOGGING_CATEGORY(KIO_CORE_DIRLISTER, "kf.kio.core.dirlister", QtWarningMsg)
 
 QThreadStorage<KCoreDirListerCache> s_kDirListerCache;
 
+// A directory that no lister holds any more is kept for a while, so that going back to it is listed
+// from memory. Three of them cover the way back, and one that has not been wanted for this long is
+// dropped, so that the items of a listing left behind are not held for the life of the application.
+static constexpr int s_maxCachedDirectories = 3;
+static constexpr std::chrono::minutes s_cachedDirectoryLifetime{3};
+
+// Keep the last few directories' ".hidden" files around, so revisiting one does not re-read it.
+static constexpr int s_maxCachedDotHiddenFiles = 10;
+
 KCoreDirListerCache::KCoreDirListerCache()
-    : itemsCached(10)
-    , // keep the last 10 directories around
-    m_cacheHiddenFiles(10) // keep the last 10 ".hidden" files around
+    : itemsCached(s_maxCachedDirectories)
+    , m_cacheHiddenFiles(s_maxCachedDotHiddenFiles)
 {
     qCDebug(KIO_CORE_DIRLISTER);
 
+    cachedDirectoryLifetime = s_cachedDirectoryLifetime;
+    cachePruneTimer.setInterval(cachedDirectoryLifetime);
+    connect(&cachePruneTimer, &QTimer::timeout, this, &KCoreDirListerCache::pruneCachedDirectories);
+
     connect(&pendingUpdateTimer, &QTimer::timeout, this, &KCoreDirListerCache::processPendingUpdates);
     pendingUpdateTimer.setSingleShot(true);
 
@@ -533,7 +545,15 @@ void KCoreDirListerCache::forgetDirs(KCoreDirLister *lister, const QUrl &_url, b
     // Inserting into QCache must be done last, since it might delete the item
     if (item && insertIntoCache) {
         qCDebug(KIO_CORE_DIRLISTER) << lister << "item moved into cache:" << url;
+        item->lastWanted = std::chrono::steady_clock::now();
         itemsCached.insert(url, item);
+#ifdef BUILD_TESTING
+        // A test cannot wait minutes for a directory to be dropped, so it says how long to keep one.
+        const int ms = qEnvironmentVariableIntValue("KIO_DIRLISTER_CACHE_LIFETIME_MS");
+        cachedDirectoryLifetime = ms > 0 ? std::chrono::milliseconds(ms) : std::chrono::milliseconds(s_cachedDirectoryLifetime);
+        cachePruneTimer.setInterval(cachedDirectoryLifetime);
+#endif
+        cachePruneTimer.start();
     }
 }
 
@@ -667,10 +687,30 @@ KCoreDirListerCache::DirItem *KCoreDirListerCache::dirItemForUrl(const QUrl &dir
     DirItem *item = itemsInUse.value(url);
     if (!item) {
         item = itemsCached[url];
+        if (item) {
+            item->lastWanted = std::chrono::steady_clock::now();
+        }
     }
     return item;
 }
 
+void KCoreDirListerCache::pruneCachedDirectories()
+{
+    const auto now = std::chrono::steady_clock::now();
+    const QList<QUrl> cached = itemsCached.keys();
+    for (const QUrl &url : cached) {
+        const DirItem *item = itemsCached.object(url);
+        if (item && now - item->lastWanted >= cachedDirectoryLifetime) {
+            qCDebug(KIO_CORE_DIRLISTER) << "dropping" << url << "from the cache";
+            itemsCached.remove(url);
+        }
+    }
+
+    if (itemsCached.isEmpty()) {
+        cachePruneTimer.stop();
+    }
+}
+
 QList<KFileItem> *KCoreDirListerCache::itemsForDir(const QUrl &dir) const
 {
     DirItem *item = dirItemForUrl(dir);
diff --git a/src/core/kcoredirlister_p.h b/src/core/kcoredirlister_p.h
index 518f7fd60f..70fc8b1375 100644
--- a/src/core/kcoredirlister_p.h
+++ b/src/core/kcoredirlister_p.h
@@ -23,6 +23,8 @@
 #include <QTimer>
 #include <QUrl>
 
+#include <chrono>
+
 #include <KDirWatch>
 #include <kio/global.h>
 
@@ -257,6 +259,8 @@ public Q_SLOTS:
     void slotFileRenamed(const QString &srcUrl, const QString &dstUrl, const QString &dstPath);
 
 private Q_SLOTS:
+    // drops the cached directories that no lister has wanted for a while
+    void pruneCachedDirectories();
     void slotFileDirty(const QString &_file);
     void slotFileCreated(const QString &_file);
     void slotFileDeleted(const QString &_file);
@@ -393,6 +397,7 @@ private:
         DirItem(const QUrl &dir, const QString &canonicalPath)
             : url(dir)
             , m_canonicalPath(canonicalPath)
+            , lastWanted(std::chrono::steady_clock::now())
         {
             autoUpdates = 0;
             complete = false;
@@ -513,6 +518,9 @@ private:
         // the local path, with symlinks resolved, so that KDirWatch works
         QString m_canonicalPath;
 
+        // when a lister last held this directory or looked for it in the cache
+        std::chrono::steady_clock::time_point lastWanted;
+
         // KFileItem representing the root of this directory.
         // Remember that this is optional. FTP sites don't return '.' in
         // the list, so they give no root item
@@ -544,6 +552,9 @@ private:
     std::set<QString /*path*/> pendingDirectoryUpdates;
     // The timer for doing the delayed updates
     QTimer pendingUpdateTimer;
+    QTimer cachePruneTimer;
+    // how long a cached directory is kept, which a test shortens
+    std::chrono::milliseconds cachedDirectoryLifetime;
 
     // Set of remote files that have changed recently -- but we can't emit those
     // changes yet, we need to wait for the "update" directory listing.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.