diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6aee7d95..f9fbf2ee 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -39,6 +39,7 @@ set(HOTSPOT_SRCS resultsbottomuppage.cpp resultsflamegraphpage.cpp resultscallercalleepage.cpp + resultsbyfilepage.cpp resultsdisassemblypage.cpp resultsutil.cpp costheaderview.cpp @@ -66,6 +67,7 @@ set(HOTSPOT_SRCS resultsbottomuppage.ui resultsflamegraphpage.ui resultscallercalleepage.ui + resultsbyfilepage.ui resultsdisassemblypage.ui timelinewidget.ui unwindsettingspage.ui diff --git a/src/models/CMakeLists.txt b/src/models/CMakeLists.txt index fc7f1c7a..efafc415 100644 --- a/src/models/CMakeLists.txt +++ b/src/models/CMakeLists.txt @@ -4,6 +4,7 @@ add_library( ../util.cpp callercalleemodel.cpp callercalleeproxy.cpp + byfilemodel.cpp codedelegate.cpp costdelegate.cpp data.cpp diff --git a/src/models/byfilemodel.cpp b/src/models/byfilemodel.cpp new file mode 100644 index 00000000..97135420 --- /dev/null +++ b/src/models/byfilemodel.cpp @@ -0,0 +1,113 @@ +/* + SPDX-FileCopyrightText: Milian Wolff + SPDX-FileCopyrightText: 2024 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "byfilemodel.h" +#include "../util.h" + +#include + +ByFileModel::ByFileModel(QObject* parent) + : HashModel(parent) +{ +} + +ByFileModel::~ByFileModel() = default; + +void ByFileModel::setResults(const Data::ByFileResults& results) +{ + m_results = results; + setRows(results.entries); +} + +QVariant ByFileModel::headerCell(int column, int role) const +{ + if (role == Qt::DisplayRole) { + switch (column) { + case File: + return tr("File"); + } + column -= NUM_BASE_COLUMNS; + if (column < m_results.selfCosts.numTypes()) { + return tr("%1 (self)").arg(m_results.selfCosts.typeName(column)); + } + column -= m_results.selfCosts.numTypes(); + return tr("%1 (incl.)").arg(m_results.inclusiveCosts.typeName(column)); + } else if (role == Qt::ToolTipRole) { + switch (column) { + case File: + return tr("The name of the file. May be empty when debug information is missing."); + } + + column -= NUM_BASE_COLUMNS; + if (column < m_results.selfCosts.numTypes()) { + return tr("The aggregated sample costs directly attributed to this file."); + } + return tr("The aggregated sample costs attributed to this file, both directly and indirectly. This includes " + "the costs of all functions called by this file plus its self cost."); + } + + return {}; +} + +QVariant ByFileModel::cell(int column, int role, const QString& file, const Data::ByFileEntry& entry) const +{ + if (role == FileRole) { + return QVariant::fromValue(file); + } else if (role == SortRole) { + switch (column) { + case File: + return Util::formatString(file); + } + column -= NUM_BASE_COLUMNS; + if (column < m_results.selfCosts.numTypes()) { + return m_results.selfCosts.cost(column, entry.id); + } + column -= m_results.selfCosts.numTypes(); + return m_results.inclusiveCosts.cost(column, entry.id); + } else if (role == TotalCostRole && column >= NUM_BASE_COLUMNS) { + column -= NUM_BASE_COLUMNS; + if (column < m_results.selfCosts.numTypes()) { + return m_results.selfCosts.totalCost(column); + } + + column -= m_results.selfCosts.numTypes(); + return m_results.inclusiveCosts.totalCost(column); + } else if (role == Qt::DisplayRole) { + switch (column) { + case File: + return Util::formatString(file); + } + column -= NUM_BASE_COLUMNS; + if (column < m_results.selfCosts.numTypes()) { + return Util::formatCostRelative(m_results.selfCosts.cost(column, entry.id), + m_results.selfCosts.totalCost(column), true); + } + column -= m_results.selfCosts.numTypes(); + return Util::formatCostRelative(m_results.inclusiveCosts.cost(column, entry.id), + m_results.inclusiveCosts.totalCost(column), true); + } else if (role == SourceMapRole) { + return QVariant::fromValue(entry.sourceMap); + } else if (role == SelfCostsRole) { + return QVariant::fromValue(m_results.selfCosts); + } else if (role == InclusiveCostsRole) { + return QVariant::fromValue(m_results.inclusiveCosts); + } else if (role == Qt::ToolTipRole) { + return Util::formatFileTooltip(entry.id, file, m_results.selfCosts, m_results.inclusiveCosts); + } + + return {}; +} + +QModelIndex ByFileModel::indexForFile(const QString& file) const +{ + return indexForKey(file); +} + +int ByFileModel::numColumns() const +{ + return NUM_BASE_COLUMNS + m_results.inclusiveCosts.numTypes() + m_results.selfCosts.numTypes(); +} diff --git a/src/models/byfilemodel.h b/src/models/byfilemodel.h new file mode 100644 index 00000000..d520d629 --- /dev/null +++ b/src/models/byfilemodel.h @@ -0,0 +1,51 @@ +/* + SPDX-FileCopyrightText: Milian Wolff + SPDX-FileCopyrightText: 2024 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "data.h" +#include "hashmodel.h" + +class ByFileModel : public HashModel +{ + Q_OBJECT +public: + explicit ByFileModel(QObject* parent = nullptr); + ~ByFileModel(); + + void setResults(const Data::ByFileResults& results); + + enum Columns + { + File = 0, + }; + enum + { + NUM_BASE_COLUMNS = File + 1, + InitialSortColumn = File + 1 // the first cost column + }; + + enum Roles + { + SortRole = Qt::UserRole, + TotalCostRole, + SelfCostsRole, + InclusiveCostsRole, + SourceMapRole, + FileRole, + }; + + QVariant headerCell(int column, int role) const final override; + QVariant cell(int column, int role, const QString& file, const Data::ByFileEntry& entry) const final override; + int numColumns() const final override; + QModelIndex indexForFile(const QString& file) const; + +private: + Data::ByFileResults m_results; +}; diff --git a/src/models/callercalleeproxy.cpp b/src/models/callercalleeproxy.cpp index 5d7f43b6..4d78d011 100644 --- a/src/models/callercalleeproxy.cpp +++ b/src/models/callercalleeproxy.cpp @@ -34,6 +34,13 @@ bool match(const QSortFilterProxyModel* proxy, const Data::FileLine& fileLine) return matchImpl(pattern, fileLine.file); } + +bool match(const QSortFilterProxyModel* proxy, const QString& file) +{ + const auto pattern = proxy->filterRegularExpression(); + + return matchImpl(pattern, file); +} } SourceMapProxy::SourceMapProxy(QObject* parent) diff --git a/src/models/callercalleeproxy.h b/src/models/callercalleeproxy.h index 8e0a23af..bbe7362b 100644 --- a/src/models/callercalleeproxy.h +++ b/src/models/callercalleeproxy.h @@ -18,6 +18,7 @@ struct FileLine; namespace CallerCalleeProxyDetail { bool match(const QSortFilterProxyModel* proxy, const Data::Symbol& symbol); bool match(const QSortFilterProxyModel* proxy, const Data::FileLine& fileLine); +bool match(const QSortFilterProxyModel* proxy, const QString& file); } class SourceMapModel; diff --git a/src/models/data.h b/src/models/data.h index 0bf8778b..56ea1401 100644 --- a/src/models/data.h +++ b/src/models/data.h @@ -667,20 +667,25 @@ struct LocationCost using SourceLocationCostMap = QHash; using OffsetLocationCostMap = QHash; +inline LocationCost& source(SourceLocationCostMap& sourceMap, const FileLine& fileLine, int numTypes) +{ + auto it = sourceMap.find(fileLine); + if (it == sourceMap.end()) { + it = sourceMap.insert(fileLine, {numTypes}); + } else if (it->inclusiveCost.size() < static_cast(numTypes)) { + it->inclusiveCost.resize(numTypes); + it->selfCost.resize(numTypes); + } + return *it; +} + struct CallerCalleeEntry { quint32 id = 0; LocationCost& source(const FileLine& fileLine, int numTypes) { - auto it = sourceMap.find(fileLine); - if (it == sourceMap.end()) { - it = sourceMap.insert(fileLine, {numTypes}); - } else if (it->inclusiveCost.size() < static_cast(numTypes)) { - it->inclusiveCost.resize(numTypes); - it->selfCost.resize(numTypes); - } - return *it; + return Data::source(sourceMap, fileLine, numTypes); } ItemCost& callee(const Symbol& symbol, int numTypes) @@ -748,6 +753,36 @@ struct CallerCalleeResults void callerCalleesFromBottomUpData(const BottomUpResults& data, CallerCalleeResults* results); +struct ByFileEntry +{ + quint32 id = 0; + + LocationCost& source(const FileLine& fileLine, int numTypes) + { + return Data::source(sourceMap, fileLine, numTypes); + } + // source map for this file, i.e. locations mapped to associated costs + SourceLocationCostMap sourceMap; +}; + +using ByFileEntryMap = QHash; +struct ByFileResults +{ + ByFileEntryMap entries; + Costs selfCosts; + Costs inclusiveCosts; + + ByFileEntry& entry(const QString& file) + { + auto fileIt = entries.find(file); + if (fileIt == entries.end()) { + fileIt = entries.insert(file, {}); + fileIt->id = entries.size() - 1; + } + return *fileIt; + } +}; + const constexpr auto INVALID_CPU_ID = std::numeric_limits::max(); const constexpr int INVALID_TID = -1; const constexpr int INVALID_PID = -1; @@ -1008,6 +1043,9 @@ Q_DECLARE_TYPEINFO(Data::TopDown, Q_MOVABLE_TYPE); Q_DECLARE_METATYPE(Data::CallerCalleeEntry) Q_DECLARE_TYPEINFO(Data::CallerCalleeEntry, Q_MOVABLE_TYPE); +Q_DECLARE_METATYPE(Data::ByFileEntry) +Q_DECLARE_TYPEINFO(Data::ByFileEntry, Q_MOVABLE_TYPE); + Q_DECLARE_METATYPE(Data::BottomUpResults) Q_DECLARE_TYPEINFO(Data::BottomUpResults, Q_MOVABLE_TYPE); @@ -1020,6 +1058,9 @@ Q_DECLARE_TYPEINFO(Data::PerLibraryResults, Q_MOVABLE_TYPE); Q_DECLARE_METATYPE(Data::CallerCalleeResults) Q_DECLARE_TYPEINFO(Data::CallerCalleeResults, Q_MOVABLE_TYPE); +Q_DECLARE_METATYPE(Data::ByFileResults) +Q_DECLARE_TYPEINFO(Data::ByFileResults, Q_MOVABLE_TYPE); + Q_DECLARE_METATYPE(Data::Event) Q_DECLARE_TYPEINFO(Data::Event, Q_MOVABLE_TYPE); diff --git a/src/parsers/perf/perfparser.cpp b/src/parsers/perf/perfparser.cpp index 30f7586c..d61593cc 100644 --- a/src/parsers/perf/perfparser.cpp +++ b/src/parsers/perf/perfparser.cpp @@ -556,6 +556,25 @@ void addCallerCalleeEvent(const Data::Symbol& symbol, const Data::Location& loca recursionGuard->insert(symbol); } } +void addByFileEvent(const Data::Symbol& symbol, const Data::Location& location, int type, quint64 cost, + QSet* recursionGuard, Data::ByFileResults* byFileResult, int numCosts) +{ + const auto& key = location.fileLine.file.isEmpty() ? symbol.binary : location.fileLine.file; + auto recursionIt = recursionGuard->find(key); + if (recursionIt == recursionGuard->end()) { + auto& entry = byFileResult->entry(key); + auto& sourceCost = entry.source(location.fileLine, numCosts); + + byFileResult->inclusiveCosts.add(type, entry.id, cost); + sourceCost.inclusiveCost[type] += cost; + if (recursionGuard->isEmpty()) { + // increment self cost for leaf + byFileResult->selfCosts.add(type, entry.id, cost); + sourceCost.selfCost[type] += cost; + } + recursionGuard->insert(key); + } +} template void addBottomUpResult(Data::BottomUpResults* bottomUpResult, Settings::CostAggregation costAggregation, @@ -876,6 +895,9 @@ class PerfParserPrivate : public QObject summaryResult.threadCount = uniqueThreads.size(); summaryResult.processCount = uniqueProcess.size(); + byFileResult.inclusiveCosts.setTotalCosts(bottomUpResult.costs.totalCosts()); + byFileResult.selfCosts.setTotalCosts(bottomUpResult.costs.totalCosts()); + buildTopDownResult(); buildPerLibraryResult(); buildCallerCalleeResult(); @@ -935,6 +957,8 @@ class PerfParserPrivate : public QObject summaryResult.costs.push_back({label, 0, 0, unit}); Q_ASSERT(bottomUpResult.costs.numTypes() == costId); bottomUpResult.costs.addType(costId, label, unit); + byFileResult.inclusiveCosts.addType(costId, label, unit); + byFileResult.selfCosts.addType(costId, label, unit); return costId; } @@ -1141,6 +1165,7 @@ class PerfParserPrivate : public QObject } QSet recursionGuard; + QSet fileRecursionGuard; const auto type = attributeIdsToCostIds.value(sampleCost.attributeId, -1); if (type < 0) { @@ -1149,10 +1174,12 @@ class PerfParserPrivate : public QObject return; } - auto frameCallback = [this, &recursionGuard, &sampleCost, type](const Data::Symbol& symbol, - const Data::Location& location) { + auto frameCallback = [this, &recursionGuard, &fileRecursionGuard, &sampleCost, + type](const Data::Symbol& symbol, const Data::Location& location) { addCallerCalleeEvent(symbol, location, type, sampleCost.cost, &recursionGuard, &callerCalleeResult, bottomUpResult.costs.numTypes()); + addByFileEvent(symbol, location, type, sampleCost.cost, &fileRecursionGuard, &byFileResult, + bottomUpResult.costs.numTypes()); if (perfScriptOutput) { *perfScriptOutput << '\t' << Qt::hex << qSetFieldWidth(16) << location.address << qSetFieldWidth(0) @@ -1246,10 +1273,13 @@ class PerfParserPrivate : public QObject if (stackId != -1) { const auto& frames = eventResult.stacks[stackId]; QSet recursionGuard; - auto frameCallback = [this, &recursionGuard, switchTime](const Data::Symbol& symbol, - const Data::Location& location) { + QSet fileRecursionGuard; + auto frameCallback = [this, &recursionGuard, &fileRecursionGuard, + switchTime](const Data::Symbol& symbol, const Data::Location& location) { addCallerCalleeEvent(symbol, location, eventResult.offCpuTimeCostId, switchTime, &recursionGuard, &callerCalleeResult, bottomUpResult.costs.numTypes()); + addByFileEvent(symbol, location, eventResult.offCpuTimeCostId, switchTime, &fileRecursionGuard, + &byFileResult, bottomUpResult.costs.numTypes()); }; addBottomUpResult(eventResult.offCpuTimeCostId, switchTime, contextSwitch.pid, contextSwitch.tid, contextSwitch.cpu, frames, frameCallback); @@ -1383,6 +1413,7 @@ class PerfParserPrivate : public QObject Data::TopDownResults topDownResult; Data::PerLibraryResults perLibraryResult; Data::CallerCalleeResults callerCalleeResult; + Data::ByFileResults byFileResult; Data::EventResults eventResult; Data::TracepointResults tracepointResult; Data::FrequencyResults frequencyResult; @@ -1427,6 +1458,7 @@ PerfParser::PerfParser(QObject* parent) qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); + qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); @@ -1444,6 +1476,11 @@ PerfParser::PerfParser(QObject* parent) m_callerCalleeResults = data; } }); + connect(this, &PerfParser::byFileDataAvailable, this, [this](const Data::ByFileResults& data) { + if (m_byFileResults.entries.isEmpty()) { + m_byFileResults = data; + } + }); connect(this, &PerfParser::frequencyDataAvailable, this, [this](const Data::FrequencyResults& data) { if (m_frequencyResults.cores.isEmpty()) { m_frequencyResults = data; @@ -1576,6 +1613,7 @@ void PerfParser::startParseFile(const QString& path) m_bottomUpResults = {}; m_callerCalleeResults = {}; + m_byFileResults = {}; m_tracepointResults = {}; m_events = {}; m_frequencyResults = {}; @@ -1599,6 +1637,7 @@ void PerfParser::startParseFile(const QString& path) emit perLibraryDataAvailable(d.perLibraryResult); emit summaryDataAvailable(d.summaryResult); emit callerCalleeDataAvailable(d.callerCalleeResult); + emit byFileDataAvailable(d.byFileResult); emit tracepointDataAvailable(d.tracepointResult); emit eventsAvailable(d.eventResult); emit frequencyDataAvailable(d.frequencyResult); @@ -1727,6 +1766,7 @@ void PerfParser::filterResults(const Data::FilterAction& filter) Data::BottomUpResults bottomUp; 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(); @@ -1741,7 +1781,11 @@ void PerfParser::filterResults(const Data::FilterAction& filter) if (!filter.isValid() && !m_costAggregationChanged) { bottomUp = m_bottomUpResults; callerCallee = m_callerCalleeResults; + byFile = m_byFileResults; } else { + byFile.inclusiveCosts.initializeCostsFrom(m_bottomUpResults.costs); + byFile.selfCosts.initializeCostsFrom(m_bottomUpResults.costs); + bottomUp.symbols = m_bottomUpResults.symbols; bottomUp.locations = m_bottomUpResults.locations; bottomUp.costs.initializeCostsFrom(m_bottomUpResults.costs); @@ -1869,10 +1913,13 @@ void PerfParser::filterResults(const Data::FilterAction& filter) } QSet recursionGuard; - auto frameCallback = [&callerCallee, &recursionGuard, &event, + QSet fileRecursionGuard; + auto frameCallback = [&callerCallee, &recursionGuard, &byFile, &fileRecursionGuard, &event, numCosts](const Data::Symbol& symbol, const Data::Location& location) { addCallerCalleeEvent(symbol, location, event.type, event.cost, &recursionGuard, &callerCallee, numCosts); + addByFileEvent(symbol, location, event.type, event.cost, &fileRecursionGuard, &byFile, + numCosts); }; if (event.stackId != -1) { @@ -1918,6 +1965,7 @@ void PerfParser::filterResults(const Data::FilterAction& filter) emit topDownDataAvailable(topDown); emit perLibraryDataAvailable(perLibrary); emit callerCalleeDataAvailable(callerCallee); + emit byFileDataAvailable(byFile); emit frequencyDataAvailable(frequencyResults); emit tracepointDataAvailable(tracepointResults); emit eventsAvailable(events); diff --git a/src/parsers/perf/perfparser.h b/src/parsers/perf/perfparser.h index 984b80a2..7133f49b 100644 --- a/src/parsers/perf/perfparser.h +++ b/src/parsers/perf/perfparser.h @@ -43,6 +43,10 @@ class PerfParser : public QObject { return m_callerCalleeResults; } + Data::ByFileResults byFileResults() const + { + return m_byFileResults; + } Data::EventResults eventResults() const { return m_events; @@ -55,6 +59,7 @@ class PerfParser : public QObject void topDownDataAvailable(const Data::TopDownResults& data); 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); @@ -81,6 +86,7 @@ class PerfParser : public QObject QStringList m_parserArgs; Data::BottomUpResults m_bottomUpResults; Data::CallerCalleeResults m_callerCalleeResults; + Data::ByFileResults m_byFileResults; Data::TracepointResults m_tracepointResults; Data::EventResults m_events; Data::FrequencyResults m_frequencyResults; diff --git a/src/resultsbyfilepage.cpp b/src/resultsbyfilepage.cpp new file mode 100644 index 00000000..3bae0154 --- /dev/null +++ b/src/resultsbyfilepage.cpp @@ -0,0 +1,133 @@ +/* + SPDX-FileCopyrightText: Milian Wolff + SPDX-FileCopyrightText: 2024 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "resultsbyfilepage.h" +#include "ui_resultsbyfilepage.h" + +#include +#include + +#include "costcontextmenu.h" +#include "parsers/perf/perfparser.h" +#include "resultsutil.h" + +#include "models/byfilemodel.h" +#include "models/callercalleemodel.h" +#include "models/callercalleeproxy.h" +#include "models/filterandzoomstack.h" +#include "models/hashmodel.h" +#include "models/treemodel.h" + +#include +#include + +#include "hotspot-config.h" + +namespace { +QSortFilterProxyModel* createProxy(SourceMapModel* model) +{ + return new SourceMapProxy(model); +} + +template +QSortFilterProxyModel* createProxy(Model* model) +{ + return new CallerCalleeProxy(model); +} + +template +Model* setupModelAndProxyForView(QTreeView* view, CostContextMenu* contextMenu) +{ + auto model = new Model(view); + auto proxy = createProxy(model); + proxy->setSourceModel(model); + proxy->setSortRole(Model::SortRole); + view->setModel(proxy); + ResultsUtil::setupHeaderView(view, contextMenu); + ResultsUtil::setupCostDelegate(model, view); + view->sortByColumn(Model::InitialSortColumn, Qt::DescendingOrder); + + return model; +} +} + +ResultsByFilePage::ResultsByFilePage(FilterAndZoomStack* filterStack, PerfParser* parser, CostContextMenu* contextMenu, + QWidget* parent) + : QWidget(parent) + , ui(std::make_unique()) +{ + Q_UNUSED(filterStack); + + ui->setupUi(this); + + m_byFileCostModel = new ByFileModel(this); + m_byFileProxy = new CallerCalleeProxy(this); + m_byFileProxy->setSourceModel(m_byFileCostModel); + m_byFileProxy->setSortRole(ByFileModel::SortRole); + ResultsUtil::connectFilter(ui->byFileFilter, m_byFileProxy); + ui->byFileTableView->setSortingEnabled(true); + ui->byFileTableView->setModel(m_byFileProxy); + ResultsUtil::setupHeaderView(ui->byFileTableView, contextMenu); + ResultsUtil::setupCostDelegate(m_byFileCostModel, ui->byFileTableView); + + connect(parser, &PerfParser::byFileDataAvailable, this, [this](const Data::ByFileResults& data) { + m_byFileCostModel->setResults(data); + ResultsUtil::hideEmptyColumns(data.inclusiveCosts, ui->byFileTableView, ByFileModel::NUM_BASE_COLUMNS); + + ResultsUtil::hideEmptyColumns(data.selfCosts, ui->byFileTableView, + ByFileModel::NUM_BASE_COLUMNS + data.inclusiveCosts.numTypes()); + ResultsUtil::hideTracepointColumns(data.selfCosts, ui->byFileTableView, ByFileModel::NUM_BASE_COLUMNS); + auto view = ui->byFileTableView; + view->setCurrentIndex(view->model()->index(0, 0, {})); + ResultsUtil::hideEmptyColumns(data.inclusiveCosts, ui->sourceMapView, SourceMapModel::NUM_BASE_COLUMNS); + ResultsUtil::hideTracepointColumns(data.selfCosts, ui->sourceMapView, SourceMapModel::NUM_BASE_COLUMNS); + }); + + auto sourceMapModel = setupModelAndProxyForView(ui->sourceMapView, contextMenu); + + auto selectByFileIndex = [sourceMapModel, this](const QModelIndex& index) { + const auto costs = index.data(ByFileModel::SelfCostsRole).value(); + const auto sourceMap = index.data(ByFileModel::SourceMapRole).value(); + sourceMapModel->setResults(sourceMap, costs); + if (index.model() == m_byFileCostModel) { + ui->byFileTableView->setCurrentIndex(m_byFileProxy->mapFromSource(index)); + } + }; + + ui->sourceMapView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->sourceMapView, &QTreeView::customContextMenuRequested, this, + &ResultsByFilePage::onSourceMapContextMenu); + + connect(ui->byFileTableView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, + [selectByFileIndex](const QModelIndex& current, const QModelIndex&) { + if (current.isValid()) { + selectByFileIndex(current); + } + }); + + ResultsUtil::setupResultsAggregation(ui->costAggregationComboBox); +} + +ResultsByFilePage::~ResultsByFilePage() = default; + +void ResultsByFilePage::clear() +{ + ui->byFileFilter->setText({}); +} + +void ResultsByFilePage::onSourceMapContextMenu(QPoint point) +{ + const auto sourceMapIndex = ui->sourceMapView->indexAt(point); + if (!sourceMapIndex.isValid()) { + return; + } + + auto fileLine = sourceMapIndex.data(SourceMapModel::FileLineRole).value(); + if (fileLine.isValid()) { + emit openFileLineRequested(fileLine); + } +} diff --git a/src/resultsbyfilepage.h b/src/resultsbyfilepage.h new file mode 100644 index 00000000..c810d2aa --- /dev/null +++ b/src/resultsbyfilepage.h @@ -0,0 +1,49 @@ +/* + SPDX-FileCopyrightText: Milian Wolff + SPDX-FileCopyrightText: 2024 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +namespace Ui { +class ResultsByFilePage; +} + +namespace Data { +struct FileLine; +} + +class QSortFilterProxyModel; + +class PerfParser; +class ByFileModel; +class FilterAndZoomStack; +class CostContextMenu; + +class ResultsByFilePage : public QWidget +{ + Q_OBJECT +public: + explicit ResultsByFilePage(FilterAndZoomStack* filterStack, PerfParser* parser, CostContextMenu* contextMenu, + QWidget* parent = nullptr); + ~ResultsByFilePage(); + + void clear(); + +signals: + void openFileLineRequested(const Data::FileLine& fileLine); + +private: + void onSourceMapContextMenu(QPoint point); + + std::unique_ptr ui; + + ByFileModel* m_byFileCostModel; + QSortFilterProxyModel* m_byFileProxy; +}; diff --git a/src/resultsbyfilepage.ui b/src/resultsbyfilepage.ui new file mode 100644 index 00000000..9a1d035e --- /dev/null +++ b/src/resultsbyfilepage.ui @@ -0,0 +1,121 @@ + + + ResultsByFilePage + + + + 0 + 0 + 768 + 391 + + + + Show the profile data in a flat table view aggregated by files. + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + + + Aggregate cost by: + + + costAggregationComboBox + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Filter the call graph tree. + + + + + + + + + + Qt::Vertical + + + + true + + + false + + + true + + + true + + + + + Qt::Horizontal + + + + true + + + false + + + true + + + true + + + + + + + + + + diff --git a/src/resultscallercalleepage.cpp b/src/resultscallercalleepage.cpp index cba5baea..bc2aae85 100644 --- a/src/resultscallercalleepage.cpp +++ b/src/resultscallercalleepage.cpp @@ -179,14 +179,16 @@ ResultsCallerCalleePage::ResultsCallerCalleePage(FilterAndZoomStack* filterStack ResultsCallerCalleePage::~ResultsCallerCalleePage() = default; -ResultsCallerCalleePage::SourceMapLocation -ResultsCallerCalleePage::toSourceMapLocation(const Data::FileLine& fileLine, const Data::Symbol& symbol) const +ResultsCallerCalleePage::SourceMapLocation ResultsCallerCalleePage::toSourceMapLocation(const Data::FileLine& fileLine, + const QString& binaryPath) const { if (!fileLine.isValid()) { return {}; } SourceMapLocation ret; + ret.binaryPath = binaryPath; + auto resolvePath = [&ret, &fileLine](const QString& pathName) -> bool { const QString path = pathName + fileLine.file; if (QFileInfo::exists(path)) { @@ -199,7 +201,7 @@ ResultsCallerCalleePage::toSourceMapLocation(const Data::FileLine& fileLine, con // also try to resolve paths relative to the module output folder // fixes a common issue with qmake builds that use relative paths - const QString modulePath = QFileInfo(symbol.path).path() + QLatin1Char('/'); + const QString modulePath = QFileInfo(binaryPath).path() + QLatin1Char('/'); resolvePath(m_sysroot) || resolvePath(m_sysroot + modulePath) || resolvePath(m_appPath) || resolvePath(m_appPath + modulePath); @@ -212,7 +214,13 @@ ResultsCallerCalleePage::SourceMapLocation ResultsCallerCalleePage::toSourceMapL const auto fileLine = index.data(SourceMapModel::FileLineRole).value(); const auto symbol = ui->callerCalleeTableView->currentIndex().data(CallerCalleeModel::SymbolRole).value(); - return toSourceMapLocation(fileLine, symbol); + return toSourceMapLocation(fileLine, symbol.path); +} + +void ResultsCallerCalleePage::openFileLineRequested(const Data::FileLine& fileLine) +{ + // TODO: how should we try to find a symbol here to resolve relative paths? + showSourceMapContextMenu(toSourceMapLocation(fileLine, {}), {}); } void ResultsCallerCalleePage::onSourceMapContextMenu(QPoint point) @@ -222,31 +230,26 @@ void ResultsCallerCalleePage::onSourceMapContextMenu(QPoint point) return; } - const auto location = toSourceMapLocation(sourceMapIndex); - if (!location) { + // fetch current symbol from callerCalleeView to check if we can disassemble it + const auto symbol = + ui->callerCalleeTableView->currentIndex().data(CallerCalleeModel::SymbolRole).value(); + showSourceMapContextMenu(toSourceMapLocation(sourceMapIndex), symbol); +} + +void ResultsCallerCalleePage::showSourceMapContextMenu(const SourceMapLocation& location, const Data::Symbol& symbol) +{ + if (!location) return; - } QMenu contextMenu; auto* viewCallerCallee = contextMenu.addAction(tr("Open in Editor")); - connect(viewCallerCallee, &QAction::triggered, this, [this, location, sourceMapIndex] { - if (location) { - emit navigateToCode(location.path, location.lineNumber, 0); - } else { - const auto fileLine = sourceMapIndex.data(SourceMapModel::FileLineRole).value(); - emit navigateToCodeFailed(tr("Failed to find file for location '%1'.").arg(fileLine.toString())); - } - }); + connect(viewCallerCallee, &QAction::triggered, this, + [this, location] { emit navigateToCode(location.path, location.lineNumber, 0); }); - auto line = sourceMapIndex.data(SourceMapModel::FileLineRole).value(); auto disassemblyAction = contextMenu.addAction(tr("Disassembly")); - - // fetch current symbol from callerCalleeView to check if we can disassemble it - const auto symbol = - ui->callerCalleeTableView->currentIndex().data(CallerCalleeModel::SymbolRole).value(); disassemblyAction->setEnabled(symbol.canDisassemble()); connect(disassemblyAction, &QAction::triggered, this, - [this, symbol, line] { emit jumpToSourceCode(symbol, line); }); + [this, symbol, location] { emit jumpToSourceCode(symbol, {location.path, location.lineNumber}); }); contextMenu.exec(QCursor::pos()); } @@ -278,7 +281,7 @@ void ResultsCallerCalleePage::openEditor(const Data::Symbol& symbol) const auto map = callerCalleeIndex.data(CallerCalleeModel::SourceMapRole).value(); auto it = std::find_if(map.keyBegin(), map.keyEnd(), [&symbol, this](const Data::FileLine& fileLine) { - const auto location = toSourceMapLocation(fileLine, symbol); + const auto location = toSourceMapLocation(fileLine, symbol.path); if (location) { auto settings = Settings::instance(); const auto colon = QLatin1Char(':'); diff --git a/src/resultscallercalleepage.h b/src/resultscallercalleepage.h index c32e163a..6bb68353 100644 --- a/src/resultscallercalleepage.h +++ b/src/resultscallercalleepage.h @@ -44,9 +44,7 @@ class ResultsCallerCalleePage : public QWidget void jumpToCallerCallee(const Data::Symbol& symbol); void openEditor(const Data::Symbol& symbol); - -private slots: - void onSourceMapContextMenu(QPoint pos); + void openFileLineRequested(const Data::FileLine& fileLine); signals: void navigateToCode(const QString& url, int lineNumber, int columnNumber); @@ -56,6 +54,8 @@ private slots: void jumpToDisassembly(const Data::Symbol& symbol); private: + void onSourceMapContextMenu(QPoint pos); + struct SourceMapLocation { inline explicit operator bool() const @@ -65,9 +65,11 @@ private slots: QString path; int lineNumber = -1; + QString binaryPath; }; SourceMapLocation toSourceMapLocation(const QModelIndex& index) const; - SourceMapLocation toSourceMapLocation(const Data::FileLine& fileLine, const Data::Symbol& symbol) const; + SourceMapLocation toSourceMapLocation(const Data::FileLine& fileLine, const QString& binaryPath) const; + void showSourceMapContextMenu(const SourceMapLocation& location, const Data::Symbol& symbol); std::unique_ptr ui; CallgraphWidget* m_callgraph; diff --git a/src/resultspage.cpp b/src/resultspage.cpp index 39c6ff3c..1d997d01 100644 --- a/src/resultspage.cpp +++ b/src/resultspage.cpp @@ -15,6 +15,7 @@ #include "costcontextmenu.h" #include "dockwidgetsetup.h" #include "resultsbottomuppage.h" +#include "resultsbyfilepage.h" #include "resultscallercalleepage.h" #include "resultsdisassemblypage.h" #include "resultsflamegraphpage.h" @@ -80,6 +81,7 @@ ResultsPage::ResultsPage(PerfParser* parser, QWidget* parent) , m_resultsTopDownPage(new ResultsTopDownPage(m_filterAndZoomStack, parser, m_costContextMenu, this)) , m_resultsFlameGraphPage(new ResultsFlameGraphPage(m_filterAndZoomStack, parser, m_exportMenu, this)) , m_resultsCallerCalleePage(new ResultsCallerCalleePage(m_filterAndZoomStack, parser, m_costContextMenu, this)) + , m_resultsByFilePage(new ResultsByFilePage(m_filterAndZoomStack, parser, m_costContextMenu, this)) , m_resultsDisassemblyPage(new ResultsDisassemblyPage(m_costContextMenu, this)) , m_timeLineWidget(new TimeLineWidget(parser, m_filterMenu, m_filterAndZoomStack, this)) #if QCustomPlot_FOUND @@ -124,6 +126,8 @@ ResultsPage::ResultsPage(PerfParser* parser, QWidget* parent) m_callerCalleeDock = dockify(m_resultsCallerCalleePage, QStringLiteral("callerCallee"), tr("Ca&ller / Callee"), tr("Ctrl+L")); m_summaryPageDock->addDockWidgetAsTab(m_callerCalleeDock); + m_byFileDock = dockify(m_resultsByFilePage, QStringLiteral("byFile"), tr("&By File"), tr("Ctrl+B")); + m_summaryPageDock->addDockWidgetAsTab(m_byFileDock); m_disassemblyDock = dockify(m_resultsDisassemblyPage, QStringLiteral("disassembly"), tr("D&isassembly"), tr("Ctrl+I")); m_summaryPageDock->addDockWidgetAsTab(m_disassemblyDock, KDDockWidgets::InitialVisibilityOption::StartHidden); @@ -158,6 +162,8 @@ ResultsPage::ResultsPage(PerfParser* parser, QWidget* parent) connect(parser, &PerfParser::parserWarning, this, &ResultsPage::showError); connect(parser, &PerfParser::exportFailed, this, &ResultsPage::showError); + connect(m_resultsByFilePage, &ResultsByFilePage::openFileLineRequested, m_resultsCallerCalleePage, + &ResultsCallerCalleePage::openFileLineRequested); connect(m_resultsCallerCalleePage, &ResultsCallerCalleePage::navigateToCode, this, &ResultsPage::navigateToCode); connect(m_resultsCallerCalleePage, &ResultsCallerCalleePage::navigateToCodeFailed, this, &ResultsPage::showError); connect(m_resultsCallerCalleePage, &ResultsCallerCalleePage::selectSymbol, m_timeLineWidget, @@ -283,6 +289,7 @@ void ResultsPage::clear() m_resultsBottomUpPage->clear(); m_resultsTopDownPage->clear(); m_resultsCallerCalleePage->clear(); + m_resultsByFilePage->clear(); m_resultsFlameGraphPage->clear(); m_exportMenu->clear(); m_disassemblyDock->forceClose(); @@ -302,13 +309,12 @@ QMenu* ResultsPage::exportMenu() const QList ResultsPage::windowActions() const { - auto ret = QList - { - m_summaryPageDock->toggleAction(), m_bottomUpDock->toggleAction(), m_topDownDock->toggleAction(), - m_flameGraphDock->toggleAction(), m_callerCalleeDock->toggleAction(), m_disassemblyDock->toggleAction(), - m_timeLineDock->toggleAction(), + auto ret = QList {m_summaryPageDock->toggleAction(), m_bottomUpDock->toggleAction(), + m_topDownDock->toggleAction(), m_flameGraphDock->toggleAction(), + m_callerCalleeDock->toggleAction(), m_byFileDock->toggleAction(), + m_disassemblyDock->toggleAction(), m_timeLineDock->toggleAction(), #if QCustomPlot_FOUND - m_frequencyDock->toggleAction() + m_frequencyDock->toggleAction() #endif }; return ret; @@ -341,15 +347,10 @@ void ResultsPage::initDockWidgets(const QVector& restored) Q_ASSERT(restored.contains(summaryPageDock)); - const auto docks = { - m_bottomUpDock, - m_topDownDock, - m_flameGraphDock, - m_callerCalleeDock, - m_timeLineDock, - m_disassemblyDock, + const auto docks = {m_bottomUpDock, m_topDownDock, m_flameGraphDock, m_callerCalleeDock, + m_byFileDock, m_timeLineDock, m_disassemblyDock, #if QCustomPlot_FOUND - m_frequencyDock + m_frequencyDock #endif }; for (auto dock : docks) { diff --git a/src/resultspage.h b/src/resultspage.h index f450a0d5..abc525aa 100644 --- a/src/resultspage.h +++ b/src/resultspage.h @@ -32,6 +32,7 @@ class ResultsBottomUpPage; class ResultsTopDownPage; class ResultsFlameGraphPage; class ResultsCallerCalleePage; +class ResultsByFilePage; class ResultsDisassemblyPage; class FilterAndZoomStack; class TimeLineWidget; @@ -86,6 +87,8 @@ public slots: ResultsFlameGraphPage* m_resultsFlameGraphPage; DockWidget* m_callerCalleeDock; ResultsCallerCalleePage* m_resultsCallerCalleePage; + DockWidget* m_byFileDock; + ResultsByFilePage* m_resultsByFilePage; DockWidget* m_disassemblyDock; ResultsDisassemblyPage* m_resultsDisassemblyPage; DockWidget* m_timeLineDock; diff --git a/src/util.cpp b/src/util.cpp index 34ab73ad..95efbd37 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -359,6 +359,12 @@ QString Util::formatBinaryTooltip(int id, const Data::Symbol& symbol, const Data return formatTooltipImpl(id, Util::formatString(symbol.binary), nullptr, &costs); } +QString Util::formatFileTooltip(int id, const QString& file, const Data::Costs& selfCosts, + const Data::Costs& inclusiveCosts) +{ + return formatTooltipImpl(id, Util::formatString(file), &selfCosts, &inclusiveCosts); +} + QString Util::formatTooltip(int id, const Data::Symbol& symbol, const Data::Costs& costs) { return formatTooltipImpl(id, formatForTooltip(symbol), nullptr, &costs); diff --git a/src/util.h b/src/util.h index c1ba483b..2897ab86 100644 --- a/src/util.h +++ b/src/util.h @@ -58,6 +58,7 @@ QString formatCostRelative(quint64 selfCost, quint64 totalCost, bool addPercentS QString formatTimeString(quint64 nanoseconds, bool shortForm = false); QString formatFrequency(quint64 occurrences, quint64 nanoseconds); QString formatBinaryTooltip(int id, const Data::Symbol& symbol, const Data::Costs& costs); +QString formatFileTooltip(int id, const QString& file, const Data::Costs& selfCosts, const Data::Costs& inclusiveCosts); QString formatTooltip(int id, const Data::Symbol& symbol, const Data::Costs& costs); QString formatTooltip(int id, const Data::Symbol& symbol, const Data::Costs& selfCosts, const Data::Costs& inclusiveCosts);