diff --git a/processui/ProcessModel.h b/processui/ProcessModel.h --- a/processui/ProcessModel.h +++ b/processui/ProcessModel.h @@ -49,6 +49,12 @@ 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 @@ 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 @@ friend class ProcessModelPrivate; }; +Q_DECLARE_METATYPE(QVector); +Q_DECLARE_TYPEINFO(ProcessModel::PercentageHistoryEntry, Q_PRIMITIVE_TYPE); + #endif diff --git a/processui/ProcessModel.cpp b/processui/ProcessModel.cpp --- a/processui/ProcessModel.cpp +++ b/processui/ProcessModel.cpp @@ -797,6 +797,24 @@ 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(QDateTime::currentMSecsSinceEpoch()), usage}); + } + } } } @@ -842,6 +860,8 @@ Q_ASSERT(!mMovingRow); mRemovingRow = true; + mMapProcessCPUHistory.remove(process); + if(mSimple) { return q->beginRemoveRows(QModelIndex(), process->index(), process->index()); } else { @@ -1779,6 +1799,22 @@ 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{}); + } case Qt::DecorationRole: { if(index.column() == HeadingName) { #if HAVE_X11 diff --git a/processui/ProcessModel_p.h b/processui/ProcessModel_p.h --- a/processui/ProcessModel_p.h +++ b/processui/ProcessModel_p.h @@ -203,6 +203,11 @@ int mTimerId; QList 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> mMapProcessCPUHistory; + #ifdef HAVE_XRES bool mHaveXRes; ///< True if the XRes extension is available at run time QMap mXResClientResources; diff --git a/processui/ksysguardprocesslist.cpp b/processui/ksysguardprocesslist.cpp --- a/processui/ksysguardprocesslist.cpp +++ b/processui/ksysguardprocesslist.cpp @@ -81,16 +81,26 @@ initStyleOption(&option,index); float percentage = index.data(ProcessModel::PercentageRole).toFloat(); - if (percentage >= 0) - drawPercentageDisplay(painter,option, percentage); + auto history = index.data(ProcessModel::PercentageHistoryRole).value>(); + 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 &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 @@ 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 diff --git a/tests/processtest.h b/tests/processtest.h --- a/tests/processtest.h +++ b/tests/processtest.h @@ -39,6 +39,7 @@ void testHistories(); void testHistoriesWithWidget(); void testUpdateOrAddProcess(); + void testCPUGraphHistory(); }; #endif diff --git a/tests/processtest.cpp b/tests/processtest.cpp --- a/tests/processtest.cpp +++ b/tests/processtest.cpp @@ -196,6 +196,7 @@ processController->updateOrAddProcess(0); processController->updateOrAddProcess(-1); } + void testProcess::testHistoriesWithWidget() { KSysGuardProcessList *processList = new KSysGuardProcessList; processList->treeView()->setColumnHidden(13, false); @@ -215,6 +216,30 @@ } 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>(); + } + + 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>(); + QVERIFY(percentageHist.size() > 0); + QCOMPARE(percentage, percentageHist.constLast().value); + } +} + QTEST_MAIN(testProcess)