[system/dolphin] src: KItemViews: store per-item role values in a flat SmallHash

Méven Car <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit f9488f64666466378fa8f45c125336367eff54a2 by Méven Car.
Committed on 25/07/2026 at 14:32.
Pushed by meven into branch 'master'.

KItemViews: store per-item role values in a flat SmallHash

Each item's role values were cached in a QHash<QByteArray, QVariant>. In Qt6
a QHash grabs a ~3 KB minimum span on the first insert, so one hash per item
- a handful of roles times thousands of items - dominates the model's memory
once the values are populated (scrolling a folder, or sorting by a role that
needs them).

Introduce SmallHash, a flat vector<pair<QByteArray, QVariant>> exposing the
subset of the QHash API used here, and make it the role-values type across the
item-views layer: KItemModelBase::data()/setData(), the widgets, the roles
updater and the Baloo roles provider. For the few entries per item a linear
scan is cheap and cache-friendly, and the span overhead is gone. As it is now
used beyond kitemviews it lives in src/smallhash.h.

The url is no longer stored per item (it is derivable from the KFileItem):
data() injects it and url(int) reads it straight from the item.

Measured on 100k items, per-item values drop from ~4.7 KB to ~2.1 KB (from
~3.9 KB to ~0.18 KB with only the name role); fill and sort are unchanged or
slightly faster.

M  +1    -1    src/kitemviews/accessibility/kitemlistdelegateaccessible.cpp
M  +1    -1    src/kitemviews/kfileitemlistview.cpp
M  +1    -1    src/kitemviews/kfileitemlistwidget.cpp
M  +1    -1    src/kitemviews/kfileitemlistwidget.h
M  +25   -21   src/kitemviews/kfileitemmodel.cpp
M  +7    -4    src/kitemviews/kfileitemmodel.h
M  +19   -20   src/kitemviews/kfileitemmodelrolesupdater.cpp
M  +2    -2    src/kitemviews/kfileitemmodelrolesupdater.h
M  +3    -3    src/kitemviews/kitemlistwidget.cpp
M  +5    -4    src/kitemviews/kitemlistwidget.h
M  +1    -1    src/kitemviews/kitemmodelbase.cpp
M  +3    -2    src/kitemviews/kitemmodelbase.h
M  +13   -13   src/kitemviews/kstandarditemlistwidget.cpp
M  +4    -4    src/kitemviews/kstandarditemlistwidget.h
M  +2    -2    src/kitemviews/private/kbaloorolesprovider.cpp
M  +2    -1    src/kitemviews/private/kbaloorolesprovider.h
A  +117  -0    src/smallhash.h     [License: GPL(v2.0+)]
M  +7    -7    src/tests/kfileitemmodeltest.cpp
M  +3    -3    src/tests/kitemlistselectionmanagertest.cpp
M  +1    -1    src/views/dolphinfileitemlistwidget.cpp
M  +2    -2    src/views/dolphinview.cpp
M  +1    -1    src/views/versioncontrol/versioncontrolobserver.cpp

https://invent.kde.org/system/dolphin/-/commit/f9488f64666466378fa8f45c125336367eff54a2

