diff --git a/src/analyze/gui/flamegraph.cpp b/src/analyze/gui/flamegraph.cpp index 55088a5..144b9ed 100644 --- a/src/analyze/gui/flamegraph.cpp +++ b/src/analyze/gui/flamegraph.cpp @@ -1,619 +1,621 @@ /* * Copyright 2015-2017 Milian Wolff * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "flamegraph.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include enum CostType { Allocations, Temporary, Peak, Leaked, Allocated }; Q_DECLARE_METATYPE(CostType) class FrameGraphicsItem : public QGraphicsRectItem { public: FrameGraphicsItem(const qint64 cost, CostType costType, const QString& function, FrameGraphicsItem* parent = nullptr); FrameGraphicsItem(const qint64 cost, const QString& function, FrameGraphicsItem* parent); qint64 cost() const; void setCost(qint64 cost); QString function() const; void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget = nullptr) override; QString description() const; protected: void hoverEnterEvent(QGraphicsSceneHoverEvent* event) override; void hoverLeaveEvent(QGraphicsSceneHoverEvent* event) override; private: qint64 m_cost; QString m_function; CostType m_costType; bool m_isHovered; }; Q_DECLARE_METATYPE(FrameGraphicsItem*) FrameGraphicsItem::FrameGraphicsItem(const qint64 cost, CostType costType, const QString& function, FrameGraphicsItem* parent) : QGraphicsRectItem(parent) , m_cost(cost) , m_function(function) , m_costType(costType) , m_isHovered(false) { setFlag(QGraphicsItem::ItemIsSelectable); setAcceptHoverEvents(true); } FrameGraphicsItem::FrameGraphicsItem(const qint64 cost, const QString& function, FrameGraphicsItem* parent) : FrameGraphicsItem(cost, parent->m_costType, function, parent) { } qint64 FrameGraphicsItem::cost() const { return m_cost; } void FrameGraphicsItem::setCost(qint64 cost) { m_cost = cost; } QString FrameGraphicsItem::function() const { return m_function; } void FrameGraphicsItem::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* /*widget*/) { if (isSelected() || m_isHovered) { auto selectedColor = brush().color(); selectedColor.setAlpha(255); painter->fillRect(rect(), selectedColor); } else { painter->fillRect(rect(), brush()); } const QPen oldPen = painter->pen(); auto pen = oldPen; pen.setColor(brush().color()); if (isSelected()) { pen.setWidth(2); } painter->setPen(pen); painter->drawRect(rect()); painter->setPen(oldPen); const int margin = 4; const int width = rect().width() - 2 * margin; if (width < option->fontMetrics.averageCharWidth() * 6) { // text is too wide for the current LOD, don't paint it return; } const int height = rect().height(); painter->drawText(margin + rect().x(), rect().y(), width, height, Qt::AlignVCenter | Qt::AlignLeft | Qt::TextSingleLine, option->fontMetrics.elidedText(m_function, Qt::ElideRight, width)); } void FrameGraphicsItem::hoverEnterEvent(QGraphicsSceneHoverEvent* event) { QGraphicsRectItem::hoverEnterEvent(event); m_isHovered = true; } QString FrameGraphicsItem::description() const { // we build the tooltip text on demand, which is much faster than doing that // for potentially thousands of items when we load the data QString tooltip; KFormat format; qint64 totalCost = 0; { auto item = this; while (item->parentItem()) { item = static_cast(item->parentItem()); } totalCost = item->cost(); } const auto fraction = QString::number(double(m_cost) * 100. / totalCost, 'g', 3); const auto function = QString(QLatin1String("") + m_function.toHtmlEscaped() + QLatin1String("")); if (!parentItem()) { return function; } switch (m_costType) { case Allocations: tooltip = i18nc("%1: number of allocations, %2: relative number, %3: function label", "%1 (%2%) allocations in %3 and below.", m_cost, fraction, function); break; case Temporary: tooltip = i18nc("%1: number of temporary allocations, %2: relative number, " "%3 function label", "%1 (%2%) temporary allocations in %3 and below.", m_cost, fraction, function); break; case Peak: tooltip = i18nc("%1: peak consumption in bytes, %2: relative number, %3: " "function label", "%1 (%2%) peak consumption in %3 and below.", format.formatByteSize(m_cost), fraction, function); break; case Leaked: tooltip = i18nc("%1: leaked bytes, %2: relative number, %3: function label", "%1 (%2%) leaked in %3 and below.", format.formatByteSize(m_cost), fraction, function); break; case Allocated: tooltip = i18nc("%1: allocated bytes, %2: relative number, %3: function label", "%1 (%2%) allocated in %3 and below.", format.formatByteSize(m_cost), fraction, function); break; } return tooltip; } void FrameGraphicsItem::hoverLeaveEvent(QGraphicsSceneHoverEvent* event) { QGraphicsRectItem::hoverLeaveEvent(event); m_isHovered = false; } namespace { /** * Generate a brush from the "mem" color space used in upstream FlameGraph.pl */ QBrush brush() { // intern the brushes, to reuse them across items which can be thousands // otherwise we'd end up with dozens of allocations and higher memory // consumption static const QVector brushes = []() -> QVector { QVector brushes; std::generate_n(std::back_inserter(brushes), 100, []() { return QColor(0, 190 + 50 * qreal(rand()) / RAND_MAX, 210 * qreal(rand()) / RAND_MAX, 125); }); return brushes; }(); return brushes.at(rand() % brushes.size()); } /** * Layout the flame graph and hide tiny items. */ void layoutItems(FrameGraphicsItem* parent) { const auto& parentRect = parent->rect(); const auto pos = parentRect.topLeft(); const qreal maxWidth = parentRect.width(); const qreal h = parentRect.height(); const qreal y_margin = 2.; const qreal y = pos.y() - h - y_margin; qreal x = pos.x(); foreach (auto child, parent->childItems()) { auto frameChild = static_cast(child); const qreal w = maxWidth * double(frameChild->cost()) / parent->cost(); frameChild->setVisible(w > 1); if (frameChild->isVisible()) { frameChild->setRect(QRectF(x, y, w, h)); layoutItems(frameChild); x += w; } } } FrameGraphicsItem* findItemByFunction(const QList& items, const QString& function) { foreach (auto item_, items) { auto item = static_cast(item_); if (item->function() == function) { return item; } } return nullptr; } /** * Convert the top-down graph into a tree of FrameGraphicsItem. */ void toGraphicsItems(const QVector& data, FrameGraphicsItem* parent, int64_t AllocationData::*member, const double costThreshold, bool collapseRecursion) { foreach (const auto& row, data) { - if (collapseRecursion && row.location->function == parent->function()) { + if (collapseRecursion && row.location->function != unresolvedFunctionName() + && row.location->function == parent->function()) + { continue; } auto item = findItemByFunction(parent->childItems(), row.location->function); if (!item) { item = new FrameGraphicsItem(row.cost.*member, row.location->function, parent); item->setPen(parent->pen()); item->setBrush(brush()); } else { item->setCost(item->cost() + row.cost.*member); } if (item->cost() > costThreshold) { toGraphicsItems(row.children, item, member, costThreshold, collapseRecursion); } } } int64_t AllocationData::*memberForType(CostType type) { switch (type) { case Allocations: return &AllocationData::allocations; case Temporary: return &AllocationData::temporary; case Peak: return &AllocationData::peak; case Leaked: return &AllocationData::leaked; case Allocated: return &AllocationData::allocated; } Q_UNREACHABLE(); } FrameGraphicsItem* parseData(const QVector& topDownData, CostType type, double costThreshold, bool collapseRecursion) { auto member = memberForType(type); double totalCost = 0; foreach (const auto& frame, topDownData) { totalCost += frame.cost.*member; } KColorScheme scheme(QPalette::Active); const QPen pen(scheme.foreground().color()); KFormat format; QString label; switch (type) { case Allocations: label = i18n("%1 allocations in total", totalCost); break; case Temporary: label = i18n("%1 temporary allocations in total", totalCost); break; case Peak: label = i18n("%1 peak consumption in total", format.formatByteSize(totalCost)); break; case Leaked: label = i18n("%1 leaked in total", format.formatByteSize(totalCost)); break; case Allocated: label = i18n("%1 allocated in total", format.formatByteSize(totalCost)); break; } auto rootItem = new FrameGraphicsItem(totalCost, type, label); rootItem->setBrush(scheme.background()); rootItem->setPen(pen); toGraphicsItems(topDownData, rootItem, member, totalCost * costThreshold / 100., collapseRecursion); return rootItem; } } FlameGraph::FlameGraph(QWidget* parent, Qt::WindowFlags flags) : QWidget(parent, flags) , m_costSource(new QComboBox(this)) , m_scene(new QGraphicsScene(this)) , m_view(new QGraphicsView(this)) , m_displayLabel(new QLabel) { qRegisterMetaType(); m_costSource->addItem(i18n("Allocations"), QVariant::fromValue(Allocations)); m_costSource->setItemData(0, i18n("Show a flame graph over the number of allocations triggered by " "functions in your code."), Qt::ToolTipRole); m_costSource->addItem(i18n("Temporary Allocations"), QVariant::fromValue(Temporary)); m_costSource->setItemData(1, i18n("Show a flame graph over the number of temporary allocations " "triggered by functions in your code. " "Allocations are marked as temporary when they are immediately " "followed by their deallocation."), Qt::ToolTipRole); m_costSource->addItem(i18n("Peak Consumption"), QVariant::fromValue(Peak)); m_costSource->setItemData(2, i18n("Show a flame graph over the peak heap " "memory consumption of your application."), Qt::ToolTipRole); m_costSource->addItem(i18n("Leaked"), QVariant::fromValue(Leaked)); m_costSource->setItemData(3, i18n("Show a flame graph over the leaked heap memory of your application. " "Memory is considered to be leaked when it never got deallocated. "), Qt::ToolTipRole); m_costSource->addItem(i18n("Allocated"), QVariant::fromValue(Allocated)); m_costSource->setItemData(4, i18n("Show a flame graph over the total memory allocated by functions in " "your code. " "This aggregates all memory allocations and ignores deallocations."), Qt::ToolTipRole); connect(m_costSource, static_cast(&QComboBox::currentIndexChanged), this, &FlameGraph::showData); m_costSource->setToolTip(i18n("Select the data source that should be visualized in the flame graph.")); m_scene->setItemIndexMethod(QGraphicsScene::NoIndex); m_view->setScene(m_scene); m_view->viewport()->installEventFilter(this); m_view->viewport()->setMouseTracking(true); m_view->setFont(QFont(QStringLiteral("monospace"))); auto bottomUpCheckbox = new QCheckBox(i18n("Bottom-Down View"), this); bottomUpCheckbox->setToolTip(i18n("Enable the bottom-down flame graph view. When this is unchecked, " "the top-down view is enabled by default.")); connect(bottomUpCheckbox, &QCheckBox::toggled, this, [this, bottomUpCheckbox] { m_showBottomUpData = bottomUpCheckbox->isChecked(); showData(); }); auto collapseRecursionCheckbox = new QCheckBox(i18n("Collapse Recursion"), this); collapseRecursionCheckbox->setChecked(m_collapseRecursion); collapseRecursionCheckbox->setToolTip(i18n("Collapse stack frames for functions calling themselves. " "When this is unchecked, recursive frames will be visualized " "separately.")); connect(collapseRecursionCheckbox, &QCheckBox::toggled, this, [this, collapseRecursionCheckbox] { m_collapseRecursion = collapseRecursionCheckbox->isChecked(); showData(); }); auto costThreshold = new QDoubleSpinBox(this); costThreshold->setDecimals(2); costThreshold->setMinimum(0); costThreshold->setMaximum(99.90); costThreshold->setPrefix(i18n("Cost Threshold: ")); costThreshold->setSuffix(QStringLiteral("%")); costThreshold->setValue(m_costThreshold); costThreshold->setSingleStep(0.01); costThreshold->setToolTip(i18n("The cost threshold defines a fractional cut-off value. " "Items with a relative cost below this value will not be shown in " "the flame graph. This is done as an optimization to quickly generate " "graphs for large data sets with low memory overhead. If you need more " "details, decrease the threshold value, or set it to zero.")); connect(costThreshold, static_cast(&QDoubleSpinBox::valueChanged), this, [this](double threshold) { m_costThreshold = threshold; showData(); }); m_displayLabel->setWordWrap(true); m_displayLabel->setTextInteractionFlags(m_displayLabel->textInteractionFlags() | Qt::TextSelectableByMouse); auto controls = new QWidget(this); controls->setLayout(new QHBoxLayout); controls->layout()->addWidget(m_costSource); controls->layout()->addWidget(bottomUpCheckbox); controls->layout()->addWidget(collapseRecursionCheckbox); controls->layout()->addWidget(costThreshold); setLayout(new QVBoxLayout); layout()->addWidget(controls); layout()->addWidget(m_view); layout()->addWidget(m_displayLabel); addAction(KStandardAction::back(this, SLOT(navigateBack()), this)); addAction(KStandardAction::forward(this, SLOT(navigateForward()), this)); setContextMenuPolicy(Qt::ActionsContextMenu); } FlameGraph::~FlameGraph() = default; bool FlameGraph::eventFilter(QObject* object, QEvent* event) { bool ret = QObject::eventFilter(object, event); if (event->type() == QEvent::MouseButtonRelease) { QMouseEvent* mouseEvent = static_cast(event); if (mouseEvent->button() == Qt::LeftButton) { auto item = static_cast(m_view->itemAt(mouseEvent->pos())); if (item && item != m_selectionHistory.at(m_selectedItem)) { selectItem(item); if (m_selectedItem != m_selectionHistory.size() - 1) { m_selectionHistory.remove(m_selectedItem + 1, m_selectionHistory.size() - m_selectedItem - 1); } m_selectedItem = m_selectionHistory.size(); m_selectionHistory.push_back(item); } } } else if (event->type() == QEvent::MouseMove) { QMouseEvent* mouseEvent = static_cast(event); auto item = static_cast(m_view->itemAt(mouseEvent->pos())); setTooltipItem(item); } else if (event->type() == QEvent::Leave) { setTooltipItem(nullptr); } else if (event->type() == QEvent::Resize || event->type() == QEvent::Show) { if (!m_rootItem) { if (!m_buildingScene) { showData(); } } else { selectItem(m_selectionHistory.at(m_selectedItem)); } updateTooltip(); } else if (event->type() == QEvent::Hide) { setData(nullptr); } return ret; } void FlameGraph::setTopDownData(const TreeData& topDownData) { m_topDownData = topDownData; if (isVisible()) { showData(); } } void FlameGraph::setBottomUpData(const TreeData& bottomUpData) { m_bottomUpData = bottomUpData; } void FlameGraph::clearData() { m_topDownData = {}; m_bottomUpData = {}; setData(nullptr); } void FlameGraph::showData() { setData(nullptr); m_buildingScene = true; using namespace ThreadWeaver; auto data = m_showBottomUpData ? m_bottomUpData : m_topDownData; bool collapseRecursion = m_collapseRecursion; auto source = m_costSource->currentData().value(); auto threshold = m_costThreshold; stream() << make_job([data, source, threshold, collapseRecursion, this]() { auto parsedData = parseData(data, source, threshold, collapseRecursion); QMetaObject::invokeMethod(this, "setData", Qt::QueuedConnection, Q_ARG(FrameGraphicsItem*, parsedData)); }); } void FlameGraph::setTooltipItem(const FrameGraphicsItem* item) { if (!item && m_selectedItem != -1 && m_selectionHistory.at(m_selectedItem)) { item = m_selectionHistory.at(m_selectedItem); m_view->setCursor(Qt::ArrowCursor); } else { m_view->setCursor(Qt::PointingHandCursor); } m_tooltipItem = item; updateTooltip(); } void FlameGraph::updateTooltip() { const auto text = m_tooltipItem ? m_tooltipItem->description() : QString(); m_displayLabel->setToolTip(text); const auto metrics = m_displayLabel->fontMetrics(); // FIXME: the HTML text has tons of stuff that is not printed, // which lets the text get cut-off too soon... m_displayLabel->setText(metrics.elidedText(text, Qt::ElideRight, m_displayLabel->width())); } void FlameGraph::setData(FrameGraphicsItem* rootItem) { m_scene->clear(); m_buildingScene = false; m_rootItem = rootItem; m_selectionHistory.clear(); m_selectionHistory.push_back(rootItem); m_selectedItem = 0; if (!rootItem) { auto text = m_scene->addText(i18n("generating flame graph...")); m_view->centerOn(text); m_view->setCursor(Qt::BusyCursor); return; } m_view->setCursor(Qt::ArrowCursor); // layouting needs a root item with a given height, the rest will be // overwritten later rootItem->setRect(0, 0, 800, m_view->fontMetrics().height() + 4); m_scene->addItem(rootItem); if (isVisible()) { selectItem(m_rootItem); } } void FlameGraph::selectItem(FrameGraphicsItem* item) { if (!item) { return; } // scale item and its parents to the maximum available width // also hide all siblings of the parent items const auto rootWidth = m_view->viewport()->width() - 40; auto parent = item; while (parent) { auto rect = parent->rect(); rect.setLeft(0); rect.setWidth(rootWidth); parent->setRect(rect); if (parent->parentItem()) { foreach (auto sibling, parent->parentItem()->childItems()) { sibling->setVisible(sibling == parent); } } parent = static_cast(parent->parentItem()); } // then layout all items below the selected on layoutItems(item); // and make sure it's visible m_view->centerOn(item); setTooltipItem(item); } void FlameGraph::navigateBack() { if (m_selectedItem > 0) { --m_selectedItem; } selectItem(m_selectionHistory.at(m_selectedItem)); } void FlameGraph::navigateForward() { if ((m_selectedItem + 1) < m_selectionHistory.size()) { ++m_selectedItem; } selectItem(m_selectionHistory.at(m_selectedItem)); } diff --git a/src/analyze/gui/locationdata.h b/src/analyze/gui/locationdata.h index d9d3ac5..235fc37 100644 --- a/src/analyze/gui/locationdata.h +++ b/src/analyze/gui/locationdata.h @@ -1,81 +1,88 @@ /* * Copyright 2016-2017 Milian Wolff * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef LOCATIONDATA_H #define LOCATIONDATA_H #include #include #include +#include + struct LocationData { using Ptr = std::shared_ptr; QString function; QString file; QString module; int line; bool operator==(const LocationData& rhs) const { return function == rhs.function && file == rhs.file && module == rhs.module && line == rhs.line; } bool operator<(const LocationData& rhs) const { int i = function.compare(rhs.function); if (!i) { i = file.compare(rhs.file); } if (!i) { i = line < rhs.line ? -1 : (line > rhs.line); } if (!i) { i = module.compare(rhs.module); } return i < 0; } }; Q_DECLARE_TYPEINFO(LocationData, Q_MOVABLE_TYPE); Q_DECLARE_METATYPE(LocationData::Ptr) +inline QString unresolvedFunctionName() +{ + return i18n(""); +} + inline bool operator<(const LocationData::Ptr& lhs, const LocationData& rhs) { return *lhs < rhs; } inline uint qHash(const LocationData& location, uint seed_ = 0) { size_t seed = seed_; boost::hash_combine(seed, qHash(location.function)); boost::hash_combine(seed, qHash(location.file)); boost::hash_combine(seed, qHash(location.module)); boost::hash_combine(seed, location.line); return seed; } inline uint qHash(const LocationData::Ptr& location, uint seed = 0) { return location ? qHash(*location, seed) : seed; } #endif // LOCATIONDATA_H diff --git a/src/analyze/gui/parser.cpp b/src/analyze/gui/parser.cpp index 0be0d68..a8f1f5d 100644 --- a/src/analyze/gui/parser.cpp +++ b/src/analyze/gui/parser.cpp @@ -1,601 +1,601 @@ /* * Copyright 2015-2017 Milian Wolff * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "parser.h" #include #include #include #include "analyze/accumulatedtracedata.h" #include #include #include using namespace std; namespace { // TODO: use QString directly struct StringCache { QString func(const InstructionPointer& ip) const { if (ip.functionIndex) { // TODO: support removal of template arguments return stringify(ip.functionIndex); } else { - return i18n(""); + return unresolvedFunctionName(); } } QString file(const InstructionPointer& ip) const { if (ip.fileIndex) { return stringify(ip.fileIndex); } else { return {}; } } QString module(const InstructionPointer& ip) const { return stringify(ip.moduleIndex); } QString stringify(const StringIndex index) const { if (!index || index.index > m_strings.size()) { return {}; } else { return m_strings.at(index.index - 1); } } LocationData::Ptr location(const IpIndex& index, const InstructionPointer& ip) const { // first try a fast index-based lookup auto& location = m_locationsMap[index]; if (!location) { // slow-path, look for interned location // note that we can get the same locatoin for different IPs LocationData data = {func(ip), file(ip), module(ip), ip.line}; auto it = lower_bound(m_locations.begin(), m_locations.end(), data); if (it != m_locations.end() && **it == data) { // we got the location already from a different ip, cache it location = *it; } else { // completely new location, cache it in both containers auto interned = make_shared(data); m_locations.insert(it, interned); location = interned; } } return location; } void update(const vector& strings) { transform(strings.begin() + m_strings.size(), strings.end(), back_inserter(m_strings), [](const string& str) { return QString::fromStdString(str); }); } vector m_strings; mutable vector m_locations; mutable QHash m_locationsMap; bool diffMode = false; }; struct ChartMergeData { IpIndex ip; qint64 consumed; qint64 allocations; qint64 allocated; qint64 temporary; bool operator<(const IpIndex rhs) const { return ip < rhs; } }; const uint64_t MAX_CHART_DATAPOINTS = 500; // TODO: make this configurable via the GUI struct ParserData final : public AccumulatedTraceData { ParserData() { } void updateStringCache() { stringCache.update(strings); } void prepareBuildCharts() { if (stringCache.diffMode) { return; } consumedChartData.rows.reserve(MAX_CHART_DATAPOINTS); allocatedChartData.rows.reserve(MAX_CHART_DATAPOINTS); allocationsChartData.rows.reserve(MAX_CHART_DATAPOINTS); temporaryChartData.rows.reserve(MAX_CHART_DATAPOINTS); // start off with null data at the origin consumedChartData.rows.push_back({}); allocatedChartData.rows.push_back({}); allocationsChartData.rows.push_back({}); temporaryChartData.rows.push_back({}); // index 0 indicates the total row consumedChartData.labels[0] = i18n("total"); allocatedChartData.labels[0] = i18n("total"); allocationsChartData.labels[0] = i18n("total"); temporaryChartData.labels[0] = i18n("total"); buildCharts = true; maxConsumedSinceLastTimeStamp = 0; vector merged; merged.reserve(instructionPointers.size()); // merge the allocation cost by instruction pointer // TODO: aggregate by function instead? // TODO: traverse the merged call stack up until the first fork for (const auto& alloc : allocations) { const auto ip = findTrace(alloc.traceIndex).ipIndex; auto it = lower_bound(merged.begin(), merged.end(), ip); if (it == merged.end() || it->ip != ip) { it = merged.insert(it, {ip, 0, 0, 0, 0}); } it->consumed += alloc.peak; // we want to track the top peaks in the chart it->allocated += alloc.allocated; it->allocations += alloc.allocations; it->temporary += alloc.temporary; } // find the top hot spots for the individual data members and remember their // IP and store the label auto findTopChartEntries = [&](qint64 ChartMergeData::*member, int LabelIds::*label, ChartData* data) { sort(merged.begin(), merged.end(), [=](const ChartMergeData& left, const ChartMergeData& right) { return left.*member > right.*member; }); for (size_t i = 0; i < min(size_t(ChartRows::MAX_NUM_COST - 1), merged.size()); ++i) { const auto& alloc = merged[i]; if (!(alloc.*member)) { break; } const auto ip = alloc.ip; (labelIds[ip].*label) = i + 1; const auto function = stringCache.func(findIp(ip)); data->labels[i + 1] = function; } }; findTopChartEntries(&ChartMergeData::consumed, &LabelIds::consumed, &consumedChartData); findTopChartEntries(&ChartMergeData::allocated, &LabelIds::allocated, &allocatedChartData); findTopChartEntries(&ChartMergeData::allocations, &LabelIds::allocations, &allocationsChartData); findTopChartEntries(&ChartMergeData::temporary, &LabelIds::temporary, &temporaryChartData); } void handleTimeStamp(int64_t /*oldStamp*/, int64_t newStamp) { if (!buildCharts || stringCache.diffMode) { return; } maxConsumedSinceLastTimeStamp = max(maxConsumedSinceLastTimeStamp, totalCost.leaked); const int64_t diffBetweenTimeStamps = totalTime / MAX_CHART_DATAPOINTS; if (newStamp != totalTime && newStamp - lastTimeStamp < diffBetweenTimeStamps) { return; } const auto nowConsumed = maxConsumedSinceLastTimeStamp; maxConsumedSinceLastTimeStamp = 0; lastTimeStamp = newStamp; // create the rows auto createRow = [](int64_t timeStamp, int64_t totalCost) { ChartRows row; row.timeStamp = timeStamp; row.cost[0] = totalCost; return row; }; auto consumed = createRow(newStamp, nowConsumed); auto allocated = createRow(newStamp, totalCost.allocated); auto allocs = createRow(newStamp, totalCost.allocations); auto temporary = createRow(newStamp, totalCost.temporary); // if the cost is non-zero and the ip corresponds to a hotspot function // selected in the labels, // we add the cost to the rows column auto addDataToRow = [](int64_t cost, int labelId, ChartRows* rows) { if (!cost || labelId == -1) { return; } rows->cost[labelId] += cost; }; for (const auto& alloc : allocations) { const auto ip = findTrace(alloc.traceIndex).ipIndex; auto it = labelIds.constFind(ip); if (it == labelIds.constEnd()) { continue; } const auto& labelIds = *it; addDataToRow(alloc.leaked, labelIds.consumed, &consumed); addDataToRow(alloc.allocated, labelIds.allocated, &allocated); addDataToRow(alloc.allocations, labelIds.allocations, &allocs); addDataToRow(alloc.temporary, labelIds.temporary, &temporary); } // add the rows for this time stamp consumedChartData.rows << consumed; allocatedChartData.rows << allocated; allocationsChartData.rows << allocs; temporaryChartData.rows << temporary; } void handleAllocation(const AllocationInfo& info, const AllocationIndex index) { maxConsumedSinceLastTimeStamp = max(maxConsumedSinceLastTimeStamp, totalCost.leaked); if (index.index == allocationInfoCounter.size()) { allocationInfoCounter.push_back({info, 1}); } else { ++allocationInfoCounter[index.index].allocations; } } void handleDebuggee(const char* command) { debuggee = command; } string debuggee; struct CountedAllocationInfo { AllocationInfo info; int64_t allocations; bool operator<(const CountedAllocationInfo& rhs) const { return tie(info.size, allocations) < tie(rhs.info.size, rhs.allocations); } }; vector allocationInfoCounter; ChartData consumedChartData; ChartData allocationsChartData; ChartData allocatedChartData; ChartData temporaryChartData; // here we store the indices into ChartRows::cost for those IpIndices that // are within the top hotspots. This way, we can do one hash lookup in the // handleTimeStamp function instead of three when we'd store this data // in a per-ChartData hash. struct LabelIds { int consumed = -1; int allocations = -1; int allocated = -1; int temporary = -1; }; QHash labelIds; int64_t maxConsumedSinceLastTimeStamp = 0; int64_t lastTimeStamp = 0; StringCache stringCache; bool buildCharts = false; }; void setParents(QVector& children, const RowData* parent) { for (auto& row : children) { row.parent = parent; setParents(row.children, &row); } } TreeData mergeAllocations(const ParserData& data) { TreeData topRows; // merge allocations, leave parent pointers invalid (their location may change) for (const auto& allocation : data.allocations) { auto traceIndex = allocation.traceIndex; auto rows = &topRows; while (traceIndex) { const auto& trace = data.findTrace(traceIndex); const auto& ip = data.findIp(trace.ipIndex); auto location = data.stringCache.location(trace.ipIndex, ip); auto it = lower_bound(rows->begin(), rows->end(), location); if (it != rows->end() && it->location == location) { it->cost += allocation; } else { it = rows->insert(it, {allocation, location, nullptr, {}}); } if (data.isStopIndex(ip.functionIndex)) { break; } traceIndex = trace.parentIndex; rows = &it->children; } } // now set the parents, the data is constant from here on setParents(topRows, nullptr); return topRows; } RowData* findByLocation(const RowData& row, QVector* data) { for (int i = 0; i < data->size(); ++i) { if (data->at(i).location == row.location) { return data->data() + i; } } return nullptr; } AllocationData buildTopDown(const TreeData& bottomUpData, TreeData* topDownData) { AllocationData totalCost; for (const auto& row : bottomUpData) { // recurse and find the cost attributed to children const auto childCost = buildTopDown(row.children, topDownData); if (childCost != row.cost) { // this row is (partially) a leaf const auto cost = row.cost - childCost; // bubble up the parent chain to build a top-down tree auto node = &row; auto stack = topDownData; while (node) { auto data = findByLocation(*node, stack); if (!data) { // create an empty top-down item for this bottom-up node *stack << RowData{{}, node->location, nullptr, {}}; data = &stack->back(); } // always use the leaf node's cost and propagate that one up the chain // otherwise we'd count the cost of some nodes multiple times data->cost += cost; stack = &data->children; node = node->parent; } } totalCost += row.cost; } return totalCost; } QVector toTopDownData(const QVector& bottomUpData) { QVector topRows; buildTopDown(bottomUpData, &topRows); // now set the parents, the data is constant from here on setParents(topRows, nullptr); return topRows; } AllocationData buildCallerCallee(const TreeData& bottomUpData, CallerCalleeRows* callerCalleeData) { AllocationData totalCost; for (const auto& row : bottomUpData) { // recurse to find a leaf const auto childCost = buildCallerCallee(row.children, callerCalleeData); if (childCost != row.cost) { // this row is (partially) a leaf const auto cost = row.cost - childCost; // leaf node found, bubble up the parent chain to add cost for all frames // to the caller/callee data. this is done top-down since we must not count // symbols more than once in the caller-callee data QSet recursionGuard; auto node = &row; while (node) { const auto& location = node->location; if (!recursionGuard.contains(location)) { // aggregate caller-callee data auto it = lower_bound(callerCalleeData->begin(), callerCalleeData->end(), location, [](const CallerCalleeData& lhs, const LocationData::Ptr& rhs) { return lhs.location < rhs; }); if (it == callerCalleeData->end() || it->location != location) { it = callerCalleeData->insert(it, {{}, {}, location}); } it->inclusiveCost += cost; if (!node->parent) { it->selfCost += cost; } recursionGuard.insert(location); } node = node->parent; } } totalCost += row.cost; } return totalCost; } CallerCalleeRows toCallerCalleeData(const QVector& bottomUpData, bool diffMode) { CallerCalleeRows callerCalleeRows; buildCallerCallee(bottomUpData, &callerCalleeRows); if (diffMode) { // remove rows without cost callerCalleeRows.erase(remove_if(callerCalleeRows.begin(), callerCalleeRows.end(), [](const CallerCalleeData& data) -> bool { return data.inclusiveCost == AllocationData() && data.selfCost == AllocationData(); }), callerCalleeRows.end()); } return callerCalleeRows; } struct MergedHistogramColumnData { LocationData::Ptr location; int64_t allocations; bool operator<(const LocationData::Ptr& rhs) const { return location < rhs; } }; HistogramData buildSizeHistogram(ParserData& data) { HistogramData ret; if (data.allocationInfoCounter.empty()) { return ret; } sort(data.allocationInfoCounter.begin(), data.allocationInfoCounter.end()); const auto totalLabel = i18n("total"); HistogramRow row; const pair buckets[] = {{8, i18n("0B to 8B")}, {16, i18n("9B to 16B")}, {32, i18n("17B to 32B")}, {64, i18n("33B to 64B")}, {128, i18n("65B to 128B")}, {256, i18n("129B to 256B")}, {512, i18n("257B to 512B")}, {1024, i18n("512B to 1KB")}, {numeric_limits::max(), i18n("more than 1KB")}}; uint bucketIndex = 0; row.size = buckets[bucketIndex].first; row.sizeLabel = buckets[bucketIndex].second; vector columnData; columnData.reserve(128); auto insertColumns = [&]() { sort(columnData.begin(), columnData.end(), [](const MergedHistogramColumnData& lhs, const MergedHistogramColumnData& rhs) { return lhs.allocations > rhs.allocations; }); // -1 to account for total row for (size_t i = 0; i < min(columnData.size(), size_t(HistogramRow::NUM_COLUMNS - 1)); ++i) { const auto& column = columnData[i]; row.columns[i + 1] = {column.allocations, column.location}; } }; for (const auto& info : data.allocationInfoCounter) { if (info.info.size > row.size) { insertColumns(); columnData.clear(); ret << row; ++bucketIndex; row.size = buckets[bucketIndex].first; row.sizeLabel = buckets[bucketIndex].second; row.columns[0] = {info.allocations, {}}; } else { row.columns[0].allocations += info.allocations; } const auto ipIndex = data.findTrace(info.info.traceIndex).ipIndex; const auto ip = data.findIp(ipIndex); const auto location = data.stringCache.location(ipIndex, ip); auto it = lower_bound(columnData.begin(), columnData.end(), location); if (it == columnData.end() || it->location != location) { columnData.insert(it, {location, info.allocations}); } else { it->allocations += info.allocations; } } insertColumns(); ret << row; return ret; } } Parser::Parser(QObject* parent) : QObject(parent) { qRegisterMetaType(); } Parser::~Parser() = default; void Parser::parse(const QString& path, const QString& diffBase) { using namespace ThreadWeaver; stream() << make_job([this, path, diffBase]() { const auto stdPath = path.toStdString(); auto data = make_shared(); emit progressMessageAvailable(i18n("parsing data...")); if (!diffBase.isEmpty()) { ParserData diffData; auto readBase = async(launch::async, [&diffData, diffBase]() { return diffData.read(diffBase.toStdString()); }); if (!data->read(stdPath)) { emit failedToOpen(path); return; } if (!readBase.get()) { emit failedToOpen(diffBase); return; } data->diff(diffData); data->stringCache.diffMode = true; } else if (!data->read(stdPath)) { emit failedToOpen(path); return; } data->updateStringCache(); emit summaryAvailable({QString::fromStdString(data->debuggee), data->totalCost, data->totalTime, data->peakTime, data->peakRSS * data->systemInfo.pageSize, data->systemInfo.pages * data->systemInfo.pageSize, data->fromAttached}); emit progressMessageAvailable(i18n("merging allocations...")); // merge allocations before modifying the data again const auto mergedAllocations = mergeAllocations(*data); emit bottomUpDataAvailable(mergedAllocations); // also calculate the size histogram emit progressMessageAvailable(i18n("building size histogram...")); const auto sizeHistogram = buildSizeHistogram(*data); emit sizeHistogramDataAvailable(sizeHistogram); // now data can be modified again for the chart data evaluation const auto diffMode = data->stringCache.diffMode; emit progressMessageAvailable(i18n("building charts...")); auto parallel = new Collection; *parallel << make_job([this, mergedAllocations]() { const auto topDownData = toTopDownData(mergedAllocations); emit topDownDataAvailable(topDownData); }) << make_job([this, mergedAllocations, diffMode]() { const auto callerCalleeData = toCallerCalleeData(mergedAllocations, diffMode); emit callerCalleeDataAvailable(callerCalleeData); }); if (!data->stringCache.diffMode) { // only build charts when we are not diffing *parallel << make_job([this, data, stdPath]() { // this mutates data, and thus anything running in parallel must // not access data data->prepareBuildCharts(); data->read(stdPath); emit consumedChartDataAvailable(data->consumedChartData); emit allocationsChartDataAvailable(data->allocationsChartData); emit allocatedChartDataAvailable(data->allocatedChartData); emit temporaryChartDataAvailable(data->temporaryChartData); }); } auto sequential = new Sequence; *sequential << parallel << make_job([this]() { emit finished(); }); stream() << sequential; }); }