From 0eed4b0524f3f651d2bda530fe2c86cbd0a6c6e3 Mon Sep 17 00:00:00 2001 From: Lieven Hey Date: Mon, 18 Nov 2024 13:42:09 +0100 Subject: [PATCH 1/8] feat: remove tracepoints from timeaxisheaderview The tracepoints are getting a better visual representation on the timelinewidget so this is no longer necessary. --- src/models/timeaxisheaderview.cpp | 49 ------------------------------- src/models/timeaxisheaderview.h | 3 -- src/timelinewidget.cpp | 3 -- 3 files changed, 55 deletions(-) diff --git a/src/models/timeaxisheaderview.cpp b/src/models/timeaxisheaderview.cpp index 0481d02a..e7c60962 100644 --- a/src/models/timeaxisheaderview.cpp +++ b/src/models/timeaxisheaderview.cpp @@ -61,42 +61,6 @@ void TimeAxisHeaderView::emitHeaderDataChanged() headerDataChanged(this->orientation(), EventModel::EventsColumn, EventModel::EventsColumn); } -bool TimeAxisHeaderView::event(QEvent* event) -{ - if (event->type() == QEvent::ToolTip) { - auto helpEvent = static_cast(event); - - auto zoomTime = m_filterAndZoomStack->zoom().time; - if (!zoomTime.isValid()) - zoomTime = m_timeRange; // full - - const auto xForTime = xForTimeFactory(m_timeRange, zoomTime, sectionSize(EventModel::EventsColumn), - sectionPosition(EventModel::EventsColumn)); - - const auto oneNanoSecond = 1e-9; - for (const auto& tracepoint : std::as_const(m_tracepoints.tracepoints)) { - if (zoomTime.contains(tracepoint.time)) { - if (helpEvent->pos().x() == xForTime((tracepoint.time - m_timeRange.start) * oneNanoSecond)) { - QToolTip::showText(helpEvent->globalPos(), tracepoint.name); - return true; - } - } - } - - QToolTip::hideText(); - event->ignore(); - - return true; - } - return QHeaderView::event(event); -} - -void TimeAxisHeaderView::setTracepoints(const Data::TracepointResults& tracepoints) -{ - m_tracepoints = tracepoints; - update(); -} - void TimeAxisHeaderView::paintSection(QPainter* painter, const QRect& rect, int logicalIndex) const { if (painter == nullptr) @@ -134,19 +98,6 @@ void TimeAxisHeaderView::paintSection(QPainter* painter, const QRect& rect, int const QColor tickColor = palette().windowText().color(); const QColor prefixedColor = palette().highlight().color(); - if (!m_tracepoints.tracepoints.isEmpty()) { - const auto scheme = KColorScheme(palette().currentColorGroup()); - const auto tracepointPen = QPen(scheme.foreground(KColorScheme::LinkText), 1); - painter->setPen(tracepointPen); - - for (const auto& tracepoint : m_tracepoints.tracepoints) { - if (!zoomTime.contains(tracepoint.time)) - continue; - const auto x = xForTime((tracepoint.time - m_timeRange.start) * oneNanoSecond); - painter->drawLine(x, rect.height() / 2, x, rect.height()); - } - } - // Draw the long prefix tick and its label if (pfl.hasPrefix()) { diff --git a/src/models/timeaxisheaderview.h b/src/models/timeaxisheaderview.h index 4cc5e238..fe3a6857 100644 --- a/src/models/timeaxisheaderview.h +++ b/src/models/timeaxisheaderview.h @@ -24,15 +24,12 @@ class TimeAxisHeaderView : public QHeaderView public: void setTimeRange(Data::TimeRange timeRange); - void setTracepoints(const Data::TracepointResults& tracepoints); protected slots: void emitHeaderDataChanged(); - bool event(QEvent* event) override; private: Data::TimeRange m_timeRange; - Data::TracepointResults m_tracepoints; const FilterAndZoomStack* m_filterAndZoomStack = nullptr; protected: diff --git a/src/timelinewidget.cpp b/src/timelinewidget.cpp index 8da01028..0dc6d3a1 100644 --- a/src/timelinewidget.cpp +++ b/src/timelinewidget.cpp @@ -104,9 +104,6 @@ TimeLineWidget::TimeLineWidget(PerfParser* parser, QMenu* filterMenu, FilterAndZ } }); - connect(m_parser, &PerfParser::tracepointDataAvailable, this, - [this](const Data::TracepointResults& data) { m_timeAxisHeaderView->setTracepoints(data); }); - connect(ui->timeLineEventSource, static_cast(&QComboBox::currentIndexChanged), this, [this](int index) { const auto typeId = ui->timeLineEventSource->itemData(index).toInt(); From 97c6a37cdebd27efb222a12165a93c0b4d699bd4 Mon Sep 17 00:00:00 2001 From: Lieven Hey Date: Wed, 20 Sep 2023 13:53:10 +0200 Subject: [PATCH 2/8] feat: show tracepoints in the timelinewidget Move the tracepoints from TimeAxisHeaderView to TimeLineWidget so that we can use the header for cpu usage. This also improves usability since the tracepoints are no longer bundles in one line. They now each have their own line. --- src/models/data.h | 34 ++-- src/models/eventmodel.cpp | 256 +++++++++++++++++++++++++----- src/models/timeaxisheaderview.cpp | 1 - src/models/timelinedelegate.cpp | 4 +- src/parsers/perf/perfparser.cpp | 47 +++--- src/parsers/perf/perfparser.h | 2 - 6 files changed, 265 insertions(+), 79 deletions(-) diff --git a/src/models/data.h b/src/models/data.h index 56ea1401..fdb7187a 100644 --- a/src/models/data.h +++ b/src/models/data.h @@ -952,36 +952,37 @@ struct ThreadNames QHash> names; }; +struct TracepointEvents +{ + QString name; + Events events; + bool operator==(const TracepointEvents& rhs) const + { + return std::tie(name, events) == std::tie(rhs.name, rhs.events); + } +}; + struct EventResults { QVector threads; QVector cpus; + QVector tracepoints; QVector> stacks; QVector totalCosts; qint32 offCpuTimeCostId = -1; qint32 lostEventCostId = -1; + qint32 tracepointEventCostId = -1; ThreadEvents* findThread(qint32 pid, qint32 tid); const ThreadEvents* findThread(qint32 pid, qint32 tid) const; bool operator==(const EventResults& rhs) const { - return std::tie(threads, cpus, stacks, totalCosts, offCpuTimeCostId) - == std::tie(rhs.threads, rhs.cpus, rhs.stacks, rhs.totalCosts, rhs.offCpuTimeCostId); + return std::tie(threads, cpus, tracepoints, stacks, totalCosts, offCpuTimeCostId) + == std::tie(rhs.threads, rhs.cpus, rhs.tracepoints, rhs.stacks, rhs.totalCosts, rhs.offCpuTimeCostId); } }; -struct Tracepoint -{ - quint64 time = 0; - QString name; -}; - -struct TracepointResults -{ - QVector tracepoints; -}; - struct FilterAction { TimeRange time; @@ -1094,11 +1095,8 @@ Q_DECLARE_TYPEINFO(Data::ThreadNames, Q_MOVABLE_TYPE); Q_DECLARE_METATYPE(Data::EventResults) Q_DECLARE_TYPEINFO(Data::EventResults, Q_MOVABLE_TYPE); -Q_DECLARE_METATYPE(Data::Tracepoint) -Q_DECLARE_TYPEINFO(Data::Tracepoint, Q_MOVABLE_TYPE); - -Q_DECLARE_METATYPE(Data::TracepointResults) -Q_DECLARE_TYPEINFO(Data::TracepointResults, Q_MOVABLE_TYPE); +Q_DECLARE_METATYPE(Data::TracepointEvents) +Q_DECLARE_TYPEINFO(Data::TracepointEvents, Q_MOVABLE_TYPE); Q_DECLARE_METATYPE(Data::TimeRange) Q_DECLARE_TYPEINFO(Data::TimeRange, Q_MOVABLE_TYPE); diff --git a/src/models/eventmodel.cpp b/src/models/eventmodel.cpp index 27343d65..c90b4f72 100644 --- a/src/models/eventmodel.cpp +++ b/src/models/eventmodel.cpp @@ -9,7 +9,6 @@ #include "../util.h" -#include #include namespace { @@ -22,9 +21,20 @@ enum class Tag : quint8 Overview = 2, Cpus = 3, Processes = 4, - Threads = 5 + Threads = 5, + Tracepoints = 6, }; +enum OverviewRow : quint8 +{ + CpuRow, + ProcessRow, + TracepointRow, +}; +constexpr auto numRows = TracepointRow + 1; + +constexpr auto LAST_TAG = Tag::Tracepoints; + const auto DATATAG_SHIFT = sizeof(Tag) * 8; const auto DATATAG_UNSHIFT = (sizeof(quintptr) - sizeof(Tag)) * 8; @@ -36,7 +46,7 @@ quintptr combineDataTag(Tag tag, quintptr data) Tag dataTag(quintptr internalId) { auto ret = (internalId << DATATAG_UNSHIFT) >> DATATAG_UNSHIFT; - if (ret > static_cast(Tag::Threads)) + if (ret > static_cast(LAST_TAG)) return Tag::Invalid; return static_cast(ret); } @@ -76,15 +86,23 @@ int EventModel::rowCount(const QModelIndex& parent) const case Tag::Invalid: case Tag::Cpus: case Tag::Threads: - break; + case Tag::Tracepoints: + return 0; case Tag::Processes: return m_processes.value(parent.row()).threads.size(); case Tag::Overview: - return (parent.row() == 0) ? m_data.cpus.size() : m_processes.size(); + switch (static_cast(parent.row())) { + case OverviewRow::CpuRow: + return m_data.cpus.size(); + case OverviewRow::ProcessRow: + return m_processes.size(); + case OverviewRow::TracepointRow: + return m_data.tracepoints.size(); + } + Q_UNREACHABLE(); case Tag::Root: - return 2; - }; - + return numRows; + } return 0; } @@ -134,17 +152,29 @@ QVariant EventModel::data(const QModelIndex& index, int role) const auto tag = dataTag(index); + Q_ASSERT(static_cast(tag) <= static_cast(LAST_TAG)); + if (tag == Tag::Invalid || tag == Tag::Root) { return {}; } else if (tag == Tag::Overview) { if (role == Qt::DisplayRole) { - return index.row() == 0 ? tr("CPUs") : tr("Processes"); + switch (static_cast(index.row())) { + case OverviewRow::CpuRow: + return tr("CPUs"); + case OverviewRow::ProcessRow: + return tr("Processes"); + case OverviewRow::TracepointRow: + return tr("Tracepoints"); + } } else if (role == Qt::ToolTipRole) { - if (index.row() == 0) { + switch (static_cast(index.row())) { + case OverviewRow::CpuRow: return tr("Event timelines for all CPUs. This shows you which, and how many CPUs where leveraged." "Note that this feature relies on perf data files recorded with --sample-cpu."); - } else { + case OverviewRow::ProcessRow: return tr("Event timelines for the individual threads and processes."); + case OverviewRow::TracepointRow: + return tr("Event timelines for tracepoints"); } } else if (role == SortRole) { return index.row(); @@ -201,47 +231,163 @@ QVariant EventModel::data(const QModelIndex& index, int role) const const Data::ThreadEvents* thread = nullptr; const Data::CpuEvents* cpu = nullptr; + const Data::TracepointEvents* tracepoint = nullptr; if (tag == Tag::Cpus) { cpu = &m_data.cpus[index.row()]; - } else { - Q_ASSERT(tag == Tag::Threads); + } else if (tag == Tag::Threads) { const auto process = m_processes.value(tagData(index.internalId())); const auto tid = process.threads.value(index.row()); thread = m_data.findThread(process.pid, tid); Q_ASSERT(thread); + } else if (tag == Tag::Tracepoints) { + tracepoint = &m_data.tracepoints[index.row()]; } if (role == ThreadStartRole) { - return thread ? thread->time.start : m_time.start; + switch (tag) { + case Tag::Threads: + return thread->time.start; + case Tag::Invalid: + case Tag::Root: + case Tag::Overview: + case Tag::Cpus: + case Tag::Processes: + case Tag::Tracepoints: + return m_time.start; + } } else if (role == ThreadEndRole) { - return thread ? thread->time.end : m_time.end; + switch (tag) { + case Tag::Threads: + return thread->time.end; + case Tag::Invalid: + case Tag::Root: + case Tag::Overview: + case Tag::Cpus: + case Tag::Processes: + case Tag::Tracepoints: + return m_time.end; + } } else if (role == ThreadNameRole) { - return thread ? thread->name : tr("CPU #%1").arg(cpu->cpuId); + switch (tag) { + case Tag::Threads: + return thread->name; + case Tag::Cpus: + return tr("CPU #%1").arg(cpu->cpuId); + case Tag::Invalid: + case Tag::Root: + case Tag::Overview: + case Tag::Processes: + case Tag::Tracepoints: + return {}; + } } else if (role == ThreadIdRole) { - return thread ? thread->tid : Data::INVALID_TID; + switch (tag) { + case Tag::Threads: + return thread->tid; + case Tag::Invalid: + case Tag::Root: + case Tag::Overview: + case Tag::Cpus: + case Tag::Processes: + case Tag::Tracepoints: + return Data::INVALID_TID; + } } else if (role == ProcessIdRole) { - return thread ? thread->pid : Data::INVALID_PID; + switch (tag) { + case Tag::Threads: + return thread->pid; + case Tag::Invalid: + case Tag::Root: + case Tag::Overview: + case Tag::Cpus: + case Tag::Processes: + case Tag::Tracepoints: + return Data::INVALID_PID; + } } else if (role == CpuIdRole) { - return cpu ? cpu->cpuId : Data::INVALID_CPU_ID; + switch (tag) { + case Tag::Cpus: + return cpu->cpuId; + case Tag::Invalid: + case Tag::Root: + case Tag::Overview: + case Tag::Processes: + case Tag::Threads: + case Tag::Tracepoints: + return Data::INVALID_CPU_ID; + } } else if (role == EventsRole) { - return QVariant::fromValue(thread ? thread->events : (cpu ? cpu->events : Data::Events())); + switch (tag) { + case Tag::Threads: + return QVariant::fromValue(thread->events); + case Tag::Cpus: + return QVariant::fromValue(cpu->events); + case Tag::Tracepoints: + return QVariant::fromValue(tracepoint->events); + case Tag::Invalid: + case Tag::Root: + case Tag::Overview: + case Tag::Processes: + return {}; + } } else if (role == SortRole) { - if (index.column() == ThreadColumn) - return thread ? thread->tid : cpu->cpuId; - else - return thread ? thread->events.size() : cpu->events.size(); + if (index.column() == ThreadColumn) { + switch (tag) { + case Tag::Threads: + return thread->tid; + case Tag::Cpus: + return cpu->cpuId; + case Tag::Tracepoints: + return tracepoint->name; + case Tag::Invalid: + case Tag::Root: + case Tag::Overview: + case Tag::Processes: + return {}; + } + } else { + switch (tag) { + case Tag::Threads: + return thread->events.size(); + case Tag::Cpus: + return cpu->events.size(); + case Tag::Tracepoints: + return tracepoint->events.size(); + case Tag::Invalid: + case Tag::Root: + case Tag::Overview: + case Tag::Processes: + return {}; + } + } } switch (static_cast(index.column())) { case ThreadColumn: if (role == Qt::DisplayRole) { - return cpu ? tr("CPU #%1").arg(cpu->cpuId) : tr("%1 (#%2)").arg(thread->name, QString::number(thread->tid)); + switch (tag) { + case Tag::Cpus: + return tr("CPU #%1").arg(cpu->cpuId); + case Tag::Threads: + return tr("%1 (#%2)").arg(thread->name, QString::number(thread->tid)); + case Tag::Tracepoints: + return tracepoint->name; + case Tag::Invalid: + case Tag::Root: + case Tag::Overview: + case Tag::Processes: + return {}; + } } else if (role == Qt::ToolTipRole) { - QString tooltip = cpu ? tr("CPU #%1\n").arg(cpu->cpuId) - : tr("Thread %1, tid = %2, pid = %3\n") - .arg(thread->name, QString::number(thread->tid), QString::number(thread->pid)); - if (thread) { + QString tooltip; + int numEvents = 0; + + switch (tag) { + case Tag::Threads: { + tooltip = tr("Thread %1, tid = %2, pid = %3\n") + .arg(thread->name, QString::number(thread->tid), QString::number(thread->pid)); + const auto runtime = thread->time.delta(); const auto totalRuntime = m_time.delta(); tooltip += tr("Runtime: %1 (%2% of total runtime)\n") @@ -256,16 +402,45 @@ QVariant EventModel::data(const QModelIndex& index, int role) const Util::formatCostRelative(thread->offCpuTime, runtime), Util::formatCostRelative(thread->offCpuTime, m_totalOffCpuTime)); } + numEvents = thread->events.size(); + break; } - const auto numEvents = thread ? thread->events.size() : cpu->events.size(); + case Tag::Cpus: + tooltip = tr("CPU #%1\n").arg(cpu->cpuId); + numEvents = cpu->events.size(); + break; + case Tag::Tracepoints: + tooltip = tracepoint->name; + numEvents = tracepoint->events.size(); + break; + case Tag::Invalid: + case Tag::Root: + case Tag::Overview: + case Tag::Processes: + return {}; + } + tooltip += tr("Number of Events: %1 (%2% of the total)") .arg(QString::number(numEvents), Util::formatCostRelative(numEvents, m_totalEvents)); return tooltip; } break; case EventsColumn: - if (role == Qt::DisplayRole) - return thread ? thread->events.size() : cpu->events.size(); + if (role == Qt::DisplayRole) { + switch (tag) { + case Tag::Threads: + return thread->events.size(); + case Tag::Cpus: + return cpu->events.size(); + case Tag::Tracepoints: + return tracepoint->events.size(); + case Tag::Invalid: + case Tag::Root: + case Tag::Overview: + case Tag::Processes: + return {}; + } + } break; case NUM_COLUMNS: // nothing @@ -318,6 +493,7 @@ void EventModel::setData(const Data::EventResults& data) [](const Data::CpuEvents& cpuEvents) { return cpuEvents.events.isEmpty(); }); m_data.cpus.erase(it, m_data.cpus.end()); } + endResetModel(); } @@ -335,15 +511,21 @@ QModelIndex EventModel::index(int row, int column, const QModelIndex& parent) co switch (dataTag(parent)) { case Tag::Invalid: // leaf / invalid -> no children case Tag::Cpus: + case Tag::Tracepoints: case Tag::Threads: break; case Tag::Root: // root has the 1st level children: Overview return createIndex(row, column, static_cast(Tag::Overview)); case Tag::Overview: // 2nd level children: Cpus and the Processes - if (parent.row() == 0) + switch (static_cast(parent.row())) { + case OverviewRow::CpuRow: return createIndex(row, column, static_cast(Tag::Cpus)); - else + case OverviewRow::ProcessRow: return createIndex(row, column, static_cast(Tag::Processes)); + case OverviewRow::TracepointRow: + return createIndex(row, column, static_cast(Tag::Tracepoints)); + } + Q_UNREACHABLE(); case Tag::Processes: // 3rd level children: Threads return createIndex(row, column, combineDataTag(Tag::Threads, parent.row())); } @@ -359,9 +541,11 @@ QModelIndex EventModel::parent(const QModelIndex& child) const case Tag::Overview: break; case Tag::Cpus: - return createIndex(0, 0, static_cast(Tag::Overview)); + return createIndex(OverviewRow::CpuRow, 0, static_cast(Tag::Overview)); case Tag::Processes: - return createIndex(1, 0, static_cast(Tag::Overview)); + return createIndex(OverviewRow::ProcessRow, 0, static_cast(Tag::Overview)); + case Tag::Tracepoints: + return createIndex(OverviewRow::TracepointRow, 0, static_cast(Tag::Overview)); case Tag::Threads: { const auto parentRow = tagData(child.internalId()); return createIndex(parentRow, 0, static_cast(Tag::Processes)); diff --git a/src/models/timeaxisheaderview.cpp b/src/models/timeaxisheaderview.cpp index e7c60962..f00bbbb0 100644 --- a/src/models/timeaxisheaderview.cpp +++ b/src/models/timeaxisheaderview.cpp @@ -14,7 +14,6 @@ #include -#include "../util.h" #include "eventmodel.h" #include "filterandzoomstack.h" diff --git a/src/models/timelinedelegate.cpp b/src/models/timelinedelegate.cpp index b567f280..5a6ec7de 100644 --- a/src/models/timelinedelegate.cpp +++ b/src/models/timelinedelegate.cpp @@ -160,6 +160,7 @@ void TimeLineDelegate::paint(QPainter* painter, const QStyleOptionViewItem& opti const auto results = index.data(EventModel::EventResultsRole).value(); const auto offCpuCostId = results.offCpuTimeCostId; const auto lostEventCostId = results.lostEventCostId; + const auto tracepointEventCostId = results.tracepointEventCostId; const bool is_alternate = option.features & QStyleOptionViewItem::Alternate; const auto& palette = option.palette; @@ -229,7 +230,8 @@ void TimeLineDelegate::paint(QPainter* painter, const QStyleOptionViewItem& opti // see also: https://www.spinics.net/lists/linux-perf-users/msg03486.html for (const auto& event : data.events) { const auto isLostEvent = event.type == lostEventCostId; - if (event.type != m_eventType && !isLostEvent) { + const auto isTracepointEvent = event.type == tracepointEventCostId; + if (event.type != m_eventType && !isLostEvent && !isTracepointEvent) { continue; } diff --git a/src/parsers/perf/perfparser.cpp b/src/parsers/perf/perfparser.cpp index d61593cc..8b6b1116 100644 --- a/src/parsers/perf/perfparser.cpp +++ b/src/parsers/perf/perfparser.cpp @@ -902,6 +902,10 @@ class PerfParserPrivate : public QObject buildPerLibraryResult(); buildCallerCalleeResult(); + for (auto it = tracepoints.begin(), end = tracepoints.end(); it != end; it++) { + eventResult.tracepoints.push_back({strings[it.key()], {it.value()}}); + } + for (auto& thread : eventResult.threads) { thread.time.start = std::max(thread.time.start, applicationTime.start); thread.time.end = std::min(thread.time.end, applicationTime.end); @@ -1123,12 +1127,14 @@ class PerfParserPrivate : public QObject const auto attribute = attributes.value(event.type); if (attribute.type == static_cast(AttributesDefinition::Type::Tracepoint)) { - Data::Tracepoint tracepoint; - tracepoint.time = event.time; - tracepoint.name = strings.value(attribute.name.id); - if (tracepoint.name != QLatin1String("sched:sched_switch")) { - // sched_switch events are handled separately already - tracepointResult.tracepoints.push_back(tracepoint); + if (eventResult.tracepointEventCostId == -1) { + eventResult.tracepointEventCostId = + addCostType(QStringLiteral("Tracepoint"), Data::Costs::Unit::Tracepoint); + } + + if (attribute.name.id != m_schedSwitchId) { + auto& tracepointList = tracepoints[attribute.name.id]; + tracepointList.push_back({event.time, 0, eventResult.tracepointEventCostId}); } } } @@ -1145,6 +1151,10 @@ class PerfParserPrivate : public QObject { Q_ASSERT(string.id == strings.size()); strings.push_back(QString::fromUtf8(string.string)); + + if (string.string == QByteArray("sched:sched_switch")) { + m_schedSwitchId = string.id; + } } void addSampleToBottomUp(const Sample& sample) @@ -1415,7 +1425,7 @@ class PerfParserPrivate : public QObject Data::CallerCalleeResults callerCalleeResult; Data::ByFileResults byFileResult; Data::EventResults eventResult; - Data::TracepointResults tracepointResult; + QHash tracepoints; Data::FrequencyResults frequencyResult; Data::ThreadNames commands; std::unique_ptr perfScriptOutput; @@ -1427,6 +1437,7 @@ class PerfParserPrivate : public QObject QHash attributeNameToCostIds; qint32 m_nextCostId = 0; qint32 m_schedSwitchCostId = -1; + qint32 m_schedSwitchId = -1; QHash m_lastSampleTimePerCore; Settings::CostAggregation costAggregation; bool perfMapFileExists = false; @@ -1461,7 +1472,7 @@ PerfParser::PerfParser(QObject* parent) qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); - qRegisterMetaType(); + qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); @@ -1491,11 +1502,6 @@ PerfParser::PerfParser(QObject* parent) m_events = data; } }); - connect(this, &PerfParser::tracepointDataAvailable, this, [this](const Data::TracepointResults& data) { - if (m_tracepointResults.tracepoints.isEmpty()) { - m_tracepointResults = data; - } - }); connect(this, &PerfParser::threadNamesAvailable, this, [this](const Data::ThreadNames& threadNames) { m_threadNames = threadNames; }); connect(this, &PerfParser::parsingStarted, this, [this]() { @@ -1614,7 +1620,6 @@ void PerfParser::startParseFile(const QString& path) m_bottomUpResults = {}; m_callerCalleeResults = {}; m_byFileResults = {}; - m_tracepointResults = {}; m_events = {}; m_frequencyResults = {}; @@ -1638,7 +1643,6 @@ void PerfParser::startParseFile(const QString& path) emit summaryDataAvailable(d.summaryResult); emit callerCalleeDataAvailable(d.callerCalleeResult); emit byFileDataAvailable(d.byFileResult); - emit tracepointDataAvailable(d.tracepointResult); emit eventsAvailable(d.eventResult); emit frequencyDataAvailable(d.frequencyResult); emit threadNamesAvailable(d.commands); @@ -1767,7 +1771,6 @@ void PerfParser::filterResults(const Data::FilterAction& filter) Data::EventResults events = m_events; Data::CallerCalleeResults callerCallee; Data::ByFileResults byFile; - Data::TracepointResults tracepointResults = m_tracepointResults; auto frequencyResults = m_frequencyResults; const bool filterByTime = filter.time.isValid(); const bool filterByCpu = filter.cpuId != std::numeric_limits::max(); @@ -1849,10 +1852,13 @@ void PerfParser::filterResults(const Data::FilterAction& filter) } if (filterByTime) { - auto it = std::remove_if( - tracepointResults.tracepoints.begin(), tracepointResults.tracepoints.end(), - [filter](const Data::Tracepoint& tracepoint) { return !filter.time.contains(tracepoint.time); }); - tracepointResults.tracepoints.erase(it, tracepointResults.tracepoints.end()); + // TODO: parallelize + for (auto& tracepoints : events.tracepoints) { + auto it = std::remove_if( + tracepoints.events.begin(), tracepoints.events.end(), + [filter](const Data::Event& event) { return !filter.time.contains(event.time); }); + tracepoints.events.erase(it, tracepoints.events.end()); + } for (auto& core : frequencyResults.cores) { for (auto& costType : core.costs) { @@ -1967,7 +1973,6 @@ void PerfParser::filterResults(const Data::FilterAction& filter) emit callerCalleeDataAvailable(callerCallee); emit byFileDataAvailable(byFile); emit frequencyDataAvailable(frequencyResults); - emit tracepointDataAvailable(tracepointResults); emit eventsAvailable(events); emit parsingFinished(); }); diff --git a/src/parsers/perf/perfparser.h b/src/parsers/perf/perfparser.h index 7133f49b..988bdb3e 100644 --- a/src/parsers/perf/perfparser.h +++ b/src/parsers/perf/perfparser.h @@ -60,7 +60,6 @@ class PerfParser : public QObject void perLibraryDataAvailable(const Data::PerLibraryResults& data); void callerCalleeDataAvailable(const Data::CallerCalleeResults& data); void byFileDataAvailable(const Data::ByFileResults& data); - void tracepointDataAvailable(const Data::TracepointResults& data); void frequencyDataAvailable(const Data::FrequencyResults& data); void eventsAvailable(const Data::EventResults& events); void threadNamesAvailable(const Data::ThreadNames& threadNames); @@ -87,7 +86,6 @@ class PerfParser : public QObject Data::BottomUpResults m_bottomUpResults; Data::CallerCalleeResults m_callerCalleeResults; Data::ByFileResults m_byFileResults; - Data::TracepointResults m_tracepointResults; Data::EventResults m_events; Data::FrequencyResults m_frequencyResults; std::atomic m_isParsing; From 623cc98fe4573fa0b027648f38668a1c0d647a59 Mon Sep 17 00:00:00 2001 From: Lieven Hey Date: Thu, 21 Sep 2023 11:27:21 +0200 Subject: [PATCH 3/8] feat: simplify tag enum Enum automatically counts up so there is no need to manually set these values. --- src/models/eventmodel.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/models/eventmodel.cpp b/src/models/eventmodel.cpp index c90b4f72..a6853a90 100644 --- a/src/models/eventmodel.cpp +++ b/src/models/eventmodel.cpp @@ -17,12 +17,12 @@ constexpr auto orderProcessByPid = [](const EventModel::Process& process, qint32 enum class Tag : quint8 { Invalid = 0, - Root = 1, - Overview = 2, - Cpus = 3, - Processes = 4, - Threads = 5, - Tracepoints = 6, + Root, + Overview, + Cpus, + Processes, + Threads, + Tracepoints, }; enum OverviewRow : quint8 From 72b2321d5e8774c87f50086f9b005a17327af999 Mon Sep 17 00:00:00 2001 From: Lieven Hey Date: Thu, 21 Sep 2023 11:32:58 +0200 Subject: [PATCH 4/8] feat: add Favourites to TimeLineWidget this allows the user to group important timelines together so that he can compare them better --- src/models/eventmodel.cpp | 102 ++++++++++++++++++++++- src/models/eventmodel.h | 6 ++ src/models/timelinedelegate.cpp | 15 ++++ src/models/timelinedelegate.h | 2 + src/timelinewidget.cpp | 5 ++ tests/modeltests/tst_models.cpp | 142 +++++++++++++++++++------------- 6 files changed, 213 insertions(+), 59 deletions(-) diff --git a/src/models/eventmodel.cpp b/src/models/eventmodel.cpp index a6853a90..b4a3e01a 100644 --- a/src/models/eventmodel.cpp +++ b/src/models/eventmodel.cpp @@ -23,6 +23,7 @@ enum class Tag : quint8 Processes, Threads, Tracepoints, + Favorites, }; enum OverviewRow : quint8 @@ -30,10 +31,11 @@ enum OverviewRow : quint8 CpuRow, ProcessRow, TracepointRow, + FavoriteRow, }; -constexpr auto numRows = TracepointRow + 1; +constexpr auto numRows = FavoriteRow + 1; -constexpr auto LAST_TAG = Tag::Tracepoints; +constexpr auto LAST_TAG = Tag::Favorites; const auto DATATAG_SHIFT = sizeof(Tag) * 8; const auto DATATAG_UNSHIFT = (sizeof(quintptr) - sizeof(Tag)) * 8; @@ -87,6 +89,7 @@ int EventModel::rowCount(const QModelIndex& parent) const case Tag::Cpus: case Tag::Threads: case Tag::Tracepoints: + case Tag::Favorites: return 0; case Tag::Processes: return m_processes.value(parent.row()).threads.size(); @@ -98,6 +101,8 @@ int EventModel::rowCount(const QModelIndex& parent) const return m_processes.size(); case OverviewRow::TracepointRow: return m_data.tracepoints.size(); + case OverviewRow::FavoriteRow: + return m_favourites.size(); } Q_UNREACHABLE(); case Tag::Root: @@ -165,6 +170,8 @@ QVariant EventModel::data(const QModelIndex& index, int role) const return tr("Processes"); case OverviewRow::TracepointRow: return tr("Tracepoints"); + case OverviewRow::FavoriteRow: + return tr("Favorites"); } } else if (role == Qt::ToolTipRole) { switch (static_cast(index.row())) { @@ -175,6 +182,8 @@ QVariant EventModel::data(const QModelIndex& index, int role) const return tr("Event timelines for the individual threads and processes."); case OverviewRow::TracepointRow: return tr("Event timelines for tracepoints"); + case OverviewRow::FavoriteRow: + return tr("A list of favourites to group important events"); } } else if (role == SortRole) { return index.row(); @@ -242,6 +251,13 @@ QVariant EventModel::data(const QModelIndex& index, int role) const Q_ASSERT(thread); } else if (tag == Tag::Tracepoints) { tracepoint = &m_data.tracepoints[index.row()]; + } else if (tag == Tag::Favorites) { + if (role == IsFavoriteRole) { + return true; + } + + const auto& favourite = m_favourites[index.row()]; + return data(favourite.siblingAtColumn(index.column()), role); } if (role == ThreadStartRole) { @@ -255,6 +271,9 @@ QVariant EventModel::data(const QModelIndex& index, int role) const case Tag::Processes: case Tag::Tracepoints: return m_time.start; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); } } else if (role == ThreadEndRole) { switch (tag) { @@ -267,6 +286,9 @@ QVariant EventModel::data(const QModelIndex& index, int role) const case Tag::Processes: case Tag::Tracepoints: return m_time.end; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); } } else if (role == ThreadNameRole) { switch (tag) { @@ -280,6 +302,9 @@ QVariant EventModel::data(const QModelIndex& index, int role) const case Tag::Processes: case Tag::Tracepoints: return {}; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); } } else if (role == ThreadIdRole) { switch (tag) { @@ -292,6 +317,9 @@ QVariant EventModel::data(const QModelIndex& index, int role) const case Tag::Processes: case Tag::Tracepoints: return Data::INVALID_TID; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); } } else if (role == ProcessIdRole) { switch (tag) { @@ -304,6 +332,9 @@ QVariant EventModel::data(const QModelIndex& index, int role) const case Tag::Processes: case Tag::Tracepoints: return Data::INVALID_PID; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); } } else if (role == CpuIdRole) { switch (tag) { @@ -316,6 +347,9 @@ QVariant EventModel::data(const QModelIndex& index, int role) const case Tag::Threads: case Tag::Tracepoints: return Data::INVALID_CPU_ID; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); } } else if (role == EventsRole) { switch (tag) { @@ -330,6 +364,9 @@ QVariant EventModel::data(const QModelIndex& index, int role) const case Tag::Overview: case Tag::Processes: return {}; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); } } else if (role == SortRole) { if (index.column() == ThreadColumn) { @@ -345,6 +382,9 @@ QVariant EventModel::data(const QModelIndex& index, int role) const case Tag::Overview: case Tag::Processes: return {}; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); } } else { switch (tag) { @@ -359,8 +399,13 @@ QVariant EventModel::data(const QModelIndex& index, int role) const case Tag::Overview: case Tag::Processes: return {}; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); } } + } else if (role == IsFavoriteRole) { + return false; } switch (static_cast(index.column())) { @@ -378,6 +423,9 @@ QVariant EventModel::data(const QModelIndex& index, int role) const case Tag::Overview: case Tag::Processes: return {}; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); } } else if (role == Qt::ToolTipRole) { QString tooltip; @@ -418,6 +466,9 @@ QVariant EventModel::data(const QModelIndex& index, int role) const case Tag::Overview: case Tag::Processes: return {}; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); } tooltip += tr("Number of Events: %1 (%2% of the total)") @@ -439,6 +490,9 @@ QVariant EventModel::data(const QModelIndex& index, int role) const case Tag::Overview: case Tag::Processes: return {}; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); } } break; @@ -453,6 +507,8 @@ QVariant EventModel::data(const QModelIndex& index, int role) const void EventModel::setData(const Data::EventResults& data) { beginResetModel(); + m_favourites.clear(); + m_data = data; m_totalEvents = 0; m_maxCost = 0; @@ -513,6 +569,7 @@ QModelIndex EventModel::index(int row, int column, const QModelIndex& parent) co case Tag::Cpus: case Tag::Tracepoints: case Tag::Threads: + case Tag::Favorites: break; case Tag::Root: // root has the 1st level children: Overview return createIndex(row, column, static_cast(Tag::Overview)); @@ -524,6 +581,8 @@ QModelIndex EventModel::index(int row, int column, const QModelIndex& parent) co return createIndex(row, column, static_cast(Tag::Processes)); case OverviewRow::TracepointRow: return createIndex(row, column, static_cast(Tag::Tracepoints)); + case OverviewRow::FavoriteRow: + return createIndex(row, column, static_cast(Tag::Favorites)); } Q_UNREACHABLE(); case Tag::Processes: // 3rd level children: Threads @@ -545,7 +604,9 @@ QModelIndex EventModel::parent(const QModelIndex& child) const case Tag::Processes: return createIndex(OverviewRow::ProcessRow, 0, static_cast(Tag::Overview)); case Tag::Tracepoints: - return createIndex(OverviewRow::TracepointRow, 0, static_cast(Tag::Overview)); + return createIndex(OverviewRow::TracepointRow, 0, static_cast(Tag::Overview)); + case Tag::Favorites: + return createIndex(OverviewRow::FavoriteRow, 0, static_cast(Tag::Overview)); case Tag::Threads: { const auto parentRow = tagData(child.internalId()); return createIndex(parentRow, 0, static_cast(Tag::Processes)); @@ -554,3 +615,38 @@ QModelIndex EventModel::parent(const QModelIndex& child) const return {}; } + +void EventModel::addToFavorites(const QModelIndex& index) +{ + Q_ASSERT(index.model() == this); + + if (index.column() != 0) { + // we only want one index per row, so we force it to be column zero + // this way we can easily check if we have duplicate rows + addToFavorites(index.siblingAtColumn(0)); + return; + } + + if (m_favourites.contains(index)) { + return; + } + + const auto row = m_favourites.size(); + + beginInsertRows(createIndex(FavoriteRow, 0, static_cast(Tag::Overview)), row, row); + m_favourites.push_back(index); + endInsertRows(); +} + +void EventModel::removeFromFavorites(const QModelIndex& index) +{ + Q_ASSERT(index.model() == this); + Q_ASSERT(dataTag(index) == Tag::Favorites); + + const auto row = index.row(); + Q_ASSERT(row >= 0 && row < m_favourites.size()); + + beginRemoveRows(createIndex(FavoriteRow, 0, static_cast(Tag::Overview)), row, row); + m_favourites.remove(row); + endRemoveRows(); +} diff --git a/src/models/eventmodel.h b/src/models/eventmodel.h index 9554be52..bd2b9567 100644 --- a/src/models/eventmodel.h +++ b/src/models/eventmodel.h @@ -42,6 +42,7 @@ class EventModel : public QAbstractItemModel SortRole, TotalCostsRole, EventResultsRole, + IsFavoriteRole, }; int rowCount(const QModelIndex& parent = {}) const override; @@ -71,9 +72,14 @@ class EventModel : public QAbstractItemModel QString name; }; +public: + void addToFavorites(const QModelIndex& index); + void removeFromFavorites(const QModelIndex& index); + private: Data::EventResults m_data; QVector m_processes; + QVector m_favourites; Data::TimeRange m_time; quint64 m_totalOnCpuTime = 0; quint64 m_totalOffCpuTime = 0; diff --git a/src/models/timelinedelegate.cpp b/src/models/timelinedelegate.cpp index 5a6ec7de..48d90edc 100644 --- a/src/models/timelinedelegate.cpp +++ b/src/models/timelinedelegate.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include "../util.h" @@ -459,6 +460,20 @@ bool TimeLineDelegate::eventFilter(QObject* watched, QEvent* event) const auto isMainThread = threadStartTime == minTime && threadEndTime == maxTime; const auto cpuId = index.data(EventModel::CpuIdRole).value(); const auto numCpus = index.data(EventModel::NumCpusRole).value(); + const auto isFavorite = index.data(EventModel::IsFavoriteRole).value(); + + contextMenu->addAction(QIcon::fromTheme(QStringLiteral("favorite")), + isFavorite ? tr("Remove from favorites") : tr("Add to favorites"), this, + [this, index, isFavorite] { + auto model = qobject_cast(index.model()); + Q_ASSERT(model); + if (isFavorite) { + emit removeFromFavorites(model->mapToSource(index)); + } else { + emit addToFavorites(model->mapToSource(index)); + } + }); + if (isTimeSpanSelected && (minTime != timeSlice.start || maxTime != timeSlice.end)) { contextMenu->addAction(QIcon::fromTheme(QStringLiteral("zoom-in")), tr("Zoom In On Selection"), this, [this, timeSlice]() { m_filterAndZoomStack->zoomIn(timeSlice); }); diff --git a/src/models/timelinedelegate.h b/src/models/timelinedelegate.h index 3332f6cd..47e5e2bb 100644 --- a/src/models/timelinedelegate.h +++ b/src/models/timelinedelegate.h @@ -64,6 +64,8 @@ class TimeLineDelegate : public QStyledItemDelegate signals: void stacksHovered(const QSet& stacks); + void addToFavorites(const QModelIndex& index); + void removeFromFavorites(const QModelIndex& index); protected: bool eventFilter(QObject* watched, QEvent* event) override; diff --git a/src/timelinewidget.cpp b/src/timelinewidget.cpp index 0dc6d3a1..e38458bc 100644 --- a/src/timelinewidget.cpp +++ b/src/timelinewidget.cpp @@ -110,6 +110,11 @@ TimeLineWidget::TimeLineWidget(PerfParser* parser, QMenu* filterMenu, FilterAndZ m_timeLineDelegate->setEventType(typeId); }); + connect(m_timeLineDelegate, &TimeLineDelegate::addToFavorites, this, + [eventModel](const QModelIndex& index) { eventModel->addToFavorites(index); }); + connect(m_timeLineDelegate, &TimeLineDelegate::removeFromFavorites, this, + [eventModel](const QModelIndex& index) { eventModel->removeFromFavorites(index); }); + connect(m_timeLineDelegate, &TimeLineDelegate::stacksHovered, this, [this](const QSet& stackIds) { if (stackIds.isEmpty()) { ++m_currentHoverStacksJobId; diff --git a/tests/modeltests/tst_models.cpp b/tests/modeltests/tst_models.cpp index c0dbefa7..c8f4e74e 100644 --- a/tests/modeltests/tst_models.cpp +++ b/tests/modeltests/tst_models.cpp @@ -558,72 +558,18 @@ private slots: void testEventModel() { - Data::EventResults events; - events.cpus.resize(3); - events.cpus[0].cpuId = 0; - events.cpus[1].cpuId = 1; // empty - events.cpus[2].cpuId = 2; + const auto events = createEventModelTestData(); const int nonEmptyCpus = 2; const int processes = 2; const quint64 endTime = 1000; - const quint64 deltaTime = 10; - events.threads.resize(4); - auto& thread1 = events.threads[0]; - { - thread1.pid = 1234; - thread1.tid = 1234; - thread1.time = {0, endTime}; - thread1.name = QStringLiteral("foobar"); - } - auto& thread2 = events.threads[1]; - { - thread2.pid = 1234; - thread2.tid = 1235; - thread2.time = {deltaTime, endTime - deltaTime}; - thread2.name = QStringLiteral("asdf"); - } - auto& thread3 = events.threads[2]; - { - thread3.pid = 5678; - thread3.tid = 5678; - thread3.time = {0, endTime}; - thread3.name = QStringLiteral("barfoo"); - } - auto& thread4 = events.threads[3]; - { - thread4.pid = 5678; - thread4.tid = 5679; - thread4.time = {endTime - deltaTime, endTime}; - thread4.name = QStringLiteral("blub"); - } - - Data::CostSummary costSummary(QStringLiteral("cycles"), 0, 0, Data::Costs::Unit::Unknown); - auto generateEvent = [&costSummary, &events](quint64 time, quint32 cpuId) -> Data::Event { - Data::Event event; - event.cost = 10; - event.cpuId = cpuId; - event.type = 0; - event.time = time; - ++costSummary.sampleCount; - costSummary.totalPeriod += event.cost; - events.cpus[cpuId].events << event; - return event; - }; - for (quint64 time = 0; time < endTime; time += deltaTime) { - thread1.events << generateEvent(time, 0); - if (thread2.time.contains(time)) { - thread2.events << generateEvent(time, 2); - } - } - events.totalCosts = {costSummary}; EventModel model; QAbstractItemModelTester tester(&model); model.setData(events); QCOMPARE(model.columnCount(), static_cast(EventModel::NUM_COLUMNS)); - QCOMPARE(model.rowCount(), 2); + QCOMPARE(model.rowCount(), 4); auto simplifiedEvents = events; simplifiedEvents.cpus.remove(1); @@ -705,6 +651,27 @@ private slots: } } + void testEventModelFavorites() + { + const auto events = createEventModelTestData(); + EventModel model; + QAbstractItemModelTester tester(&model); + model.setData(events); + + const auto favoritesIndex = model.index(3, 0); + const auto processesIndex = model.index(1, 0); + + QCOMPARE(model.rowCount(favoritesIndex), 0); + QCOMPARE(model.data(model.index(0, 0, processesIndex)).toString(), QLatin1String("foobar (#1234)")); + + model.addToFavorites(model.index(0, 0, processesIndex)); + QCOMPARE(model.rowCount(favoritesIndex), 1); + QCOMPARE(model.data(model.index(0, 0, favoritesIndex)).toString(), QLatin1String("foobar (#1234)")); + + model.removeFromFavorites(model.index(0, 0, favoritesIndex)); + QCOMPARE(model.rowCount(favoritesIndex), 0); + } + void testPrettySymbol_data() { QTest::addColumn("prettySymbol"); @@ -947,6 +914,69 @@ private slots: font.setPixelSize(10); return QFontMetrics(font); } + + Data::EventResults createEventModelTestData() + { + Data::EventResults events; + events.cpus.resize(3); + events.cpus[0].cpuId = 0; + events.cpus[1].cpuId = 1; // empty + events.cpus[2].cpuId = 2; + + const quint64 endTime = 1000; + const quint64 deltaTime = 10; + events.threads.resize(4); + auto& thread1 = events.threads[0]; + { + thread1.pid = 1234; + thread1.tid = 1234; + thread1.time = {0, endTime}; + thread1.name = QStringLiteral("foobar"); + } + auto& thread2 = events.threads[1]; + { + thread2.pid = 1234; + thread2.tid = 1235; + thread2.time = {deltaTime, endTime - deltaTime}; + thread2.name = QStringLiteral("asdf"); + } + auto& thread3 = events.threads[2]; + { + thread3.pid = 5678; + thread3.tid = 5678; + thread3.time = {0, endTime}; + thread3.name = QStringLiteral("barfoo"); + } + auto& thread4 = events.threads[3]; + { + thread4.pid = 5678; + thread4.tid = 5679; + thread4.time = {endTime - deltaTime, endTime}; + thread4.name = QStringLiteral("blub"); + } + + Data::CostSummary costSummary(QStringLiteral("cycles"), 0, 0, Data::Costs::Unit::Unknown); + auto generateEvent = [&costSummary, &events](quint64 time, quint32 cpuId) -> Data::Event { + Data::Event event; + event.cost = 10; + event.cpuId = cpuId; + event.type = 0; + event.time = time; + ++costSummary.sampleCount; + costSummary.totalPeriod += event.cost; + events.cpus[cpuId].events << event; + return event; + }; + for (quint64 time = 0; time < endTime; time += deltaTime) { + thread1.events << generateEvent(time, 0); + if (thread2.time.contains(time)) { + thread2.events << generateEvent(time, 2); + } + } + events.totalCosts = {costSummary}; + + return events; + } }; HOTSPOT_GUITEST_MAIN(TestModels) From f93db35326488297f85343feb1239f23fe096acb Mon Sep 17 00:00:00 2001 From: Lieven Hey Date: Wed, 4 Oct 2023 12:28:55 +0200 Subject: [PATCH 5/8] feat: add QSFP to hide empty rows in eventmodel The favourites and tracepoint patches include some rows in the model that may be empty. To keep the code simple an readable all rows will be shown. Then a proxy model is put ontop to remove empty rows. --- src/models/CMakeLists.txt | 1 + src/models/eventmodelproxy.cpp | 43 +++++++++++++++++++++++++++++++++ src/models/eventmodelproxy.h | 21 ++++++++++++++++ src/timelinewidget.cpp | 7 ++---- tests/modeltests/tst_models.cpp | 33 +++++++++++++++++++++++++ 5 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 src/models/eventmodelproxy.cpp create mode 100644 src/models/eventmodelproxy.h diff --git a/src/models/CMakeLists.txt b/src/models/CMakeLists.txt index b1754778..16846410 100644 --- a/src/models/CMakeLists.txt +++ b/src/models/CMakeLists.txt @@ -11,6 +11,7 @@ add_library( disassemblymodel.cpp disassemblyoutput.cpp eventmodel.cpp + eventmodelproxy.cpp filterandzoomstack.cpp formattingutils.cpp frequencymodel.cpp diff --git a/src/models/eventmodelproxy.cpp b/src/models/eventmodelproxy.cpp new file mode 100644 index 00000000..12a547ba --- /dev/null +++ b/src/models/eventmodelproxy.cpp @@ -0,0 +1,43 @@ +/* + SPDX-FileCopyrightText: Lieven Hey + SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "eventmodelproxy.h" +#include "eventmodel.h" + +EventModelProxy::EventModelProxy(QObject* parent) + : QSortFilterProxyModel(parent) +{ + setDynamicSortFilter(true); + setRecursiveFilteringEnabled(true); + setSortRole(EventModel::SortRole); + setFilterKeyColumn(EventModel::ThreadColumn); + setFilterRole(Qt::DisplayRole); +} + +EventModelProxy::~EventModelProxy() = default; + +bool EventModelProxy::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const +{ + // index is invalid -> we are at the root node + // hide categories that have no children (e.g. favorites, tracepoints) + if (!source_parent.isValid()) { + const auto model = sourceModel(); + if (!model->hasChildren(model->index(source_row, 0))) + return false; + } + + auto data = sourceModel() + ->index(source_row, EventModel::EventsColumn, source_parent) + .data(EventModel::EventsRole) + .value(); + + if (data.empty()) { + return false; + } + + return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); +} diff --git a/src/models/eventmodelproxy.h b/src/models/eventmodelproxy.h new file mode 100644 index 00000000..a720fd58 --- /dev/null +++ b/src/models/eventmodelproxy.h @@ -0,0 +1,21 @@ +/* + SPDX-FileCopyrightText: Lieven Hey + SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +class EventModelProxy : public QSortFilterProxyModel +{ + Q_OBJECT +public: + explicit EventModelProxy(QObject* parent = nullptr); + ~EventModelProxy() override; + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; +}; diff --git a/src/timelinewidget.cpp b/src/timelinewidget.cpp index e38458bc..8f6cc1b5 100644 --- a/src/timelinewidget.cpp +++ b/src/timelinewidget.cpp @@ -9,6 +9,7 @@ #include "filterandzoomstack.h" #include "models/eventmodel.h" +#include "models/eventmodelproxy.h" #include "resultsutil.h" #include "timelinedelegate.h" @@ -61,12 +62,8 @@ TimeLineWidget::TimeLineWidget(PerfParser* parser, QMenu* filterMenu, FilterAndZ ui->setupUi(this); auto* eventModel = new EventModel(this); - auto* timeLineProxy = new QSortFilterProxyModel(this); - timeLineProxy->setRecursiveFilteringEnabled(true); + auto* timeLineProxy = new EventModelProxy(this); timeLineProxy->setSourceModel(eventModel); - timeLineProxy->setSortRole(EventModel::SortRole); - timeLineProxy->setFilterKeyColumn(EventModel::ThreadColumn); - timeLineProxy->setFilterRole(Qt::DisplayRole); ResultsUtil::connectFilter(ui->timeLineSearch, timeLineProxy, ui->regexCheckBox); ui->timeLineView->setModel(timeLineProxy); ui->timeLineView->setSortingEnabled(true); diff --git a/tests/modeltests/tst_models.cpp b/tests/modeltests/tst_models.cpp index c8f4e74e..48358956 100644 --- a/tests/modeltests/tst_models.cpp +++ b/tests/modeltests/tst_models.cpp @@ -21,6 +21,7 @@ #include #include +#include #include namespace { @@ -672,6 +673,38 @@ private slots: QCOMPARE(model.rowCount(favoritesIndex), 0); } + void testEventModelProxy() + { + const auto events = createEventModelTestData(); + EventModel model; + QAbstractItemModelTester tester(&model); + model.setData(events); + + EventModelProxy proxy; + proxy.setSourceModel(&model); + + const auto favoritesIndex = model.index(3, 0); + const auto processesIndex = model.index(1, 0); + + QCOMPARE(model.rowCount(), 4); + QCOMPARE(proxy.rowCount(), 2); + + proxy.setFilterRegularExpression(QStringLiteral("this does not match")); + QCOMPARE(proxy.rowCount(), 0); + proxy.setFilterRegularExpression(QString()); + QCOMPARE(proxy.rowCount(), 2); + + // add the first data trace to favourites + // adding the whole process doesn't work currently + auto firstProcess = model.index(0, 0, processesIndex); + model.addToFavorites(model.index(0, 0, firstProcess)); + + QCOMPARE(proxy.rowCount(), 3); + + model.removeFromFavorites(model.index(0, 0, favoritesIndex)); + QCOMPARE(proxy.rowCount(), 2); + } + void testPrettySymbol_data() { QTest::addColumn("prettySymbol"); From fd61b13b52d922dc7f884671a7a339381b3ebbaf Mon Sep 17 00:00:00 2001 From: Milian Wolff Date: Wed, 4 Oct 2023 22:06:05 +0200 Subject: [PATCH 6/8] feat: Always put the favorite contents on the top of the view This way we can more easily find them and changing the sort order doesn't move them to the bottom. --- src/models/eventmodel.cpp | 2 ++ src/models/eventmodel.h | 1 + src/models/eventmodelproxy.cpp | 16 ++++++++++++++++ src/models/eventmodelproxy.h | 1 + tests/modeltests/tst_models.cpp | 31 +++++++++++++++++++++++++------ 5 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/models/eventmodel.cpp b/src/models/eventmodel.cpp index b4a3e01a..c0cf3f05 100644 --- a/src/models/eventmodel.cpp +++ b/src/models/eventmodel.cpp @@ -187,6 +187,8 @@ QVariant EventModel::data(const QModelIndex& index, int role) const } } else if (role == SortRole) { return index.row(); + } else if (role == IsFavoritesSectionRole) { + return index.row() == OverviewRow::FavoriteRow; } return {}; } else if (tag == Tag::Processes) { diff --git a/src/models/eventmodel.h b/src/models/eventmodel.h index bd2b9567..23e7a552 100644 --- a/src/models/eventmodel.h +++ b/src/models/eventmodel.h @@ -43,6 +43,7 @@ class EventModel : public QAbstractItemModel TotalCostsRole, EventResultsRole, IsFavoriteRole, + IsFavoritesSectionRole, }; int rowCount(const QModelIndex& parent = {}) const override; diff --git a/src/models/eventmodelproxy.cpp b/src/models/eventmodelproxy.cpp index 12a547ba..19e96a81 100644 --- a/src/models/eventmodelproxy.cpp +++ b/src/models/eventmodelproxy.cpp @@ -16,6 +16,7 @@ EventModelProxy::EventModelProxy(QObject* parent) setSortRole(EventModel::SortRole); setFilterKeyColumn(EventModel::ThreadColumn); setFilterRole(Qt::DisplayRole); + sort(0); } EventModelProxy::~EventModelProxy() = default; @@ -41,3 +42,18 @@ bool EventModelProxy::filterAcceptsRow(int source_row, const QModelIndex& source return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); } + +bool EventModelProxy::lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const +{ + const auto lhsIsFavoritesSection = source_left.data(EventModel::IsFavoritesSectionRole).toBool(); + const auto rhsIsFavoritesSection = source_right.data(EventModel::IsFavoritesSectionRole).toBool(); + if (lhsIsFavoritesSection != rhsIsFavoritesSection) { + // always put the favorites section on the top + if (sortOrder() == Qt::AscendingOrder) + return lhsIsFavoritesSection > rhsIsFavoritesSection; + else + return lhsIsFavoritesSection < rhsIsFavoritesSection; + } + + return QSortFilterProxyModel::lessThan(source_left, source_right); +} diff --git a/src/models/eventmodelproxy.h b/src/models/eventmodelproxy.h index a720fd58..eaedf37a 100644 --- a/src/models/eventmodelproxy.h +++ b/src/models/eventmodelproxy.h @@ -18,4 +18,5 @@ class EventModelProxy : public QSortFilterProxyModel protected: bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; + bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override; }; diff --git a/tests/modeltests/tst_models.cpp b/tests/modeltests/tst_models.cpp index 48358956..f9d3e69b 100644 --- a/tests/modeltests/tst_models.cpp +++ b/tests/modeltests/tst_models.cpp @@ -701,7 +701,26 @@ private slots: QCOMPARE(proxy.rowCount(), 3); + { + // verify that favorites remain at the top + QCOMPARE(proxy.sortOrder(), Qt::AscendingOrder); + QCOMPARE(proxy.sortColumn(), 0); + + // favorites on top + QVERIFY(proxy.index(0, 0, proxy.index(0, 0)).data(EventModel::IsFavoriteRole).toBool()); + // followed by CPUs + QCOMPARE(proxy.index(0, 0, proxy.index(1, 0)).data(EventModel::CpuIdRole).value(), 1); + + proxy.sort(0, Qt::DescendingOrder); + + // favorites are still on top + QVERIFY(proxy.index(0, 0, proxy.index(0, 0)).data(EventModel::IsFavoriteRole).toBool()); + // followed by processes + QCOMPARE(proxy.index(0, 0, proxy.index(1, 0)).data(EventModel::ProcessIdRole).value(), 1234); + } + model.removeFromFavorites(model.index(0, 0, favoritesIndex)); + QCOMPARE(proxy.rowCount(), 2); } @@ -952,9 +971,9 @@ private slots: { Data::EventResults events; events.cpus.resize(3); - events.cpus[0].cpuId = 0; - events.cpus[1].cpuId = 1; // empty - events.cpus[2].cpuId = 2; + events.cpus[0].cpuId = 1; + events.cpus[1].cpuId = 2; // empty + events.cpus[2].cpuId = 3; const quint64 endTime = 1000; const quint64 deltaTime = 10; @@ -997,13 +1016,13 @@ private slots: event.time = time; ++costSummary.sampleCount; costSummary.totalPeriod += event.cost; - events.cpus[cpuId].events << event; + events.cpus[cpuId - 1].events << event; return event; }; for (quint64 time = 0; time < endTime; time += deltaTime) { - thread1.events << generateEvent(time, 0); + thread1.events << generateEvent(time, 1); if (thread2.time.contains(time)) { - thread2.events << generateEvent(time, 2); + thread2.events << generateEvent(time, 3); } } events.totalCosts = {costSummary}; From 2cc266d4e609470458f22e1c94af8009c99316b0 Mon Sep 17 00:00:00 2001 From: Lieven Hey Date: Mon, 15 Jan 2024 15:56:54 +0100 Subject: [PATCH 7/8] feat: Show multiple costs in timelinewidget Showing only one cost is fine if we only show a hardware event, but since we now support tracepoints and some come in an enter/exit pair it requires us to rework the timeline delegate. This patch makes the event source combobox multi select and allows to select multiple event sources. --- src/models/eventmodelproxy.cpp | 14 +++++++++++++- src/models/eventmodelproxy.h | 7 +++++++ src/models/timelinedelegate.cpp | 33 +++++++++++++++++++++------------ src/models/timelinedelegate.h | 2 +- src/resultsutil.cpp | 32 ++++++++++++++++++++++++++++++++ src/resultsutil.h | 1 + src/timelinewidget.cpp | 29 ++++++++++++++++++++--------- 7 files changed, 95 insertions(+), 23 deletions(-) diff --git a/src/models/eventmodelproxy.cpp b/src/models/eventmodelproxy.cpp index 19e96a81..721d86d6 100644 --- a/src/models/eventmodelproxy.cpp +++ b/src/models/eventmodelproxy.cpp @@ -21,6 +21,18 @@ EventModelProxy::EventModelProxy(QObject* parent) EventModelProxy::~EventModelProxy() = default; +void EventModelProxy::showCostId(qint32 costId) +{ + m_hiddenCostIds.remove(costId); + invalidate(); +} + +void EventModelProxy::hideCostId(qint32 costId) +{ + m_hiddenCostIds.insert(costId); + invalidate(); +} + bool EventModelProxy::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { // index is invalid -> we are at the root node @@ -36,7 +48,7 @@ bool EventModelProxy::filterAcceptsRow(int source_row, const QModelIndex& source .data(EventModel::EventsRole) .value(); - if (data.empty()) { + if (data.empty() || m_hiddenCostIds.contains(data[0].type)) { return false; } diff --git a/src/models/eventmodelproxy.h b/src/models/eventmodelproxy.h index eaedf37a..a382aa3c 100644 --- a/src/models/eventmodelproxy.h +++ b/src/models/eventmodelproxy.h @@ -7,6 +7,7 @@ #pragma once +#include #include class EventModelProxy : public QSortFilterProxyModel @@ -16,7 +17,13 @@ class EventModelProxy : public QSortFilterProxyModel explicit EventModelProxy(QObject* parent = nullptr); ~EventModelProxy() override; + void showCostId(qint32 costId); + void hideCostId(qint32 costId); + protected: bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override; + +private: + QSet m_hiddenCostIds; }; diff --git a/src/models/timelinedelegate.cpp b/src/models/timelinedelegate.cpp index 48d90edc..b4c1c831 100644 --- a/src/models/timelinedelegate.cpp +++ b/src/models/timelinedelegate.cpp @@ -161,7 +161,6 @@ void TimeLineDelegate::paint(QPainter* painter, const QStyleOptionViewItem& opti const auto results = index.data(EventModel::EventResultsRole).value(); const auto offCpuCostId = results.offCpuTimeCostId; const auto lostEventCostId = results.lostEventCostId; - const auto tracepointEventCostId = results.tracepointEventCostId; const bool is_alternate = option.features & QStyleOptionViewItem::Alternate; const auto& palette = option.palette; @@ -231,10 +230,6 @@ void TimeLineDelegate::paint(QPainter* painter, const QStyleOptionViewItem& opti // see also: https://www.spinics.net/lists/linux-perf-users/msg03486.html for (const auto& event : data.events) { const auto isLostEvent = event.type == lostEventCostId; - const auto isTracepointEvent = event.type == tracepointEventCostId; - if (event.type != m_eventType && !isLostEvent && !isTracepointEvent) { - continue; - } const auto x = data.mapTimeToX(event.time); if (x < TimeLineData::padding || x >= data.w) { @@ -334,14 +329,22 @@ bool TimeLineDelegate::helpEvent(QHelpEvent* event, QAbstractItemView* view, con Util::formatTimeString(found.totalCost), Util::formatTimeString(found.maxCost))); } else if (found.numSamples > 0) { - QToolTip::showText(event->globalPos(), - tr("time: %1\n%5 samples: %2\ntotal sample cost: %3\nmax sample cost: %4") - .arg(formattedTime, QString::number(found.numSamples), - Util::formatCost(found.totalCost), Util::formatCost(found.maxCost), - totalCosts.value(found.type).label)); + if (m_eventType == results.tracepointEventCostId) { + // currently tracepoint cost is saying nothig, so don't show it + QToolTip::showText( + event->globalPos(), + tr("time: %1\n%3 samples: %2") + .arg(formattedTime, QString::number(found.numSamples), results.tracepoints[index.row()].name)); + + } else { + QToolTip::showText(event->globalPos(), + tr("time: %1\n%5 samples: %2\ntotal sample cost: %3\nmax sample cost: %4") + .arg(formattedTime, QString::number(found.numSamples), + Util::formatCost(found.totalCost), Util::formatCost(found.maxCost), + totalCosts.value(found.type).label)); + } } else { - QToolTip::showText(event->globalPos(), - tr("time: %1 (no %2 samples)").arg(formattedTime, totalCosts.value(m_eventType).label)); + QToolTip::showText(event->globalPos(), tr("time: %1 (no samples)").arg(formattedTime)); } return true; } @@ -390,6 +393,12 @@ bool TimeLineDelegate::eventFilter(QObject* watched, QEvent* event) const auto time = data.mapXToTime(pos.x() - visualRect.left() - TimeLineData::padding); const auto start = findEvent(data.events.constBegin(), data.events.constEnd(), time); + + // we can show multiple events in one row so we need to dynamically figure out which costId is needed + auto hoveringEntry = std::find_if(start, data.events.cend(), + [time](const Data::Event& event) { return event.time >= time; }); + setEventType(hoveringEntry != data.events.cend() ? hoveringEntry->type : 0); + auto findSamples = [&](int costType, bool contains) { bool foundAny = false; data.findSamples(hoverX, costType, results.lostEventCostId, contains, start, diff --git a/src/models/timelinedelegate.h b/src/models/timelinedelegate.h index 47e5e2bb..af610b0e 100644 --- a/src/models/timelinedelegate.h +++ b/src/models/timelinedelegate.h @@ -59,7 +59,6 @@ class TimeLineDelegate : public QStyledItemDelegate bool helpEvent(QHelpEvent* event, QAbstractItemView* view, const QStyleOptionViewItem& option, const QModelIndex& index) override; - void setEventType(int type); void setSelectedStacks(const QSet& selectedStacks); signals: @@ -71,6 +70,7 @@ class TimeLineDelegate : public QStyledItemDelegate bool eventFilter(QObject* watched, QEvent* event) override; private: + void setEventType(int type); void updateView(); void updateZoomState(); diff --git a/src/resultsutil.cpp b/src/resultsutil.cpp index 6fac24f7..7699b229 100644 --- a/src/resultsutil.cpp +++ b/src/resultsutil.cpp @@ -19,6 +19,8 @@ #include #include +#include + #include "models/costdelegate.h" #include "models/data.h" #include "models/filterandzoomstack.h" @@ -219,6 +221,36 @@ void fillEventSourceComboBox(QComboBox* combo, const Data::Costs& costs, const Q } } +void fillEventSourceComboBoxMultiSelect(QComboBox* combo, const Data::Costs& costs, const QString& /*tooltipTemplate*/) +{ + // restore selection if possible + const auto oldData = combo->currentData(); + + combo->clear(); + + auto model = new QStandardItemModel(costs.numTypes(), 1, combo); + int itemCounter = 0; + for (int costId = 0, c = costs.numTypes(); costId < c; costId++) { + if (!costs.totalCost(costId)) { + continue; + } + + auto item = new QStandardItem(costs.typeName(costId)); + item->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); + item->setData(Qt::Checked, Qt::CheckStateRole); + item->setData(costId, Qt::UserRole + 1); + model->setItem(itemCounter, item); + itemCounter++; + } + model->setRowCount(itemCounter); + combo->setModel(model); + + const auto index = combo->findData(oldData); + if (index != -1) { + combo->setCurrentIndex(index); + } +} + void setupResultsAggregation(QComboBox* costAggregationComboBox) { struct AggregationType diff --git a/src/resultsutil.h b/src/resultsutil.h index 568d8870..0551a007 100644 --- a/src/resultsutil.h +++ b/src/resultsutil.h @@ -100,6 +100,7 @@ void hideEmptyColumns(const Data::Costs& costs, QTreeView* view, int numBaseColu void hideTracepointColumns(const Data::Costs& costs, QTreeView* view, int numBaseColumns); void fillEventSourceComboBox(QComboBox* combo, const Data::Costs& costs, const QString& tooltipTemplate); +void fillEventSourceComboBoxMultiSelect(QComboBox* combo, const Data::Costs& costs, const QString& tooltipTemplate); void setupResultsAggregation(QComboBox* costAggregationComboBox); } diff --git a/src/timelinewidget.cpp b/src/timelinewidget.cpp index 8f6cc1b5..e714421c 100644 --- a/src/timelinewidget.cpp +++ b/src/timelinewidget.cpp @@ -17,9 +17,11 @@ #include "parsers/perf/perfparser.h" #include +#include #include #include #include +#include #include #include @@ -82,9 +84,24 @@ TimeLineWidget::TimeLineWidget(PerfParser* parser, QMenu* filterMenu, FilterAndZ connect(timeLineProxy, &QAbstractItemModel::rowsInserted, this, [this]() { ui->timeLineView->expandToDepth(1); }); connect(timeLineProxy, &QAbstractItemModel::modelReset, this, [this]() { ui->timeLineView->expandToDepth(1); }); - connect(m_parser, &PerfParser::bottomUpDataAvailable, this, [this](const Data::BottomUpResults& data) { - ResultsUtil::fillEventSourceComboBox(ui->timeLineEventSource, data.costs, tr("Show timeline for %1 events.")); - }); + connect(m_parser, &PerfParser::bottomUpDataAvailable, this, + [this, timeLineProxy](const Data::BottomUpResults& data) { + ResultsUtil::fillEventSourceComboBoxMultiSelect(ui->timeLineEventSource, data.costs, + tr("Show timeline for %1 events.")); + + auto model = qobject_cast(ui->timeLineEventSource->model()); + connect(ui->timeLineEventSource->model(), &QStandardItemModel::dataChanged, model, + [timeLineProxy](const QModelIndex& topLeft, const QModelIndex& /*bottomRight*/, + const QVector& /*roles*/) { + auto checkState = topLeft.data(Qt::CheckStateRole).value(); + + if (checkState == Qt::CheckState::Checked) { + timeLineProxy->showCostId(topLeft.data(Qt::UserRole + 1).toUInt()); + } else { + timeLineProxy->hideCostId(topLeft.data(Qt::UserRole + 1).toUInt()); + } + }); + }); connect(m_parser, &PerfParser::eventsAvailable, this, [this, eventModel](const Data::EventResults& data) { eventModel->setData(data); @@ -101,12 +118,6 @@ TimeLineWidget::TimeLineWidget(PerfParser* parser, QMenu* filterMenu, FilterAndZ } }); - connect(ui->timeLineEventSource, static_cast(&QComboBox::currentIndexChanged), this, - [this](int index) { - const auto typeId = ui->timeLineEventSource->itemData(index).toInt(); - m_timeLineDelegate->setEventType(typeId); - }); - connect(m_timeLineDelegate, &TimeLineDelegate::addToFavorites, this, [eventModel](const QModelIndex& index) { eventModel->addToFavorites(index); }); connect(m_timeLineDelegate, &TimeLineDelegate::removeFromFavorites, this, From 525448f72ef3a5a228b129c5d11307fddb5cdabd Mon Sep 17 00:00:00 2001 From: Lieven Hey Date: Tue, 19 Nov 2024 14:30:40 +0100 Subject: [PATCH 8/8] chore: update perfparser to forward tracepoint information --- 3rdparty/perfparser | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3rdparty/perfparser b/3rdparty/perfparser index a6aeb012..0bfbbb55 160000 --- a/3rdparty/perfparser +++ b/3rdparty/perfparser @@ -1 +1 @@ -Subproject commit a6aeb012bd8556d0506484892dd99dc504da530c +Subproject commit 0bfbbb554e830364ce5de99c9fede8672479ee38