diff --git a/3rdparty/perfparser b/3rdparty/perfparser index a6aeb012b..0bfbbb554 160000 --- a/3rdparty/perfparser +++ b/3rdparty/perfparser @@ -1 +1 @@ -Subproject commit a6aeb012bd8556d0506484892dd99dc504da530c +Subproject commit 0bfbbb554e830364ce5de99c9fede8672479ee38 diff --git a/src/models/CMakeLists.txt b/src/models/CMakeLists.txt index b17547780..168464109 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/data.h b/src/models/data.h index 56ea14014..fdb7187ad 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 27343d656..c0cf3f05e 100644 --- a/src/models/eventmodel.cpp +++ b/src/models/eventmodel.cpp @@ -9,7 +9,6 @@ #include "../util.h" -#include #include namespace { @@ -18,13 +17,26 @@ constexpr auto orderProcessByPid = [](const EventModel::Process& process, qint32 enum class Tag : quint8 { Invalid = 0, - Root = 1, - Overview = 2, - Cpus = 3, - Processes = 4, - Threads = 5 + Root, + Overview, + Cpus, + Processes, + Threads, + Tracepoints, + Favorites, }; +enum OverviewRow : quint8 +{ + CpuRow, + ProcessRow, + TracepointRow, + FavoriteRow, +}; +constexpr auto numRows = FavoriteRow + 1; + +constexpr auto LAST_TAG = Tag::Favorites; + const auto DATATAG_SHIFT = sizeof(Tag) * 8; const auto DATATAG_UNSHIFT = (sizeof(quintptr) - sizeof(Tag)) * 8; @@ -36,7 +48,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 +88,26 @@ int EventModel::rowCount(const QModelIndex& parent) const case Tag::Invalid: case Tag::Cpus: case Tag::Threads: - break; + case Tag::Tracepoints: + case Tag::Favorites: + 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(); + case OverviewRow::FavoriteRow: + return m_favourites.size(); + } + Q_UNREACHABLE(); case Tag::Root: - return 2; - }; - + return numRows; + } return 0; } @@ -134,20 +157,38 @@ 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"); + case OverviewRow::FavoriteRow: + return tr("Favorites"); + } } 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"); + case OverviewRow::FavoriteRow: + return tr("A list of favourites to group important events"); } } else if (role == SortRole) { return index.row(); + } else if (role == IsFavoritesSectionRole) { + return index.row() == OverviewRow::FavoriteRow; } return {}; } else if (tag == Tag::Processes) { @@ -201,47 +242,202 @@ 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()]; + } 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) { - 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; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); + } } 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; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); + } } 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 {}; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); + } } 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; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); + } } 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; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); + } } 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; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); + } } 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 {}; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); + } } 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 {}; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); + } + } 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 {}; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); + } + } + } else if (role == IsFavoriteRole) { + return false; } 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 {}; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); + } } 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 +452,51 @@ 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; + } + 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 {}; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); } - const auto numEvents = thread ? thread->events.size() : cpu->events.size(); + 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 {}; + case Tag::Favorites: + // they are handled elsewhere + Q_UNREACHABLE(); + } + } break; case NUM_COLUMNS: // nothing @@ -278,6 +509,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; @@ -318,6 +551,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 +569,24 @@ 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: + case Tag::Favorites: 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)); + case OverviewRow::FavoriteRow: + return createIndex(row, column, static_cast(Tag::Favorites)); + } + Q_UNREACHABLE(); case Tag::Processes: // 3rd level children: Threads return createIndex(row, column, combineDataTag(Tag::Threads, parent.row())); } @@ -359,9 +602,13 @@ 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::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)); @@ -370,3 +617,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 9554be52a..23e7a552a 100644 --- a/src/models/eventmodel.h +++ b/src/models/eventmodel.h @@ -42,6 +42,8 @@ class EventModel : public QAbstractItemModel SortRole, TotalCostsRole, EventResultsRole, + IsFavoriteRole, + IsFavoritesSectionRole, }; int rowCount(const QModelIndex& parent = {}) const override; @@ -71,9 +73,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/eventmodelproxy.cpp b/src/models/eventmodelproxy.cpp new file mode 100644 index 000000000..721d86d6b --- /dev/null +++ b/src/models/eventmodelproxy.cpp @@ -0,0 +1,71 @@ +/* + 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); + sort(0); +} + +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 + // 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() || m_hiddenCostIds.contains(data[0].type)) { + return false; + } + + 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 new file mode 100644 index 000000000..a382aa3c3 --- /dev/null +++ b/src/models/eventmodelproxy.h @@ -0,0 +1,29 @@ +/* + 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 +#include + +class EventModelProxy : public QSortFilterProxyModel +{ + Q_OBJECT +public: + 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/timeaxisheaderview.cpp b/src/models/timeaxisheaderview.cpp index 0481d02a4..f00bbbb0e 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" @@ -61,42 +60,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 +97,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 4cc5e2387..fe3a6857e 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/models/timelinedelegate.cpp b/src/models/timelinedelegate.cpp index b567f280b..b4c1c8314 100644 --- a/src/models/timelinedelegate.cpp +++ b/src/models/timelinedelegate.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include "../util.h" @@ -229,9 +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; - if (event.type != m_eventType && !isLostEvent) { - continue; - } const auto x = data.mapTimeToX(event.time); if (x < TimeLineData::padding || x >= data.w) { @@ -331,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; } @@ -387,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, @@ -457,6 +469,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 3332f6cd8..af610b0e2 100644 --- a/src/models/timelinedelegate.h +++ b/src/models/timelinedelegate.h @@ -59,16 +59,18 @@ 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: void stacksHovered(const QSet& stacks); + void addToFavorites(const QModelIndex& index); + void removeFromFavorites(const QModelIndex& index); protected: bool eventFilter(QObject* watched, QEvent* event) override; private: + void setEventType(int type); void updateView(); void updateZoomState(); diff --git a/src/parsers/perf/perfparser.cpp b/src/parsers/perf/perfparser.cpp index d61593cc5..8b6b11164 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 7133f49b1..988bdb3e9 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; diff --git a/src/resultsutil.cpp b/src/resultsutil.cpp index 6fac24f78..7699b2297 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 568d88709..0551a0071 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 8da01028b..e714421cd 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" @@ -16,9 +17,11 @@ #include "parsers/perf/perfparser.h" #include +#include #include #include #include +#include #include #include @@ -61,12 +64,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); @@ -85,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); @@ -104,14 +118,10 @@ 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(); - 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()) { diff --git a/tests/modeltests/tst_models.cpp b/tests/modeltests/tst_models.cpp index c0dbefa7b..f9d3e69bc 100644 --- a/tests/modeltests/tst_models.cpp +++ b/tests/modeltests/tst_models.cpp @@ -21,6 +21,7 @@ #include #include +#include #include namespace { @@ -558,72 +559,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 +652,78 @@ 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 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); + + { + // 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); + } + void testPrettySymbol_data() { QTest::addColumn("prettySymbol"); @@ -947,6 +966,69 @@ private slots: font.setPixelSize(10); return QFontMetrics(font); } + + Data::EventResults createEventModelTestData() + { + Data::EventResults events; + events.cpus.resize(3); + events.cpus[0].cpuId = 1; + events.cpus[1].cpuId = 2; // empty + events.cpus[2].cpuId = 3; + + 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 - 1].events << event; + return event; + }; + for (quint64 time = 0; time < endTime; time += deltaTime) { + thread1.events << generateEvent(time, 1); + if (thread2.time.contains(time)) { + thread2.events << generateEvent(time, 3); + } + } + events.totalCosts = {costSummary}; + + return events; + } }; HOTSPOT_GUITEST_MAIN(TestModels)