https://bugs.kde.org/show_bug.cgi?id=525978

            Bug ID: 525978
           Summary: ComponentCacheProxyModel never deletes cached
                    instances on row/column removal; process-table widget
                    leaks HistoryProxySource timers until plasmashell pins
                    a core
    Classification: Applications
           Product: plasma-systemmonitor
      Version First 6.7.5
       Reported In:
          Platform: Fedora RPMs
                OS: Linux
            Status: REPORTED
          Severity: normal
          Priority: NOR
         Component: general
          Assignee: [email protected]
          Reporter: [email protected]
                CC: [email protected], [email protected]
  Target Milestone: ---

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)

-- 
You are receiving this mail because:
You are watching all bug changes.

Reply via email to