diff --git a/src/kitemviews/accessibility/kitemlistdelegateaccessible.cpp b/src/kitemviews/accessibility/kitemlistdelegateaccessible.cpp
index b50b71daf5..82bba1c5c6 100644
--- a/src/kitemviews/accessibility/kitemlistdelegateaccessible.cpp
+++ b/src/kitemviews/accessibility/kitemlistdelegateaccessible.cpp
@@ -130,7 +130,7 @@ QRect KItemListDelegateAccessible::rect() const
 
 QString KItemListDelegateAccessible::text(QAccessible::Text t) const
 {
-    const QHash<QByteArray, QVariant> data = m_view->model()->data(m_index);
+    const SmallHash data = m_view->model()->data(m_index);
     switch (t) {
     case QAccessible::Name: {
         return data["text"].toString();
diff --git a/src/kitemviews/kfileitemlistview.cpp b/src/kitemviews/kfileitemlistview.cpp
index 585a972a6e..495d6a1f30 100644
--- a/src/kitemviews/kfileitemlistview.cpp
+++ b/src/kitemviews/kfileitemlistview.cpp
@@ -204,7 +204,7 @@ void KFileItemListView::initializeItemListWidget(KItemListWidget *item)
     KStandardItemListView::initializeItemListWidget(item);
 
     // Make sure that the item has an icon.
-    QHash<QByteArray, QVariant> data = item->data();
+    SmallHash data = item->data();
     if (!data.contains("iconName") && data["iconPixmap"].value<QPixmap>().isNull()) {
         Q_ASSERT(qobject_cast<KFileItemModel *>(model()));
         KFileItemModel *fileItemModel = static_cast<KFileItemModel *>(model());
diff --git a/src/kitemviews/kfileitemlistwidget.cpp b/src/kitemviews/kfileitemlistwidget.cpp
index ccb1819af2..d2ef75dfdd 100644
--- a/src/kitemviews/kfileitemlistwidget.cpp
+++ b/src/kitemviews/kfileitemlistwidget.cpp
@@ -43,7 +43,7 @@ bool KFileItemListWidgetInformant::itemIsLink(int index, const KItemListView *vi
     return item.isLink();
 }
 
-QString KFileItemListWidgetInformant::roleText(const QByteArray &role, const QHash<QByteArray, QVariant> &values, ForUsageAs forUsageAs) const
+QString KFileItemListWidgetInformant::roleText(const QByteArray &role, const SmallHash &values, ForUsageAs forUsageAs) const
 {
     QString text;
     const QVariant roleValue = values.value(role);
diff --git a/src/kitemviews/kfileitemlistwidget.h b/src/kitemviews/kfileitemlistwidget.h
index e2db43178b..3d4a78f3a8 100644
--- a/src/kitemviews/kfileitemlistwidget.h
+++ b/src/kitemviews/kfileitemlistwidget.h
@@ -29,7 +29,7 @@ protected:
     QString itemText(int index, const KItemListView *view) const override;
     bool itemIsLink(int index, const KItemListView *view) const override;
     /** @see KStandardItemListWidget::roleText(). */
-    QString roleText(const QByteArray &role, const QHash<QByteArray, QVariant> &values, ForUsageAs forUsageAs = ForUsageAs::DisplayedText) const override;
+    QString roleText(const QByteArray &role, const SmallHash &values, ForUsageAs forUsageAs = ForUsageAs::DisplayedText) const override;
     QFont customizedFontForLinks(const QFont &baseFont) const override;
 
     friend class KItemListDelegateAccessible;
diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp
index 0dd9f58d81..542c32ae6d 100644
--- a/src/kitemviews/kfileitemmodel.cpp
+++ b/src/kitemviews/kfileitemmodel.cpp
@@ -379,7 +379,7 @@ int KFileItemModel::count() const
     return m_itemData.count();
 }
 
-QHash<QByteArray, QVariant> KFileItemModel::data(int index) const
+SmallHash KFileItemModel::data(int index) const
 {
     if (index >= 0 && index < count()) {
         ItemData *data = m_itemData.at(index);
@@ -399,27 +399,32 @@ QHash<QByteArray, QVariant> KFileItemModel::data(int index) const
             }
         }
 
-        return data->values;
+        SmallHash result = data->values;
+        // The url is not stored per item (it is derivable from the KFileItem); inject it here.
+        result.insert(sharedValue("url"), data->item.url());
+        return result;
     }
-    return QHash<QByteArray, QVariant>();
+    return SmallHash();
+}
+
+QUrl KFileItemModel::url(int index) const
+{
+    // "url" is not stored in the per-item role cache; read it straight from the item.
+    return fileItem(index).url();
 }
 
-bool KFileItemModel::setData(int index, const QHash<QByteArray, QVariant> &values)
+bool KFileItemModel::setData(int index, const SmallHash &values)
 {
     if (index < 0 || index >= count()) {
         return false;
     }
 
-    QHash<QByteArray, QVariant> currentValues = data(index);
+    SmallHash currentValues = data(index);
 
     // Determine which roles have been changed
     QSet<QByteArray> changedRoles;
-    QHashIterator<QByteArray, QVariant> it(values);
-    while (it.hasNext()) {
-        it.next();
-        const QByteArray role = sharedValue(it.key());
-        const QVariant value = it.value();
-
+    for (const auto &[key, value] : values) {
+        const QByteArray role = sharedValue(key);
         if (currentValues[role] != value) {
             currentValues[role] = value;
             changedRoles.insert(role);
@@ -443,6 +448,8 @@ bool KFileItemModel::setData(int index, const QHash<QByteArray, QVariant> &value
             currentValues["url"] = url;
         }
     }
+    // "url" is injected by data(), not stored per item.
+    currentValues.remove(sharedValue("url"));
     m_itemData[index]->values = currentValues;
 
     emitItemsChangedAndTriggerResorting(KItemRangeList() << KItemRange(index, 1), changedRoles);
@@ -870,7 +877,7 @@ bool KFileItemModel::setExpanded(int index, bool expanded)
         return false;
     }
 
-    QHash<QByteArray, QVariant> values;
+    SmallHash values;
     values.insert(sharedValue("isExpanded"), expanded);
     if (!setData(index, values)) {
         return false;
@@ -1650,12 +1657,10 @@ void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem>> &
             // Keep old values as long as possible if they could not retrieved synchronously yet.
             // The update of the values will be done asynchronously by KFileItemModelRolesUpdater.
             ItemData *const itemData = m_itemData.at(indexForItem);
-            QHashIterator<QByteArray, QVariant> it(retrieveData(newItem, itemData->parent));
-            while (it.hasNext()) {
-                it.next();
-                const QByteArray &role = it.key();
-                if (itemData->values.value(role) != it.value()) {
-                    itemData->values.insert(role, it.value());
+            const SmallHash newData = retrieveData(newItem, itemData->parent);
+            for (const auto &[role, value] : newData) {
+                if (itemData->values.value(role) != value) {
+                    itemData->values.insert(role, value);
                     changedRoles.insert(role);
                 }
             }
@@ -2203,13 +2208,12 @@ QByteArray KFileItemModel::roleForType(RoleType roleType) const
     return roles.value(roleType);
 }
 
-QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem &item, const ItemData *parent) const
+SmallHash KFileItemModel::retrieveData(const KFileItem &item, const ItemData *parent) const
 {
     // It is important to insert only roles that are fast to retrieve. E.g.
     // KFileItem::iconName() can be very expensive if the MIME-type is unknown
     // and hence will be retrieved asynchronously by KFileItemModelRolesUpdater.
-    QHash<QByteArray, QVariant> data;
-    data.insert(sharedValue("url"), item.url());
+    SmallHash data;
 
     const bool isDir = item.isDir();
     if (m_requestRole[IsDirRole] && isDir) {
diff --git a/src/kitemviews/kfileitemmodel.h b/src/kitemviews/kfileitemmodel.h
index 9e30486079..dca78a263b 100644
--- a/src/kitemviews/kfileitemmodel.h
+++ b/src/kitemviews/kfileitemmodel.h
@@ -10,6 +10,7 @@
 #include "dolphin_export.h"
 #include "kitemviews/kitemmodelbase.h"
 #include "kitemviews/private/kfileitemmodelfilter.h"
+#include "smallhash.h"
 
 #include <KFileItem>
 #include <KLazyLocalizedString>
@@ -18,6 +19,7 @@
 #include <QHash>
 #include <QSet>
 #include <QUrl>
+#include <QVariant>
 
 #include <functional>
 
@@ -77,8 +79,9 @@ public:
     void cancelDirectoryLoading();
 
     int count() const override;
-    QHash<QByteArray, QVariant> data(int index) const override;
-    bool setData(int index, const QHash<QByteArray, QVariant> &values) override;
+    SmallHash data(int index) const override;
+    QUrl url(int index) const override;
+    bool setData(int index, const SmallHash &values) override;
 
     /**
      * Sets a separate sorting with directories first (true) or a mixed
@@ -373,7 +376,7 @@ private:
 
     struct ItemData {
         KFileItem item;
-        QHash<QByteArray, QVariant> values;
+        SmallHash values;
         ItemData *parent;
     };
 
@@ -429,7 +432,7 @@ private:
      */
     QByteArray roleForType(RoleType roleType) const;
 
-    QHash<QByteArray, QVariant> retrieveData(const KFileItem &item, const ItemData *parent) const;
+    SmallHash retrieveData(const KFileItem &item, const ItemData *parent) const;
 
     /**
      * @return True if role values benefit from natural or case insensitive sorting.
diff --git a/src/kitemviews/kfileitemmodelrolesupdater.cpp b/src/kitemviews/kfileitemmodelrolesupdater.cpp
index 6f86e1b382..af6fc46c44 100644
--- a/src/kitemviews/kfileitemmodelrolesupdater.cpp
+++ b/src/kitemviews/kfileitemmodelrolesupdater.cpp
@@ -571,7 +571,7 @@ void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem &item, const QPi
         return;
     }
 
-    QHash<QByteArray, QVariant> data = rolesData(item, index);
+    SmallHash data = rolesData(item, index);
     data.insert("iconPixmap", transformPreviewPixmap(pixmap));
     data.insert("supportsSequencing", m_previewJob->handlesSequences());
 
@@ -592,7 +592,7 @@ void KFileItemModelRolesUpdater::slotPreviewFailed(const KFileItem &item)
 
     const int index = m_model->index(item);
     if (index >= 0) {
-        QHash<QByteArray, QVariant> data;
+        SmallHash data;
         data.insert("iconPixmap", QPixmap());
 
         setModelData(index, data);
@@ -628,7 +628,7 @@ void KFileItemModelRolesUpdater::slotHoverSequenceGotPreview(const KFileItem &it
         return;
     }
 
-    QHash<QByteArray, QVariant> data = m_model->data(index);
+    SmallHash data = m_model->data(index);
     QVector<QPixmap> pixmaps = data["hoverSequencePixmaps"].value<QVector<QPixmap>>();
     const int loadedIndex = pixmaps.size();
 
@@ -674,7 +674,7 @@ void KFileItemModelRolesUpdater::slotHoverSequencePreviewFailed(const KFileItem
 
     static const int numRetries = 2;
 
-    QHash<QByteArray, QVariant> data = m_model->data(index);
+    SmallHash data = m_model->data(index);
     QVector<QPixmap> pixmaps = data["hoverSequencePixmaps"].value<QVector<QPixmap>>();
 
     qCDebug(DolphinDebug).nospace() << "Failed to generate hover sequence preview #" << pixmaps.size() << " for file " << item.url().toString() << " (attempt "
@@ -788,7 +788,7 @@ void KFileItemModelRolesUpdater::resolveNextPendingRoles()
         if (m_clearPreviews) {
             // Only go through the list if there are items which might still have previews.
             if (m_finishedItems.count() != m_model->count()) {
-                QHash<QByteArray, QVariant> data;
+                SmallHash data;
                 data.insert("iconPixmap", QPixmap());
                 data.insert("hoverSequencePixmaps", QVariant::fromValue(QVector<QPixmap>()));
 
@@ -837,7 +837,7 @@ void KFileItemModelRolesUpdater::applyChangedBalooRolesForItem(const KFileItem &
     file.load();
 
     const KBalooRolesProvider &rolesProvider = KBalooRolesProvider::instance();
-    QHash<QByteArray, QVariant> data;
+    SmallHash data;
 
     const auto roles = rolesProvider.roles();
     for (const QByteArray &role : roles) {
@@ -847,10 +847,9 @@ void KFileItemModelRolesUpdater::applyChangedBalooRolesForItem(const KFileItem &
         data.insert(role, QVariant());
     }
 
-    QHashIterator<QByteArray, QVariant> it(rolesProvider.roleValues(file, m_roles));
-    while (it.hasNext()) {
-        it.next();
-        data.insert(it.key(), it.value());
+    const SmallHash roleValues = rolesProvider.roleValues(file, m_roles);
+    for (const auto &[key, value] : roleValues) {
+        data.insert(key, value);
     }
 
     const int index = m_model->index(item);
@@ -870,7 +869,7 @@ void KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived(const QStrin
     if (getSizeRole || getIsExpandableRole) {
         const int index = m_model->index(QUrl::fromLocalFile(path));
         if (index >= 0) {
-            QHash<QByteArray, QVariant> data;
+            SmallHash data;
 
             if (getSizeRole) {
                 data.insert("count", count);
@@ -1055,7 +1054,7 @@ void KFileItemModelRolesUpdater::loadNextHoverSequencePreview()
     // We generate the next few sequence indices in advance (buffering)
     const int maxSeqIdx = m_hoverSequenceIndex + 5;
 
-    QHash<QByteArray, QVariant> data = m_model->data(index);
+    SmallHash data = m_model->data(index);
 
     if (!data.contains("hoverSequencePixmaps")) {
         // The pixmap at index 0 isn't used ("iconPixmap" will be used instead)
@@ -1180,7 +1179,7 @@ void KFileItemModelRolesUpdater::updateChangedItems()
     }
 }
 
-void KFileItemModelRolesUpdater::setModelData(int index, const QHash<QByteArray, QVariant> &data)
+void KFileItemModelRolesUpdater::setModelData(int index, const SmallHash &data)
 {
     const QScopedValueRollback<bool> guard(m_applyingChangesToModel, true);
     m_model->setData(index, data);
@@ -1188,7 +1187,7 @@ void KFileItemModelRolesUpdater::setModelData(int index, const QHash<QByteArray,
 
 void KFileItemModelRolesUpdater::applySortRole(int index)
 {
-    QHash<QByteArray, QVariant> data;
+    SmallHash data;
     const KFileItem item = m_model->fileItem(index);
 
     if (m_model->sortRole() == "type") {
@@ -1234,7 +1233,7 @@ bool KFileItemModelRolesUpdater::applyResolvedRoles(int index, ResolveHint hint,
             return false;
         }
 
-        QHash<QByteArray, QVariant> data;
+        SmallHash data;
         if (resolveAll) {
             data = rolesData(item, index);
         }
@@ -1321,7 +1320,7 @@ void KFileItemModelRolesUpdater::startDirectorySizeCounting(const KFileItem &ite
             auto data = m_model->data(index);
             int origCount = data.value("count").toInt();
 
-            QHash<QByteArray, QVariant> newData;
+            SmallHash newData;
             QVariant expandable = data.value("isExpandable");
             if (expandable.isNull() || expandable.toBool() != (entryCount > 0)) {
                 // if expandable has changed
@@ -1348,9 +1347,9 @@ void KFileItemModelRolesUpdater::startDirectorySizeCounting(const KFileItem &ite
     m_directoryContentsCounter->scanDirectory(path, priority);
 }
 
-QHash<QByteArray, QVariant> KFileItemModelRolesUpdater::rolesData(const KFileItem &item, int index)
+SmallHash KFileItemModelRolesUpdater::rolesData(const KFileItem &item, int index)
 {
-    QHash<QByteArray, QVariant> data;
+    SmallHash data;
 
     const bool getSizeRole = m_roles.contains("size");
     const bool getIsExpandableRole = m_roles.contains("isExpandable");
@@ -1392,7 +1391,7 @@ void KFileItemModelRolesUpdater::slotOverlaysChanged(const QUrl &url, const QStr
         return;
     }
     const int index = m_model->index(item);
-    QHash<QByteArray, QVariant> data = m_model->data(index);
+    SmallHash data = m_model->data(index);
     QStringList overlays = item.overlays();
     for (KOverlayIconPlugin *it : std::as_const(m_overlayIconsPlugin)) {
         overlays.append(it->getOverlays(url));
@@ -1502,7 +1501,7 @@ void KFileItemModelRolesUpdater::trimHoverSequenceLoadedItems()
 
         const int index = m_model->index(item);
         if (index >= 0) {
-            QHash<QByteArray, QVariant> data = m_model->data(index);
+            SmallHash data = m_model->data(index);
             data["hoverSequencePixmaps"] = QVariant::fromValue(QVector<QPixmap>() << QPixmap());
             m_model->setData(index, data);
         }
diff --git a/src/kitemviews/kfileitemmodelrolesupdater.h b/src/kitemviews/kfileitemmodelrolesupdater.h
index 61e85188e5..6ecc577f3e 100644
--- a/src/kitemviews/kfileitemmodelrolesupdater.h
+++ b/src/kitemviews/kfileitemmodelrolesupdater.h
@@ -315,14 +315,14 @@ private:
 
     enum ResolveHint { ResolveFast, ResolveAll };
     bool applyResolvedRoles(int index, ResolveHint hint, const KFileItem &referenceItem = KFileItem());
-    QHash<QByteArray, QVariant> rolesData(const KFileItem &item, int index);
+    SmallHash rolesData(const KFileItem &item, int index);
 
     /**
      * Sets \a data on the model item at \a index without re-entering
      * slotItemsChanged() for this self-induced change (other listeners, e.g. the
      * view, still get the change). Replaces a manual disconnect/setData/connect.
      */
-    void setModelData(int index, const QHash<QByteArray, QVariant> &data);
+    void setModelData(int index, const SmallHash &data);
 
     /**
      * Must be invoked if a property has been changed that affects
diff --git a/src/kitemviews/kitemlistwidget.cpp b/src/kitemviews/kitemlistwidget.cpp
index e792844b03..272db71671 100644
--- a/src/kitemviews/kitemlistwidget.cpp
+++ b/src/kitemviews/kitemlistwidget.cpp
@@ -75,7 +75,7 @@ int KItemListWidget::index() const
     return m_index;
 }
 
-void KItemListWidget::setData(const QHash<QByteArray, QVariant> &data, const QSet<QByteArray> &roles)
+void KItemListWidget::setData(const SmallHash &data, const QSet<QByteArray> &roles)
 {
     clearHoverCache();
     if (roles.isEmpty()) {
@@ -90,7 +90,7 @@ void KItemListWidget::setData(const QHash<QByteArray, QVariant> &data, const QSe
     update();
 }
 
-QHash<QByteArray, QVariant> KItemListWidget::data() const
+SmallHash KItemListWidget::data() const
 {
     return m_data;
 }
@@ -456,7 +456,7 @@ void KItemListWidget::startActivateSoonAnimation(int timeUntilActivation)
     Q_UNUSED(timeUntilActivation)
 }
 
-void KItemListWidget::dataChanged(const QHash<QByteArray, QVariant> &current, const QSet<QByteArray> &roles)
+void KItemListWidget::dataChanged(const SmallHash &current, const QSet<QByteArray> &roles)
 {
     Q_UNUSED(current)
     Q_UNUSED(roles)
diff --git a/src/kitemviews/kitemlistwidget.h b/src/kitemviews/kitemlistwidget.h
index 100c13466f..d4098545c1 100644
--- a/src/kitemviews/kitemlistwidget.h
+++ b/src/kitemviews/kitemlistwidget.h
@@ -11,6 +11,7 @@
 
 #include "dolphin_export.h"
 #include "kitemviews/kitemliststyleoption.h"
+#include "smallhash.h"
 
 #include <QBitArray>
 #include <QGraphicsWidget>
@@ -59,8 +60,8 @@ public:
     void setIndex(int index);
     int index() const;
 
-    void setData(const QHash<QByteArray, QVariant> &data, const QSet<QByteArray> &roles = QSet<QByteArray>());
-    QHash<QByteArray, QVariant> data() const;
+    void setData(const SmallHash &data, const QSet<QByteArray> &roles = QSet<QByteArray>());
+    SmallHash data() const;
     QVariant value(const QByteArray &key) const;
 
     /**
@@ -206,7 +207,7 @@ Q_SIGNALS:
     void roleEditingFinished(int index, const QByteArray &role, const QVariant &value);
 
 protected:
-    virtual void dataChanged(const QHash<QByteArray, QVariant> &current, const QSet<QByteArray> &roles = QSet<QByteArray>());
+    virtual void dataChanged(const SmallHash &current, const QSet<QByteArray> &roles = QSet<QByteArray>());
     virtual void visibleRolesChanged(const QList<QByteArray> &current, const QList<QByteArray> &previous);
     virtual void columnWidthChanged(const QByteArray &role, qreal current, qreal previous);
     virtual void sidePaddingChanged(qreal leftPaddingWidth, qreal rightPaddingWidth);
@@ -267,7 +268,7 @@ private:
     bool m_alternateBackground;
     bool m_enabledSelectionToggle;
     bool m_clickHighlighted;
-    QHash<QByteArray, QVariant> m_data;
+    SmallHash m_data;
     QList<QByteArray> m_visibleRoles;
     QHash<QByteArray, qreal> m_columnWidths;
     qreal m_leftPadding;
diff --git a/src/kitemviews/kitemmodelbase.cpp b/src/kitemviews/kitemmodelbase.cpp
index 7cff2a9c48..5c384f28dd 100644
--- a/src/kitemviews/kitemmodelbase.cpp
+++ b/src/kitemviews/kitemmodelbase.cpp
@@ -26,7 +26,7 @@ KItemModelBase::KItemModelBase(const QByteArray &sortRole, QObject *parent)
 
 KItemModelBase::~KItemModelBase() = default;
 
-bool KItemModelBase::setData(int index, const QHash<QByteArray, QVariant> &values)
+bool KItemModelBase::setData(int index, const SmallHash &values)
 {
     Q_UNUSED(index)
     Q_UNUSED(values)
diff --git a/src/kitemviews/kitemmodelbase.h b/src/kitemviews/kitemmodelbase.h
index 5d968ea069..1a3d19f525 100644
--- a/src/kitemviews/kitemmodelbase.h
+++ b/src/kitemviews/kitemmodelbase.h
@@ -12,6 +12,7 @@
 #include "dolphin_export.h"
 #include "kitemviews/kitemrange.h"
 #include "kitemviews/kitemset.h"
+#include "smallhash.h"
 
 #include <QHash>
 #include <QObject>
@@ -47,7 +48,7 @@ public:
     /** @return The number of items. */
     virtual int count() const = 0;
 
-    virtual QHash<QByteArray, QVariant> data(int index) const = 0;
+    virtual SmallHash data(int index) const = 0;
 
     /**
      * Sets the data for the item at \a index to the given \a values. Returns true
@@ -56,7 +57,7 @@ public:
      * The default implementation does not set the data, and will always return
      * false.
      */
-    virtual bool setData(int index, const QHash<QByteArray, QVariant> &values);
+    virtual bool setData(int index, const SmallHash &values);
 
     /**
      * Enables/disables the grouped sorting. The method KItemModelBase::onGroupedSortingChanged() will be
diff --git a/src/kitemviews/kstandarditemlistwidget.cpp b/src/kitemviews/kstandarditemlistwidget.cpp
index edc8bfa789..88d2ddad64 100644
--- a/src/kitemviews/kstandarditemlistwidget.cpp
+++ b/src/kitemviews/kstandarditemlistwidget.cpp
@@ -63,7 +63,7 @@ void KStandardItemListWidgetInformant::calculateItemSizeHints(QVector<std::pair<
 
 qreal KStandardItemListWidgetInformant::preferredRoleColumnWidth(const QByteArray &role, int index, const KItemListView *view) const
 {
-    const QHash<QByteArray, QVariant> values = view->model()->data(index);
+    const SmallHash values = view->model()->data(index);
     const KItemListStyleOption &option = view->styleOption();
 
     const QString text = roleText(role, values);
@@ -108,7 +108,7 @@ bool KStandardItemListWidgetInformant::itemIsLink(int index, const KItemListView
     return false;
 }
 
-QString KStandardItemListWidgetInformant::roleText(const QByteArray &role, const QHash<QByteArray, QVariant> &values, ForUsageAs forUsageAs) const
+QString KStandardItemListWidgetInformant::roleText(const QByteArray &role, const SmallHash &values, ForUsageAs forUsageAs) const
 {
     if (role == "rating") {
         if (forUsageAs == ForUsageAs::DisplayedText) {
@@ -221,7 +221,7 @@ void KStandardItemListWidgetInformant::calculateCompactLayoutItemSizeHints(QVect
         if (showOnlyTextRole) {
             maximumRequiredWidth = fontMetrics.horizontalAdvance(itemText(index, view));
         } else {
-            const QHash<QByteArray, QVariant> &values = view->model()->data(index);
+            const SmallHash &values = view->model()->data(index);
             for (const QByteArray &role : visibleRoles) {
                 const QString &text = roleText(role, values);
                 const qreal requiredWidth = fontMetrics.horizontalAdvance(text);
@@ -628,7 +628,7 @@ void KStandardItemListWidget::startActivateSoonAnimation(int timeUntilActivation
             wantedIconName = "folder-open";
         }
 
-        QHash<QByteArray, QVariant> itemData{data()};
+        SmallHash itemData{data()};
         if (itemData["iconName"] != wantedIconName) {
             itemData.insert("iconName", wantedIconName);
             setData(itemData);
@@ -637,7 +637,7 @@ void KStandardItemListWidget::startActivateSoonAnimation(int timeUntilActivation
     });
 
     connect(m_activateSoonAnimation, &QObject::destroyed, this, [originalIconName, this]() {
-        QHash<QByteArray, QVariant> itemData{data()};
+        SmallHash itemData{data()};
         if (itemData["iconName"] == "folder-open") {
             itemData.insert("iconName", originalIconName);
             setData(itemData);
@@ -740,12 +740,12 @@ QHash<Qt::Corner, QString> KStandardItemListWidget::overlays() const
     return m_overlays;
 }
 
-QString KStandardItemListWidget::roleText(const QByteArray &role, const QHash<QByteArray, QVariant> &values) const
+QString KStandardItemListWidget::roleText(const QByteArray &role, const SmallHash &values) const
 {
     return static_cast<const KStandardItemListWidgetInformant *>(informant())->roleText(role, values);
 }
 
-void KStandardItemListWidget::dataChanged(const QHash<QByteArray, QVariant> &current, const QSet<QByteArray> &roles)
+void KStandardItemListWidget::dataChanged(const SmallHash &current, const QSet<QByteArray> &roles)
 {
     Q_UNUSED(current)
 
@@ -1027,7 +1027,7 @@ void KStandardItemListWidget::triggerCacheRefreshing()
 
     refreshCache();
 
-    const QHash<QByteArray, QVariant> values = data();
+    const SmallHash values = data();
     m_isExpandable = m_supportsItemExpanding && values["isExpandable"].toBool();
     m_isHidden = isHidden();
     m_customizedFont = customizedFont(styleOption().font);
@@ -1049,7 +1049,7 @@ void KStandardItemListWidget::triggerCacheRefreshing()
 void KStandardItemListWidget::updateExpansionArea()
 {
     if (m_supportsItemExpanding) {
-        const QHash<QByteArray, QVariant> values = data();
+        const SmallHash values = data();
         const int expandedParentsCount = values.value("expandedParentsCount", 0).toInt();
         if (expandedParentsCount >= 0) {
             const int widgetIconSize = styleOption().iconSize;
@@ -1086,7 +1086,7 @@ void KStandardItemListWidget::updatePixmapCache()
     const int maxIconWidth = iconOnTop ? widgetSize.width() - 2 * padding : widgetIconSize;
     const int maxIconHeight = widgetIconSize;
 
-    const QHash<QByteArray, QVariant> values = data();
+    const SmallHash values = data();
 
     bool updatePixmap = (m_pixmap.width() != maxIconWidth || m_pixmap.height() != maxIconHeight);
     if (!updatePixmap && m_dirtyContent) {
@@ -1322,7 +1322,7 @@ void KStandardItemListWidget::updateIconsLayoutTextCache()
     //  Additional role 1
     //  Additional role 2
 
-    const QHash<QByteArray, QVariant> values = data();
+    const SmallHash values = data();
 
     const KItemListStyleOption &option = styleOption();
     const qreal padding = option.padding;
@@ -1441,7 +1441,7 @@ void KStandardItemListWidget::updateCompactLayoutTextCache()
     // | Icon |  Additional role 1
     // +------+  Additional role 2
 
-    const QHash<QByteArray, QVariant> values = data();
+    const SmallHash values = data();
 
     const KItemListStyleOption &option = styleOption();
     const qreal widgetHeight = size().height();
@@ -1493,7 +1493,7 @@ void KStandardItemListWidget::updateDetailsLayoutTextCache()
     m_textRect = QRectF();
 
     const KItemListStyleOption &option = styleOption();
-    const QHash<QByteArray, QVariant> values = data();
+    const SmallHash values = data();
 
     const qreal widgetHeight = size().height();
     const int fontHeight = m_customizedFontMetrics.height();
diff --git a/src/kitemviews/kstandarditemlistwidget.h b/src/kitemviews/kstandarditemlistwidget.h
index e973169192..1f92998a69 100644
--- a/src/kitemviews/kstandarditemlistwidget.h
+++ b/src/kitemviews/kstandarditemlistwidget.h
@@ -42,7 +42,7 @@ protected:
      * @return The value of the "text" role. The default implementation returns
      *         view->model()->data(index)["text"]. If a derived class can
      *         prevent the (possibly expensive) construction of the
-     *         QHash<QByteArray, QVariant> returned by KItemModelBase::data(int),
+     *         SmallHash returned by KItemModelBase::data(int),
      *         it can reimplement this function.
      */
     virtual QString itemText(int index, const KItemListView *view) const;
@@ -66,7 +66,7 @@ protected:
      *         a role might depend on other roles, so the values of all roles
      *         are passed as parameter.
      */
-    virtual QString roleText(const QByteArray &role, const QHash<QByteArray, QVariant> &values, ForUsageAs forUsageAs = ForUsageAs::DisplayedText) const;
+    virtual QString roleText(const QByteArray &role, const SmallHash &values, ForUsageAs forUsageAs = ForUsageAs::DisplayedText) const;
 
     /**
     * @return A font based on baseFont which is customized for symlinks.
@@ -164,7 +164,7 @@ protected:
     /**
      * @see KStandardItemListWidgetInformant::roleText().
      */
-    QString roleText(const QByteArray &role, const QHash<QByteArray, QVariant> &values) const;
+    QString roleText(const QByteArray &role, const SmallHash &values) const;
 
     static int numberOfUnicodeCharactersIn(const QString &text);
 
@@ -173,7 +173,7 @@ protected:
      */
     virtual int selectionLength(const QString &text) const;
 
-    void dataChanged(const QHash<QByteArray, QVariant> &current, const QSet<QByteArray> &roles = QSet<QByteArray>()) override;
+    void dataChanged(const SmallHash &current, const QSet<QByteArray> &roles = QSet<QByteArray>()) override;
     void visibleRolesChanged(const QList<QByteArray> &current, const QList<QByteArray> &previous) override;
     void columnWidthChanged(const QByteArray &role, qreal current, qreal previous) override;
     void sidePaddingChanged(qreal leftPaddingWidth, qreal rightPaddingWidth) override;
diff --git a/src/kitemviews/private/kbaloorolesprovider.cpp b/src/kitemviews/private/kbaloorolesprovider.cpp
index 9e396ede51..030f0ed378 100644
--- a/src/kitemviews/private/kbaloorolesprovider.cpp
+++ b/src/kitemviews/private/kbaloorolesprovider.cpp
@@ -85,9 +85,9 @@ QSet<QByteArray> KBalooRolesProvider::roles() const
     return m_roles;
 }
 
-QHash<QByteArray, QVariant> KBalooRolesProvider::roleValues(const Baloo::File &file, const QSet<QByteArray> &roles) const
+SmallHash KBalooRolesProvider::roleValues(const Baloo::File &file, const QSet<QByteArray> &roles) const
 {
-    QHash<QByteArray, QVariant> values;
+    SmallHash values;
 
     using entry = std::pair<const KFileMetaData::Property::Property &, const QVariant &>;
 
diff --git a/src/kitemviews/private/kbaloorolesprovider.h b/src/kitemviews/private/kbaloorolesprovider.h
index 5ff33e1c61..5e690afab5 100644
--- a/src/kitemviews/private/kbaloorolesprovider.h
+++ b/src/kitemviews/private/kbaloorolesprovider.h
@@ -9,6 +9,7 @@
 #define KBALOO_ROLESPROVIDER_H
 
 #include "dolphin_export.h"
+#include "smallhash.h"
 
 #include <QHash>
 #include <QSet>
@@ -40,7 +41,7 @@ public:
      * @return Values for the roles \a roles that can be determined from the file
      *         with the URL \a url.
      */
-    QHash<QByteArray, QVariant> roleValues(const Baloo::File &file, const QSet<QByteArray> &roles) const;
+    SmallHash roleValues(const Baloo::File &file, const QSet<QByteArray> &roles) const;
 
 protected:
     KBalooRolesProvider();
diff --git a/src/smallhash.h b/src/smallhash.h
new file mode 100644
index 0000000000..db8a2d870b
--- /dev/null
+++ b/src/smallhash.h
@@ -0,0 +1,117 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Méven Car <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef SMALLHASH_H
+#define SMALLHASH_H
+
+#include <QByteArray>
+#include <QHash>
+#include <QVariant>
+
+#include <utility>
+#include <vector>
+
+/**
+ * A minimal QHash-like map backed by a flat vector<pair<QByteArray, QVariant>>.
+ *
+ * Qt6's QHash allocates a ~3 KB minimum span on the first insert, so one QHash
+ * per item (a handful of entries times N items) dominates memory; a flat vector
+ * holds the same few entries in a fraction of that, with O(n) lookups that are
+ * cheap for the small n involved. Only the subset of the QHash API the callers
+ * need is provided, plus toHash()/fromHash() to bridge to code expecting a QHash.
+ */
+class SmallHash
+{
+public:
+    bool isEmpty() const
+    {
+        return m_data.empty();
+    }
+    int count() const
+    {
+        return static_cast<int>(m_data.size());
+    }
+    bool contains(const QByteArray &key) const
+    {
+        return indexOf(key) >= 0;
+    }
+    QVariant value(const QByteArray &key, const QVariant &defaultValue = QVariant()) const
+    {
+        const int i = indexOf(key);
+        return i >= 0 ? m_data[i].second : defaultValue;
+    }
+    void insert(const QByteArray &key, const QVariant &value)
+    {
+        (*this)[key] = value;
+    }
+    QVariant operator[](const QByteArray &key) const
+    {
+        return value(key);
+    }
+    // Returns a modifiable reference to the value for key, inserting a default
+    // one if absent (like QHash). The reference is valid until the next insert.
+    QVariant &operator[](const QByteArray &key)
+    {
+        const int i = indexOf(key);
+        if (i >= 0) {
+            return m_data[i].second;
+        }
+        m_data.emplace_back(key, QVariant());
+        return m_data.back().second;
+    }
+    void remove(const QByteArray &key)
+    {
+        const int i = indexOf(key);
+        if (i >= 0) {
+            m_data.erase(m_data.begin() + i);
+        }
+    }
+    void clear()
+    {
+        m_data.clear();
+        m_data.shrink_to_fit();
+    }
+    QHash<QByteArray, QVariant> toHash() const
+    {
+        QHash<QByteArray, QVariant> hash;
+        hash.reserve(static_cast<int>(m_data.size()));
+        for (const auto &[key, value] : m_data) {
+            hash.insert(key, value);
+        }
+        return hash;
+    }
+    static SmallHash fromHash(const QHash<QByteArray, QVariant> &hash)
+    {
+        SmallHash values;
+        values.m_data.reserve(hash.size());
+        for (auto it = hash.cbegin(); it != hash.cend(); ++it) {
+            values.m_data.emplace_back(it.key(), it.value());
+        }
+        return values;
+    }
+    std::vector<std::pair<QByteArray, QVariant>>::const_iterator begin() const
+    {
+        return m_data.begin();
+    }
+    std::vector<std::pair<QByteArray, QVariant>>::const_iterator end() const
+    {
+        return m_data.end();
+    }
+
+private:
+    int indexOf(const QByteArray &key) const
+    {
+        for (size_t i = 0; i < m_data.size(); ++i) {
+            if (m_data[i].first == key) {
+                return static_cast<int>(i);
+            }
+        }
+        return -1;
+    }
+    std::vector<std::pair<QByteArray, QVariant>> m_data;
+};
+
+#endif // SMALLHASH_H
diff --git a/src/tests/kfileitemmodeltest.cpp b/src/tests/kfileitemmodeltest.cpp
index 81540a1f1d..46e4e47083 100644
--- a/src/tests/kfileitemmodeltest.cpp
+++ b/src/tests/kfileitemmodeltest.cpp
@@ -261,7 +261,7 @@ void KFileItemModelTest::testSetData()
     m_model->loadDirectory(m_testDir->url());
     QVERIFY(itemsInsertedSpy.wait());
 
-    QHash<QByteArray, QVariant> values;
+    SmallHash values;
     values.insert("customRole1", "Test1");
     values.insert("customRole2", "Test2");
 
@@ -327,15 +327,15 @@ void KFileItemModelTest::testSetDataWithModifiedSortRole()
     // b.txt -> 4
     // c.txt -> 6
 
-    QHash<QByteArray, QVariant> ratingA;
+    SmallHash ratingA;
     ratingA.insert("rating", 2);
     m_model->setData(0, ratingA);
 
-    QHash<QByteArray, QVariant> ratingB;
+    SmallHash ratingB;
     ratingB.insert("rating", 4);
     m_model->setData(1, ratingB);
 
-    QHash<QByteArray, QVariant> ratingC;
+    SmallHash ratingC;
     ratingC.insert("rating", 6);
     m_model->setData(2, ratingC);
 
@@ -345,7 +345,7 @@ void KFileItemModelTest::testSetDataWithModifiedSortRole()
 
     // Now change the rating from a.txt. This usually results
     // in reordering of the items.
-    QHash<QByteArray, QVariant> rating;
+    SmallHash rating;
     rating.insert("rating", changedRating);
     m_model->setData(changedIndex, rating);
 
@@ -427,7 +427,7 @@ void KFileItemModelTest::testResortAfterChangingName()
 
     // We rename a.txt to d.txt. Even though the size has not changed at all,
     // the model must re-sort the items.
-    QHash<QByteArray, QVariant> data;
+    SmallHash data;
     data.insert("text", "d.txt");
     m_model->setData(0, data);
 
@@ -2170,7 +2170,7 @@ void KFileItemModelTest::testNameRoleGroups()
     QCOMPARE(m_model->groups(), expectedGroups);
 
     // Rename d.txt to a.txt.
-    QHash<QByteArray, QVariant> data;
+    SmallHash data;
     data.insert("text", "a.txt");
     m_model->setData(2, data);
     QVERIFY(itemsMovedSpy.wait());
diff --git a/src/tests/kitemlistselectionmanagertest.cpp b/src/tests/kitemlistselectionmanagertest.cpp
index da761d9a74..d727cbf3c0 100644
--- a/src/tests/kitemlistselectionmanagertest.cpp
+++ b/src/tests/kitemlistselectionmanagertest.cpp
@@ -19,7 +19,7 @@ public:
     DummyModel();
     void setCount(int count);
     int count() const override;
-    QHash<QByteArray, QVariant> data(int index) const override;
+    SmallHash data(int index) const override;
 
 private:
     int m_count;
@@ -41,10 +41,10 @@ int DummyModel::count() const
     return m_count;
 }
 
-QHash<QByteArray, QVariant> DummyModel::data(int index) const
+SmallHash DummyModel::data(int index) const
 {
     Q_UNUSED(index)
-    return QHash<QByteArray, QVariant>();
+    return SmallHash();
 }
 
 class KItemListSelectionManagerTest : public QObject
diff --git a/src/views/dolphinfileitemlistwidget.cpp b/src/views/dolphinfileitemlistwidget.cpp
index daa1add81e..0163faac58 100644
--- a/src/views/dolphinfileitemlistwidget.cpp
+++ b/src/views/dolphinfileitemlistwidget.cpp
@@ -21,7 +21,7 @@ DolphinFileItemListWidget::~DolphinFileItemListWidget() = default;
 void DolphinFileItemListWidget::refreshCache()
 {
     QColor color;
-    const QHash<QByteArray, QVariant> values = data();
+    const SmallHash values = data();
     QHash<Qt::Corner, QString> overlays;
     if (values.contains("version")) {
         // The item is under version control. Apply the text color corresponding
diff --git a/src/views/dolphinview.cpp b/src/views/dolphinview.cpp
index ac69a408ee..68f9e0bc15 100644
--- a/src/views/dolphinview.cpp
+++ b/src/views/dolphinview.cpp
@@ -878,7 +878,7 @@ void DolphinView::renameSelectedItems()
             for (int i = 0; i < items.count(); ++i) {
                 const int index = m_model->index(items[i]);
                 if (index >= 0) {
-                    QHash<QByteArray, QVariant> data;
+                    SmallHash data;
                     data.insert("text", urls[i].fileName());
                     m_model->setData(index, data);
                 }
@@ -2163,7 +2163,7 @@ void DolphinView::slotRenamingResult(KJob *job)
         const QUrl oldUrl = copyJob->srcUrls().at(0);
         const int index = m_model->index(newUrl);
         if (m_model->index(oldUrl) == index) {
-            QHash<QByteArray, QVariant> data;
+            SmallHash data;
             data.insert("text", newUrl.fileName());
             m_model->setData(index, data);
         }
diff --git a/src/views/versioncontrol/versioncontrolobserver.cpp b/src/views/versioncontrol/versioncontrolobserver.cpp
index de81156c5f..b993656b14 100644
--- a/src/views/versioncontrol/versioncontrolobserver.cpp
+++ b/src/views/versioncontrol/versioncontrolobserver.cpp
@@ -210,7 +210,7 @@ void VersionControlObserver::slotThreadFinished()
         for (const ItemState &item : items) {
             const KFileItem &fileItem = item.first;
             const KVersionControlPlugin::ItemVersion version = item.second;
-            QHash<QByteArray, QVariant> values;
+            SmallHash values;
             values.insert("version", QVariant(version));
             m_model->setData(m_model->index(fileItem), values);
         }
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.