File indexing completed on 2024-05-19 05:44:23

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 "flamegraph.h"
0008 
0009 #include <cmath>
0010 
0011 #include <QAction>
0012 #include <QApplication>
0013 #include <QCheckBox>
0014 #include <QClipboard>
0015 #include <QComboBox>
0016 #include <QCursor>
0017 #include <QDebug>
0018 #include <QDoubleSpinBox>
0019 #include <QEvent>
0020 #include <QGraphicsRectItem>
0021 #include <QGraphicsScene>
0022 #include <QGraphicsView>
0023 #include <QLabel>
0024 #include <QLineEdit>
0025 #include <QMenu>
0026 #include <QPushButton>
0027 #include <QStyleOption>
0028 #include <QToolTip>
0029 #include <QVBoxLayout>
0030 #include <QWheelEvent>
0031 
0032 #include <KColorScheme>
0033 #include <KLocalizedString>
0034 #include <KStandardAction>
0035 #include <ThreadWeaver/ThreadWeaver>
0036 
0037 #include "resultdata.h"
0038 #include "util.h"
0039 
0040 enum CostType
0041 {
0042     Allocations,
0043     Temporary,
0044     Peak,
0045     Leaked,
0046 };
0047 Q_DECLARE_METATYPE(CostType)
0048 
0049 namespace {
0050 enum SearchMatchType
0051 {
0052     NoSearch,
0053     NoMatch,
0054     DirectMatch,
0055     ChildMatch
0056 };
0057 }
0058 
0059 class FrameGraphicsItem : public QGraphicsRectItem
0060 {
0061 public:
0062     FrameGraphicsItem(const qint64 cost, CostType costType, const Symbol& symbol,
0063                       std::shared_ptr<const ResultData> resultData, FrameGraphicsItem* parent = nullptr);
0064     FrameGraphicsItem(const qint64 cost, const Symbol& symbol, std::shared_ptr<const ResultData> resultData,
0065                       FrameGraphicsItem* parent);
0066 
0067     qint64 cost() const;
0068     void setCost(qint64 cost);
0069     Symbol symbol() const;
0070 
0071     void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget = nullptr) override;
0072 
0073     QString description() const;
0074 
0075     bool match(const QString& searchValue) const;
0076     void setSearchMatchType(SearchMatchType matchType);
0077 
0078 protected:
0079     void hoverEnterEvent(QGraphicsSceneHoverEvent* event) override;
0080     void hoverLeaveEvent(QGraphicsSceneHoverEvent* event) override;
0081 
0082 private:
0083     std::shared_ptr<const ResultData> m_resultData;
0084     qint64 m_cost;
0085     Symbol m_symbol;
0086     CostType m_costType;
0087     bool m_isHovered;
0088     SearchMatchType m_searchMatch = NoSearch;
0089 };
0090 
0091 Q_DECLARE_METATYPE(FrameGraphicsItem*)
0092 
0093 FrameGraphicsItem::FrameGraphicsItem(const qint64 cost, CostType costType, const Symbol& symbol,
0094                                      std::shared_ptr<const ResultData> resultData, FrameGraphicsItem* parent)
0095     : QGraphicsRectItem(parent)
0096     , m_resultData(std::move(resultData))
0097     , m_cost(cost)
0098     , m_symbol(symbol)
0099     , m_costType(costType)
0100     , m_isHovered(false)
0101 {
0102     setFlag(QGraphicsItem::ItemIsSelectable);
0103     setAcceptHoverEvents(true);
0104 }
0105 
0106 FrameGraphicsItem::FrameGraphicsItem(const qint64 cost, const Symbol& symbol,
0107                                      std::shared_ptr<const ResultData> resultData, FrameGraphicsItem* parent)
0108     : FrameGraphicsItem(cost, parent->m_costType, symbol, std::move(resultData), parent)
0109 {
0110 }
0111 
0112 qint64 FrameGraphicsItem::cost() const
0113 {
0114     return m_cost;
0115 }
0116 
0117 void FrameGraphicsItem::setCost(qint64 cost)
0118 {
0119     m_cost = cost;
0120 }
0121 
0122 Symbol FrameGraphicsItem::symbol() const
0123 {
0124     return m_symbol;
0125 }
0126 
0127 void FrameGraphicsItem::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* /*widget*/)
0128 {
0129     if (isSelected() || m_isHovered || m_searchMatch == DirectMatch) {
0130         auto selectedColor = brush().color();
0131         selectedColor.setAlpha(255);
0132         painter->fillRect(rect(), selectedColor);
0133     } else if (m_searchMatch == NoMatch) {
0134         auto noMatchColor = brush().color();
0135         noMatchColor.setAlpha(50);
0136         painter->fillRect(rect(), noMatchColor);
0137     } else { // default, when no search is running, or a sub-item is matched
0138         painter->fillRect(rect(), brush());
0139     }
0140 
0141     const QPen oldPen = painter->pen();
0142     auto pen = oldPen;
0143     if (m_searchMatch != NoMatch) {
0144         pen.setColor(brush().color());
0145         if (isSelected()) {
0146             pen.setWidth(2);
0147         }
0148         painter->setPen(pen);
0149         painter->drawRect(rect());
0150         painter->setPen(oldPen);
0151     }
0152 
0153     const int margin = 4;
0154     const int width = rect().width() - 2 * margin;
0155     if (width < option->fontMetrics.averageCharWidth() * 6) {
0156         // text is too wide for the current LOD, don't paint it
0157         return;
0158     }
0159 
0160     if (m_searchMatch == NoMatch) {
0161         auto color = oldPen.color();
0162         color.setAlpha(125);
0163         pen.setColor(color);
0164         painter->setPen(pen);
0165     }
0166 
0167     auto label = [this]() {
0168         if (m_symbol.isValid()) {
0169             return m_resultData->string(m_symbol.functionId);
0170         }
0171 
0172         // root
0173         switch (m_costType) {
0174         case Allocations:
0175             return i18n("%1 allocations in total", m_cost);
0176         case Temporary:
0177             return i18n("%1 temporary allocations in total", m_cost);
0178         case Peak:
0179             return i18n("%1 peak memory consumption", Util::formatBytes(m_cost));
0180         case Leaked:
0181             return i18n("%1 leaked in total", Util::formatBytes(m_cost));
0182         }
0183         Q_UNREACHABLE();
0184     }();
0185 
0186     const int height = rect().height();
0187     painter->drawText(margin + rect().x(), rect().y(), width, height,
0188                       Qt::AlignVCenter | Qt::AlignLeft | Qt::TextSingleLine,
0189                       option->fontMetrics.elidedText(label, Qt::ElideRight, width));
0190 
0191     if (m_searchMatch == NoMatch) {
0192         painter->setPen(oldPen);
0193     }
0194 }
0195 
0196 void FrameGraphicsItem::hoverEnterEvent(QGraphicsSceneHoverEvent* event)
0197 {
0198     QGraphicsRectItem::hoverEnterEvent(event);
0199     m_isHovered = true;
0200     update();
0201 }
0202 
0203 void FrameGraphicsItem::hoverLeaveEvent(QGraphicsSceneHoverEvent* event)
0204 {
0205     QGraphicsRectItem::hoverLeaveEvent(event);
0206     m_isHovered = false;
0207     update();
0208 }
0209 
0210 QString FrameGraphicsItem::description() const
0211 {
0212     const auto symbol = Util::toString(m_symbol, *m_resultData, Util::Short);
0213 
0214     // we build the tooltip text on demand, which is much faster than doing that
0215     // for potentially thousands of items when we load the data
0216     if (!parentItem()) {
0217         return symbol;
0218     }
0219 
0220     qint64 totalCost = 0;
0221     {
0222         auto item = this;
0223         while (item->parentItem()) {
0224             item = static_cast<const FrameGraphicsItem*>(item->parentItem());
0225         }
0226         totalCost = item->cost();
0227     }
0228     const auto fraction = Util::formatCostRelative(m_cost, totalCost);
0229 
0230     QString tooltip;
0231     switch (m_costType) {
0232     case Allocations:
0233         tooltip = i18nc("%1: number of allocations, %2: relative number, %3: function label",
0234                         "%1 (%2%) allocations in %3 and below.", m_cost, fraction, symbol);
0235         break;
0236     case Temporary:
0237         tooltip = i18nc("%1: number of temporary allocations, %2: relative number, "
0238                         "%3 function label",
0239                         "%1 (%2%) temporary allocations in %3 and below.", m_cost, fraction, symbol);
0240         break;
0241     case Peak:
0242         tooltip = i18nc("%1: peak consumption in bytes, %2: relative number, %3: "
0243                         "function label",
0244                         "%1 (%2%) contribution to peak consumption in %3 and below.", Util::formatBytes(m_cost),
0245                         fraction, symbol);
0246         break;
0247     case Leaked:
0248         tooltip = i18nc("%1: leaked bytes, %2: relative number, %3: function label", "%1 (%2%) leaked in %3 and below.",
0249                         Util::formatBytes(m_cost), fraction, symbol);
0250         break;
0251     }
0252 
0253     return tooltip;
0254 }
0255 
0256 bool FrameGraphicsItem::match(const QString& searchValue) const
0257 {
0258     auto match = [&](StringIndex index) {
0259         return m_resultData->string(index).contains(searchValue, Qt::CaseInsensitive);
0260     };
0261     return match(m_symbol.functionId) || match(m_symbol.moduleId);
0262 }
0263 
0264 void FrameGraphicsItem::setSearchMatchType(SearchMatchType matchType)
0265 {
0266     if (m_searchMatch != matchType) {
0267         m_searchMatch = matchType;
0268         update();
0269     }
0270 }
0271 
0272 namespace {
0273 
0274 /**
0275  * Generate a brush from the "mem" color space used in upstream FlameGraph.pl
0276  */
0277 QBrush brush()
0278 {
0279     // intern the brushes, to reuse them across items which can be thousands
0280     // otherwise we'd end up with dozens of allocations and higher memory
0281     // consumption
0282     static const QVector<QBrush> brushes = []() -> QVector<QBrush> {
0283         QVector<QBrush> brushes;
0284         std::generate_n(std::back_inserter(brushes), 100, []() {
0285             return QColor(0, 190 + 50 * qreal(rand()) / RAND_MAX, 210 * qreal(rand()) / RAND_MAX, 125);
0286         });
0287         return brushes;
0288     }();
0289     return brushes.at(rand() % brushes.size());
0290 }
0291 
0292 /**
0293  * Layout the flame graph and hide tiny items.
0294  */
0295 void layoutItems(FrameGraphicsItem* parent)
0296 {
0297     const auto& parentRect = parent->rect();
0298     const auto pos = parentRect.topLeft();
0299     const qreal maxWidth = parentRect.width();
0300     const qreal h = parentRect.height();
0301     const qreal y_margin = 2.;
0302     const qreal y = pos.y() - h - y_margin;
0303     qreal x = pos.x();
0304 
0305     const auto children = parent->childItems();
0306     for (auto child : children) {
0307         auto frameChild = static_cast<FrameGraphicsItem*>(child);
0308         const qreal w = maxWidth * double(frameChild->cost()) / parent->cost();
0309         frameChild->setVisible(w > 1);
0310         if (frameChild->isVisible()) {
0311             frameChild->setRect(QRectF(x, y, w, h));
0312             layoutItems(frameChild);
0313             x += w;
0314         }
0315     }
0316 }
0317 
0318 FrameGraphicsItem* findItemBySymbol(const QList<QGraphicsItem*>& items, const Symbol& symbol)
0319 {
0320     for (auto item_ : items) {
0321         auto item = static_cast<FrameGraphicsItem*>(item_);
0322         if (item->symbol() == symbol) {
0323             return item;
0324         }
0325     }
0326     return nullptr;
0327 }
0328 
0329 /**
0330  * Convert the top-down graph into a tree of FrameGraphicsItem.
0331  */
0332 void toGraphicsItems(const std::shared_ptr<const ResultData>& resultData, const QVector<RowData>& data,
0333                      FrameGraphicsItem* parent, int64_t AllocationData::*member, const double costThreshold,
0334                      bool collapseRecursion)
0335 {
0336     for (const auto& row : data) {
0337         if (collapseRecursion && row.symbol.functionId && row.symbol == parent->symbol()) {
0338             toGraphicsItems(resultData, row.children, parent, member, costThreshold, collapseRecursion);
0339             continue;
0340         }
0341         auto item = findItemBySymbol(parent->childItems(), row.symbol);
0342         if (!item) {
0343             item = new FrameGraphicsItem(row.cost.*member, row.symbol, resultData, parent);
0344             item->setPen(parent->pen());
0345             item->setBrush(brush());
0346         } else {
0347             item->setCost(item->cost() + row.cost.*member);
0348         }
0349         if (item->cost() > costThreshold) {
0350             toGraphicsItems(resultData, row.children, item, member, costThreshold, collapseRecursion);
0351         }
0352     }
0353 }
0354 
0355 int64_t AllocationData::*memberForType(CostType type)
0356 {
0357     switch (type) {
0358     case Allocations:
0359         return &AllocationData::allocations;
0360     case Temporary:
0361         return &AllocationData::temporary;
0362     case Peak:
0363         return &AllocationData::peak;
0364     case Leaked:
0365         return &AllocationData::leaked;
0366     }
0367     Q_UNREACHABLE();
0368 }
0369 
0370 FrameGraphicsItem* parseData(const TreeData& data, CostType type, double costThreshold, bool collapseRecursion)
0371 {
0372     auto member = memberForType(type);
0373 
0374     const auto totalCost = data.resultData->totalCosts().*member;
0375 
0376     KColorScheme scheme(QPalette::Active);
0377     const QPen pen(scheme.foreground().color());
0378 
0379     auto rootItem = new FrameGraphicsItem(totalCost, type, {}, data.resultData);
0380     rootItem->setBrush(scheme.background());
0381     rootItem->setPen(pen);
0382     toGraphicsItems(data.resultData, data.rows, rootItem, member, totalCost * costThreshold / 100., collapseRecursion);
0383     return rootItem;
0384 }
0385 
0386 struct SearchResults
0387 {
0388     SearchMatchType matchType = NoMatch;
0389     qint64 directCost = 0;
0390 };
0391 
0392 SearchResults applySearch(FrameGraphicsItem* item, const QString& searchValue)
0393 {
0394     SearchResults result;
0395     if (searchValue.isEmpty()) {
0396         result.matchType = NoSearch;
0397     } else if (item->match(searchValue)) {
0398         result.directCost += item->cost();
0399         result.matchType = DirectMatch;
0400     }
0401 
0402     // recurse into the child items, we always need to update all items
0403     for (auto* child : item->childItems()) {
0404         auto* childFrame = static_cast<FrameGraphicsItem*>(child);
0405         auto childMatch = applySearch(childFrame, searchValue);
0406         if (result.matchType != DirectMatch
0407             && (childMatch.matchType == DirectMatch || childMatch.matchType == ChildMatch)) {
0408             result.matchType = ChildMatch;
0409             result.directCost += childMatch.directCost;
0410         }
0411     }
0412 
0413     item->setSearchMatchType(result.matchType);
0414     return result;
0415 }
0416 }
0417 
0418 FlameGraph::FlameGraph(QWidget* parent)
0419     : QWidget(parent)
0420     , m_costSource(new QComboBox(this))
0421     , m_scene(new QGraphicsScene(this))
0422     , m_view(new QGraphicsView(this))
0423     , m_displayLabel(new QLabel)
0424     , m_searchResultsLabel(new QLabel)
0425 {
0426     qRegisterMetaType<FrameGraphicsItem*>();
0427 
0428     m_costSource->addItem(i18n("Memory Peak"), QVariant::fromValue(Peak));
0429     m_costSource->setItemData(2,
0430                               i18n("Show a flame graph over the contributions to the peak heap "
0431                                    "memory consumption of your application."),
0432                               Qt::ToolTipRole);
0433     m_costSource->addItem(i18n("Leaked"), QVariant::fromValue(Leaked));
0434     m_costSource->setItemData(3,
0435                               i18n("Show a flame graph over the leaked heap memory of your application. "
0436                                    "Memory is considered to be leaked when it never got deallocated. "),
0437                               Qt::ToolTipRole);
0438     m_costSource->addItem(i18n("Allocations"), QVariant::fromValue(Allocations));
0439     m_costSource->setItemData(0,
0440                               i18n("Show a flame graph over the number of allocations triggered by "
0441                                    "functions in your code."),
0442                               Qt::ToolTipRole);
0443     m_costSource->addItem(i18n("Temporary Allocations"), QVariant::fromValue(Temporary));
0444     m_costSource->setItemData(1,
0445                               i18n("Show a flame graph over the number of temporary allocations "
0446                                    "triggered by functions in your code. "
0447                                    "Allocations are marked as temporary when they are immediately "
0448                                    "followed by their deallocation."),
0449                               Qt::ToolTipRole);
0450     connect(m_costSource, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
0451             &FlameGraph::showData);
0452     m_costSource->setToolTip(i18n("Select the data source that should be visualized in the flame graph."));
0453 
0454     m_scene->setItemIndexMethod(QGraphicsScene::NoIndex);
0455     m_view->setScene(m_scene);
0456     m_view->viewport()->installEventFilter(this);
0457     m_view->viewport()->setMouseTracking(true);
0458     m_view->setFont(QFont(QStringLiteral("monospace")));
0459 
0460     m_backButton = new QPushButton(this);
0461     m_backButton->setIcon(QIcon::fromTheme(QStringLiteral("go-previous")));
0462     m_backButton->setToolTip(QStringLiteral("Go back in symbol view history"));
0463     m_forwardButton = new QPushButton(this);
0464     m_forwardButton->setIcon(QIcon::fromTheme(QStringLiteral("go-next")));
0465     m_forwardButton->setToolTip(QStringLiteral("Go forward in symbol view history"));
0466 
0467     auto bottomUpCheckbox = new QCheckBox(i18n("Bottom-Up View"), this);
0468     bottomUpCheckbox->setToolTip(i18n("Enable the bottom-up flame graph view. When this is unchecked, "
0469                                       "the top-down view is enabled by default."));
0470     bottomUpCheckbox->setChecked(m_showBottomUpData);
0471     connect(bottomUpCheckbox, &QCheckBox::toggled, this, [this, bottomUpCheckbox] {
0472         m_showBottomUpData = bottomUpCheckbox->isChecked();
0473         showData();
0474     });
0475 
0476     auto collapseRecursionCheckbox = new QCheckBox(i18n("Collapse Recursion"), this);
0477     collapseRecursionCheckbox->setChecked(m_collapseRecursion);
0478     collapseRecursionCheckbox->setToolTip(i18n("Collapse stack frames for functions calling themselves. "
0479                                                "When this is unchecked, recursive frames will be visualized "
0480                                                "separately."));
0481     connect(collapseRecursionCheckbox, &QCheckBox::toggled, this, [this, collapseRecursionCheckbox] {
0482         m_collapseRecursion = collapseRecursionCheckbox->isChecked();
0483         showData();
0484     });
0485 
0486     auto costThreshold = new QDoubleSpinBox(this);
0487     costThreshold->setDecimals(2);
0488     costThreshold->setMinimum(0);
0489     costThreshold->setMaximum(99.90);
0490     costThreshold->setPrefix(i18n("Cost Threshold: "));
0491     costThreshold->setSuffix(QStringLiteral("%"));
0492     costThreshold->setValue(m_costThreshold);
0493     costThreshold->setSingleStep(0.01);
0494     costThreshold->setToolTip(i18n("<qt>The cost threshold defines a fractional cut-off value. "
0495                                    "Items with a relative cost below this value will not be shown in "
0496                                    "the flame graph. This is done as an optimization to quickly generate "
0497                                    "graphs for large data sets with low memory overhead. If you need more "
0498                                    "details, decrease the threshold value, or set it to zero.</qt>"));
0499     connect(costThreshold, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), this,
0500             [this](double threshold) {
0501                 m_costThreshold = threshold;
0502                 showData();
0503             });
0504 
0505     m_searchInput = new QLineEdit(this);
0506     m_searchInput->setPlaceholderText(i18n("Search..."));
0507     m_searchInput->setToolTip(i18n("<qt>Search the flame graph for a symbol.</qt>"));
0508     m_searchInput->setClearButtonEnabled(true);
0509     connect(m_searchInput, &QLineEdit::textChanged, this, &FlameGraph::setSearchValue);
0510 
0511     auto controls = new QWidget(this);
0512     controls->setLayout(new QHBoxLayout);
0513     controls->layout()->addWidget(m_backButton);
0514     controls->layout()->addWidget(m_forwardButton);
0515     controls->layout()->addWidget(m_costSource);
0516     controls->layout()->addWidget(bottomUpCheckbox);
0517     controls->layout()->addWidget(collapseRecursionCheckbox);
0518     controls->layout()->addWidget(costThreshold);
0519     controls->layout()->addWidget(m_searchInput);
0520 
0521     m_displayLabel->setWordWrap(true);
0522     m_displayLabel->setTextInteractionFlags(m_displayLabel->textInteractionFlags() | Qt::TextSelectableByMouse);
0523 
0524     m_searchResultsLabel->setWordWrap(true);
0525     m_searchResultsLabel->setTextInteractionFlags(m_searchResultsLabel->textInteractionFlags()
0526                                                   | Qt::TextSelectableByMouse);
0527     m_searchResultsLabel->hide();
0528 
0529     setLayout(new QVBoxLayout);
0530     layout()->setContentsMargins(0, 0, 0, 0);
0531     layout()->setSpacing(0);
0532     layout()->addWidget(controls);
0533     layout()->addWidget(m_view);
0534     layout()->addWidget(m_displayLabel);
0535     layout()->addWidget(m_searchResultsLabel);
0536 
0537     m_backAction = KStandardAction::back(this, SLOT(navigateBack()), this);
0538     addAction(m_backAction);
0539     connect(m_backButton, &QPushButton::released, m_backAction, &QAction::trigger);
0540 
0541     m_forwardAction = KStandardAction::forward(this, SLOT(navigateForward()), this);
0542     addAction(m_forwardAction);
0543     connect(m_forwardButton, &QPushButton::released, m_forwardAction, &QAction::trigger);
0544 
0545     m_resetAction = new QAction(QIcon::fromTheme(QStringLiteral("go-first")), i18n("Reset View"), this);
0546     m_resetAction->setShortcut(Qt::Key_Escape);
0547     connect(m_resetAction, &QAction::triggered, this, [this]() { selectItem(0); });
0548     addAction(m_resetAction);
0549     updateNavigationActions();
0550     m_view->setContextMenuPolicy(Qt::CustomContextMenu);
0551     connect(m_view, &QWidget::customContextMenuRequested, this, [this](const QPoint& point) {
0552         auto* menu = new QMenu(this);
0553         menu->setAttribute(Qt::WA_DeleteOnClose, true);
0554         if (auto item = static_cast<const FrameGraphicsItem*>(m_view->itemAt(point))) {
0555             auto* action = menu->addAction(i18n("View Caller/Callee"));
0556             connect(action, &QAction::triggered, this,
0557                     [this, item]() { emit callerCalleeViewRequested(item->symbol()); });
0558 
0559             auto* copy = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), tr("Copy"));
0560             connect(copy, &QAction::triggered, this, [item]() { qApp->clipboard()->setText(item->description()); });
0561 
0562             menu->addSeparator();
0563         }
0564         menu->addActions(actions());
0565         menu->popup(m_view->mapToGlobal(point));
0566     });
0567 }
0568 
0569 FlameGraph::~FlameGraph() = default;
0570 
0571 bool FlameGraph::eventFilter(QObject* object, QEvent* event)
0572 {
0573     bool ret = QObject::eventFilter(object, event);
0574 
0575     if (event->type() == QEvent::MouseButtonRelease) {
0576         QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
0577         if (mouseEvent->button() == Qt::LeftButton) {
0578             auto item = static_cast<FrameGraphicsItem*>(m_view->itemAt(mouseEvent->pos()));
0579             if (item && item != m_selectionHistory.at(m_selectedItem)) {
0580                 selectItem(item);
0581                 if (m_selectedItem != m_selectionHistory.size() - 1) {
0582                     m_selectionHistory.remove(m_selectedItem + 1, m_selectionHistory.size() - m_selectedItem - 1);
0583                 }
0584                 m_selectedItem = m_selectionHistory.size();
0585                 m_selectionHistory.push_back(item);
0586                 updateNavigationActions();
0587             }
0588         }
0589     } else if (event->type() == QEvent::MouseMove) {
0590         QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
0591         auto item = static_cast<FrameGraphicsItem*>(m_view->itemAt(mouseEvent->pos()));
0592         setTooltipItem(item);
0593     } else if (event->type() == QEvent::Leave) {
0594         setTooltipItem(nullptr);
0595     } else if (event->type() == QEvent::Resize || event->type() == QEvent::Show) {
0596         if (!m_rootItem) {
0597             if (!m_buildingScene) {
0598                 showData();
0599             }
0600         } else {
0601             selectItem(m_selectionHistory.at(m_selectedItem));
0602         }
0603         updateTooltip();
0604     } else if (event->type() == QEvent::ToolTip) {
0605         auto tooltip = m_displayLabel->toolTip();
0606 
0607         if (m_tooltipItem != m_view->itemAt(m_view->mapFromGlobal(QCursor::pos()))) {
0608             // don't show a tooltip when the cursor is in the empty region
0609             tooltip.clear();
0610         }
0611 
0612         if (tooltip.isEmpty()) {
0613             QToolTip::hideText();
0614         } else {
0615             QToolTip::showText(QCursor::pos(), QLatin1String("<qt>") + tooltip.toHtmlEscaped() + QLatin1String("</qt>"),
0616                                this);
0617         }
0618         event->accept();
0619         return true;
0620     }
0621     return ret;
0622 }
0623 
0624 void FlameGraph::setTopDownData(const TreeData& topDownData)
0625 {
0626     m_topDownData = topDownData;
0627 
0628     if (isVisible()) {
0629         showData();
0630     }
0631 }
0632 
0633 void FlameGraph::setBottomUpData(const TreeData& bottomUpData)
0634 {
0635     m_bottomUpData = bottomUpData;
0636 }
0637 
0638 void FlameGraph::clearData()
0639 {
0640     m_topDownData = {};
0641     m_bottomUpData = {};
0642 
0643     setData(nullptr);
0644 }
0645 
0646 void FlameGraph::showData()
0647 {
0648     setData(nullptr);
0649 
0650     using namespace ThreadWeaver;
0651     auto data = m_showBottomUpData ? m_bottomUpData : m_topDownData;
0652     if (!data.resultData)
0653         return;
0654 
0655     m_buildingScene = true;
0656     bool collapseRecursion = m_collapseRecursion;
0657     auto source = m_costSource->currentData().value<CostType>();
0658     auto threshold = m_costThreshold;
0659     stream() << make_job([data, source, threshold, collapseRecursion, this]() {
0660         auto parsedData = parseData(data, source, threshold, collapseRecursion);
0661         QMetaObject::invokeMethod(this, "setData", Qt::QueuedConnection, Q_ARG(FrameGraphicsItem*, parsedData));
0662     });
0663 }
0664 
0665 void FlameGraph::setTooltipItem(const FrameGraphicsItem* item)
0666 {
0667     if (!item && m_selectedItem != -1 && m_selectionHistory.at(m_selectedItem)) {
0668         item = m_selectionHistory.at(m_selectedItem);
0669         m_view->setCursor(Qt::ArrowCursor);
0670     } else {
0671         m_view->setCursor(Qt::PointingHandCursor);
0672     }
0673     m_tooltipItem = item;
0674     updateTooltip();
0675 }
0676 
0677 void FlameGraph::updateTooltip()
0678 {
0679     const auto text = m_tooltipItem ? m_tooltipItem->description() : QString();
0680     m_displayLabel->setToolTip(text);
0681     const auto metrics = m_displayLabel->fontMetrics();
0682     m_displayLabel->setText(metrics.elidedText(text, Qt::ElideRight, m_displayLabel->width()));
0683 }
0684 
0685 void FlameGraph::setData(FrameGraphicsItem* rootItem)
0686 {
0687     m_scene->clear();
0688     m_buildingScene = false;
0689     m_tooltipItem = nullptr;
0690     m_rootItem = rootItem;
0691     m_selectionHistory.clear();
0692     m_selectionHistory.push_back(rootItem);
0693     m_selectedItem = 0;
0694     updateNavigationActions();
0695     if (!rootItem) {
0696         auto text = m_scene->addText(i18n("generating flame graph..."));
0697         m_view->centerOn(text);
0698         m_view->setCursor(Qt::BusyCursor);
0699         return;
0700     }
0701 
0702     m_view->setCursor(Qt::ArrowCursor);
0703     // layouting needs a root item with a given height, the rest will be
0704     // overwritten later
0705     rootItem->setRect(0, 0, 800, m_view->fontMetrics().height() + 4);
0706     m_scene->addItem(rootItem);
0707 
0708     if (!m_searchInput->text().isEmpty()) {
0709         setSearchValue(m_searchInput->text());
0710     }
0711 
0712     if (isVisible()) {
0713         selectItem(m_rootItem);
0714     }
0715 }
0716 
0717 void FlameGraph::selectItem(int item)
0718 {
0719     m_selectedItem = item;
0720     updateNavigationActions();
0721     selectItem(m_selectionHistory.at(m_selectedItem));
0722 }
0723 
0724 void FlameGraph::selectItem(FrameGraphicsItem* item)
0725 {
0726     if (!item) {
0727         return;
0728     }
0729 
0730     // scale item and its parents to the maximum available width
0731     // also hide all siblings of the parent items
0732     const auto rootWidth = m_view->viewport()->width() - 40;
0733     auto parent = item;
0734     while (parent) {
0735         auto rect = parent->rect();
0736         rect.setLeft(0);
0737         rect.setWidth(rootWidth);
0738         parent->setRect(rect);
0739         if (parent->parentItem()) {
0740             const auto children = parent->parentItem()->childItems();
0741             for (auto sibling : children) {
0742                 sibling->setVisible(sibling == parent);
0743             }
0744         }
0745         parent = static_cast<FrameGraphicsItem*>(parent->parentItem());
0746     }
0747 
0748     // then layout all items below the selected on
0749     layoutItems(item);
0750 
0751     // and make sure it's visible
0752     m_view->centerOn(item);
0753 
0754     setTooltipItem(item);
0755 }
0756 
0757 void FlameGraph::setSearchValue(const QString& value)
0758 {
0759     if (!m_rootItem) {
0760         return;
0761     }
0762 
0763     auto match = applySearch(m_rootItem, value);
0764 
0765     if (value.isEmpty()) {
0766         m_searchResultsLabel->hide();
0767     } else {
0768         QString label;
0769         const auto costFraction = Util::formatCostRelative(match.directCost, m_rootItem->cost());
0770         switch (m_costSource->currentData().value<CostType>()) {
0771         case Allocations:
0772         case Temporary:
0773             label = i18n("%1 (%2% of total of %3) allocations matched by search.", match.directCost, costFraction,
0774                          m_rootItem->cost());
0775             break;
0776         case Peak:
0777         case Leaked:
0778             label = i18n("%1 (%2% of total of %3) matched by search.", Util::formatBytes(match.directCost),
0779                          costFraction, Util::formatBytes(m_rootItem->cost()));
0780             break;
0781         }
0782         m_searchResultsLabel->setText(label);
0783         m_searchResultsLabel->show();
0784     }
0785 }
0786 
0787 void FlameGraph::navigateBack()
0788 {
0789     if (m_selectedItem > 0) {
0790         selectItem(m_selectedItem - 1);
0791     }
0792 }
0793 
0794 void FlameGraph::navigateForward()
0795 {
0796     if ((m_selectedItem + 1) < m_selectionHistory.size()) {
0797         selectItem(m_selectedItem + 1);
0798     }
0799 }
0800 
0801 void FlameGraph::updateNavigationActions()
0802 {
0803     const bool hasItems = m_selectedItem > 0;
0804     const bool isNotLastItem = m_selectedItem + 1 < m_selectionHistory.size();
0805     m_backAction->setEnabled(hasItems);
0806     m_forwardAction->setEnabled(isNotLastItem);
0807     m_resetAction->setEnabled(hasItems);
0808     m_backButton->setEnabled(hasItems);
0809     m_forwardButton->setEnabled(isNotLastItem);
0810 }
0811 
0812 #include "moc_flamegraph.cpp"