diff --git a/src/analyze/accumulatedtracedata.cpp b/src/analyze/accumulatedtracedata.cpp index 1323897..1d2729a 100644 --- a/src/analyze/accumulatedtracedata.cpp +++ b/src/analyze/accumulatedtracedata.cpp @@ -1,669 +1,669 @@ /* * Copyright 2015-2017 Milian Wolff * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "accumulatedtracedata.h" #include #include #include #include #include #include #include #include "util/config.h" #include "util/linereader.h" #include "util/pointermap.h" #ifdef __GNUC__ #define POTENTIALLY_UNUSED __attribute__((unused)) #else #define POTENTIALLY_UNUSED #endif using namespace std; namespace { template bool operator>>(LineReader& reader, Index& index) { return reader.readHex(index.index); } template ostream& operator<<(ostream& out, const Index index) { out << index.index; return out; } } AccumulatedTraceData::AccumulatedTraceData() { instructionPointers.reserve(16384); traces.reserve(65536); strings.reserve(4096); allocations.reserve(16384); stopIndices.reserve(4); opNewIpIndices.reserve(16); } const string& AccumulatedTraceData::stringify(const StringIndex stringId) const { if (!stringId || stringId.index > strings.size()) { static const string empty; return empty; } else { return strings.at(stringId.index - 1); } } string AccumulatedTraceData::prettyFunction(const string& function) const { if (!shortenTemplates) { return function; } string ret; ret.reserve(function.size()); int depth = 0; for (size_t i = 0; i < function.size(); ++i) { const auto c = function[i]; if ((c == '<' || c == '>') && ret.size() >= 8) { // don't get confused by C++ operators const char* cmp = "operator"; if (ret.back() == c) { // skip second angle bracket for operator<< or operator>> if (c == '<') { cmp = "operator<"; } else { cmp = "operator>"; } } if (boost::algorithm::ends_with(ret, cmp)) { ret.push_back(c); continue; } } if (c == '<') { ++depth; if (depth == 1) { ret.push_back(c); } } else if (c == '>') { --depth; } if (depth) { continue; } ret.push_back(c); } return ret; } bool AccumulatedTraceData::read(const string& inputFile) { const bool isCompressed = boost::algorithm::ends_with(inputFile, ".gz"); ifstream file(inputFile, isCompressed ? ios_base::in | ios_base::binary : ios_base::in); if (!file.is_open()) { cerr << "Failed to open heaptrack log file: " << inputFile << endl; return false; } boost::iostreams::filtering_istream in; if (isCompressed) { in.push(boost::iostreams::gzip_decompressor()); } in.push(file); return read(in); } bool AccumulatedTraceData::read(istream& in) { LineReader reader; int64_t timeStamp = 0; vector opNewStrings = { // 64 bit "operator new(unsigned long)", "operator new[](unsigned long)", // 32 bit "operator new(unsigned int)", "operator new[](unsigned int)", }; vector opNewStrIndices; opNewStrIndices.reserve(opNewStrings.size()); vector stopStrings = {"main", "__libc_start_main", "__static_initialization_and_destruction_0"}; const bool reparsing = totalTime != 0; m_maxAllocationTraceIndex.index = 0; totalCost = {}; peakTime = 0; systemInfo = {}; peakRSS = 0; allocations.clear(); uint fileVersion = 0; // required for backwards compatibility // newer versions handle this in heaptrack_interpret already AllocationInfoSet allocationInfoSet; PointerMap pointers; // in older files, this contains the pointer address, in newer formats // it holds the allocation info index. both can be used to find temporary // allocations, i.e. when a deallocation follows with the same data uint64_t lastAllocationPtr = 0; while (reader.getLine(in)) { if (reader.mode() == 's') { if (reparsing) { continue; } strings.push_back(reader.line().substr(2)); StringIndex index; index.index = strings.size(); auto opNewIt = find(opNewStrings.begin(), opNewStrings.end(), strings.back()); if (opNewIt != opNewStrings.end()) { opNewStrIndices.push_back(index); opNewStrings.erase(opNewIt); } else { auto stopIt = find(stopStrings.begin(), stopStrings.end(), strings.back()); if (stopIt != stopStrings.end()) { stopIndices.push_back(index); stopStrings.erase(stopIt); } } } else if (reader.mode() == 't') { if (reparsing) { continue; } TraceNode node; reader >> node.ipIndex; reader >> node.parentIndex; // skip operator new and operator new[] at the beginning of traces while (find(opNewIpIndices.begin(), opNewIpIndices.end(), node.ipIndex) != opNewIpIndices.end()) { node = findTrace(node.parentIndex); } traces.push_back(node); } else if (reader.mode() == 'i') { if (reparsing) { continue; } InstructionPointer ip; reader >> ip.instructionPointer; reader >> ip.moduleIndex; auto readFrame = [&reader](Frame* frame) { return (reader >> frame->functionIndex) && (reader >> frame->fileIndex) && (reader >> frame->line); }; if (readFrame(&ip.frame)) { Frame inlinedFrame; while (readFrame(&inlinedFrame)) { ip.inlined.push_back(inlinedFrame); } } instructionPointers.push_back(ip); if (find(opNewStrIndices.begin(), opNewStrIndices.end(), ip.frame.functionIndex) != opNewStrIndices.end()) { IpIndex index; index.index = instructionPointers.size(); opNewIpIndices.push_back(index); } } else if (reader.mode() == '+') { AllocationInfo info; AllocationIndex allocationIndex; if (fileVersion >= 1) { if (!(reader >> allocationIndex.index)) { cerr << "failed to parse line: " << reader.line() << endl; continue; } else if (allocationIndex.index >= allocationInfos.size()) { cerr << "allocation index out of bounds: " << allocationIndex.index << ", maximum is: " << allocationInfos.size() << endl; continue; } info = allocationInfos[allocationIndex.index]; lastAllocationPtr = allocationIndex.index; } else { // backwards compatibility uint64_t ptr = 0; if (!(reader >> info.size) || !(reader >> info.traceIndex) || !(reader >> ptr)) { cerr << "failed to parse line: " << reader.line() << endl; continue; } if (allocationInfoSet.add(info.size, info.traceIndex, &allocationIndex)) { allocationInfos.push_back(info); } pointers.addPointer(ptr, allocationIndex); lastAllocationPtr = ptr; } auto& allocation = findAllocation(info.traceIndex); allocation.leaked += info.size; allocation.allocated += info.size; ++allocation.allocations; - if (allocation.leaked > allocation.peak) { - allocation.peak = allocation.leaked; - } ++totalCost.allocations; totalCost.allocated += info.size; totalCost.leaked += info.size; if (totalCost.leaked > totalCost.peak) { totalCost.peak = totalCost.leaked; peakTime = timeStamp; + for (auto& allocation : allocations) { + allocation.peak = allocation.leaked; + } } handleAllocation(info, allocationIndex); } else if (reader.mode() == '-') { AllocationIndex allocationInfoIndex; bool temporary = false; if (fileVersion >= 1) { if (!(reader >> allocationInfoIndex.index)) { cerr << "failed to parse line: " << reader.line() << endl; continue; } temporary = lastAllocationPtr == allocationInfoIndex.index; } else { // backwards compatibility uint64_t ptr = 0; if (!(reader >> ptr)) { cerr << "failed to parse line: " << reader.line() << endl; continue; } auto taken = pointers.takePointer(ptr); if (!taken.second) { // happens when we attached to a running application continue; } allocationInfoIndex = taken.first; temporary = lastAllocationPtr == ptr; } lastAllocationPtr = 0; const auto& info = allocationInfos[allocationInfoIndex.index]; auto& allocation = findAllocation(info.traceIndex); allocation.leaked -= info.size; totalCost.leaked -= info.size; if (temporary) { ++allocation.temporary; ++totalCost.temporary; } } else if (reader.mode() == 'a') { if (reparsing) { continue; } AllocationInfo info; if (!(reader >> info.size) || !(reader >> info.traceIndex)) { cerr << "failed to parse line: " << reader.line() << endl; continue; } allocationInfos.push_back(info); } else if (reader.mode() == '#') { // comment or empty line continue; } else if (reader.mode() == 'c') { int64_t newStamp = 0; if (!(reader >> newStamp)) { cerr << "Failed to read time stamp: " << reader.line() << endl; continue; } handleTimeStamp(timeStamp, newStamp); timeStamp = newStamp; } else if (reader.mode() == 'R') { // RSS timestamp int64_t rss = 0; reader >> rss; if (rss > peakRSS) { peakRSS = rss; } } else if (reader.mode() == 'X') { handleDebuggee(reader.line().c_str() + 2); } else if (reader.mode() == 'A') { totalCost = {}; fromAttached = true; } else if (reader.mode() == 'v') { uint heaptrackVersion = 0; reader >> heaptrackVersion; if (!(reader >> fileVersion) && heaptrackVersion == 0x010200) { // backwards compatibility: before the 1.0.0, I actually // bumped the version to 0x010200 already and used that // as file version. This is what we now consider v1 of the // file format fileVersion = 1; } if (fileVersion > HEAPTRACK_FILE_FORMAT_VERSION) { cerr << "The data file has version " << hex << fileVersion << " and was written by heaptrack version " << hex << heaptrackVersion << ")\n" << "This is not compatible with this build of heaptrack (version " << hex << HEAPTRACK_VERSION << "), which can read file format version " << hex << HEAPTRACK_FILE_FORMAT_VERSION << " and below" << endl; return false; } } else if (reader.mode() == 'I') { // system information reader >> systemInfo.pageSize; reader >> systemInfo.pages; } else { cerr << "failed to parse line: " << reader.line() << endl; } } if (!reparsing) { totalTime = timeStamp + 1; } handleTimeStamp(timeStamp, totalTime); return true; } namespace { // helpers for diffing template vector sortedIndices(size_t numIndices, SortF sorter) { vector indices; indices.resize(numIndices); for (size_t i = 0; i < numIndices; ++i) { indices[i].index = (i + 1); } sort(indices.begin(), indices.end(), sorter); return indices; } vector remapStrings(vector& lhs, const vector& rhs) { unordered_map stringRemapping; StringIndex stringIndex; { stringRemapping.reserve(lhs.size()); for (const auto& string : lhs) { ++stringIndex.index; stringRemapping.insert(make_pair(string, stringIndex)); } } vector map; { map.reserve(rhs.size() + 1); map.push_back({}); for (const auto& string : rhs) { auto it = stringRemapping.find(string); if (it == stringRemapping.end()) { ++stringIndex.index; lhs.push_back(string); map.push_back(stringIndex); } else { map.push_back(it->second); } } } return map; } template inline const T& identity(const T& t) { return t; } template int compareTraceIndices(const TraceIndex& lhs, const AccumulatedTraceData& lhsData, const TraceIndex& rhs, const AccumulatedTraceData& rhsData, IpMapper ipMapper) { if (!lhs && !rhs) { return 0; } else if (lhs && !rhs) { return 1; } else if (rhs && !lhs) { return -1; } else if (&lhsData == &rhsData && lhs == rhs) { // fast-path if both indices are equal and we compare the same data return 0; } const auto& lhsTrace = lhsData.findTrace(lhs); const auto& rhsTrace = rhsData.findTrace(rhs); const int parentComparsion = compareTraceIndices(lhsTrace.parentIndex, lhsData, rhsTrace.parentIndex, rhsData, ipMapper); if (parentComparsion != 0) { return parentComparsion; } // else fall-through to below, parents are equal const auto& lhsIp = lhsData.findIp(lhsTrace.ipIndex); const auto& rhsIp = ipMapper(rhsData.findIp(rhsTrace.ipIndex)); if (lhsIp.equalWithoutAddress(rhsIp)) { return 0; } return lhsIp.compareWithoutAddress(rhsIp) ? -1 : 1; } POTENTIALLY_UNUSED void printTrace(const AccumulatedTraceData& data, TraceIndex index) { do { const auto trace = data.findTrace(index); const auto& ip = data.findIp(trace.ipIndex); cerr << index << " (" << trace.ipIndex << ", " << trace.parentIndex << ")" << '\t' << data.stringify(ip.frame.functionIndex) << " in " << data.stringify(ip.moduleIndex) << " at " << data.stringify(ip.frame.fileIndex) << ':' << ip.frame.line << '\n'; for (const auto& inlined : ip.inlined) { cerr << '\t' << data.stringify(inlined.functionIndex) << " at " << data.stringify(inlined.fileIndex) << ':' << inlined.line << '\n'; } index = trace.parentIndex; } while (index); cerr << "---\n"; } } void AccumulatedTraceData::diff(const AccumulatedTraceData& base) { totalCost -= base.totalCost; totalTime -= base.totalTime; peakRSS -= base.peakRSS; systemInfo.pages -= base.systemInfo.pages; systemInfo.pageSize -= base.systemInfo.pageSize; // step 1: sort trace indices of allocations for efficient lookup // step 2: while at it, also merge equal allocations vector allocationTraceNodes; allocationTraceNodes.reserve(allocations.size()); for (auto it = allocations.begin(); it != allocations.end();) { const auto& allocation = *it; auto sortedIt = lower_bound(allocationTraceNodes.begin(), allocationTraceNodes.end(), allocation.traceIndex, [this](const TraceIndex& lhs, const TraceIndex& rhs) -> bool { return compareTraceIndices(lhs, *this, rhs, *this, identity) < 0; }); if (sortedIt == allocationTraceNodes.end() || compareTraceIndices(allocation.traceIndex, *this, *sortedIt, *this, identity) != 0) { allocationTraceNodes.insert(sortedIt, allocation.traceIndex); ++it; } else if (*sortedIt != allocation.traceIndex) { findAllocation(*sortedIt) += allocation; it = allocations.erase(it); } else { ++it; } } // step 3: map string indices from rhs to lhs data const auto& stringMap = remapStrings(strings, base.strings); auto remapString = [&stringMap](StringIndex& index) { if (index) { index.index = stringMap[index.index].index; } }; auto remapFrame = [&remapString](Frame frame) -> Frame { remapString(frame.functionIndex); remapString(frame.fileIndex); return frame; }; auto remapIp = [&remapString, &remapFrame](InstructionPointer ip) -> InstructionPointer { remapString(ip.moduleIndex); remapFrame(ip.frame); for (auto& inlined : ip.inlined) { inlined = remapFrame(inlined); } return ip; }; // step 4: iterate over rhs data and find matching traces // if no match is found, copy the data over auto sortedIps = sortedIndices(instructionPointers.size(), [this](const IpIndex& lhs, const IpIndex& rhs) { return findIp(lhs).compareWithoutAddress(findIp(rhs)); }); // map an IpIndex from the rhs data into the lhs data space, or copy the data // if it does not exist yet auto remapIpIndex = [&sortedIps, this, &base, &remapIp](IpIndex rhsIndex) -> IpIndex { if (!rhsIndex) { return rhsIndex; } const auto& rhsIp = base.findIp(rhsIndex); const auto& lhsIp = remapIp(rhsIp); auto it = lower_bound(sortedIps.begin(), sortedIps.end(), lhsIp, [this](const IpIndex& lhs, const InstructionPointer& rhs) { return findIp(lhs).compareWithoutAddress(rhs); }); if (it != sortedIps.end() && findIp(*it).equalWithoutAddress(lhsIp)) { return *it; } instructionPointers.push_back(lhsIp); IpIndex ret; ret.index = instructionPointers.size(); sortedIps.insert(it, ret); return ret; }; // copy the rhs trace index and the data it references into the lhs data, // recursively function copyTrace = [this, &base, remapIpIndex, ©Trace](TraceIndex rhsIndex) -> TraceIndex { if (!rhsIndex) { return rhsIndex; } // new location, add it const auto& rhsTrace = base.findTrace(rhsIndex); TraceNode node; node.parentIndex = copyTrace(rhsTrace.parentIndex); node.ipIndex = remapIpIndex(rhsTrace.ipIndex); traces.push_back(node); TraceIndex ret; ret.index = traces.size(); return ret; }; // find an equivalent trace or copy the data over if it does not exist yet // a trace is equivalent if the complete backtrace has equal // InstructionPointer // data while ignoring the actual pointer address auto remapTrace = [&base, &allocationTraceNodes, this, remapIp, copyTrace](TraceIndex rhsIndex) -> TraceIndex { if (!rhsIndex) { return rhsIndex; } auto it = lower_bound(allocationTraceNodes.begin(), allocationTraceNodes.end(), rhsIndex, [&base, this, remapIp](const TraceIndex& lhs, const TraceIndex& rhs) -> bool { return compareTraceIndices(lhs, *this, rhs, base, remapIp) < 0; }); if (it != allocationTraceNodes.end() && compareTraceIndices(*it, *this, rhsIndex, base, remapIp) == 0) { return *it; } TraceIndex ret = copyTrace(rhsIndex); allocationTraceNodes.insert(it, ret); return ret; }; for (const auto& rhsAllocation : base.allocations) { const auto lhsTrace = remapTrace(rhsAllocation.traceIndex); assert(remapIp(base.findIp(base.findTrace(rhsAllocation.traceIndex).ipIndex)) .equalWithoutAddress(findIp(findTrace(lhsTrace).ipIndex))); findAllocation(lhsTrace) -= rhsAllocation; } // step 5: remove allocations that don't show any differences // note that when there are differences in the backtraces, // we can still end up with merged backtraces that have a total // of 0, but different "tails" of different origin with non-zero cost allocations.erase(remove_if(allocations.begin(), allocations.end(), [](const Allocation& allocation) -> bool { return allocation == AllocationData(); }), allocations.end()); } Allocation& AccumulatedTraceData::findAllocation(const TraceIndex traceIndex) { if (traceIndex < m_maxAllocationTraceIndex) { // only need to search when the trace index is previously known auto it = lower_bound(allocations.begin(), allocations.end(), traceIndex, [](const Allocation& allocation, const TraceIndex traceIndex) -> bool { return allocation.traceIndex < traceIndex; }); if (it == allocations.end() || it->traceIndex != traceIndex) { Allocation allocation; allocation.traceIndex = traceIndex; it = allocations.insert(it, allocation); } return *it; } else if (traceIndex == m_maxAllocationTraceIndex && !allocations.empty()) { // reuse the last allocation assert(allocations.back().traceIndex == traceIndex); } else { // actually a new allocation Allocation allocation; allocation.traceIndex = traceIndex; allocations.push_back(allocation); m_maxAllocationTraceIndex = traceIndex; } return allocations.back(); } InstructionPointer AccumulatedTraceData::findIp(const IpIndex ipIndex) const { if (!ipIndex || ipIndex.index > instructionPointers.size()) { return {}; } else { return instructionPointers[ipIndex.index - 1]; } } TraceNode AccumulatedTraceData::findTrace(const TraceIndex traceIndex) const { if (!traceIndex || traceIndex.index > traces.size()) { return {}; } else { return traces[traceIndex.index - 1]; } } bool AccumulatedTraceData::isStopIndex(const StringIndex index) const { return find(stopIndices.begin(), stopIndices.end(), index) != stopIndices.end(); } diff --git a/src/analyze/gui/flamegraph.cpp b/src/analyze/gui/flamegraph.cpp index e975b5e..aee21aa 100644 --- a/src/analyze/gui/flamegraph.cpp +++ b/src/analyze/gui/flamegraph.cpp @@ -1,769 +1,769 @@ /* * Copyright 2015-2017 Milian Wolff * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; 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 #include enum CostType { Allocations, Temporary, Peak, Leaked, Allocated }; Q_DECLARE_METATYPE(CostType) namespace { QString fraction(qint64 cost, qint64 totalCost) { return QString::number(double(cost) * 100. / totalCost, 'g', 3); } enum SearchMatchType { NoSearch, NoMatch, DirectMatch, ChildMatch }; } 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; void setSearchMatchType(SearchMatchType matchType); protected: void hoverEnterEvent(QGraphicsSceneHoverEvent* event) override; void hoverLeaveEvent(QGraphicsSceneHoverEvent* event) override; private: qint64 m_cost; QString m_function; CostType m_costType; bool m_isHovered; SearchMatchType m_searchMatch = NoSearch; }; 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 || m_searchMatch == DirectMatch) { auto selectedColor = brush().color(); selectedColor.setAlpha(255); painter->fillRect(rect(), selectedColor); } else if (m_searchMatch == NoMatch) { auto noMatchColor = brush().color(); noMatchColor.setAlpha(50); painter->fillRect(rect(), noMatchColor); } else { // default, when no search is running, or a sub-item is matched painter->fillRect(rect(), brush()); } const QPen oldPen = painter->pen(); auto pen = oldPen; if (m_searchMatch != NoMatch) { 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; } if (m_searchMatch == NoMatch) { auto color = oldPen.color(); color.setAlpha(125); pen.setColor(color); painter->setPen(pen); } 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)); if (m_searchMatch == NoMatch) { painter->setPen(oldPen); } } void FrameGraphicsItem::hoverEnterEvent(QGraphicsSceneHoverEvent* event) { QGraphicsRectItem::hoverEnterEvent(event); m_isHovered = true; update(); } void FrameGraphicsItem::hoverLeaveEvent(QGraphicsSceneHoverEvent* event) { QGraphicsRectItem::hoverLeaveEvent(event); m_isHovered = false; update(); } 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 = m_function; 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.", + "%1 (%2%) contribution to peak consumption in %3 and below.", format.formatByteSize(m_cost, 1, KFormat::MetricBinaryDialect), 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, 1, KFormat::MetricBinaryDialect), 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, 1, KFormat::MetricBinaryDialect), fraction, function); break; } return tooltip; } void FrameGraphicsItem::setSearchMatchType(SearchMatchType matchType) { if (m_searchMatch != matchType) { m_searchMatch = matchType; update(); } } 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 != 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, 1, KFormat::MetricBinaryDialect)); + label = i18n("%1 contribution to peak consumption", format.formatByteSize(totalCost, 1, KFormat::MetricBinaryDialect)); break; case Leaked: label = i18n("%1 leaked in total", format.formatByteSize(totalCost, 1, KFormat::MetricBinaryDialect)); break; case Allocated: label = i18n("%1 allocated in total", format.formatByteSize(totalCost, 1, KFormat::MetricBinaryDialect)); 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; } struct SearchResults { SearchMatchType matchType = NoMatch; qint64 directCost = 0; }; SearchResults applySearch(FrameGraphicsItem* item, const QString& searchValue) { SearchResults result; if (searchValue.isEmpty()) { result.matchType = NoSearch; } else if (item->function().contains(searchValue, Qt::CaseInsensitive)) { result.directCost += item->cost(); result.matchType = DirectMatch; } // recurse into the child items, we always need to update all items for (auto* child : item->childItems()) { auto* childFrame = static_cast(child); auto childMatch = applySearch(childFrame, searchValue); if (result.matchType != DirectMatch && (childMatch.matchType == DirectMatch || childMatch.matchType == ChildMatch)) { result.matchType = ChildMatch; result.directCost += childMatch.directCost; } } item->setSearchMatchType(result.matchType); return result; } } 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) , m_searchResultsLabel(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 " + m_costSource->addItem(i18n("Memory Peak"), QVariant::fromValue(Peak)); + m_costSource->setItemData(2, i18n("Show a flame graph over the contributions to 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.")); bottomUpCheckbox->setChecked(m_showBottomUpData); 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_searchInput = new QLineEdit(this); m_searchInput->setPlaceholderText(i18n("Search...")); m_searchInput->setToolTip(i18n("Search the flame graph for a symbol.")); m_searchInput->setClearButtonEnabled(true); connect(m_searchInput, &QLineEdit::textChanged, this, &FlameGraph::setSearchValue); 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); controls->layout()->addWidget(m_searchInput); m_displayLabel->setWordWrap(true); m_displayLabel->setTextInteractionFlags(m_displayLabel->textInteractionFlags() | Qt::TextSelectableByMouse); m_searchResultsLabel->setWordWrap(true); m_searchResultsLabel->setTextInteractionFlags(m_searchResultsLabel->textInteractionFlags() | Qt::TextSelectableByMouse); m_searchResultsLabel->hide(); setLayout(new QVBoxLayout); layout()->addWidget(controls); layout()->addWidget(m_view); layout()->addWidget(m_displayLabel); layout()->addWidget(m_searchResultsLabel); m_backAction = KStandardAction::back(this, SLOT(navigateBack()), this); addAction(m_backAction); m_forwardAction = KStandardAction::forward(this, SLOT(navigateForward()), this); addAction(m_forwardAction); updateNavigationActions(); 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); updateNavigationActions(); } } } 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::ToolTip) { const auto& tooltip = m_displayLabel->toolTip(); if (tooltip.isEmpty()) { QToolTip::hideText(); } else { QToolTip::showText(QCursor::pos(), QLatin1String("") + tooltip.toHtmlEscaped() + QLatin1String(""), this); } event->accept(); return true; } 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(); m_displayLabel->setText(metrics.elidedText(text, Qt::ElideRight, m_displayLabel->width())); } void FlameGraph::setData(FrameGraphicsItem* rootItem) { m_scene->clear(); m_buildingScene = false; m_tooltipItem = nullptr; m_rootItem = rootItem; m_selectionHistory.clear(); m_selectionHistory.push_back(rootItem); m_selectedItem = 0; updateNavigationActions(); 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 (!m_searchInput->text().isEmpty()) { setSearchValue(m_searchInput->text()); } 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::setSearchValue(const QString& value) { if (!m_rootItem) { return; } auto match = applySearch(m_rootItem, value); if (value.isEmpty()) { m_searchResultsLabel->hide(); } else { QString label; KFormat format; const auto costFraction = fraction(match.directCost, m_rootItem->cost()); switch (m_costSource->currentData().value()) { case Allocations: case Temporary: label = i18n("%1 (%2% of total of %3) allocations matched by search.", match.directCost, costFraction, m_rootItem->cost()); break; case Peak: case Leaked: case Allocated: label = i18n("%1 (%2% of total of %3) matched by search.", format.formatByteSize(match.directCost, 1, KFormat::MetricBinaryDialect), costFraction, format.formatByteSize(m_rootItem->cost(), 1, KFormat::MetricBinaryDialect)); break; } m_searchResultsLabel->setText(label); m_searchResultsLabel->show(); } } void FlameGraph::navigateBack() { if (m_selectedItem > 0) { --m_selectedItem; } updateNavigationActions(); selectItem(m_selectionHistory.at(m_selectedItem)); } void FlameGraph::navigateForward() { if ((m_selectedItem + 1) < m_selectionHistory.size()) { ++m_selectedItem; } updateNavigationActions(); selectItem(m_selectionHistory.at(m_selectedItem)); } void FlameGraph::updateNavigationActions() { m_backAction->setEnabled(m_selectedItem > 0); m_forwardAction->setEnabled(m_selectedItem + 1 < m_selectionHistory.size()); } diff --git a/src/analyze/gui/mainwindow.ui b/src/analyze/gui/mainwindow.ui index 5d44d4c..d484d5e 100644 --- a/src/analyze/gui/mainwindow.ui +++ b/src/analyze/gui/mainwindow.ui @@ -1,809 +1,809 @@ MainWindow 0 0 1332 896 MainWindow 0 0 0 0 KMessageWidget::Information .. Qt::Vertical 20 40 Open Heaptrack Data <qt><p>This field specifies the primary heaptrack data file. These files are called <tt>heaptrack.$APP.$PID.gz</tt>. You can produce such a file by profiling your application, e.g. via:</p> <pre><code>heaptrack &lt;yourapplication&gt; ...</code></pre> <p>Or, alternatively, you can attach to a running process via</p> <pre><code>heaptrack --pid $(pidof &lt;yourapplication&gt;)</code></pre></qt> heaptrack.*.*.gz path/to/heaptrack.$APP.$PID.gz Qt::WindowModal <qt>You can optionally specify a second heaptrack data file to compare to. If set, this file will be used as a base and its cost gets subtracted from the primary data costs.</qt> heaptrack.*.*.gz path/to/heaptrack.$APP.$PID.gz Qt::WindowModal Profile &Data: openFile Compare to: compareTo false QDialogButtonBox::Open Qt::Vertical 20 40 Qt::Vertical 20 40 Loading file, please wait... Qt::AlignCenter 24 Progress Message... Qt::AlignCenter true Qt::Vertical 20 40 0 0 0 0 0 Summary summary true Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse summary true Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse 0 0 summary goes here Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop true Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse Qt::Horizontal 0 0 Qt::Horizontal 75 true List of functions that allocated the most memory at a given time. - Highest Memory Peaks + Peak Contributions Qt::AlignCenter List of functions that allocated the most memory at a given time. true false true 75 true List of functions that leak the most memory. Largest Memory Leaks Qt::AlignCenter List of functions that leak the most memory. true false true 75 true List of functions that allocate memory most often. Most Memory Allocations Qt::AlignCenter List of functions that allocate memory most often. true false true 75 true List of functions that produced the most temporary memory allocations. Most Temporary Allocations Qt::AlignCenter List of functions that produced the most temporary memory allocations. true false true 75 true List of functions that allocated the most memory overall, ignoring deallocations. Most Memory Allocated Qt::AlignCenter List of functions that allocated the most memory overall, ignoring deallocations. true false true Bottom-Up 6 0 0 0 0 filter by function... filter by file... filter by module... true 10 true true true Caller / Callee 6 0 0 0 0 filter by function... filter by file... filter by module... true false true true Top-Down 0 0 0 0 filter by function... filter by file... filter by module... true 10 true true true Flame Graph QDockWidget::NoDockWidgetFeatures S&tacks 2 0 0 0 0 Selected Stack: false true 0 0 1332 30 &File KMessageWidget QFrame
kmessagewidget.h
KUrlRequester QWidget
kurlrequester.h
FlameGraph QWidget
flamegraph.h
1
diff --git a/src/analyze/gui/treemodel.cpp b/src/analyze/gui/treemodel.cpp index c410073..d10dc6e 100644 --- a/src/analyze/gui/treemodel.cpp +++ b/src/analyze/gui/treemodel.cpp @@ -1,346 +1,346 @@ /* * Copyright 2015-2017 Milian Wolff * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "treemodel.h" #include #include #include #include #include namespace { int indexOf(const RowData* row, const TreeData& siblings) { Q_ASSERT(siblings.data() <= row); Q_ASSERT(siblings.data() + siblings.size() > row); return row - siblings.data(); } const RowData* rowAt(const TreeData& rows, int row) { Q_ASSERT(rows.size() > row); return rows.data() + row; } /// @return the parent row containing @p index const RowData* toParentRow(const QModelIndex& index) { return static_cast(index.internalPointer()); } QString basename(const QString& path) { int idx = path.lastIndexOf(QLatin1Char('/')); return path.mid(idx + 1); } } TreeModel::TreeModel(QObject* parent) : QAbstractItemModel(parent) { qRegisterMetaType(); } TreeModel::~TreeModel() { } QVariant TreeModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation != Qt::Horizontal || section < 0 || section >= NUM_COLUMNS) { return {}; } if (role == Qt::InitialSortOrderRole) { if (section == AllocatedColumn || section == AllocationsColumn || section == PeakColumn || section == LeakedColumn || section == TemporaryColumn) { return Qt::DescendingOrder; } } if (role == Qt::DisplayRole) { switch (static_cast(section)) { case FileColumn: return i18n("File"); case LineColumn: return i18n("Line"); case FunctionColumn: return i18n("Function"); case ModuleColumn: return i18n("Module"); case AllocationsColumn: return i18n("Allocations"); case TemporaryColumn: return i18n("Temporary"); case PeakColumn: return i18n("Peak"); case LeakedColumn: return i18n("Leaked"); case AllocatedColumn: return i18n("Allocated"); case LocationColumn: return i18n("Location"); case NUM_COLUMNS: break; } } else if (role == Qt::ToolTipRole) { switch (static_cast(section)) { case FileColumn: return i18n("The file where the allocation function was called from. " "May be empty when debug information is missing."); case LineColumn: return i18n("The line number where the allocation function was called from. " "May be empty when debug information is missing."); case FunctionColumn: return i18n("The parent function that called an allocation function. " "May be unknown when debug information is missing."); case ModuleColumn: return i18n("The module, i.e. executable or shared library, from " "which an allocation function was " "called."); case AllocationsColumn: return i18n("The number of times an allocation function was called " "from this location."); case TemporaryColumn: return i18n("The number of temporary allocations. These allocations " "are directly followed by a free " "without any other allocations in-between."); case PeakColumn: - return i18n("The maximum heap memory in bytes consumed from " - "allocations originating at this location. " - "This takes deallocations into account."); + return i18n("The contributions from a given location to the maximum heap " + "memory consumption in bytes. This takes deallocations " + "into account."); case LeakedColumn: return i18n("The bytes allocated at this location that have not been " "deallocated."); case AllocatedColumn: return i18n("The sum of all bytes allocated from this location, " "ignoring deallocations."); case LocationColumn: return i18n("The location from which an allocation function was " "called. Function symbol and file " "information " "may be unknown when debug information was missing when " "heaptrack was run."); case NUM_COLUMNS: break; } } return {}; } QVariant TreeModel::data(const QModelIndex& index, int role) const { if (index.row() < 0 || index.column() < 0 || index.column() > NUM_COLUMNS) { return {}; } const auto row = (role == MaxCostRole) ? &m_maxCost : toRow(index); if (role == Qt::DisplayRole || role == SortRole || role == MaxCostRole) { switch (static_cast(index.column())) { case AllocatedColumn: if (role == SortRole || role == MaxCostRole) { return static_cast(abs(row->cost.allocated)); } return m_format.formatByteSize(row->cost.allocated, 1, KFormat::MetricBinaryDialect); case AllocationsColumn: if (role == SortRole || role == MaxCostRole) { return static_cast(abs(row->cost.allocations)); } return static_cast(row->cost.allocations); case TemporaryColumn: if (role == SortRole || role == MaxCostRole) { return static_cast(abs(row->cost.temporary)); } return static_cast(row->cost.temporary); case PeakColumn: if (role == SortRole || role == MaxCostRole) { return static_cast(abs(row->cost.peak)); } else { return m_format.formatByteSize(row->cost.peak, 1, KFormat::MetricBinaryDialect); } case LeakedColumn: if (role == SortRole || role == MaxCostRole) { return static_cast(abs(row->cost.leaked)); } else { return m_format.formatByteSize(row->cost.leaked, 1, KFormat::MetricBinaryDialect); } case FunctionColumn: return row->location->function; case ModuleColumn: return row->location->module; case FileColumn: return row->location->file; case LineColumn: return row->location->line; case LocationColumn: if (row->location->file.isEmpty()) { return i18n("%1 in ?? (%2)", basename(row->location->function), basename(row->location->module)); } else { return i18n("%1 in %2:%3 (%4)", row->location->function, basename(row->location->file), row->location->line, basename(row->location->module)); } case NUM_COLUMNS: break; } } else if (role == Qt::ToolTipRole) { QString tooltip; QTextStream stream(&tooltip); stream << "
";
         if (row->location->line > 0) {
             stream << i18nc("1: function, 2: file, 3: line, 4: module", "%1\n  at %2:%3\n  in %4",
                             row->location->function.toHtmlEscaped(), row->location->file.toHtmlEscaped(),
                             row->location->line, row->location->module.toHtmlEscaped());
         } else {
             stream << i18nc("1: function, 2: module", "%1\n  in %2", row->location->function.toHtmlEscaped(),
                             row->location->module.toHtmlEscaped());
         }
         stream << '\n';
         stream << '\n';
         KFormat format;
         const auto allocatedFraction =
             QString::number(double(row->cost.allocated) * 100. / m_maxCost.cost.allocated, 'g', 3);
         const auto peakFraction = QString::number(double(row->cost.peak) * 100. / m_maxCost.cost.peak, 'g', 3);
         const auto leakedFraction = QString::number(double(row->cost.leaked) * 100. / m_maxCost.cost.leaked, 'g', 3);
         const auto allocationsFraction =
             QString::number(double(row->cost.allocations) * 100. / m_maxCost.cost.allocations, 'g', 3);
         const auto temporaryFraction =
             QString::number(double(row->cost.temporary) * 100. / row->cost.allocations, 'g', 3);
         const auto temporaryFractionTotal =
             QString::number(double(row->cost.temporary) * 100. / m_maxCost.cost.temporary, 'g', 3);
         stream << i18n("allocated: %1 (%2% of total)\n",
                        format.formatByteSize(row->cost.allocated, 1, KFormat::MetricBinaryDialect), allocatedFraction);
-        stream << i18n("peak: %1 (%2% of total)\n",
+        stream << i18n("peak contribution: %1 (%2% of total)\n",
                        format.formatByteSize(row->cost.peak, 1, KFormat::MetricBinaryDialect), peakFraction);
         stream << i18n("leaked: %1 (%2% of total)\n",
                        format.formatByteSize(row->cost.leaked, 1, KFormat::MetricBinaryDialect), leakedFraction);
         stream << i18n("allocations: %1 (%2% of total)\n", row->cost.allocations, allocationsFraction);
         stream << i18n("temporary: %1 (%2% of allocations, %3% of total)\n", row->cost.temporary, temporaryFraction,
                        temporaryFractionTotal);
         if (!row->children.isEmpty()) {
             auto child = row;
             int max = 5;
             if (child->children.count() == 1) {
                 stream << '\n' << i18n("backtrace:") << '\n';
             }
             while (child->children.count() == 1 && max-- > 0) {
                 stream << "\n";
                 if (child->location->line > 0) {
                     stream << i18nc("1: function, 2: file, 3: line, 4: module", "%1\n  at %2:%3\n  in %4",
                                     child->location->function.toHtmlEscaped(), child->location->file.toHtmlEscaped(),
                                     child->location->line, child->location->module.toHtmlEscaped());
                 } else {
                     stream << i18nc("1: function, 2: module", "%1\n  in %2", child->location->function.toHtmlEscaped(),
                                     child->location->module.toHtmlEscaped());
                 }
                 child = child->children.data();
             }
             if (child->children.count() > 1) {
                 stream << "\n";
                 stream << i18np("called from one location", "called from %1 locations", child->children.count());
             }
         }
         stream << "
"; return tooltip; } else if (role == LocationRole) { return QVariant::fromValue(row->location); } return {}; } QModelIndex TreeModel::index(int row, int column, const QModelIndex& parent) const { if (row < 0 || column < 0 || column >= NUM_COLUMNS || row >= rowCount(parent)) { return QModelIndex(); } return createIndex(row, column, const_cast(reinterpret_cast(toRow(parent)))); } QModelIndex TreeModel::parent(const QModelIndex& child) const { if (!child.isValid()) { return {}; } const auto parent = toParentRow(child); if (!parent) { return {}; } return createIndex(rowOf(parent), 0, const_cast(reinterpret_cast(parent->parent))); } int TreeModel::rowCount(const QModelIndex& parent) const { if (!parent.isValid()) { return m_data.size(); } else if (parent.column() != 0) { return 0; } auto row = toRow(parent); Q_ASSERT(row); return row->children.size(); } int TreeModel::columnCount(const QModelIndex& /*parent*/) const { return NUM_COLUMNS; } void TreeModel::resetData(const TreeData& data) { beginResetModel(); m_data = data; endResetModel(); } void TreeModel::setSummary(const SummaryData& data) { beginResetModel(); m_maxCost.cost = data.cost; endResetModel(); } void TreeModel::clearData() { beginResetModel(); m_data = {}; m_maxCost = {}; endResetModel(); } const RowData* TreeModel::toRow(const QModelIndex& index) const { if (!index.isValid()) { return nullptr; } if (const auto parent = toParentRow(index)) { return rowAt(parent->children, index.row()); } else { return rowAt(m_data, index.row()); } } int TreeModel::rowOf(const RowData* row) const { if (auto parent = row->parent) { return indexOf(row, parent->children); } else { return indexOf(row, m_data); } } diff --git a/src/analyze/print/heaptrack_print.cpp b/src/analyze/print/heaptrack_print.cpp index a67e371..aedecd4 100644 --- a/src/analyze/print/heaptrack_print.cpp +++ b/src/analyze/print/heaptrack_print.cpp @@ -1,774 +1,764 @@ /* * Copyright 2014-2016 Milian Wolff * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ /** * @file heaptrack_print.cpp * * @brief Evaluate and print the collected heaptrack data. */ #include #include "analyze/accumulatedtracedata.h" #include #include #include #include "util/config.h" using namespace std; namespace po = boost::program_options; namespace { /** * Merged allocation information by instruction pointer outside of alloc funcs */ struct MergedAllocation : public AllocationData { // individual backtraces std::vector traces; // location IpIndex ipIndex; }; class formatBytes { public: formatBytes(int64_t bytes) : m_bytes(bytes) { } friend ostream& operator<<(ostream& out, const formatBytes data); private: int64_t m_bytes; }; ostream& operator<<(ostream& out, const formatBytes data) { if (data.m_bytes < 0) { // handle negative values return out << '-' << formatBytes(-data.m_bytes); } if (data.m_bytes < 1000) { // no fancy formatting for plain byte values, esp. no .00 factions return out << data.m_bytes << 'B'; } static const auto units = {"B", "KB", "MB", "GB", "TB"}; auto unit = units.begin(); size_t i = 0; double bytes = data.m_bytes; while (i < units.size() - 1 && bytes > 1000.) { bytes /= 1000.; ++i; ++unit; } return out << fixed << setprecision(2) << bytes << *unit; } struct Printer final : public AccumulatedTraceData { void finalize() { filterAllocations(); mergedAllocations = mergeAllocations(allocations); } void mergeAllocation(vector* mergedAllocations, const Allocation& allocation) const { const auto trace = findTrace(allocation.traceIndex); const auto traceIp = findIp(trace.ipIndex); auto it = lower_bound(mergedAllocations->begin(), mergedAllocations->end(), traceIp, [this](const MergedAllocation& allocation, const InstructionPointer traceIp) -> bool { // Compare meta data without taking the instruction pointer address into account. // This is useful since sometimes, esp. when we lack debug symbols, the same // function allocates memory at different IP addresses which is pretty useless // information most of the time // TODO: make this configurable, but on-by-default const auto allocationIp = findIp(allocation.ipIndex); return allocationIp.compareWithoutAddress(traceIp); }); if (it == mergedAllocations->end() || !findIp(it->ipIndex).equalWithoutAddress(traceIp)) { MergedAllocation merged; merged.ipIndex = trace.ipIndex; it = mergedAllocations->insert(it, merged); } it->traces.push_back(allocation); } // merge allocations so that different traces that point to the same // instruction pointer at the end where the allocation function is // called are combined vector mergeAllocations(const vector& allocations) const { // TODO: merge deeper traces, i.e. A,B,C,D and A,B,C,F // should be merged to A,B,C: D & F // currently the below will only merge it to: A: B,C,D & B,C,F vector ret; ret.reserve(allocations.size()); for (const Allocation& allocation : allocations) { if (allocation.traceIndex) { mergeAllocation(&ret, allocation); } } for (MergedAllocation& merged : ret) { for (const Allocation& allocation : merged.traces) { merged.allocated += allocation.allocated; merged.allocations += allocation.allocations; merged.leaked += allocation.leaked; merged.peak += allocation.peak; merged.temporary += allocation.temporary; } } return ret; } void filterAllocations() { if (filterBtFunction.empty()) { return; } allocations.erase(remove_if(allocations.begin(), allocations.end(), [&](const Allocation& allocation) -> bool { auto node = findTrace(allocation.traceIndex); while (node.ipIndex) { const auto& ip = findIp(node.ipIndex); if (isStopIndex(ip.frame.functionIndex)) { break; } auto matchFunction = [this](const Frame& frame) { return stringify(frame.functionIndex).find(filterBtFunction) != string::npos; }; if (matchFunction(ip.frame)) { return false; } for (const auto& inlined : ip.inlined) { if (matchFunction(inlined)) { return false; } } node = findTrace(node.parentIndex); }; return true; }), allocations.end()); } void printIndent(ostream& out, size_t indent, const char* indentString = " ") const { while (indent--) { out << indentString; } } void printIp(const IpIndex ip, ostream& out, const size_t indent = 0) const { printIp(findIp(ip), out, indent); } void printIp(const InstructionPointer& ip, ostream& out, const size_t indent = 0, bool flameGraph = false) const { printIndent(out, indent); if (ip.frame.functionIndex) { out << prettyFunction(stringify(ip.frame.functionIndex)); } else { out << "0x" << hex << ip.instructionPointer << dec; } if (flameGraph) { // only print the file name but nothing else auto printFile = [this, &out](FileIndex fileIndex) { const auto& file = stringify(fileIndex); auto idx = file.find_last_of('/') + 1; out << " (" << file.substr(idx) << ")"; }; if (ip.frame.fileIndex) { printFile(ip.frame.fileIndex); } out << ';'; for (const auto& inlined : ip.inlined) { out << prettyFunction(stringify(inlined.functionIndex)); printFile(inlined.fileIndex); out << ';'; } return; } out << '\n'; printIndent(out, indent + 1); if (ip.frame.fileIndex) { out << "at " << stringify(ip.frame.fileIndex) << ':' << ip.frame.line << '\n'; printIndent(out, indent + 1); } if (ip.moduleIndex) { out << "in " << stringify(ip.moduleIndex); } else { out << "in ??"; } out << '\n'; for (const auto& inlined : ip.inlined) { printIndent(out, indent); out << prettyFunction(stringify(inlined.functionIndex)) << '\n'; printIndent(out, indent + 1); out << "at " << stringify(inlined.fileIndex) << ':' << inlined.line << '\n'; } } void printBacktrace(const TraceIndex traceIndex, ostream& out, const size_t indent = 0, bool skipFirst = false) const { if (!traceIndex) { out << " ??"; return; } printBacktrace(findTrace(traceIndex), out, indent, skipFirst); } void printBacktrace(TraceNode node, ostream& out, const size_t indent = 0, bool skipFirst = false) const { while (node.ipIndex) { const auto& ip = findIp(node.ipIndex); if (!skipFirst) { printIp(ip, out, indent); } skipFirst = false; if (isStopIndex(ip.frame.functionIndex)) { break; } node = findTrace(node.parentIndex); }; } /** * recursive top-down printer in the format * * func1;func2 (file);func2 (file); */ void printFlamegraph(TraceNode node, ostream& out) const { if (!node.ipIndex) { return; } const auto& ip = findIp(node.ipIndex); if (!isStopIndex(ip.frame.functionIndex)) { printFlamegraph(findTrace(node.parentIndex), out); } printIp(ip, out, 0, true); } template void printAllocations(T AllocationData::*member, LabelPrinter label, SubLabelPrinter sublabel) { if (mergeBacktraces) { printMerged(member, label, sublabel); } else { printUnmerged(member, label); } } template void printMerged(T AllocationData::*member, LabelPrinter label, SubLabelPrinter sublabel) { auto sortOrder = [member](const AllocationData& l, const AllocationData& r) { return std::abs(l.*member) > std::abs(r.*member); }; sort(mergedAllocations.begin(), mergedAllocations.end(), sortOrder); for (size_t i = 0; i < min(peakLimit, mergedAllocations.size()); ++i) { auto& allocation = mergedAllocations[i]; if (!(allocation.*member)) { break; } label(allocation); printIp(allocation.ipIndex, cout); sort(allocation.traces.begin(), allocation.traces.end(), sortOrder); int64_t handled = 0; for (size_t j = 0; j < min(subPeakLimit, allocation.traces.size()); ++j) { const auto& trace = allocation.traces[j]; if (!(trace.*member)) { break; } sublabel(trace); handled += trace.*member; printBacktrace(trace.traceIndex, cout, 2, true); } if (allocation.traces.size() > subPeakLimit) { cout << " and "; if (member == &AllocationData::allocations) { cout << (allocation.*member - handled); } else { cout << formatBytes(allocation.*member - handled); } cout << " from " << (allocation.traces.size() - subPeakLimit) << " other places\n"; } cout << '\n'; } } template void printUnmerged(T AllocationData::*member, LabelPrinter label) { sort(allocations.begin(), allocations.end(), [member](const Allocation& l, const Allocation& r) { return std::abs(l.*member) > std::abs(r.*member); }); for (size_t i = 0; i < min(peakLimit, allocations.size()); ++i) { const auto& allocation = allocations[i]; if (!(allocation.*member)) { break; } label(allocation); printBacktrace(allocation.traceIndex, cout, 1); cout << '\n'; } cout << endl; } void writeMassifHeader(const char* command) { // write massif header massifOut << "desc: heaptrack\n" << "cmd: " << command << '\n' << "time_unit: s\n"; } void writeMassifSnapshot(size_t timeStamp, bool isLast) { if (!lastMassifPeak) { lastMassifPeak = totalCost.leaked; massifAllocations = allocations; } massifOut << "#-----------\n" << "snapshot=" << massifSnapshotId << '\n' << "#-----------\n" << "time=" << (0.001 * timeStamp) << '\n' << "mem_heap_B=" << lastMassifPeak << '\n' << "mem_heap_extra_B=0\n" << "mem_stacks_B=0\n"; if (massifDetailedFreq && (isLast || !(massifSnapshotId % massifDetailedFreq))) { massifOut << "heap_tree=detailed\n"; const size_t threshold = double(lastMassifPeak) * massifThreshold * 0.01; writeMassifBacktrace(massifAllocations, lastMassifPeak, threshold, IpIndex()); } else { massifOut << "heap_tree=empty\n"; } ++massifSnapshotId; lastMassifPeak = 0; } void writeMassifBacktrace(const vector& allocations, size_t heapSize, size_t threshold, const IpIndex& location, size_t depth = 0) { int64_t skippedLeaked = 0; size_t numAllocs = 0; size_t skipped = 0; auto mergedAllocations = mergeAllocations(allocations); sort(mergedAllocations.begin(), mergedAllocations.end(), [](const MergedAllocation& l, const MergedAllocation& r) { return l.leaked > r.leaked; }); const auto ip = findIp(location); // skip anything below main const bool shouldStop = isStopIndex(ip.frame.functionIndex); if (!shouldStop) { for (auto& merged : mergedAllocations) { if (merged.leaked < 0) { // list is sorted, so we can bail out now - these entries are // uninteresting for massif break; } // skip items below threshold if (static_cast(merged.leaked) >= threshold) { ++numAllocs; // skip the first level of the backtrace, otherwise we'd endlessly // recurse for (auto& alloc : merged.traces) { alloc.traceIndex = findTrace(alloc.traceIndex).parentIndex; } } else { ++skipped; skippedLeaked += merged.leaked; } } } // TODO: write inlined frames out to massif files printIndent(massifOut, depth, " "); massifOut << 'n' << (numAllocs + (skipped ? 1 : 0)) << ": " << heapSize; if (!depth) { massifOut << " (heap allocation functions) malloc/new/new[], " "--alloc-fns, etc.\n"; } else { massifOut << " 0x" << hex << ip.instructionPointer << dec << ": "; if (ip.frame.functionIndex) { massifOut << stringify(ip.frame.functionIndex); } else { massifOut << "???"; } massifOut << " ("; if (ip.frame.fileIndex) { massifOut << stringify(ip.frame.fileIndex) << ':' << ip.frame.line; } else if (ip.moduleIndex) { massifOut << stringify(ip.moduleIndex); } else { massifOut << "???"; } massifOut << ")\n"; } auto writeSkipped = [&] { if (skipped) { printIndent(massifOut, depth, " "); massifOut << " n0: " << skippedLeaked << " in " << skipped << " places, all below massif's threshold (" << massifThreshold << ")\n"; skipped = 0; } }; if (!shouldStop) { for (const auto& merged : mergedAllocations) { if (merged.leaked > 0 && static_cast(merged.leaked) >= threshold) { if (skippedLeaked > merged.leaked) { // manually inject this entry to keep the output sorted writeSkipped(); } writeMassifBacktrace(merged.traces, merged.leaked, threshold, merged.ipIndex, depth + 1); } } writeSkipped(); } } void handleAllocation(const AllocationInfo& info, const AllocationIndex /*index*/) override { if (printHistogram) { ++sizeHistogram[info.size]; } if (totalCost.leaked > 0 && static_cast(totalCost.leaked) > lastMassifPeak && massifOut.is_open()) { massifAllocations = allocations; lastMassifPeak = totalCost.leaked; } } void handleTimeStamp(int64_t /*oldStamp*/, int64_t newStamp) override { if (massifOut.is_open()) { writeMassifSnapshot(newStamp, newStamp == totalTime); } } void handleDebuggee(const char* command) override { cout << "Debuggee command was: " << command << endl; if (massifOut.is_open()) { writeMassifHeader(command); } } bool printHistogram = false; bool mergeBacktraces = true; vector mergedAllocations; std::map sizeHistogram; uint64_t massifSnapshotId = 0; uint64_t lastMassifPeak = 0; vector massifAllocations; ofstream massifOut; double massifThreshold = 1; uint64_t massifDetailedFreq = 1; string filterBtFunction; size_t peakLimit = 10; size_t subPeakLimit = 5; }; } int main(int argc, char** argv) { po::options_description desc("Options", 120, 60); desc.add_options()("file,f", po::value(), "The heaptrack data file to print.")( "diff,d", po::value()->default_value({}), "Find the differences to this file.")( "shorten-templates,t", po::value()->default_value(true)->implicit_value(true), "Shorten template identifiers.")("merge-backtraces,m", po::value()->default_value(true)->implicit_value(true), "Merge backtraces.\nNOTE: the merged peak consumption is not correct.")( "print-peaks,p", po::value()->default_value(true)->implicit_value(true), "Print backtraces to top allocators, sorted by peak consumption.")( "print-allocators,a", po::value()->default_value(true)->implicit_value(true), "Print backtraces to top allocators, sorted by number of calls to " "allocation functions.")("print-temporary,T", po::value()->default_value(true)->implicit_value(true), "Print backtraces to top allocators, sorted by number of temporary " "allocations.")("print-leaks,l", po::value()->default_value(false)->implicit_value(true), "Print backtraces to leaked memory allocations.")( "print-overall-allocated,o", po::value()->default_value(false)->implicit_value(true), "Print top overall allocators, ignoring memory frees.")( "peak-limit,n", po::value()->default_value(10)->implicit_value(10), "Limit the number of reported peaks.")("sub-peak-limit,s", po::value()->default_value(5)->implicit_value(5), "Limit the number of reported backtraces of merged peak locations.")( "print-histogram,H", po::value()->default_value(string()), "Path to output file where an allocation size histogram will be written " "to.")("print-flamegraph,F", po::value()->default_value(string()), "Path to output file where a flame-graph compatible stack file will be " "written to.\n" "To visualize the resulting file, use flamegraph.pl from " "https://github.com/brendangregg/FlameGraph:\n" " heaptrack_print heaptrack.someapp.PID.gz -F stacks.txt\n" " # optionally pass --reverse to flamegraph.pl\n" " flamegraph.pl --title \"heaptrack: allocations\" --colors mem \\\n" " --countname allocations < stacks.txt > heaptrack.someapp.PID.svg\n" " [firefox|chromium] heaptrack.someapp.PID.svg\n")( "print-massif,M", po::value()->default_value(string()), "Path to output file where a massif compatible data file will be written " "to.")("massif-threshold", po::value()->default_value(1.), "Percentage of current memory usage, below which allocations are " "aggregated into a 'below threshold' entry.\n" "This is only used in the massif output file so far.\n")( "massif-detailed-freq", po::value()->default_value(2), "Frequency of detailed snapshots in the massif output file. Increase " "this to reduce the file size.\n" "You can set the value to zero to disable detailed snapshots.\n")( "filter-bt-function", po::value()->default_value(string()), "Only print allocations where the backtrace contains the given " "function.")("help,h", "Show this help message.")("version,v", "Displays version information."); po::positional_options_description p; p.add("file", -1); po::variables_map vm; try { po::store(po::command_line_parser(argc, argv).options(desc).positional(p).run(), vm); if (vm.count("help")) { cout << "heaptrack_print - analyze heaptrack data files.\n" << "\n" << "heaptrack is a heap memory profiler which records information\n" << "about calls to heap allocation functions such as malloc, " "operator new etc. pp.\n" << "This print utility can then be used to analyze the generated " "data files.\n\n" << desc << endl; return 0; } else if (vm.count("version")) { cout << "heaptrack_print " << HEAPTRACK_VERSION_STRING << endl; return 0; } po::notify(vm); } catch (const po::error& error) { cerr << "ERROR: " << error.what() << endl << endl << desc << endl; return 1; } if (!vm.count("file")) { // NOTE: stay backwards compatible to old boost 1.41 available in RHEL 6 // otherwise, we could simplify this by setting the file option // as ->required() using the new 1.42 boost API cerr << "ERROR: the option '--file' is required but missing\n\n" << desc << endl; return 1; } Printer data; const auto inputFile = vm["file"].as(); const auto diffFile = vm["diff"].as(); data.shortenTemplates = vm["shorten-templates"].as(); data.mergeBacktraces = vm["merge-backtraces"].as(); data.filterBtFunction = vm["filter-bt-function"].as(); data.peakLimit = vm["peak-limit"].as(); data.subPeakLimit = vm["sub-peak-limit"].as(); const string printHistogram = vm["print-histogram"].as(); data.printHistogram = !printHistogram.empty(); const string printFlamegraph = vm["print-flamegraph"].as(); const string printMassif = vm["print-massif"].as(); if (!printMassif.empty()) { data.massifOut.open(printMassif, ios_base::out); if (!data.massifOut.is_open()) { cerr << "Failed to open massif output file \"" << printMassif << "\"." << endl; return 1; } data.massifThreshold = vm["massif-threshold"].as(); data.massifDetailedFreq = vm["massif-detailed-freq"].as(); } const bool printLeaks = vm["print-leaks"].as(); const bool printOverallAlloc = vm["print-overall-allocated"].as(); const bool printPeaks = vm["print-peaks"].as(); const bool printAllocs = vm["print-allocators"].as(); const bool printTemporary = vm["print-temporary"].as(); cout << "reading file \"" << inputFile << "\" - please wait, this might take some time..." << endl; if (!diffFile.empty()) { cout << "reading diff file \"" << diffFile << "\" - please wait, this might take some time..." << endl; Printer diffData; auto diffRead = async(launch::async, [&diffData, diffFile]() { return diffData.read(diffFile); }); if (!data.read(inputFile) || !diffRead.get()) { return 1; } data.diff(diffData); } else if (!data.read(inputFile)) { return 1; } data.finalize(); cout << "finished reading file, now analyzing data:\n" << endl; if (printAllocs) { // sort by amount of allocations cout << "MOST CALLS TO ALLOCATION FUNCTIONS\n"; data.printAllocations(&AllocationData::allocations, [](const AllocationData& data) { cout << data.allocations << " calls to allocation functions with " << formatBytes(data.peak) << " peak consumption from\n"; }, [](const AllocationData& data) { cout << data.allocations << " calls with " << formatBytes(data.peak) << " peak consumption from:\n"; }); cout << endl; } if (printOverallAlloc) { cout << "MOST BYTES ALLOCATED OVER TIME (ignoring deallocations)\n"; data.printAllocations(&AllocationData::allocated, [](const AllocationData& data) { cout << formatBytes(data.allocated) << " allocated over " << data.allocations << " calls from\n"; }, [](const AllocationData& data) { cout << formatBytes(data.allocated) << " allocated over " << data.allocations << " calls from:\n"; }); cout << endl; } if (printPeaks) { - /// FIXME: find a way to merge this without breaking temporal dependency. - /// I.e. a given function could be called N times from different places - /// and allocate M bytes each, but free it thereafter. - /// Then the below would give a wrong total peak size of N * M instead - /// of just N! cout << "PEAK MEMORY CONSUMERS\n"; - if (data.mergeBacktraces) { - cout << "\nWARNING - the data below is not an accurate calculation of" - " the total peak consumption and can easily be wrong.\n" - " For an accurate overview, disable backtrace merging.\n"; - } - data.printAllocations(&AllocationData::peak, [](const AllocationData& data) { cout << formatBytes(data.peak) << " peak memory consumed over " << data.allocations << " calls from\n"; }, [](const AllocationData& data) { cout << formatBytes(data.peak) << " consumed over " << data.allocations << " calls from:\n"; }); + cout << endl; } if (printLeaks) { // sort by amount of leaks cout << "MEMORY LEAKS\n"; data.printAllocations(&AllocationData::leaked, [](const AllocationData& data) { cout << formatBytes(data.leaked) << " leaked over " << data.allocations << " calls from\n"; }, [](const AllocationData& data) { cout << formatBytes(data.leaked) << " leaked over " << data.allocations << " calls from:\n"; }); cout << endl; } if (printTemporary) { // sort by amount of temporary allocations cout << "MOST TEMPORARY ALLOCATIONS\n"; data.printAllocations(&AllocationData::temporary, [](const AllocationData& data) { cout << data.temporary << " temporary allocations of " << data.allocations << " allocations in total (" << fixed << setprecision(2) << (float(data.temporary) * 100.f / data.allocations) << "%) from\n"; }, [](const AllocationData& data) { cout << data.temporary << " temporary allocations of " << data.allocations << " allocations in total (" << fixed << setprecision(2) << (float(data.temporary) * 100.f / data.allocations) << "%) from:\n"; }); cout << endl; } const double totalTimeS = 0.001 * data.totalTime; cout << "total runtime: " << fixed << totalTimeS << "s.\n" << "bytes allocated in total (ignoring deallocations): " << formatBytes(data.totalCost.allocated) << " (" << formatBytes(data.totalCost.allocated / totalTimeS) << "/s)" << '\n' << "calls to allocation functions: " << data.totalCost.allocations << " (" << int64_t(data.totalCost.allocations / totalTimeS) << "/s)\n" << "temporary memory allocations: " << data.totalCost.temporary << " (" << int64_t(data.totalCost.temporary / totalTimeS) << "/s)\n" << "peak heap memory consumption: " << formatBytes(data.totalCost.peak) << '\n' << "peak RSS (including heaptrack overhead): " << formatBytes(data.peakRSS * data.systemInfo.pageSize) << '\n' << "total memory leaked: " << formatBytes(data.totalCost.leaked) << '\n'; if (!printHistogram.empty()) { ofstream histogram(printHistogram, ios_base::out); if (!histogram.is_open()) { cerr << "Failed to open histogram output file \"" << printHistogram << "\"." << endl; } else { for (auto entry : data.sizeHistogram) { histogram << entry.first << '\t' << entry.second << '\n'; } } } if (!printFlamegraph.empty()) { ofstream flamegraph(printFlamegraph, ios_base::out); if (!flamegraph.is_open()) { cerr << "Failed to open flamegraph output file \"" << printFlamegraph << "\"." << endl; } else { for (const auto& allocation : data.allocations) { if (!allocation.traceIndex) { flamegraph << "??"; } else { data.printFlamegraph(data.findTrace(allocation.traceIndex), flamegraph); } flamegraph << ' ' << allocation.allocations << '\n'; } } } return 0; } diff --git a/tests/manual/CMakeLists.txt b/tests/manual/CMakeLists.txt index 0a978f2..fe9602b 100644 --- a/tests/manual/CMakeLists.txt +++ b/tests/manual/CMakeLists.txt @@ -1,23 +1,28 @@ set(CMAKE_BUILD_TYPE Debug) add_executable(test_c test.c) add_executable(test_cpp test.cpp) add_executable(threaded threaded.cpp) target_link_libraries(threaded ${CMAKE_THREAD_LIBS_INIT}) add_executable(callgraph callgraph.cpp) add_library(testlib SHARED lib.cpp) add_executable(test_lib test_lib.cpp) target_link_libraries(test_lib testlib) add_executable(test_aggregation test_aggregation.cpp) add_executable(signals signals.cpp) target_link_libraries(signals ${CMAKE_THREAD_LIBS_INIT}) add_executable(libc_leaks libc_leaks.c) +add_executable(peak peak.c) +set_target_properties(peak PROPERTIES + COMPILE_FLAGS "-g3 -O0" +) + set(CMAKE_BUILD_TYPE RelWithDebInfo) add_executable(inlining inlining.cpp) diff --git a/tests/manual/peak.c b/tests/manual/peak.c new file mode 100644 index 0000000..0ca53aa --- /dev/null +++ b/tests/manual/peak.c @@ -0,0 +1,31 @@ +#include + +char* allocate_something(int size) +{ + return malloc(size); +} + +char* foo() +{ + return allocate_something(100); +} + +char* bar() +{ + return allocate_something(25); +} + +int main() +{ + char* f1 = foo(); + char* b2 = bar(); + free(f1); + char* b3 = bar(); + char* b4 = bar(); + free(b2); + free(b3); + free(b4); + char* f2 = foo(); + free(f2); + return 0; +}