Bug 525978 - ComponentCacheProxyModel never deletes cached instances on row/column removal; process-table widget leaks HistoryProxySource timers until plasmashell pins a core
Summary: ComponentCacheProxyModel never deletes cached instances on row/column removal...
Status: RESOLVED FIXED
Alias: None
Product: plasma-systemmonitor
Classification: Applications
Component: general (other bugs)
Version First Reported In: 6.7.5
Platform: Fedora RPMs Linux
: HI normal
Target Milestone: ---
Assignee: KSysGuard Developers
URL:
Keywords: efficiency-and-performance
Depends on:
Blocks:
 
Reported: 2026-09-20 13:54 UTC by Chris Purcell
Modified: 2026-10-02 21:21 UTC (History)
4 users (show)

See Also:
Latest Commit:
Version Fixed/Implemented In: 6.7.6
Sentry Crash Report:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Chris Purcell 2026-09-20 13:54:29 UTC
SUMMARY

A System Monitor widget using the "Process Table" face, left in a panel, makes plasmashell progressively slower until the whole shell lags. After 6.9 days of uptime plasmashell's main thread was at 100% of one core and held 104,660 live 2000 ms QTimers. Anonymous RSS had grown to about 1 GiB; a fresh plasmashell on the same layout uses about 370 MiB and 4% CPU. Restarting plasmashell clears it, and removing the widget stops it from returning.

The instances come from src/table/ComponentCacheProxyModel.cpp, which removes cache entries on rowsRemoved and columnsRemoved without deleting the objects.

ROOT CAUSE

ProcessTableView.qml gives ComponentCacheProxyModel a Charts.HistoryProxySource component with `interval: 2000`, so every requested cell gets an instance that owns a repeating timer. HistoryProxySource creates that timer with std::make_unique<QTimer>() and no parent.

onRowsRemoved() and onColumnsRemoved() have three problems:

1. They call m_instances.remove(...) and never delete the instance. clear() does this correctly with qDeleteAll(). Instances are parented to the model (instance->setParent(this)), so they stay alive, with their timers running, until the widget itself is destroyed.

2. rowsRemoved is emitted after the rows are gone. index(row, column, parent) therefore resolves to whichever surviving row now occupies that position. The code evicts a live row's entry (leaking that instance, and a new one is created on the next data() call), while entries for the rows that were really removed remain under invalidated QPersistentModelIndex keys.

3. The loops use `row < end` and `column < end`, but Qt's `end`/`last` argument is inclusive, so the final row or column of each range is skipped.

Connecting to rowsAboutToBeRemoved / columnsAboutToBeRemoved, iterating to `<= end`, and deleting each instance (delete or deleteLater) before removing its key should fix all three.

The Applications Table face uses the same model and component and is probably affected too; I did not test it.

WHY IT COSTS CPU

The slots are cheap. The cost is in Qt's timer bookkeeping: QTimerInfoList re-inserts each timer into one sorted per-thread list after it fires, moving the array tail with memmove. With 104,660 timers at a 2 s interval that is roughly 52,000 activations per second over an 837 KB array. Four of six eu-stack samples of the main thread were in memcpy called directly from QTimerInfoList::activateTimers().

STEPS TO REPRODUCE

1. Add a System Monitor widget to a panel and set its display style to "Process Table".
2. Leave the session running for several days on a machine where the visible part of the process list changes.
3. Watch `top -H -p $(pgrep -x plasmashell)`.

A faster check, without waiting: count live HistoryProxySource children of the ComponentCacheProxyModel (GammaRay), or add a debug counter in createPendingInstance() and the HistoryProxySource destructor. Instances are created but never destroyed while the widget exists.

Instances are created on demand, only for cells a delegate requests, so the leak follows churn in the visible rows. Spawning 240 short-lived processes that never ranked into the visible rows (list sorted by network download) added no timers, while the count kept drifting upward at about one timer every 6 s from ordinary activity.

OBSERVED RESULT

plasmashell main-thread CPU and memory grow without bound. The panel, system tray, launcher and notifications become sluggish while the rest of the system is idle (93% idle CPU, no PSI pressure).

EXPECTED RESULT

Cached instances are destroyed when their row or column is removed, and plasmashell's timer count stays roughly constant.

HOW THE TIMERS WERE IDENTIFIED

