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"