Commit f2807208 authored by Fabian Vogt's avatar Fabian Vogt

Add a per-process CPU usage graph shown in the process list

Summary:
By adding a new PercentageHistoryRole returning a QVector with timestamps and
values, the delegate can show the history of the percentage as background.

This is currently only done for the CPU usage, but can easily be added for
memory as well.

The history vector is only created and filled after the first access on each
process.

Test Plan:
Added a small testcase which verifies that the history vector has an entry
with the current value after the first refresh after the initial access.
The end result looks like this:
https://phabricator.kde.org/file/data/mwuxhmkgb7p5gr2jl6ax/PHID-FILE-v76g3drarovozb4fue3w/KSysguard_with_per-process_cpu_graph

Reviewers: #plasma

Subscribers: plasma-devel

Tags: #plasma

Differential Revision: https://phabricator.kde.org/D9689
parent d7fc3edd
......@@ -797,6 +797,24 @@ void ProcessModelPrivate::processChanged(KSysGuard::Process *process, bool onlyT
index = q->createIndex(row, ProcessModel::HeadingIoWrite, process);
emit q->dataChanged(index, index);
}
/* Normally this would only be called if changes() tells
* us to. We need to update the timestamp even if the value
* didn't change though. */
auto historyMapEntry = mMapProcessCPUHistory.find(process);
if(historyMapEntry != mMapProcessCPUHistory.end()) {
auto &history = *historyMapEntry;
unsigned long timestamp = QDateTime::currentMSecsSinceEpoch();
// Only add an entry if the latest one is older than MIN_HIST_AGE
if(history.isEmpty() || timestamp - history.constLast().timestamp > MIN_HIST_AGE) {
if(history.size() == MAX_HIST_ENTRIES) {
history.removeFirst();
}
float usage = (process->totalUserUsage() + process->totalSysUsage()) / (100.0f * mNumProcessorCores);
history.push_back({static_cast<unsigned long>(QDateTime::currentMSecsSinceEpoch()), usage});
}
}
}
}
......@@ -842,6 +860,8 @@ void ProcessModelPrivate::beginRemoveRow( KSysGuard::Process *process )
Q_ASSERT(!mMovingRow);
mRemovingRow = true;
mMapProcessCPUHistory.remove(process);
if(mSimple) {
return q->beginRemoveRows(QModelIndex(), process->index(), process->index());
} else {
......@@ -1779,6 +1799,22 @@ QVariant ProcessModel::data(const QModelIndex &index, int role) const
return -1;
}
}
case PercentageHistoryRole: {
KSysGuard::Process *process = reinterpret_cast< KSysGuard::Process * > (index.internalPointer());
Q_CHECK_PTR(process);
switch(index.column()) {
case HeadingCPUUsage: {
auto it = d->mMapProcessCPUHistory.find(process);
if (it == d->mMapProcessCPUHistory.end()) {
it = d->mMapProcessCPUHistory.insert(process, {});
it->reserve(ProcessModelPrivate::MAX_HIST_ENTRIES);
}
return QVariant::fromValue(*it);
}
default: {}
}
return QVariant::fromValue(QVector<PercentageHistoryEntry>{});
}
case Qt::DecorationRole: {
if(index.column() == HeadingName) {
#if HAVE_X11
......
......@@ -49,6 +49,12 @@ class KSYSGUARD_EXPORT ProcessModel : public QAbstractItemModel
Q_ENUMS(Units)
public:
/** Storage for history values. PercentageHistoryRole returns a QVector of this. */
struct PercentageHistoryEntry {
unsigned long timestamp; // in ms, origin undefined as only the delta matters
float value;
};
ProcessModel(QObject* parent = nullptr, const QString &host = QString() );
~ProcessModel() override;
......@@ -151,7 +157,7 @@ class KSYSGUARD_EXPORT ProcessModel : public QAbstractItemModel
HeadingXTitle
};
enum { UidRole = Qt::UserRole, SortingValueRole, WindowIdRole, PlainValueRole, PercentageRole };
enum { UidRole = Qt::UserRole, SortingValueRole, WindowIdRole, PlainValueRole, PercentageRole, PercentageHistoryRole };
bool showTotals() const;
......@@ -200,6 +206,9 @@ class KSYSGUARD_EXPORT ProcessModel : public QAbstractItemModel
friend class ProcessModelPrivate;
};
Q_DECLARE_METATYPE(QVector<ProcessModel::PercentageHistoryEntry>);
Q_DECLARE_TYPEINFO(ProcessModel::PercentageHistoryEntry, Q_PRIMITIVE_TYPE);
#endif
......@@ -203,6 +203,11 @@ class ProcessModelPrivate : public QObject
int mTimerId;
QList<long> mPidsToUpdate; ///< A list of pids that we need to emit dataChanged() for regularly
static const int MAX_HIST_ENTRIES = 100;
static const int MIN_HIST_AGE = 200; ///< If the latest history entry is at least this ms old, a new one gets added
/** Storage for the history entries. We need one per percentage column. */
QHash<KSysGuard::Process *, QVector<ProcessModel::PercentageHistoryEntry>> mMapProcessCPUHistory;
#ifdef HAVE_XRES
bool mHaveXRes; ///< True if the XRes extension is available at run time
QMap<qlonglong, XID> mXResClientResources;
......
......@@ -81,16 +81,26 @@ class ProgressBarItemDelegate : public QStyledItemDelegate
initStyleOption(&option,index);
float percentage = index.data(ProcessModel::PercentageRole).toFloat();
if (percentage >= 0)
drawPercentageDisplay(painter,option, percentage);
auto history = index.data(ProcessModel::PercentageHistoryRole).value<QVector<ProcessModel::PercentageHistoryEntry>>();
if (percentage >= 0 || history.size() > 1)
drawPercentageDisplay(painter, option, percentage, history);
else
QStyledItemDelegate::paint(painter, option, index);
}
private:
inline void drawPercentageDisplay(QPainter *painter, QStyleOptionViewItemV4 &option, float percentage) const
inline void drawPercentageDisplay(QPainter *painter, QStyleOptionViewItemV4 &option, float percentage, const QVector<ProcessModel::PercentageHistoryEntry> &history) const
{
QStyle *style = option.widget ? option.widget->style() : QApplication::style();
const QRect &rect = option.rect;
const int HIST_MS_PER_PX = 100; // 100 ms = 1 px -> 1 s = 10 px
bool hasHistory = history.size() > 1;
// Make sure that more than one entry is visible
if (hasHistory) {
int width = (history.crbegin()->timestamp - (history.crbegin() + 1)->timestamp) / HIST_MS_PER_PX;
hasHistory = width < rect.width();
}
// draw the background
style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, option.widget);
......@@ -101,14 +111,46 @@ class ProgressBarItemDelegate : public QStyledItemDelegate
cg = QPalette::Inactive;
//Now draw our percentage thingy
const QRect &rect = option.rect;
int size = qMin(percentage,1.0f) * rect.width();
int size = qMin(int(percentage * rect.height()), rect.height());
if(size > 2 ) { //make sure the line will have a width of more than 1 pixel
painter->setPen(Qt::NoPen);
QColor color = option.palette.color(cg, QPalette::Link);
color.setAlpha(50);
color.setAlpha(33);
painter->fillRect( rect.x(), rect.y() + rect.height() - size, rect.width(), size, color);
}
// Draw the history graph
if(hasHistory) {
QColor color = option.palette.color(cg, QPalette::Link);
color.setAlpha(66);
painter->setPen(Qt::NoPen);
QPainterPath path;
// From right to left
path.moveTo(rect.right(), rect.bottom());
int xNow = rect.right();
auto now = history.constLast();
int height = qMin(int(rect.height() * now.value), rect.height());
path.lineTo(xNow, rect.bottom() - height);
for(int index = history.size() - 2; index >= 0 && xNow > rect.left(); --index) {
auto next = history.at(index);
int width = (now.timestamp - next.timestamp) / HIST_MS_PER_PX;
int xNext = qMax(xNow - width, rect.left());
now = next;
xNow = xNext;
int height = qMin(int(rect.height() * now.value), rect.height());
path.lineTo(xNow, rect.bottom() - height);
}
path.lineTo(xNow, rect.bottom());
path.lineTo(rect.right(), rect.bottom());
painter->fillRect( rect.x(), rect.y(), size, rect.height(), color);
painter->fillPath(path, color);
}
// draw the text
......
......@@ -196,6 +196,7 @@ void testProcess::testUpdateOrAddProcess() {
processController->updateOrAddProcess(0);
processController->updateOrAddProcess(-1);
}
void testProcess::testHistoriesWithWidget() {
KSysGuardProcessList *processList = new KSysGuardProcessList;
processList->treeView()->setColumnHidden(13, false);
......@@ -215,6 +216,30 @@ void testProcess::testHistoriesWithWidget() {
}
delete processList;
}
void testProcess::testCPUGraphHistory() {
KSysGuardProcessList processList;
processList.show();
QTest::qWaitForWindowExposed(&processList);
auto model = processList.processModel();
// Access the PercentageHistoryRole to enable collection
for(int i = 0; i < model->rowCount(); i++) {
auto index = model->index(i, ProcessModel::HeadingCPUUsage, {});
auto percentageHist = index.data(ProcessModel::PercentageHistoryRole).value<QVector<ProcessModel::PercentageHistoryEntry>>();
}
processList.updateList();
// Verify that the current value is the newest history entry
for(int i = 0; i < model->rowCount(); i++) {
auto index = model->index(i, ProcessModel::HeadingCPUUsage, {});
auto percentage = index.data(ProcessModel::PercentageRole).toFloat();
auto percentageHist = index.data(ProcessModel::PercentageHistoryRole).value<QVector<ProcessModel::PercentageHistoryEntry>>();
QVERIFY(percentageHist.size() > 0);
QCOMPARE(percentage, percentageHist.constLast().value);
}
}
QTEST_MAIN(testProcess)
......
......@@ -39,6 +39,7 @@ class testProcess : public QObject
void testHistories();
void testHistoriesWithWidget();
void testUpdateOrAddProcess();
void testCPUGraphHistory();
};
#endif
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment