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"