No debug symbols were available, so this came from a gcore snapshot. Each leaked QTimer had no parent, a 2000 ms interval, and a live connection to HistoryProxySource::update() (QtPrivate::QCallableObject<void (HistoryProxySource::*)()>::impl). Objects reachable from the same connection data included KSysGuard::SensorFaceController and ComponentCacheProxyModel. The list size was read from the QList backing QTimerInfoList, and then polled live from /proc/<pid>/mem to measure the growth rate.

SOFTWARE/OS VERSIONS

Operating System: Fedora Linux 44 (KDE Plasma Desktop Edition)
KDE Plasma Version: 6.7.5 (plasma-workspace, plasma-systemmonitor, libksysguard 6.7.5-1.fc44)
KDE Frameworks Version: 6.30.0 (kf6-kquickcharts 6.30.0-1.fc44)
Qt Version: 6.11.2
Kernel Version: 7.2.4-200.fc44.x86_64
Graphics Platform: Wayland
Hardware: System76 Darter Pro, Intel Meteor Lake (xe)
Comment 1 Bug Janitor Service 2026-09-21 11:08:12 UTC
A possibly relevant merge request was started @ https://invent.kde.org/plasma/plasma-systemmonitor/-/merge_requests/458
Comment 2 David Edmundson 2026-09-26 06:51:10 UTC
Git commit f9c3f4c5d35b5ef2d6de42c4625c2216203e18cb by David Edmundson.
Committed on 26/09/2026 at 06:48.
Pushed by davidedmundson into branch 'master'.

Fix cache clean up with ComponentCacheModel

There were numerous bugs with the ComponentCacheModel:

Cached instances were never deleted up on row removal or insertion, only
from a model clear or when the model itself gets deleted.

This hid three other bugs:
 - we cannot safely use "index" after a row has been removed

- start and end in the arguments are inclusive. i.e a removal of row 0
to 0 removes 1 row.

- in the connect the parent model index is from the parent, index
requires an index from this model

Credit to: Chris Purcell

M  +8    -9    src/table/ComponentCacheProxyModel.cpp
M  +2    -2    src/table/ComponentCacheProxyModel.h

https://invent.kde.org/plasma/plasma-systemmonitor/-/commit/f9c3f4c5d35b5ef2d6de42c4625c2216203e18cb
Comment 3 David Edmundson 2026-09-28 10:16:44 UTC
Git commit e1bcd2d326e4812c08225da6b19bf9f5d55ee0b8 by David Edmundson.
Committed on 26/09/2026 at 06:52.
Pushed by davidedmundson into branch 'Plasma/6.8'.

Fix cache clean up with ComponentCacheModel

There were numerous bugs with the ComponentCacheModel:

Cached instances were never deleted up on row removal or insertion, only
from a model clear or when the model itself gets deleted.

This hid two other bugs:
 - we cannot safely use "index" after a row has been removed

- start and end in the arguments are inclusive. i.e a removal of row 0
to 0 removes 1 row.

Credit to: Chris Purcell


(cherry picked from commit ff3e893ea9eb035875fb479037a2381fb33f4e0c)

Co-authored-by: David Edmundson <kde@davidedmundson.co.uk>

M  +8    -9    src/table/ComponentCacheProxyModel.cpp
M  +2    -2    src/table/ComponentCacheProxyModel.h

https://invent.kde.org/plasma/plasma-systemmonitor/-/commit/e1bcd2d326e4812c08225da6b19bf9f5d55ee0b8
Comment 4 David Edmundson 2026-09-28 10:25:55 UTC
Git commit 1527d54a935b3291f1224df9054d5779a6df3e09 by David Edmundson.
Committed on 26/09/2026 at 06:53.
Pushed by davidedmundson into branch 'Plasma/6.7'.

Fix cache clean up with ComponentCacheModel

There were numerous bugs with the ComponentCacheModel:

Cached instances were never deleted up on row removal or insertion, only
from a model clear or when the model itself gets deleted.

This hid two other bugs:
 - we cannot safely use "index" after a row has been removed

- start and end in the arguments are inclusive. i.e a removal of row 0
to 0 removes 1 row.

Credit to: Chris Purcell


(cherry picked from commit ff3e893ea9eb035875fb479037a2381fb33f4e0c)

Co-authored-by: David Edmundson <kde@davidedmundson.co.uk>

M  +8    -9    src/table/ComponentCacheProxyModel.cpp
M  +2    -2    src/table/ComponentCacheProxyModel.h

https://invent.kde.org/plasma/plasma-systemmonitor/-/commit/1527d54a935b3291f1224df9054d5779a6df3e09