diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5214b810..0bb8e26f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -43,6 +43,7 @@ set(HOTSPOT_SRCS resultscallercalleepage.cpp resultsdisassemblypage.cpp resultsutil.cpp + resultspagediff.cpp costheaderview.cpp timelinewidget.cpp dockwidgetsetup.cpp @@ -52,6 +53,7 @@ set(HOTSPOT_SRCS perfoutputwidgettext.cpp perfoutputwidgetkonsole.cpp costcontextmenu.cpp + diffreportdialog.cpp # ui files: mainwindow.ui aboutdialog.ui @@ -64,6 +66,7 @@ set(HOTSPOT_SRCS resultsflamegraphpage.ui resultscallercalleepage.ui resultsdisassemblypage.ui + resultspagediff.ui timelinewidget.ui unwindsettingspage.ui flamegraphsettingspage.ui @@ -71,6 +74,7 @@ set(HOTSPOT_SRCS callgraphwidget.ui callgraphsettingspage.ui frequencypage.ui + diffreportdialog.ui # resources: resources.qrc ) diff --git a/src/diffreportdialog.cpp b/src/diffreportdialog.cpp new file mode 100644 index 00000000..409d5e7c --- /dev/null +++ b/src/diffreportdialog.cpp @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: Lieven Hey + SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "diffreportdialog.h" +#include "ui_diffreportdialog.h" + +DiffReportDialog::DiffReportDialog(QWidget* parent) + : QDialog(parent) + , ui(new Ui::DiffReportDialog) +{ + ui->setupUi(this); + const auto filterString = QStringLiteral("perf*.data\nperf.data*"); + ui->file_a->setFilter(filterString); + ui->file_b->setFilter(filterString); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accepted); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::close); +} + +DiffReportDialog::~DiffReportDialog() = default; + +QString DiffReportDialog::fileA() const +{ + return ui->file_a->url().toLocalFile(); +} + +QString DiffReportDialog::fileB() const +{ + return ui->file_b->url().toLocalFile(); +} diff --git a/src/diffreportdialog.h b/src/diffreportdialog.h new file mode 100644 index 00000000..dcc62b13 --- /dev/null +++ b/src/diffreportdialog.h @@ -0,0 +1,27 @@ +/* + SPDX-FileCopyrightText: Lieven Hey + SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +namespace Ui { +class DiffReportDialog; +} + +class DiffReportDialog : public QDialog +{ + Q_OBJECT +public: + explicit DiffReportDialog(QWidget* parent = nullptr); + ~DiffReportDialog(); + + QString fileA() const; + QString fileB() const; + +private: + QScopedPointer ui; +}; diff --git a/src/diffreportdialog.ui b/src/diffreportdialog.ui new file mode 100644 index 00000000..5c83c796 --- /dev/null +++ b/src/diffreportdialog.ui @@ -0,0 +1,70 @@ + + + DiffReportDialog + + + + 0 + 0 + 400 + 300 + + + + Diff two files + + + false + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + File A: + + + + + + + File B: + + + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Open + + + + + + + + KUrlRequester + QWidget +
kurlrequester.h
+
+
+ + +
diff --git a/src/dockwidgetsetup.cpp b/src/dockwidgetsetup.cpp index 33d44725..2d6833fa 100644 --- a/src/dockwidgetsetup.cpp +++ b/src/dockwidgetsetup.cpp @@ -7,7 +7,10 @@ #include "dockwidgetsetup.h" +#include + #include +#include #include #include @@ -53,4 +56,13 @@ KDDockWidgets::MainWindow* createDockingArea(const QString& id, QWidget* parent) return ret; } +KDDockWidgets::DockWidget* dockify(QWidget* widget, const QString& id, const QString& title, const QString& shortcut) +{ + auto* dock = new KDDockWidgets::DockWidget(id); + dock->setWidget(widget); + dock->setTitle(title); + dock->toggleAction()->setShortcut(shortcut); + return dock; +} + #include "dockwidgetsetup.moc" diff --git a/src/dockwidgetsetup.h b/src/dockwidgetsetup.h index 4e65efc4..3ffa2742 100644 --- a/src/dockwidgetsetup.h +++ b/src/dockwidgetsetup.h @@ -9,6 +9,7 @@ namespace KDDockWidgets { class MainWindow; +class DockWidget; } class QWidget; @@ -16,3 +17,4 @@ class QString; void setupDockWidgets(); KDDockWidgets::MainWindow* createDockingArea(const QString& id, QWidget* parent); +KDDockWidgets::DockWidget* dockify(QWidget* widget, const QString& id, const QString& title, const QString& shortcut); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 4e9aeb53..073443fb 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -10,6 +10,7 @@ #include "costcontextmenu.h" #include "recordpage.h" #include "resultspage.h" +#include "resultspagediff.h" #include "settings.h" #include "settingsdialog.h" #include "startpage.h" @@ -45,6 +46,7 @@ #include #include "aboutdialog.h" +#include "diffreportdialog.h" #include "parsers/perf/perfparser.h" @@ -103,13 +105,16 @@ MainWindow::MainWindow(QWidget* parent) , m_startPage(new StartPage(this)) , m_recordPage(new RecordPage(this)) , m_resultsPage(new ResultsPage(m_parser, this)) + , m_resultsPageDiff(new ResultsPageDiff(this)) , m_settingsDialog(new SettingsDialog(this)) + , m_diffReportDialog(new DiffReportDialog(this)) { ui->setupUi(this); m_pageStack->addWidget(m_startPage); m_pageStack->addWidget(m_resultsPage); m_pageStack->addWidget(m_recordPage); + m_pageStack->addWidget(m_resultsPageDiff); QVBoxLayout* layout = new QVBoxLayout; layout->setContentsMargins(0, 0, 0, 0); @@ -137,6 +142,7 @@ MainWindow::MainWindow(QWidget* parent) connect(m_startPage, &StartPage::recordButtonClicked, this, &MainWindow::onRecordButtonClicked); connect(m_startPage, &StartPage::stopParseButtonClicked, this, static_cast(&MainWindow::clear)); + connect(m_startPage, &StartPage::createDiffReportButtonClicked, this, [this] { m_diffReportDialog->show(); }); connect(m_parser, &PerfParser::progress, m_startPage, &StartPage::onParseFileProgress); connect(m_parser, &PerfParser::debugInfoDownloadProgress, m_startPage, &StartPage::onDebugInfoDownloadProgress); connect(this, &MainWindow::openFileError, m_startPage, &StartPage::onOpenFileError); @@ -262,6 +268,13 @@ MainWindow::MainWindow(QWidget* parent) const auto restored = serializer.restoredDockWidgets(); m_resultsPage->initDockWidgets(restored); + + connect(m_diffReportDialog, &QDialog::accepted, this, [this] { + m_diffReportDialog->close(); + clear(); + m_pageStack->setCurrentWidget(m_resultsPageDiff); + m_resultsPageDiff->createDiffReport(m_diffReportDialog->fileA(), m_diffReportDialog->fileB()); + }); } MainWindow::~MainWindow() = default; diff --git a/src/mainwindow.h b/src/mainwindow.h index defa8967..973c5ece 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -25,8 +25,10 @@ class KRecentFilesAction; class StartPage; class ResultsPage; +class ResultsPageDiff; class RecordPage; class SettingsDialog; +class DiffReportDialog; class MainWindow : public KParts::MainWindow { @@ -77,7 +79,9 @@ public slots: StartPage* m_startPage; RecordPage* m_recordPage; ResultsPage* m_resultsPage; + ResultsPageDiff* m_resultsPageDiff; SettingsDialog* m_settingsDialog; + DiffReportDialog* m_diffReportDialog; KRecentFilesAction* m_recentFilesAction = nullptr; QAction* m_reloadAction = nullptr; diff --git a/src/models/costdelegate.cpp b/src/models/costdelegate.cpp index 79487f49..78d0eadb 100644 --- a/src/models/costdelegate.cpp +++ b/src/models/costdelegate.cpp @@ -31,7 +31,8 @@ void CostDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, } const auto totalCost = index.data(m_totalCostRole).toULongLong(); - const auto fraction = std::abs(float(cost) / totalCost); + // TODO C++17: std::clamp + const auto fraction = std::max(0.f, std::min(1.f, std::abs(float(cost) / totalCost))); auto rect = option.rect; rect.setWidth(rect.width() * fraction); diff --git a/src/models/costproxy.h b/src/models/costproxy.h index 3490254b..6ffdaa85 100644 --- a/src/models/costproxy.h +++ b/src/models/costproxy.h @@ -10,7 +10,9 @@ #include +#include "../util.h" #include "callercalleeproxy.h" +#include "treemodel.h" template class CostProxy : public QSortFilterProxyModel @@ -39,3 +41,85 @@ class CostProxy : public QSortFilterProxyModel return CallerCalleeProxyDetail::match(this, item->symbol); } }; + +namespace CostProxyUtil { +inline int cost(const BottomUpModel* model, int column, int nodeid) +{ + return model->results().costs.cost(column, nodeid); +} +inline int cost(const TopDownModel* model, int column, int nodeid) +{ + const auto inclusiveTypes = model->results().inclusiveCosts.numTypes(); + if (column >= inclusiveTypes) { + return model->results().selfCosts.cost(column - inclusiveTypes, nodeid); + } + return model->results().inclusiveCosts.cost(column, nodeid); +} + +inline int totalCost(const BottomUpModel* model, int column) +{ + return model->results().costs.totalCost(column); +} +inline int totalCost(const TopDownModel* model, int column) +{ + const auto inclusiveTypes = model->results().inclusiveCosts.numTypes(); + if (column >= inclusiveTypes) { + return model->results().selfCosts.totalCost(column - inclusiveTypes); + } + return model->results().inclusiveCosts.totalCost(column); +} +} + +// TODO dedicated cost role +// The DiffCostProxy does all the heavy lifting of diffing +// its gets it data from a Model with baseline cost and the file to diff cost in alternating columns +// this proxy return for every even column the base cost and for every uneven the calculated diff cost +// this simplifies the other models, since we don't need to add this logic there +template +class DiffCostProxy : public CostProxy +{ +public: + explicit DiffCostProxy(QObject* parent = nullptr) + : CostProxy(parent) + { + } + + QVariant data(const QModelIndex& index, int role) const override + { + if (index.column() >= Model::NUM_BASE_COLUMNS) { + const auto model = qobject_cast(CostProxy::sourceModel()); + Q_ASSERT(model); + + const auto node = model->itemFromIndex(CostProxy::mapToSource(index)); + + const auto baseColumn = (index.column() - Model::NUM_BASE_COLUMNS) / 2; + const auto column = baseColumn + (index.column() - Model::NUM_BASE_COLUMNS) % 2; + + auto cost = [model, node](int column) { return CostProxyUtil::cost(model, column, node->id); }; + + auto totalCost = [model](int column) { return CostProxyUtil::totalCost(model, column); }; + + if (column == baseColumn) { + if (role == Model::TotalCostRole) { + return totalCost(column); + } else if (role == Model::SortRole) { + return cost(column); + } else if (role == Qt::DisplayRole) { + return Util::formatCostRelative(cost(column), totalCost(column), true); + } + } else { + if (role == Model::TotalCostRole) { + return cost(baseColumn); + } else if (role == Model::SortRole) { + if (cost(baseColumn) == 0) + return 0; + return cost(column); + } else if (role == Qt::DisplayRole) { + return Util::formatCostRelative(cost(column), cost(baseColumn), true); + } + } + } + + return CostProxy::data(index, role); + } +}; diff --git a/src/models/data.cpp b/src/models/data.cpp index 0cd2e9ed..ec81c2b2 100644 --- a/src/models/data.cpp +++ b/src/models/data.cpp @@ -280,6 +280,30 @@ void buildPerLibrary(const TopDown* node, PerLibraryResults& results, QHash +void diffResults(const ResultType& a, const ResultType* b, ResultType* result_node, const Costs& costs_a, + const Costs& costs_b, Costs* costs_result) +{ + for (const auto& node : a.children) { + const auto sibling = b->entryForSymbol(node.symbol); + if (sibling) { + ResultType diffed; + diffed.id = node.id; + diffed.symbol = node.symbol; + + for (int i = 0; i < costs_a.numTypes(); i++) { + costs_result->add(2 * i, diffed.id, costs_a.cost(i, node.id)); + costs_result->add(2 * i + 1, diffed.id, costs_b.cost(i, sibling->id)); + } + + if (addResultNode) { + result_node->children.push_back(diffed); + } + diffResults(node, sibling, &result_node->children.back(), costs_a, costs_b, costs_result); + } + } +} } QString Data::prettifySymbol(const QString& name) @@ -288,7 +312,7 @@ QString Data::prettifySymbol(const QString& name) return result == name ? name : result; } -TopDownResults TopDownResults::fromBottomUp(const BottomUpResults& bottomUpData, bool skipFirstLevel) +TopDownResults Data::TopDownResults::fromBottomUp(const BottomUpResults& bottomUpData, bool skipFirstLevel) { TopDownResults results; results.selfCosts.initializeCostsFrom(bottomUpData.costs); @@ -397,3 +421,75 @@ const Data::ThreadEvents* Data::EventResults::findThread(qint32 pid, qint32 tid) { return const_cast(this)->findThread(pid, tid); } + +Data::BottomUpResults BottomUpResults::diffBottomUpResults(const Data::BottomUpResults& a, + const Data::BottomUpResults& b) +{ + if (a.costs.numTypes() != b.costs.numTypes()) { + return {}; + } + + BottomUpResults results; + + // mimic perf diff -c ratio + for (int i = 0; i < a.costs.numTypes(); i++) { + // only diff same type of costs + if (a.costs.typeName(i) != b.costs.typeName(i)) { + return {}; + } + + results.costs.addType(2 * i, QLatin1String("baseline %1").arg(a.costs.typeName(i)), a.costs.unit(i)); + results.costs.addTotalCost(2 * i, a.costs.totalCost(i)); + + const auto costBType = 2 * i + 1; + results.costs.addType(costBType, QLatin1String("ratio of %1").arg(b.costs.typeName(i)), Costs::Unit::Unknown); + results.costs.addTotalCost(costBType, b.costs.totalCost(0)); + } + + diffResults(a.root, &b.root, &results.root, a.costs, b.costs, &results.costs); + + BottomUp::initializeParents(&results.root); + + return results; +} + +TopDownResults TopDownResults::diffTopDownResults(const TopDownResults& a, const TopDownResults& b) +{ + if (a.selfCosts.numTypes() != b.selfCosts.numTypes()) { + return {}; + } + + TopDownResults results; + + for (int i = 0; i < a.selfCosts.numTypes(); i++) { + // only diff same type of costs + if (a.selfCosts.typeName(i) != b.selfCosts.typeName(i)) { + return {}; + } + + results.selfCosts.addType(2 * i, QLatin1String("baseline %1").arg(a.selfCosts.typeName(i)), + a.selfCosts.unit(i)); + results.selfCosts.addTotalCost(2 * i, a.selfCosts.totalCost(i)); + + results.inclusiveCosts.addType(2 * i, QLatin1String("baseline %1").arg(a.inclusiveCosts.typeName(i)), + a.inclusiveCosts.unit(i)); + results.inclusiveCosts.addTotalCost(2 * i, a.inclusiveCosts.totalCost(i)); + + const auto costBType = 2 * i + 1; + results.selfCosts.addType(costBType, QLatin1String("ratio of %1").arg(b.selfCosts.typeName(i)), + Costs::Unit::Unknown); + results.selfCosts.addTotalCost(costBType, b.selfCosts.totalCost(0)); + + results.inclusiveCosts.addType(costBType, QLatin1String("ratio of %1").arg(b.inclusiveCosts.typeName(i)), + Costs::Unit::Unknown); + results.inclusiveCosts.addTotalCost(costBType, b.inclusiveCosts.totalCost(0)); + } + + diffResults(a.root, &b.root, &results.root, a.selfCosts, b.selfCosts, &results.selfCosts); + diffResults(a.root, &b.root, &results.root, a.inclusiveCosts, b.inclusiveCosts, + &results.inclusiveCosts); + + Data::TopDown::initializeParents(&results.root); + + return results; +} diff --git a/src/models/data.h b/src/models/data.h index 1c489c29..d11af448 100644 --- a/src/models/data.h +++ b/src/models/data.h @@ -480,6 +480,8 @@ struct BottomUpResults return parent; } + static BottomUpResults diffBottomUpResults(const BottomUpResults& a, const BottomUpResults& b); + private: quint32 maxBottomUpId = 0; QHash tidToBottomUp; @@ -524,7 +526,9 @@ struct TopDownResults TopDown root; Costs selfCosts; Costs inclusiveCosts; - static TopDownResults fromBottomUp(const Data::BottomUpResults& bottomUpData, bool skipFirstLevel); + static TopDownResults fromBottomUp(const Data::BottomUpResults& bottomUpData, bool skipFirestLevel); + + static TopDownResults diffTopDownResults(const Data::TopDownResults& a, const Data::TopDownResults& b); }; struct PerLibrary : SymbolTree diff --git a/src/resultsbottomuppage.cpp b/src/resultsbottomuppage.cpp index b9ff74af..95818dea 100644 --- a/src/resultsbottomuppage.cpp +++ b/src/resultsbottomuppage.cpp @@ -58,43 +58,18 @@ void stackCollapsedExport(QFile& file, int type, const Data::BottomUpResults& re ResultsBottomUpPage::ResultsBottomUpPage(FilterAndZoomStack* filterStack, PerfParser* parser, CostContextMenu* contextMenu, QMenu* exportMenu, QWidget* parent) : QWidget(parent) + , m_model(new BottomUpModel(this)) + , m_exportMenu(exportMenu) , ui(new Ui::ResultsBottomUpPage) { ui->setupUi(this); - auto bottomUpCostModel = new BottomUpModel(this); - ResultsUtil::setupTreeView(ui->bottomUpTreeView, contextMenu, ui->bottomUpSearch, bottomUpCostModel); - ResultsUtil::setupCostDelegate(bottomUpCostModel, ui->bottomUpTreeView); - ResultsUtil::setupContextMenu(ui->bottomUpTreeView, contextMenu, bottomUpCostModel, filterStack, this); - - connect( - parser, &PerfParser::bottomUpDataAvailable, this, - [this, bottomUpCostModel, exportMenu](const Data::BottomUpResults& data) { - bottomUpCostModel->setData(data); - ResultsUtil::hideEmptyColumns(data.costs, ui->bottomUpTreeView, BottomUpModel::NUM_BASE_COLUMNS); - - { - auto stackCollapsed = - exportMenu->addMenu(QIcon::fromTheme(QStringLiteral("text-plain")), tr("Stack Collapsed")); - stackCollapsed->setToolTip(tr("Export data in textual form compatible with flamegraph.pl.")); - for (int i = 0; i < data.costs.numTypes(); ++i) { - const auto costName = data.costs.typeName(i); - stackCollapsed->addAction(costName, this, [this, i, bottomUpCostModel, costName]() { - const auto fileName = QFileDialog::getSaveFileName(this, tr("Export %1 Data").arg(costName)); - if (fileName.isEmpty()) - return; - QFile file(fileName); - if (!file.open(QIODevice::Text | QIODevice::WriteOnly)) { - QMessageBox::warning( - this, tr("Failed to export data"), - tr("Failed to export stack collapsed data:\n%1").arg(file.errorString())); - return; - } - stackCollapsedExport(file, i, bottomUpCostModel->results()); - }); - } - } - }); + ResultsUtil::setupTreeViewDiff(ui->bottomUpTreeView, contextMenu, ui->bottomUpSearch, m_model); + ResultsUtil::setupCostDelegate(m_model, ui->bottomUpTreeView); + ResultsUtil::setupContextMenu(ui->bottomUpTreeView, contextMenu, m_model, filterStack, this); + + if (parser) + connect(parser, &PerfParser::bottomUpDataAvailable, this, &ResultsBottomUpPage::setBottomUpResults); ResultsUtil::setupResultsAggregation(ui->costAggregationComboBox); } @@ -105,3 +80,27 @@ void ResultsBottomUpPage::clear() { ui->bottomUpSearch->setText({}); } + +void ResultsBottomUpPage::setBottomUpResults(const Data::BottomUpResults& results) +{ + m_model->setData(results); + ResultsUtil::hideEmptyColumns(results.costs, ui->bottomUpTreeView, BottomUpModel::NUM_BASE_COLUMNS); + + auto stackCollapsed = m_exportMenu->addMenu(QIcon::fromTheme(QStringLiteral("text-plain")), tr("Stack Collapsed")); + stackCollapsed->setToolTip(tr("Export data in textual form compatible with flamegraph.pl.")); + for (int i = 0; i < results.costs.numTypes(); ++i) { + const auto costName = results.costs.typeName(i); + stackCollapsed->addAction(costName, this, [this, i, costName]() { + const auto fileName = QFileDialog::getSaveFileName(this, tr("Export %1 Data").arg(costName)); + if (fileName.isEmpty()) + return; + QFile file(fileName); + if (!file.open(QIODevice::Text | QIODevice::WriteOnly)) { + QMessageBox::warning(this, tr("Failed to export data"), + tr("Failed to export stack collapsed data:\n%1").arg(file.errorString())); + return; + } + stackCollapsedExport(file, i, m_model->results()); + }); + } +} diff --git a/src/resultsbottomuppage.h b/src/resultsbottomuppage.h index 463f8534..d94f22d8 100644 --- a/src/resultsbottomuppage.h +++ b/src/resultsbottomuppage.h @@ -18,6 +18,7 @@ class ResultsBottomUpPage; namespace Data { struct Symbol; +struct BottomUpResults; } class QTreeView; @@ -25,6 +26,7 @@ class QTreeView; class PerfParser; class FilterAndZoomStack; class CostContextMenu; +class BottomUpModel; class ResultsBottomUpPage : public QWidget { @@ -36,6 +38,9 @@ class ResultsBottomUpPage : public QWidget void clear(); +public slots: + void setBottomUpResults(const Data::BottomUpResults& results); + signals: void jumpToCallerCallee(const Data::Symbol& symbol); void jumpToDisassembly(const Data::Symbol& symbol); @@ -43,5 +48,7 @@ class ResultsBottomUpPage : public QWidget void selectSymbol(const Data::Symbol& symbol); private: + BottomUpModel* m_model = nullptr; + QMenu* m_exportMenu; QScopedPointer ui; }; diff --git a/src/resultspage.cpp b/src/resultspage.cpp index 5eed7cca..4bd3e6f6 100644 --- a/src/resultspage.cpp +++ b/src/resultspage.cpp @@ -63,17 +63,7 @@ ResultsPage::ResultsPage(PerfParser* parser, QWidget* parent) #endif , m_timelineVisible(true) { - m_exportMenu->setIcon(QIcon::fromTheme(QStringLiteral("document-export"))); - { - const auto actions = m_filterAndZoomStack->actions(); - m_filterMenu->addAction(actions.filterOut); - m_filterMenu->addAction(actions.resetFilter); - m_filterMenu->addSeparator(); - m_filterMenu->addAction(actions.zoomOut); - m_filterMenu->addAction(actions.resetZoom); - m_filterMenu->addSeparator(); - m_filterMenu->addAction(actions.resetFilterAndZoom); - } + ResultsUtil::setupMenues(m_filterAndZoomStack, m_exportMenu, m_filterMenu); ui->setupUi(this); ui->verticalLayout->addWidget(m_contents); @@ -81,14 +71,6 @@ ResultsPage::ResultsPage(PerfParser* parser, QWidget* parent) ui->errorWidget->hide(); ui->lostMessage->hide(); - auto dockify = [](QWidget* widget, const QString& id, const QString& title, const QString& shortcut) { - auto* dock = new KDDockWidgets::DockWidget(id); - dock->setWidget(widget); - dock->setTitle(title); - dock->toggleAction()->setShortcut(shortcut); - return dock; - }; - m_summaryPageDock = dockify(m_resultsSummaryPage, QStringLiteral("summary"), tr("Summar&y"), tr("Ctrl+Y")); m_contents->addDockWidget(m_summaryPageDock, KDDockWidgets::Location_OnTop); m_bottomUpDock = dockify(m_resultsBottomUpPage, QStringLiteral("bottomUp"), tr("Bottom &Up"), tr("Ctrl+U")); @@ -180,21 +162,7 @@ ResultsPage::ResultsPage(PerfParser* parser, QWidget* parent) m_filterBusyIndicator->setVisible(false); }); - { - // create a busy indicator - m_filterBusyIndicator = new QWidget(this); - m_filterBusyIndicator->setMinimumHeight(100); - m_filterBusyIndicator->setVisible(false); - m_filterBusyIndicator->setToolTip(i18n("Filtering in progress, please wait...")); - auto layout = new QVBoxLayout(m_filterBusyIndicator); - layout->setAlignment(Qt::AlignCenter); - auto progressBar = new QProgressBar(m_filterBusyIndicator); - layout->addWidget(progressBar); - progressBar->setMaximum(0); - auto label = new QLabel(m_filterBusyIndicator->toolTip(), m_filterBusyIndicator); - label->setAlignment(Qt::AlignCenter); - layout->addWidget(label); - } + m_filterBusyIndicator = ResultsUtil::createBusyIndicator(this); connect(Settings::instance(), &Settings::costAggregationChanged, this, [this, parser] { parser->filterResults(m_filterAndZoomStack->filter()); }); diff --git a/src/resultspagediff.cpp b/src/resultspagediff.cpp new file mode 100644 index 00000000..2e9dc79e --- /dev/null +++ b/src/resultspagediff.cpp @@ -0,0 +1,175 @@ +/* + SPDX-FileCopyrightText: Lieven Hey + SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "resultspagediff.h" +#include "ui_resultspagediff.h" + +#include "parsers/perf/perfparser.h" +#include "settings.h" + +#include "costcontextmenu.h" +#include "dockwidgetsetup.h" +#include "resultsbottomuppage.h" +#include "resultstopdownpage.h" +#include "resultsutil.h" + +#include "models/filterandzoomstack.h" + +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include + +ResultsPageDiff::ResultsPageDiff(QWidget* parent) + : QWidget(parent) + , m_fileA(new PerfParser(this)) + , m_fileB(new PerfParser(this)) + , ui(new Ui::ResultsPageDiff) + , m_contents(createDockingArea(QStringLiteral("resultsDiff"), this)) + , m_filterAndZoomStack(new FilterAndZoomStack(this)) + , m_costContextMenu(new CostContextMenu(this)) + , m_filterMenu(new QMenu(this)) + , m_exportMenu(new QMenu(tr("Export"), this)) + , m_resultsBottomUpPage( + new ResultsBottomUpPage(m_filterAndZoomStack, nullptr, m_costContextMenu, m_exportMenu, this)) + , m_resultsTopDownPage(new ResultsTopDownPage(m_filterAndZoomStack, nullptr, m_costContextMenu, this)) +{ + ResultsUtil::setupMenues(m_filterAndZoomStack, m_exportMenu, m_filterMenu); + + ui->setupUi(this); + ui->verticalLayout->addWidget(m_contents); + + ui->errorWidget->hide(); + ui->lostMessage->hide(); + + m_bottomUpDock = dockify(m_resultsBottomUpPage, QStringLiteral("dbottomUp"), tr("Bottom &Up"), tr("Ctrl+U")); + m_contents->addDockWidget(m_bottomUpDock, KDDockWidgets::Location_OnTop); + m_topDownDock = dockify(m_resultsTopDownPage, QStringLiteral("dtopDown"), tr("Top &Down"), tr("Ctrl+D")); + m_bottomUpDock->addDockWidgetAsTab(m_topDownDock); + m_bottomUpDock->setAsCurrentTab(); + + connect(m_filterAndZoomStack, &FilterAndZoomStack::filterChanged, m_fileA, &PerfParser::filterResults); + + connect(m_fileA, &PerfParser::parserWarning, this, &ResultsPageDiff::showError); + + connect(m_fileA, &PerfParser::parsingStarted, this, [this]() { + // disable when we apply a filter + m_contents->setEnabled(false); + repositionFilterBusyIndicator(); + m_filterBusyIndicator->setVisible(true); + }); + connect(m_fileA, &PerfParser::parsingFinished, this, [this]() { + // re-enable when we finished filtering + m_contents->setEnabled(true); + m_filterBusyIndicator->setVisible(false); + }); + + for (const auto& parser : {m_fileA, m_fileB}) { + connect(parser, &PerfParser::parsingStarted, this, [this] { m_runningParsersCounter++; }); + connect(parser, &PerfParser::parsingFailed, this, [this] { + m_runningParsersCounter--; + ui->errorWidget->setText(QStringLiteral("Failed to parse file")); + }); + connect(parser, &PerfParser::parsingFinished, this, [this] { + m_runningParsersCounter--; + if (m_runningParsersCounter == 0) { + emit parsingFinished(); + } + }); + } + + connect(this, &ResultsPageDiff::parsingFinished, this, [this] { + auto bottomUpData = + Data::BottomUpResults::diffBottomUpResults(m_fileA->bottomUpResults(), m_fileB->bottomUpResults()); + m_resultsBottomUpPage->setBottomUpResults(bottomUpData); + + auto skipFirstLevel = Settings::instance()->costAggregation() == Settings::CostAggregation::BySymbol; + + auto topDownData = Data::TopDownResults::diffTopDownResults( + Data::TopDownResults::fromBottomUp(m_fileA->bottomUpResults(), skipFirstLevel), + Data::TopDownResults::fromBottomUp(m_fileB->bottomUpResults(), skipFirstLevel)); + m_resultsTopDownPage->setTopDownResults(topDownData); + }); + + m_filterBusyIndicator = ResultsUtil::createBusyIndicator(this); +} + +ResultsPageDiff::~ResultsPageDiff() = default; + +void ResultsPageDiff::clear() +{ + m_resultsBottomUpPage->clear(); + m_resultsTopDownPage->clear(); + m_exportMenu->clear(); + + m_filterAndZoomStack->clear(); +} + +QMenu* ResultsPageDiff::filterMenu() const +{ + return m_filterMenu; +} + +QMenu* ResultsPageDiff::exportMenu() const +{ + return m_exportMenu; +} + +QList ResultsPageDiff::windowActions() const +{ + auto ret = QList {m_bottomUpDock->toggleAction(), m_topDownDock->toggleAction()}; + return ret; +} + +void ResultsPageDiff::resizeEvent(QResizeEvent* event) +{ + QWidget::resizeEvent(event); + repositionFilterBusyIndicator(); +} + +void ResultsPageDiff::repositionFilterBusyIndicator() +{ + auto geometry = m_filterBusyIndicator->geometry(); + geometry.setWidth(width() / 2); + geometry.moveCenter(rect().center()); + m_filterBusyIndicator->setGeometry(geometry); +} + +void ResultsPageDiff::showError(const QString& message) +{ + ui->errorWidget->setText(message); + ui->errorWidget->animatedShow(); + QTimer::singleShot(5000, ui->errorWidget, &KMessageWidget::animatedHide); +} + +void ResultsPageDiff::initDockWidgets(const QVector& restored) +{ + Q_ASSERT(restored.contains(m_bottomUpDock)); + + const auto docks = {m_bottomUpDock, m_topDownDock}; + for (auto dock : docks) { + if (!dock || restored.contains(dock)) + continue; + + auto initialOption = KDDockWidgets::InitialOption {}; + m_bottomUpDock->addDockWidgetAsTab(dock, initialOption); + } +} + +void ResultsPageDiff::createDiffReport(const QString& fileA, const QString& fileB) +{ + m_fileA->startParseFile(fileA); + m_fileB->startParseFile(fileB); +} diff --git a/src/resultspagediff.h b/src/resultspagediff.h new file mode 100644 index 00000000..364918e4 --- /dev/null +++ b/src/resultspagediff.h @@ -0,0 +1,77 @@ +/* + SPDX-FileCopyrightText: Lieven Hey + SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +class QMenu; +class QAction; + +namespace Ui { +class ResultsPageDiff; +} + +namespace Data { +struct Symbol; +} + +namespace KDDockWidgets { +class MainWindow; +class DockWidget; +class DockWidgetBase; +} + +class PerfParser; +class ResultsBottomUpPage; +class ResultsTopDownPage; +class FilterAndZoomStack; +class CostContextMenu; + +class ResultsPageDiff : public QWidget +{ + Q_OBJECT +public: + explicit ResultsPageDiff(QWidget* parent = nullptr); + ~ResultsPageDiff(); + + void clear(); + QMenu* filterMenu() const; + QMenu* exportMenu() const; + QList windowActions() const; + + void initDockWidgets(const QVector& restored); + +public slots: + void showError(const QString& message); + void createDiffReport(const QString& fileA, const QString& fileB); + +signals: + void navigateToCode(const QString& url, int lineNumber, int columnNumber); + void parsingFinished(); + +private: + void resizeEvent(QResizeEvent* event) override; + void repositionFilterBusyIndicator(); + + PerfParser* m_fileA; + PerfParser* m_fileB; + + QScopedPointer ui; + KDDockWidgets::MainWindow* m_contents; + FilterAndZoomStack* m_filterAndZoomStack; + CostContextMenu* m_costContextMenu; + QMenu* m_filterMenu; + QMenu* m_exportMenu; + KDDockWidgets::DockWidget* m_bottomUpDock; + ResultsBottomUpPage* m_resultsBottomUpPage; + KDDockWidgets::DockWidget* m_topDownDock; + ResultsTopDownPage* m_resultsTopDownPage; + QWidget* m_filterBusyIndicator = nullptr; + + int m_runningParsersCounter = 0; +}; diff --git a/src/resultspagediff.ui b/src/resultspagediff.ui new file mode 100644 index 00000000..d2b8a8e4 --- /dev/null +++ b/src/resultspagediff.ui @@ -0,0 +1,50 @@ + + + ResultsPageDiff + + + + 0 + 0 + 744 + 510 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + false + + + KMessageWidget::Warning + + + + + + + + + + + KMessageWidget + QFrame +
kmessagewidget.h
+
+
+ + +
diff --git a/src/resultstopdownpage.cpp b/src/resultstopdownpage.cpp index e2a5d431..6782b3d8 100644 --- a/src/resultstopdownpage.cpp +++ b/src/resultstopdownpage.cpp @@ -9,6 +9,7 @@ #include "resultstopdownpage.h" #include "ui_resultstopdownpage.h" +#include "data.h" #include "parsers/perf/perfparser.h" #include "resultsutil.h" @@ -18,38 +19,17 @@ ResultsTopDownPage::ResultsTopDownPage(FilterAndZoomStack* filterStack, PerfParser* parser, CostContextMenu* contextMenu, QWidget* parent) : QWidget(parent) + , m_model(new TopDownModel(this)) , ui(new Ui::ResultsTopDownPage) { ui->setupUi(this); - auto topDownCostModel = new TopDownModel(this); - ResultsUtil::setupTreeView(ui->topDownTreeView, contextMenu, ui->topDownSearch, topDownCostModel); - ResultsUtil::setupCostDelegate(topDownCostModel, ui->topDownTreeView); - ResultsUtil::setupContextMenu(ui->topDownTreeView, contextMenu, topDownCostModel, filterStack, this); - - connect(parser, &PerfParser::topDownDataAvailable, this, - [this, topDownCostModel](const Data::TopDownResults& data) { - topDownCostModel->setData(data); - ResultsUtil::hideEmptyColumns(data.inclusiveCosts, ui->topDownTreeView, TopDownModel::NUM_BASE_COLUMNS); - - ResultsUtil::hideEmptyColumns(data.selfCosts, ui->topDownTreeView, - TopDownModel::NUM_BASE_COLUMNS + data.inclusiveCosts.numTypes()); - ResultsUtil::hideTracepointColumns(data.selfCosts, ui->topDownTreeView, - TopDownModel::NUM_BASE_COLUMNS + data.inclusiveCosts.numTypes()); - - // hide self cost columns for sched:sched_switch and off-CPU - // quasi all rows will have a cost of 0%, and only the leaves will show - // a non-zero value that is equal to the inclusive cost then - const auto costs = data.inclusiveCosts.numTypes(); - const auto schedSwitchName = QLatin1String("sched:sched_switch"); - const auto offCpuName = PerfParser::tr("off-CPU Time"); - for (int i = 0; i < costs; ++i) { - const auto typeName = data.inclusiveCosts.typeName(i); - if (typeName == schedSwitchName || typeName == offCpuName) { - ui->topDownTreeView->hideColumn(topDownCostModel->selfCostColumn(i)); - } - } - }); + ResultsUtil::setupTreeViewDiff(ui->topDownTreeView, contextMenu, ui->topDownSearch, m_model); + ResultsUtil::setupCostDelegate(m_model, ui->topDownTreeView); + ResultsUtil::setupContextMenu(ui->topDownTreeView, contextMenu, m_model, filterStack, this); + + if (parser) + connect(parser, &PerfParser::topDownDataAvailable, this, &ResultsTopDownPage::setTopDownResults); ResultsUtil::setupResultsAggregation(ui->costAggregationComboBox); } @@ -60,3 +40,28 @@ void ResultsTopDownPage::clear() { ui->topDownSearch->setText({}); } + +void ResultsTopDownPage::setTopDownResults(const Data::TopDownResults& data) +{ + m_model->setData(data); + ResultsUtil::hideEmptyColumns(data.inclusiveCosts, ui->topDownTreeView, TopDownModel::NUM_BASE_COLUMNS); + + ResultsUtil::hideEmptyColumns(data.selfCosts, ui->topDownTreeView, + TopDownModel::NUM_BASE_COLUMNS + data.inclusiveCosts.numTypes()); + ResultsUtil::hideTracepointColumns(data.selfCosts, ui->topDownTreeView, + TopDownModel::NUM_BASE_COLUMNS + data.inclusiveCosts.numTypes()); + + // hide self cost columns for sched:sched_switch and off-CPU + // quasi all rows will have a cost of 0%, and only the leaves will show + // a non-zero value that is equal to the inclusive cost then + const auto costs = data.inclusiveCosts.numTypes(); + const auto schedSwitchName = QLatin1String("sched:sched_switch"); + const auto offCpuName = PerfParser::tr("off-CPU Time"); + for (int i = 0; i < costs; ++i) { + const auto typeName = data.inclusiveCosts.typeName(i); + // use contains to also work in diff view + if (typeName.contains(schedSwitchName) || typeName.contains(offCpuName)) { + ui->topDownTreeView->hideColumn(m_model->selfCostColumn(i)); + } + } +} diff --git a/src/resultstopdownpage.h b/src/resultstopdownpage.h index c99c0e42..6b68e87b 100644 --- a/src/resultstopdownpage.h +++ b/src/resultstopdownpage.h @@ -16,6 +16,7 @@ class ResultsTopDownPage; namespace Data { struct Symbol; +struct TopDownResults; } class QTreeView; @@ -23,6 +24,7 @@ class QTreeView; class PerfParser; class FilterAndZoomStack; class CostContextMenu; +class TopDownModel; class ResultsTopDownPage : public QWidget { @@ -34,6 +36,9 @@ class ResultsTopDownPage : public QWidget void clear(); +public slots: + void setTopDownResults(const Data::TopDownResults& data); + signals: void jumpToCallerCallee(const Data::Symbol& symbol); void openEditor(const Data::Symbol& symbol); @@ -41,5 +46,6 @@ class ResultsTopDownPage : public QWidget void jumpToDisassembly(const Data::Symbol& symbol); private: + TopDownModel* m_model = nullptr; QScopedPointer ui; }; diff --git a/src/resultsutil.cpp b/src/resultsutil.cpp index c3672370..5140e998 100644 --- a/src/resultsutil.cpp +++ b/src/resultsutil.cpp @@ -11,11 +11,14 @@ #include #include #include +#include #include #include +#include #include #include #include +#include #include "models/costdelegate.h" #include "models/data.h" @@ -235,4 +238,34 @@ void setupResultsAggregation(QComboBox* costAggregationComboBox) Settings::instance()->setCostAggregation(aggregation); }); } + +void setupMenues(FilterAndZoomStack* filterAndZoomStack, QMenu* exportMenu, QMenu* filterMenu) +{ + exportMenu->setIcon(QIcon::fromTheme(QStringLiteral("document-export"))); + const auto actions = filterAndZoomStack->actions(); + filterMenu->addAction(actions.filterOut); + filterMenu->addAction(actions.resetFilter); + filterMenu->addSeparator(); + filterMenu->addAction(actions.zoomOut); + filterMenu->addAction(actions.resetZoom); + filterMenu->addSeparator(); + filterMenu->addAction(actions.resetFilterAndZoom); +} + +QWidget* createBusyIndicator(QWidget* parent) +{ + auto filterBusyIndicator = new QWidget(parent); + filterBusyIndicator->setMinimumHeight(100); + filterBusyIndicator->setVisible(false); + filterBusyIndicator->setToolTip(QObject::tr("Filtering in progress, please wait...")); + auto layout = new QVBoxLayout(filterBusyIndicator); + layout->setAlignment(Qt::AlignCenter); + auto progressBar = new QProgressBar(filterBusyIndicator); + layout->addWidget(progressBar); + progressBar->setMaximum(0); + auto label = new QLabel(filterBusyIndicator->toolTip(), filterBusyIndicator); + label->setAlignment(Qt::AlignCenter); + layout->addWidget(label); + return filterBusyIndicator; +} } diff --git a/src/resultsutil.h b/src/resultsutil.h index 77288b73..66e52fb8 100644 --- a/src/resultsutil.h +++ b/src/resultsutil.h @@ -45,6 +45,15 @@ void setupTreeView(QTreeView* view, CostContextMenu* costContextMenu, QLineEdit* Model::SortRole); } +template +void setupTreeViewDiff(QTreeView* view, CostContextMenu* costContextMenu, QLineEdit* filter, Model* model) +{ + auto* proxy = new DiffCostProxy(view); + proxy->setSourceModel(model); + setupTreeView(view, costContextMenu, filter, qobject_cast(proxy), Model::InitialSortColumn, + Model::SortRole); +} + void setupCostDelegate(QAbstractItemModel* model, QTreeView* view, int sortRole, int totalCostRole, int numBaseColumns); template @@ -100,4 +109,8 @@ void hideTracepointColumns(const Data::Costs& costs, QTreeView* view, int numBas void fillEventSourceComboBox(QComboBox* combo, const Data::Costs& costs, const QString& tooltipTemplate); void setupResultsAggregation(QComboBox* costAggregationComboBox); + +void setupMenues(FilterAndZoomStack* filterAndZoomStack, QMenu* exportMenu, QMenu* filterMenu); + +QWidget* createBusyIndicator(QWidget* parent); } diff --git a/src/startpage.cpp b/src/startpage.cpp index e2ec5077..568d3f37 100644 --- a/src/startpage.cpp +++ b/src/startpage.cpp @@ -23,6 +23,7 @@ StartPage::StartPage(QWidget* parent) connect(ui->recordDataButton, &QAbstractButton::clicked, this, &StartPage::recordButtonClicked); connect(ui->stopParseButton, &QAbstractButton::clicked, this, &StartPage::stopParseButtonClicked); connect(ui->pathSettings, &QAbstractButton::clicked, this, &StartPage::pathSettingsButtonClicked); + connect(ui->diffButton, &QAbstractButton::clicked, this, &StartPage::createDiffReportButtonClicked); ui->openFileButton->setFocus(); updateBackground(); diff --git a/src/startpage.h b/src/startpage.h index 2e6f73d6..5c1b2ed8 100644 --- a/src/startpage.h +++ b/src/startpage.h @@ -40,6 +40,7 @@ public slots: void recordButtonClicked(); void stopParseButtonClicked(); void pathSettingsButtonClicked(); + void createDiffReportButtonClicked(); private: void updateBackground(); diff --git a/src/startpage.ui b/src/startpage.ui index 33d2ac84..522ba48f 100644 --- a/src/startpage.ui +++ b/src/startpage.ui @@ -71,49 +71,49 @@ - - + + - + 0 0 - Record perf data + Configure hotspot settings to properly load data files from embedded systems or remote machines - Record Data + ... - + .. - - true + + QToolButton::InstantPopup - - + + - + 0 0 - Configure hotspot settings to properly load data files from embedded systems or remote machines + Record perf data - ... + Record Data - + .. - - QToolButton::InstantPopup + + true @@ -134,6 +134,13 @@ + + + + Diff Report + + +