File indexing completed on 2024-05-19 05:44:25
0001 /* 0002 SPDX-FileCopyrightText: 2015-2019 Milian Wolff <mail@milianw.de> 0003 0004 SPDX-License-Identifier: LGPL-2.1-or-later 0005 */ 0006 0007 #include "mainwindow.h" 0008 0009 #include <ui_mainwindow.h> 0010 0011 #include <cmath> 0012 0013 #include <KConfigGroup> 0014 #include <KLocalizedString> 0015 #include <KShell> 0016 #include <KStandardAction> 0017 #include <kio_version.h> 0018 0019 #include <QAction> 0020 #include <QActionGroup> 0021 #include <QClipboard> 0022 #include <QDebug> 0023 #include <QDesktopServices> 0024 #include <QFileDialog> 0025 #include <QInputDialog> 0026 #include <QMenu> 0027 #include <QProcess> 0028 #include <QShortcut> 0029 #include <QStatusBar> 0030 0031 #include "analyze/suppressions.h" 0032 0033 #include "callercalleemodel.h" 0034 #include "costdelegate.h" 0035 #include "costheaderview.h" 0036 #include "parser.h" 0037 #include "stacksmodel.h" 0038 #include "suppressionsmodel.h" 0039 #include "topproxy.h" 0040 #include "treemodel.h" 0041 #include "treeproxy.h" 0042 0043 #include "gui_config.h" 0044 0045 #if KChart_FOUND 0046 #include "chartmodel.h" 0047 #include "chartproxy.h" 0048 #include "chartwidget.h" 0049 #include "histogrammodel.h" 0050 #include "histogramwidget.h" 0051 #endif 0052 0053 using namespace std; 0054 0055 namespace { 0056 const int MAINWINDOW_VERSION = 1; 0057 0058 namespace Config { 0059 namespace Groups { 0060 const QString MainWindow() { return QStringLiteral("MainWindow"); } 0061 const QString CodeNavigation() { return QStringLiteral("CodeNavigation"); } 0062 } 0063 namespace Entries { 0064 const char State[] = "State"; 0065 const char CustomCommand[] = "CustomCommand"; 0066 const char IDE[] = "IDE"; 0067 } 0068 } 0069 0070 enum IDE 0071 { 0072 KDevelop, 0073 Kate, 0074 KWrite, 0075 GEdit, 0076 GVim, 0077 QtCreator, 0078 LAST_IDE 0079 }; 0080 struct IdeSettings 0081 { 0082 QString app; 0083 QString args; 0084 QString name; 0085 0086 bool isAppAvailable() const 0087 { 0088 return !QStandardPaths::findExecutable(app).isEmpty(); 0089 } 0090 }; 0091 0092 IdeSettings ideSettings(IDE ide) 0093 { 0094 switch (ide) { 0095 case KDevelop: 0096 return {QStringLiteral("kdevelop"), QStringLiteral("%f:%l:%c"), MainWindow::tr("KDevelop")}; 0097 case Kate: 0098 return {QStringLiteral("kate"), QStringLiteral("%f --line %l --column %c"), MainWindow::tr("Kate")}; 0099 case KWrite: 0100 return {QStringLiteral("kwrite"), QStringLiteral("%f --line %l --column %c"), MainWindow::tr("KWrite")}; 0101 case GEdit: 0102 return {QStringLiteral("gedit"), QStringLiteral("%f +%l:%c"), MainWindow::tr("gedit")}; 0103 case GVim: 0104 return {QStringLiteral("gvim"), QStringLiteral("%f +%l"), MainWindow::tr("gvim")}; 0105 case QtCreator: 0106 return {QStringLiteral("qtcreator"), QStringLiteral("-client %f:%l"), MainWindow::tr("Qt Creator")}; 0107 case LAST_IDE: 0108 break; 0109 }; 0110 Q_UNREACHABLE(); 0111 }; 0112 0113 int firstAvailableIde() 0114 { 0115 for (int i = 0; i < LAST_IDE; ++i) { 0116 if (ideSettings(static_cast<IDE>(i)).isAppAvailable()) { 0117 return i; 0118 } 0119 } 0120 return -1; 0121 } 0122 0123 template <typename T> 0124 void setupContextMenu(QTreeView* view, T callback) 0125 { 0126 view->setContextMenuPolicy(Qt::CustomContextMenu); 0127 QObject::connect(view, &QTreeView::customContextMenuRequested, view, [view, callback](const QPoint& point) { 0128 const auto index = view->indexAt(point); 0129 if (!index.isValid()) { 0130 return; 0131 } 0132 0133 callback(index); 0134 }); 0135 } 0136 0137 template <typename T> 0138 void setupTreeContextMenu(QTreeView* view, T callback) 0139 { 0140 setupContextMenu(view, [callback](const QModelIndex& index) { 0141 QMenu contextMenu; 0142 auto* viewCallerCallee = contextMenu.addAction(i18n("View Caller/Callee")); 0143 auto* action = contextMenu.exec(QCursor::pos()); 0144 if (action == viewCallerCallee) { 0145 const auto symbol = index.data(TreeModel::SymbolRole).value<Symbol>(); 0146 0147 if (symbol.isValid()) { 0148 callback(symbol); 0149 } 0150 } 0151 }); 0152 } 0153 0154 void addLocationContextMenu(QTreeView* treeView, MainWindow* window) 0155 { 0156 treeView->setContextMenuPolicy(Qt::CustomContextMenu); 0157 QObject::connect(treeView, &QTreeView::customContextMenuRequested, treeView, [treeView, window](const QPoint& pos) { 0158 auto index = treeView->indexAt(pos); 0159 if (!index.isValid()) { 0160 return; 0161 } 0162 const auto resultData = index.data(SourceMapModel::ResultDataRole).value<const ResultData*>(); 0163 Q_ASSERT(resultData); 0164 const auto location = index.data(SourceMapModel::LocationRole).value<FileLine>(); 0165 const auto file = resultData->string(location.fileId); 0166 if (!QFile::exists(file)) { 0167 return; 0168 } 0169 auto menu = new QMenu(treeView); 0170 auto openFile = 0171 new QAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open file in editor"), menu); 0172 QObject::connect(openFile, &QAction::triggered, openFile, 0173 [file, line = location.line, window] { window->navigateToCode(file, line); }); 0174 menu->addAction(openFile); 0175 menu->popup(treeView->mapToGlobal(pos)); 0176 }); 0177 QObject::connect(treeView, &QTreeView::activated, window, [window](const QModelIndex& index) { 0178 const auto resultData = index.data(SourceMapModel::ResultDataRole).value<const ResultData*>(); 0179 Q_ASSERT(resultData); 0180 const auto location = index.data(SourceMapModel::LocationRole).value<FileLine>(); 0181 const auto file = resultData->string(location.fileId); 0182 if (QFile::exists(file)) 0183 window->navigateToCode(file, location.line); 0184 }); 0185 } 0186 0187 Qt::SortOrder defaultSortOrder(QAbstractItemModel* model, int column) 0188 { 0189 auto initialSortOrder = model->headerData(column, Qt::Horizontal, Qt::InitialSortOrderRole); 0190 if (initialSortOrder.canConvert<Qt::SortOrder>()) 0191 return initialSortOrder.value<Qt::SortOrder>(); 0192 return Qt::AscendingOrder; 0193 } 0194 0195 void sortByColumn(QTreeView* view, int column) 0196 { 0197 view->sortByColumn(column, defaultSortOrder(view->model(), column)); 0198 } 0199 0200 template <typename T> 0201 void setupTopView(TreeModel* source, QTreeView* view, TopProxy::Type type, T callback) 0202 { 0203 auto proxy = new TopProxy(type, source); 0204 proxy->setSourceModel(source); 0205 proxy->setSortRole(TreeModel::SortRole); 0206 view->setModel(proxy); 0207 sortByColumn(view, 1); 0208 view->header()->setStretchLastSection(true); 0209 setupTreeContextMenu(view, callback); 0210 } 0211 0212 #if KChart_FOUND 0213 ChartWidget* addChartTab(QTabWidget* tabWidget, const QString& title, ChartModel::Type type, const Parser* parser, 0214 void (Parser::*dataReady)(const ChartData&), MainWindow* window) 0215 { 0216 auto tab = new ChartWidget(tabWidget->parentWidget()); 0217 QObject::connect(parser, &Parser::summaryAvailable, tab, &ChartWidget::setSummaryData); 0218 tabWidget->addTab(tab, title); 0219 tabWidget->setTabEnabled(tabWidget->indexOf(tab), false); 0220 auto model = new ChartModel(type, tab); 0221 tab->setModel(model); 0222 QObject::connect(parser, dataReady, tab, [=](const ChartData& data) { 0223 model->resetData(data); 0224 tabWidget->setTabEnabled(tabWidget->indexOf(tab), true); 0225 }); 0226 QObject::connect(window, &MainWindow::clearData, model, &ChartModel::clearData); 0227 QObject::connect(window, &MainWindow::clearData, tab, [tab]() { tab->setSelection({}); }); 0228 QObject::connect(tab, &ChartWidget::filterRequested, window, &MainWindow::reparse); 0229 return tab; 0230 } 0231 #endif 0232 0233 template <typename T> 0234 void setupTreeModel(TreeModel* model, QTreeView* view, CostDelegate* costDelegate, QLineEdit* filterFunction, 0235 QLineEdit* filterModule, T callback) 0236 { 0237 auto proxy = new TreeProxy(TreeModel::SymbolRole, TreeModel::ResultDataRole, model); 0238 proxy->setSourceModel(model); 0239 proxy->setSortRole(TreeModel::SortRole); 0240 0241 view->setModel(proxy); 0242 sortByColumn(view, TreeModel::PeakColumn); 0243 view->setItemDelegateForColumn(TreeModel::PeakColumn, costDelegate); 0244 view->setItemDelegateForColumn(TreeModel::LeakedColumn, costDelegate); 0245 view->setItemDelegateForColumn(TreeModel::AllocationsColumn, costDelegate); 0246 view->setItemDelegateForColumn(TreeModel::TemporaryColumn, costDelegate); 0247 view->setHeader(new CostHeaderView(view)); 0248 0249 QObject::connect(filterFunction, &QLineEdit::textChanged, proxy, &TreeProxy::setFunctionFilter); 0250 QObject::connect(filterModule, &QLineEdit::textChanged, proxy, &TreeProxy::setModuleFilter); 0251 setupTreeContextMenu(view, callback); 0252 } 0253 0254 void setupCallerCallee(CallerCalleeModel* model, QTreeView* view, QLineEdit* filterFunction, QLineEdit* filterModule) 0255 { 0256 auto costDelegate = new CostDelegate(CallerCalleeModel::SortRole, CallerCalleeModel::TotalCostRole, view); 0257 auto callerCalleeProxy = new TreeProxy(CallerCalleeModel::SymbolRole, CallerCalleeModel::ResultDataRole, model); 0258 callerCalleeProxy->setSourceModel(model); 0259 callerCalleeProxy->setSortRole(CallerCalleeModel::SortRole); 0260 view->setModel(callerCalleeProxy); 0261 sortByColumn(view, CallerCalleeModel::InclusivePeakColumn); 0262 view->setItemDelegateForColumn(CallerCalleeModel::SelfPeakColumn, costDelegate); 0263 view->setItemDelegateForColumn(CallerCalleeModel::SelfLeakedColumn, costDelegate); 0264 view->setItemDelegateForColumn(CallerCalleeModel::SelfAllocationsColumn, costDelegate); 0265 view->setItemDelegateForColumn(CallerCalleeModel::SelfTemporaryColumn, costDelegate); 0266 view->setItemDelegateForColumn(CallerCalleeModel::InclusivePeakColumn, costDelegate); 0267 view->setItemDelegateForColumn(CallerCalleeModel::InclusiveLeakedColumn, costDelegate); 0268 view->setItemDelegateForColumn(CallerCalleeModel::InclusiveAllocationsColumn, costDelegate); 0269 view->setItemDelegateForColumn(CallerCalleeModel::InclusiveTemporaryColumn, costDelegate); 0270 view->setHeader(new CostHeaderView(view)); 0271 QObject::connect(filterFunction, &QLineEdit::textChanged, callerCalleeProxy, &TreeProxy::setFunctionFilter); 0272 QObject::connect(filterModule, &QLineEdit::textChanged, callerCalleeProxy, &TreeProxy::setModuleFilter); 0273 } 0274 0275 template <typename Model> 0276 Model* setupModelAndProxyForView(QTreeView* view) 0277 { 0278 auto model = new Model(view); 0279 auto proxy = new QSortFilterProxyModel(model); 0280 proxy->setSourceModel(model); 0281 proxy->setSortRole(Model::SortRole); 0282 view->setModel(proxy); 0283 sortByColumn(view, Model::InitialSortColumn); 0284 auto costDelegate = new CostDelegate(Model::SortRole, Model::TotalCostRole, view); 0285 for (int i = 1; i < Model::NUM_COLUMNS; ++i) { 0286 view->setItemDelegateForColumn(i, costDelegate); 0287 } 0288 0289 view->setHeader(new CostHeaderView(view)); 0290 0291 return model; 0292 } 0293 0294 template <typename Model, typename Handler> 0295 void connectCallerOrCalleeModel(QTreeView* view, CallerCalleeModel* callerCalleeCostModel, Handler handler) 0296 { 0297 QObject::connect(view, &QTreeView::activated, view, [callerCalleeCostModel, handler](const QModelIndex& index) { 0298 const auto symbol = index.data(Model::SymbolRole).template value<Symbol>(); 0299 auto sourceIndex = callerCalleeCostModel->indexForKey(symbol); 0300 handler(sourceIndex); 0301 }); 0302 } 0303 0304 QString insertWordWrapMarkers(QString text) 0305 { 0306 // insert zero-width spaces after every 50 word characters to enable word wrap in the middle of words 0307 static const QRegularExpression pattern(QStringLiteral("(\\w{50})")); 0308 return text.replace(pattern, QStringLiteral("\\1\u200B")); 0309 } 0310 } 0311 0312 MainWindow::MainWindow(QWidget* parent) 0313 : QMainWindow(parent) 0314 , m_ui(new Ui::MainWindow) 0315 , m_parser(new Parser(this)) 0316 , m_config(KSharedConfig::openConfig(QStringLiteral("heaptrack_gui"))) 0317 { 0318 m_ui->setupUi(this); 0319 0320 auto group = m_config->group(Config::Groups::MainWindow()); 0321 auto state = group.readEntry(Config::Entries::State, QByteArray()); 0322 restoreState(state, MAINWINDOW_VERSION); 0323 0324 m_ui->pages->setCurrentWidget(m_ui->openPage); 0325 // TODO: proper progress report 0326 m_ui->loadingProgress->setMinimum(0); 0327 m_ui->loadingProgress->setMaximum(1000); // range is set as 0 to 1000 for fractional % bar display 0328 m_ui->loadingProgress->setValue(0); 0329 0330 auto bottomUpModel = new TreeModel(this); 0331 auto topDownModel = new TreeModel(this); 0332 auto callerCalleeModel = new CallerCalleeModel(this); 0333 connect(this, &MainWindow::clearData, bottomUpModel, &TreeModel::clearData); 0334 connect(this, &MainWindow::clearData, topDownModel, &TreeModel::clearData); 0335 connect(this, &MainWindow::clearData, callerCalleeModel, &CallerCalleeModel::clearData); 0336 connect(this, &MainWindow::clearData, m_ui->flameGraphTab, &FlameGraph::clearData); 0337 0338 m_ui->tabWidget->setTabEnabled(m_ui->tabWidget->indexOf(m_ui->callerCalleeTab), false); 0339 m_ui->tabWidget->setTabEnabled(m_ui->tabWidget->indexOf(m_ui->topDownTab), false); 0340 m_ui->tabWidget->setTabEnabled(m_ui->tabWidget->indexOf(m_ui->flameGraphTab), false); 0341 0342 auto* suppressionsModel = new SuppressionsModel(this); 0343 { 0344 auto* proxy = new QSortFilterProxyModel(this); 0345 proxy->setSourceModel(suppressionsModel); 0346 m_ui->suppressionsView->setModel(proxy); 0347 auto* delegate = new CostDelegate(SuppressionsModel::SortRole, SuppressionsModel::TotalCostRole, this); 0348 m_ui->suppressionsView->setItemDelegateForColumn(static_cast<int>(SuppressionsModel::Columns::Leaked), 0349 delegate); 0350 m_ui->suppressionsView->setItemDelegateForColumn(static_cast<int>(SuppressionsModel::Columns::Matches), 0351 delegate); 0352 0353 auto margins = m_ui->suppressionBox->contentsMargins(); 0354 margins.setLeft(0); 0355 m_ui->suppressionBox->setContentsMargins(margins); 0356 } 0357 0358 connect(m_parser, &Parser::bottomUpDataAvailable, this, [=](const TreeData& data) { 0359 bottomUpModel->resetData(data); 0360 if (!m_diffMode) { 0361 m_ui->flameGraphTab->setBottomUpData(data); 0362 } 0363 m_ui->progressLabel->setAlignment(Qt::AlignVCenter | Qt::AlignRight); 0364 statusBar()->addWidget(m_ui->progressLabel, 1); 0365 statusBar()->addWidget(m_ui->loadingProgress); 0366 m_ui->pages->setCurrentWidget(m_ui->resultsPage); 0367 m_ui->tabWidget->setTabEnabled(m_ui->tabWidget->indexOf(m_ui->bottomUpTab), true); 0368 }); 0369 connect(m_parser, &Parser::callerCalleeDataAvailable, this, [=](const CallerCalleeResults& data) { 0370 callerCalleeModel->setResults(data); 0371 m_ui->tabWidget->setTabEnabled(m_ui->tabWidget->indexOf(m_ui->callerCalleeTab), true); 0372 }); 0373 connect(m_parser, &Parser::topDownDataAvailable, this, [=](const TreeData& data) { 0374 topDownModel->resetData(data); 0375 m_ui->tabWidget->setTabEnabled(m_ui->tabWidget->indexOf(m_ui->topDownTab), true); 0376 if (!m_diffMode) { 0377 m_ui->flameGraphTab->setTopDownData(data); 0378 } 0379 m_ui->tabWidget->setTabEnabled(m_ui->tabWidget->indexOf(m_ui->flameGraphTab), !m_diffMode); 0380 }); 0381 connect(m_parser, &Parser::summaryAvailable, this, [=](const SummaryData& data) { 0382 bottomUpModel->setSummary(data); 0383 topDownModel->setSummary(data); 0384 suppressionsModel->setSuppressions(data); 0385 m_ui->suppressionBox->setVisible(suppressionsModel->rowCount() > 0); 0386 const auto isFiltered = data.filterParameters.isFilteredByTime(data.totalTime); 0387 QString textLeft; 0388 QString textCenter; 0389 QString textRight; 0390 { 0391 QTextStream stream(&textLeft); 0392 const auto debuggee = insertWordWrapMarkers(data.debuggee); 0393 stream << "<qt><dl>" 0394 << (data.fromAttached ? i18n("<dt><b>debuggee</b>:</dt><dd " 0395 "style='font-family:monospace;'>%1 <i>(attached)</i></dd>", 0396 debuggee) 0397 : i18n("<dt><b>debuggee</b>:</dt><dd " 0398 "style='font-family:monospace;'>%1</dd>", 0399 debuggee)); 0400 if (isFiltered) { 0401 stream << i18n("<dt><b>total runtime</b>:</dt><dd>%1, filtered from %2 to %3 (%4)</dd>", 0402 Util::formatTime(data.totalTime), Util::formatTime(data.filterParameters.minTime), 0403 Util::formatTime(data.filterParameters.maxTime), 0404 Util::formatTime(data.filterParameters.maxTime - data.filterParameters.minTime)); 0405 } else { 0406 stream << i18n("<dt><b>total runtime</b>:</dt><dd>%1</dd>", Util::formatTime(data.totalTime)); 0407 } 0408 stream << i18n("<dt><b>total system memory</b>:</dt><dd>%1</dd>", Util::formatBytes(data.totalSystemMemory)) 0409 << "</dl></qt>"; 0410 } 0411 { 0412 QTextStream stream(&textCenter); 0413 const double totalTimeS = 0.001 * (data.filterParameters.maxTime - data.filterParameters.minTime); 0414 stream << "<qt><dl>" 0415 << i18n("<dt><b>calls to allocation functions</b>:</dt><dd>%1 " 0416 "(%2/s)</dd>", 0417 data.cost.allocations, qint64(data.cost.allocations / totalTimeS)) 0418 << i18n("<dt><b>temporary allocations</b>:</dt><dd>%1 (%2%, " 0419 "%3/s)</dd>", 0420 data.cost.temporary, 0421 std::round(float(data.cost.temporary) * 100.f * 100.f / data.cost.allocations) / 100.f, 0422 qint64(data.cost.temporary / totalTimeS)) 0423 << "</dl></qt>"; 0424 } 0425 { 0426 QTextStream stream(&textRight); 0427 stream << "<qt><dl>" 0428 << i18n("<dt><b>peak heap memory consumption</b>:</dt><dd>%1 " 0429 "after %2</dd>", 0430 Util::formatBytes(data.cost.peak), Util::formatTime(data.peakTime)) 0431 << i18n("<dt><b>peak RSS</b> (including heaptrack " 0432 "overhead):</dt><dd>%1</dd>", 0433 Util::formatBytes(data.peakRSS)); 0434 if (isFiltered) { 0435 stream << i18n("<dt><b>memory consumption delta</b>:</dt><dd>%1</dd>", 0436 Util::formatBytes(data.cost.leaked)); 0437 } else { 0438 if (data.totalLeakedSuppressed) { 0439 stream << i18n("<dt><b>total memory leaked</b>:</dt><dd>%1 (%2 suppressed)</dd>", 0440 Util::formatBytes(data.cost.leaked), Util::formatBytes(data.totalLeakedSuppressed)); 0441 } else { 0442 stream << i18n("<dt><b>total memory leaked</b>:</dt><dd>%1</dd>", 0443 Util::formatBytes(data.cost.leaked)); 0444 } 0445 } 0446 stream << "</dl></qt>"; 0447 } 0448 0449 m_ui->summaryLeft->setText(textLeft); 0450 m_ui->summaryCenter->setText(textCenter); 0451 m_ui->summaryRight->setText(textRight); 0452 m_ui->tabWidget->setTabEnabled(m_ui->tabWidget->indexOf(m_ui->summaryTab), true); 0453 }); 0454 connect(m_parser, &Parser::progressMessageAvailable, m_ui->progressLabel, &QLabel::setText); 0455 connect(m_parser, &Parser::progress, m_ui->loadingProgress, &QProgressBar::setValue); 0456 auto removeProgress = [this] { 0457 auto layout = qobject_cast<QVBoxLayout*>(m_ui->loadingPage->layout()); 0458 Q_ASSERT(layout); 0459 const auto idx = layout->indexOf(m_ui->loadingLabel) + 1; 0460 layout->insertWidget(idx, m_ui->loadingProgress); 0461 layout->insertWidget(idx + 1, m_ui->progressLabel); 0462 m_ui->progressLabel->setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); 0463 m_closeAction->setEnabled(true); 0464 m_openAction->setEnabled(true); 0465 }; 0466 connect(m_parser, &Parser::finished, this, removeProgress); 0467 connect(m_parser, &Parser::failedToOpen, this, [this, removeProgress](const QString& failedFile) { 0468 removeProgress(); 0469 m_ui->pages->setCurrentWidget(m_ui->openPage); 0470 showError(i18n("Failed to parse file %1.", failedFile)); 0471 }); 0472 m_ui->messages->hide(); 0473 0474 #if KChart_FOUND 0475 auto consumedTab = addChartTab(m_ui->tabWidget, i18n("Consumed"), ChartModel::Consumed, m_parser, 0476 &Parser::consumedChartDataAvailable, this); 0477 auto allocationsTab = addChartTab(m_ui->tabWidget, i18n("Allocations"), ChartModel::Allocations, m_parser, 0478 &Parser::allocationsChartDataAvailable, this); 0479 auto temporaryAllocationsTab = addChartTab(m_ui->tabWidget, i18n("Temporary Allocations"), ChartModel::Temporary, 0480 m_parser, &Parser::temporaryChartDataAvailable, this); 0481 auto syncSelection = [=](const ChartWidget::Range& selection) { 0482 consumedTab->setSelection(selection); 0483 allocationsTab->setSelection(selection); 0484 temporaryAllocationsTab->setSelection(selection); 0485 }; 0486 connect(consumedTab, &ChartWidget::selectionChanged, syncSelection); 0487 connect(allocationsTab, &ChartWidget::selectionChanged, syncSelection); 0488 connect(temporaryAllocationsTab, &ChartWidget::selectionChanged, syncSelection); 0489 0490 auto sizesTab = new HistogramWidget(this); 0491 m_ui->tabWidget->addTab(sizesTab, i18n("Sizes")); 0492 m_ui->tabWidget->setTabEnabled(m_ui->tabWidget->indexOf(sizesTab), false); 0493 auto sizeHistogramModel = new HistogramModel(this); 0494 sizesTab->setModel(sizeHistogramModel); 0495 connect(this, &MainWindow::clearData, sizeHistogramModel, &HistogramModel::clearData); 0496 0497 connect(m_parser, &Parser::sizeHistogramDataAvailable, this, [=](const HistogramData& data) { 0498 sizeHistogramModel->resetData(data); 0499 m_ui->tabWidget->setTabEnabled(m_ui->tabWidget->indexOf(sizesTab), true); 0500 }); 0501 #endif 0502 0503 auto calleesModel = setupModelAndProxyForView<CalleeModel>(m_ui->calleeView); 0504 auto callersModel = setupModelAndProxyForView<CallerModel>(m_ui->callerView); 0505 auto sourceMapModel = setupModelAndProxyForView<SourceMapModel>(m_ui->locationView); 0506 0507 auto selectCallerCaleeeIndex = [callerCalleeModel, calleesModel, callersModel, sourceMapModel, 0508 this](const QModelIndex& index) { 0509 const auto resultData = callerCalleeModel->results().resultData; 0510 const auto callees = index.data(CallerCalleeModel::CalleesRole).value<CalleeMap>(); 0511 calleesModel->setResults(callees, resultData); 0512 const auto callers = index.data(CallerCalleeModel::CallersRole).value<CallerMap>(); 0513 callersModel->setResults(callers, resultData); 0514 const auto sourceMap = index.data(CallerCalleeModel::SourceMapRole).value<LocationCostMap>(); 0515 sourceMapModel->setResults(sourceMap, resultData); 0516 if (index.model() != m_ui->callerCalleeResults->model()) { 0517 m_ui->callerCalleeResults->setCurrentIndex( 0518 qobject_cast<QSortFilterProxyModel*>(m_ui->callerCalleeResults->model())->mapFromSource(index)); 0519 } 0520 }; 0521 auto showSymbolInCallerCallee = [this, callerCalleeModel, selectCallerCaleeeIndex](const Symbol& symbol) { 0522 m_ui->tabWidget->setCurrentWidget(m_ui->callerCalleeTab); 0523 selectCallerCaleeeIndex(callerCalleeModel->indexForSymbol(symbol)); 0524 }; 0525 connect(m_ui->flameGraphTab, &FlameGraph::callerCalleeViewRequested, this, showSymbolInCallerCallee); 0526 0527 auto costDelegate = new CostDelegate(TreeModel::SortRole, TreeModel::MaxCostRole, this); 0528 setupTreeModel(bottomUpModel, m_ui->bottomUpResults, costDelegate, m_ui->bottomUpFilterFunction, 0529 m_ui->bottomUpFilterModule, showSymbolInCallerCallee); 0530 0531 setupTreeModel(topDownModel, m_ui->topDownResults, costDelegate, m_ui->topDownFilterFunction, 0532 m_ui->topDownFilterModule, showSymbolInCallerCallee); 0533 0534 setupCallerCallee(callerCalleeModel, m_ui->callerCalleeResults, m_ui->callerCalleeFilterFunction, 0535 m_ui->callerCalleeFilterModule); 0536 0537 connectCallerOrCalleeModel<CalleeModel>(m_ui->calleeView, callerCalleeModel, selectCallerCaleeeIndex); 0538 connectCallerOrCalleeModel<CallerModel>(m_ui->callerView, callerCalleeModel, selectCallerCaleeeIndex); 0539 addLocationContextMenu(m_ui->locationView, this); 0540 0541 connect(m_ui->callerCalleeResults->selectionModel(), &QItemSelectionModel::currentRowChanged, this, 0542 [selectCallerCaleeeIndex](const QModelIndex& current, const QModelIndex&) { 0543 if (current.isValid()) { 0544 selectCallerCaleeeIndex(current); 0545 } 0546 }); 0547 0548 auto validateInputFile = [this](const QString& path, bool allowEmpty) -> bool { 0549 if (path.isEmpty()) { 0550 return allowEmpty; 0551 } 0552 0553 const auto file = QFileInfo(path); 0554 if (!file.exists()) { 0555 showError(i18n("Input data %1 does not exist.", path)); 0556 } else if (!file.isFile()) { 0557 showError(i18n("Input data %1 is not a file.", path)); 0558 } else if (!file.isReadable()) { 0559 showError(i18n("Input data %1 is not readable.", path)); 0560 } else { 0561 return true; 0562 } 0563 return false; 0564 }; 0565 0566 const QString heaptrackFileFilter = QStringLiteral("heaptrack.*.*.gz heaptrack.*.*.zst"); 0567 #if KIO_VERSION >= QT_VERSION_CHECK(5, 108, 0) 0568 const QStringList heaptrackFileFilters = {heaptrackFileFilter}; 0569 m_ui->openFile->setNameFilters(heaptrackFileFilters); 0570 m_ui->compareTo->setNameFilters(heaptrackFileFilters); 0571 #else 0572 m_ui->openFile->setFilter(heaptrackFileFilter); 0573 m_ui->compareTo->setFilter(heaptrackFileFilter); 0574 #endif 0575 0576 auto validateInput = [this, validateInputFile]() { 0577 m_ui->messages->hide(); 0578 m_ui->buttonBox->setEnabled(validateInputFile(m_ui->openFile->url().toLocalFile(), false) 0579 && validateInputFile(m_ui->compareTo->url().toLocalFile(), true) 0580 && validateInputFile(m_ui->suppressions->url().toLocalFile(), true)); 0581 }; 0582 0583 connect(m_ui->openFile, &KUrlRequester::textChanged, this, validateInput); 0584 connect(m_ui->compareTo, &KUrlRequester::textChanged, this, validateInput); 0585 connect(m_ui->suppressions, &KUrlRequester::textChanged, this, validateInput); 0586 connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, [this]() { 0587 const auto path = m_ui->openFile->url().toLocalFile(); 0588 Q_ASSERT(!path.isEmpty()); 0589 const auto base = m_ui->compareTo->url().toLocalFile(); 0590 0591 bool parsedOk = false; 0592 m_lastFilterParameters.suppressions = 0593 parseSuppressions(m_ui->suppressions->url().toLocalFile().toStdString(), &parsedOk); 0594 if (parsedOk) { 0595 loadFile(path, base); 0596 } else { 0597 showError(i18n("Failed to parse suppression file.")); 0598 } 0599 }); 0600 0601 setupStacks(); 0602 0603 setupTopView(bottomUpModel, m_ui->topPeak, TopProxy::Peak, showSymbolInCallerCallee); 0604 m_ui->topPeak->setItemDelegate(costDelegate); 0605 setupTopView(bottomUpModel, m_ui->topLeaked, TopProxy::Leaked, showSymbolInCallerCallee); 0606 m_ui->topLeaked->setItemDelegate(costDelegate); 0607 setupTopView(bottomUpModel, m_ui->topAllocations, TopProxy::Allocations, showSymbolInCallerCallee); 0608 m_ui->topAllocations->setItemDelegate(costDelegate); 0609 setupTopView(bottomUpModel, m_ui->topTemporary, TopProxy::Temporary, showSymbolInCallerCallee); 0610 m_ui->topTemporary->setItemDelegate(costDelegate); 0611 0612 setWindowTitle(i18n("Heaptrack")); 0613 // closing the current file shows the stack page to open a new one 0614 m_openAction = KStandardAction::open(this, SLOT(closeFile()), this); 0615 m_openAction->setEnabled(false); 0616 m_ui->menu_File->addAction(m_openAction); 0617 m_openNewAction = KStandardAction::openNew(this, SLOT(openNewFile()), this); 0618 m_ui->menu_File->addAction(m_openNewAction); 0619 m_closeAction = KStandardAction::close(this, SLOT(close()), this); 0620 m_ui->menu_File->addAction(m_closeAction); 0621 m_quitAction = KStandardAction::quit(qApp, SLOT(quit()), this); 0622 m_ui->menu_File->addAction(m_quitAction); 0623 QShortcut* shortcut = new QShortcut(QKeySequence(QKeySequence::Copy), m_ui->stacksTree); 0624 connect(shortcut, &QShortcut::activated, this, [this]() { 0625 QTreeView* view = m_ui->stacksTree; 0626 if (view->selectionModel()->hasSelection()) { 0627 QString text; 0628 const auto range = view->selectionModel()->selection().first(); 0629 for (auto i = range.top(); i <= range.bottom(); ++i) { 0630 QStringList rowContents; 0631 for (auto j = range.left(); j <= range.right(); ++j) 0632 rowContents << view->model()->index(i, j).data().toString(); 0633 text += rowContents.join(QLatin1Char('\t')); 0634 text += QLatin1Char('\n'); 0635 } 0636 QApplication::clipboard()->setText(text); 0637 } 0638 }); 0639 0640 m_disableEmbeddedSuppressions = m_ui->menu_Settings->addAction(i18n("Disable Embedded Suppressions")); 0641 m_disableEmbeddedSuppressions->setToolTip( 0642 i18n("Ignore suppression definitions that are embedded into the heaptrack data file. By default, heaptrack " 0643 "will copy the suppressions optionally defined via a `const char *__lsan_default_suppressions()` symbol " 0644 "in the debuggee application. These are then always applied when analyzing the data, unless this feature " 0645 "is explicitly disabled using this command line option.")); 0646 m_disableEmbeddedSuppressions->setCheckable(true); 0647 connect(m_disableEmbeddedSuppressions, &QAction::toggled, this, [this]() { 0648 m_lastFilterParameters.disableEmbeddedSuppressions = m_disableEmbeddedSuppressions->isChecked(); 0649 reparse(m_lastFilterParameters.minTime, m_lastFilterParameters.maxTime); 0650 }); 0651 0652 m_disableBuiltinSuppressions = m_ui->menu_Settings->addAction(i18n("Disable Builtin Suppressions")); 0653 m_disableBuiltinSuppressions->setToolTip(i18n( 0654 "Ignore suppression definitions that are built into heaptrack. By default, heaptrack will suppress certain " 0655 "known leaks from common system libraries.")); 0656 m_disableBuiltinSuppressions->setCheckable(true); 0657 connect(m_disableBuiltinSuppressions, &QAction::toggled, this, [this]() { 0658 m_lastFilterParameters.disableBuiltinSuppressions = m_disableBuiltinSuppressions->isChecked(); 0659 reparse(m_lastFilterParameters.minTime, m_lastFilterParameters.maxTime); 0660 }); 0661 0662 setupCodeNavigationMenu(); 0663 0664 m_ui->actionResetFilter->setEnabled(false); 0665 connect(m_ui->actionResetFilter, &QAction::triggered, this, 0666 [this]() { reparse(0, std::numeric_limits<int64_t>::max()); }); 0667 QObject::connect(m_parser, &Parser::finished, this, 0668 [this]() { m_ui->actionResetFilter->setEnabled(m_parser->isFiltered()); }); 0669 } 0670 0671 MainWindow::~MainWindow() 0672 { 0673 auto state = saveState(MAINWINDOW_VERSION); 0674 auto group = m_config->group(Config::Groups::MainWindow()); 0675 group.writeEntry(Config::Entries::State, state); 0676 } 0677 0678 void MainWindow::loadFile(const QString& file, const QString& diffBase) 0679 { 0680 // TODO: support canceling of ongoing parse jobs 0681 m_closeAction->setEnabled(false); 0682 m_ui->loadingLabel->setText(i18n("Loading file %1, please wait...", file)); 0683 if (diffBase.isEmpty()) { 0684 setWindowTitle(i18nc("%1: file name that is open", "Heaptrack - %1", QFileInfo(file).fileName())); 0685 m_diffMode = false; 0686 } else { 0687 setWindowTitle(i18nc("%1, %2: file names that are open", "Heaptrack - %1 compared to %2", 0688 QFileInfo(file).fileName(), QFileInfo(diffBase).fileName())); 0689 m_diffMode = true; 0690 } 0691 m_ui->pages->setCurrentWidget(m_ui->loadingPage); 0692 m_parser->parse(file, diffBase, m_lastFilterParameters); 0693 } 0694 0695 void MainWindow::reparse(int64_t minTime, int64_t maxTime) 0696 { 0697 if (m_ui->pages->currentWidget() != m_ui->resultsPage) { 0698 return; 0699 } 0700 0701 m_closeAction->setEnabled(false); 0702 m_ui->flameGraphTab->clearData(); 0703 m_ui->loadingLabel->setText(i18n("Reparsing file, please wait...")); 0704 m_ui->pages->setCurrentWidget(m_ui->loadingPage); 0705 m_lastFilterParameters.minTime = minTime; 0706 m_lastFilterParameters.maxTime = maxTime; 0707 m_parser->reparse(m_lastFilterParameters); 0708 } 0709 0710 void MainWindow::openNewFile() 0711 { 0712 auto window = new MainWindow; 0713 window->setAttribute(Qt::WA_DeleteOnClose, true); 0714 window->show(); 0715 window->setDisableEmbeddedSuppressions(m_lastFilterParameters.disableEmbeddedSuppressions); 0716 window->setSuppressions(m_lastFilterParameters.suppressions); 0717 } 0718 0719 void MainWindow::closeFile() 0720 { 0721 m_ui->pages->setCurrentWidget(m_ui->openPage); 0722 0723 m_ui->tabWidget->setCurrentIndex(m_ui->tabWidget->indexOf(m_ui->summaryTab)); 0724 for (int i = 0, c = m_ui->tabWidget->count(); i < c; ++i) { 0725 m_ui->tabWidget->setTabEnabled(i, false); 0726 } 0727 0728 m_openAction->setEnabled(false); 0729 emit clearData(); 0730 } 0731 0732 void MainWindow::showError(const QString& message) 0733 { 0734 m_ui->messages->setText(message); 0735 m_ui->messages->show(); 0736 } 0737 0738 void MainWindow::setupStacks() 0739 { 0740 auto stacksModel = new StacksModel(this); 0741 m_ui->stacksTree->setModel(stacksModel); 0742 m_ui->stacksTree->setRootIsDecorated(false); 0743 0744 auto updateStackSpinner = [this](int stacks) { 0745 m_ui->stackSpinner->setMinimum(min(stacks, 1)); 0746 m_ui->stackSpinner->setSuffix(i18n(" / %1", stacks)); 0747 m_ui->stackSpinner->setMaximum(stacks); 0748 }; 0749 updateStackSpinner(0); 0750 connect(stacksModel, &StacksModel::stacksFound, this, updateStackSpinner); 0751 connect(m_ui->stackSpinner, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), stacksModel, 0752 &StacksModel::setStackIndex); 0753 0754 auto fillFromIndex = [stacksModel](const QModelIndex& current) { 0755 if (!current.isValid()) { 0756 stacksModel->clear(); 0757 } else { 0758 auto proxy = qobject_cast<const TreeProxy*>(current.model()); 0759 Q_ASSERT(proxy); 0760 auto leaf = proxy->mapToSource(current); 0761 stacksModel->fillFromIndex(leaf); 0762 } 0763 }; 0764 connect(m_ui->bottomUpResults->selectionModel(), &QItemSelectionModel::currentChanged, this, fillFromIndex); 0765 connect(m_ui->topDownResults->selectionModel(), &QItemSelectionModel::currentChanged, this, fillFromIndex); 0766 0767 auto tabChanged = [this, fillFromIndex](int tabIndex) { 0768 const auto widget = m_ui->tabWidget->widget(tabIndex); 0769 const bool showDocks = (widget == m_ui->topDownTab || widget == m_ui->bottomUpTab); 0770 m_ui->stacksDock->setVisible(showDocks); 0771 if (showDocks) { 0772 auto tree = (widget == m_ui->topDownTab) ? m_ui->topDownResults : m_ui->bottomUpResults; 0773 fillFromIndex(tree->selectionModel()->currentIndex()); 0774 } 0775 }; 0776 connect(m_ui->tabWidget, &QTabWidget::currentChanged, this, tabChanged); 0777 connect(m_parser, &Parser::bottomUpDataAvailable, this, [tabChanged]() { tabChanged(0); }); 0778 0779 m_ui->stacksDock->setVisible(false); 0780 } 0781 0782 void MainWindow::setupCodeNavigationMenu() 0783 { 0784 // Code Navigation 0785 QAction* configAction = 0786 new QAction(QIcon::fromTheme(QStringLiteral("applications-development")), i18n("Code Navigation"), this); 0787 auto menu = new QMenu(this); 0788 auto group = new QActionGroup(this); 0789 group->setExclusive(true); 0790 0791 const auto settings = m_config->group(Config::Groups::CodeNavigation()); 0792 const auto currentIdx = settings.readEntry(Config::Entries::IDE, firstAvailableIde()); 0793 0794 for (int i = 0; i < LAST_IDE; ++i) { 0795 auto action = new QAction(menu); 0796 auto ide = ideSettings(static_cast<IDE>(i)); 0797 action->setText(ide.name); 0798 auto icon = QIcon::fromTheme(ide.app); 0799 if (icon.isNull()) { 0800 icon = QIcon::fromTheme(QStringLiteral("application-x-executable")); 0801 } 0802 action->setIcon(icon); 0803 action->setCheckable(true); 0804 action->setChecked(currentIdx == i); 0805 action->setData(i); 0806 action->setEnabled(ide.isAppAvailable()); 0807 group->addAction(action); 0808 menu->addAction(action); 0809 } 0810 menu->addSeparator(); 0811 0812 QAction* action = new QAction(menu); 0813 action->setText(i18n("Custom...")); 0814 action->setCheckable(true); 0815 action->setChecked(currentIdx == -1); 0816 action->setData(-1); 0817 action->setIcon(QIcon::fromTheme(QStringLiteral("application-x-executable-script"))); 0818 group->addAction(action); 0819 menu->addAction(action); 0820 0821 #if defined(Q_OS_WIN) || defined(Q_OS_OSX) 0822 // This is a workaround for the cases, where we can't safely do assumptions 0823 // about the install location of the IDE 0824 action = new QAction(menu); 0825 action->setText(i18n("Automatic (No Line numbers)")); 0826 action->setCheckable(true); 0827 action->setChecked(currentIdx == -2); 0828 action->setData(-2); 0829 group->addAction(action); 0830 menu->addAction(action); 0831 #endif 0832 0833 QObject::connect(group, &QActionGroup::triggered, this, &MainWindow::setCodeNavigationIDE); 0834 0835 configAction->setMenu(menu); 0836 m_ui->menu_Settings->addMenu(menu); 0837 } 0838 0839 void MainWindow::setCodeNavigationIDE(QAction* action) 0840 { 0841 auto settings = m_config->group(Config::Groups::CodeNavigation()); 0842 0843 if (action->data() == -1) { 0844 const auto customCmd = 0845 QInputDialog::getText(this, i18n("Custom Code Navigation"), 0846 i18n("Specify command to use for code navigation, '%f' will be replaced by the file " 0847 "name, '%l' by the line number and '%c' by the column number."), 0848 QLineEdit::Normal, settings.readEntry(Config::Entries::CustomCommand)); 0849 if (!customCmd.isEmpty()) { 0850 settings.writeEntry(Config::Entries::CustomCommand, customCmd); 0851 settings.writeEntry(Config::Entries::IDE, -1); 0852 } 0853 return; 0854 } 0855 0856 const auto defaultIde = action->data().toInt(); 0857 settings.writeEntry(Config::Entries::IDE, defaultIde); 0858 } 0859 0860 void MainWindow::navigateToCode(const QString& filePath, int lineNumber, int columnNumber) 0861 { 0862 const auto settings = m_config->group(Config::Groups::CodeNavigation()); 0863 const auto ideIdx = settings.readEntry(Config::Entries::IDE, firstAvailableIde()); 0864 0865 QString command; 0866 if (ideIdx >= 0 && ideIdx < LAST_IDE) { 0867 auto ide = ideSettings(static_cast<IDE>(ideIdx)); 0868 command = ide.app + QLatin1Char(' ') + ide.args; 0869 } else if (ideIdx == -1) { 0870 command = settings.readEntry(Config::Entries::CustomCommand); 0871 } 0872 0873 if (!command.isEmpty()) { 0874 command.replace(QStringLiteral("%f"), filePath); 0875 command.replace(QStringLiteral("%l"), QString::number(std::max(1, lineNumber))); 0876 command.replace(QStringLiteral("%c"), QString::number(std::max(1, columnNumber))); 0877 0878 auto splitted = KShell::splitArgs(command); 0879 QProcess::startDetached(splitted.takeFirst(), splitted); 0880 } else { 0881 QDesktopServices::openUrl(QUrl::fromLocalFile(filePath)); 0882 } 0883 } 0884 0885 void MainWindow::setDisableEmbeddedSuppressions(bool disable) 0886 { 0887 m_disableEmbeddedSuppressions->setChecked(disable); 0888 } 0889 0890 void MainWindow::setDisableBuiltinSuppressions(bool disable) 0891 { 0892 m_disableBuiltinSuppressions->setChecked(disable); 0893 } 0894 0895 void MainWindow::setSuppressions(std::vector<std::string> suppressions) 0896 { 0897 m_lastFilterParameters.suppressions = std::move(suppressions); 0898 } 0899 0900 #include "moc_mainwindow.cpp"