From 1539ea535090c7036b5ab81b776299b675485a3e Mon Sep 17 00:00:00 2001 From: Andrey Kvochko Date: Wed, 20 Sep 2017 21:50:20 +0300 Subject: [PATCH] Implement heap tab --- profiler/profiler/src/profiler.cpp | 95 ++++++++++++++- profiler/profiler/src/stackentry.h | 3 + src/analyze/accumulatedtracedata.cpp | 21 ++++ src/analyze/accumulatedtracedata.h | 18 ++- src/analyze/gui/CMakeLists.txt | 2 + src/analyze/gui/mainwindow.cpp | 34 +++++- src/analyze/gui/mainwindow.ui | 68 ++++++++--- src/analyze/gui/objecttreemodel.cpp | 210 ++++++++++++++++++++++++++++++++++ src/analyze/gui/objecttreemodel.h | 91 +++++++++++++++ src/analyze/gui/objecttreeproxy.cpp | 95 +++++++++++++++ src/analyze/gui/objecttreeproxy.h | 46 ++++++++ src/analyze/gui/parser.cpp | 195 +++++++++++++++++++++++++++++++ src/analyze/gui/parser.h | 3 + src/interpret/heaptrack_interpret.cpp | 61 ++++++++-- src/track/libheaptrack.cpp | 44 ++++++- src/track/objectgraph.h | 84 ++++++++++++++ src/util/indices.h | 3 + src/util/pointermap.h | 18 +++ 18 files changed, 1057 insertions(+), 34 deletions(-) create mode 100644 src/analyze/gui/objecttreemodel.cpp create mode 100644 src/analyze/gui/objecttreemodel.h create mode 100644 src/analyze/gui/objecttreeproxy.cpp create mode 100644 src/analyze/gui/objecttreeproxy.h create mode 100644 src/track/objectgraph.h diff --git a/profiler/profiler/src/profiler.cpp b/profiler/profiler/src/profiler.cpp index 9907633..11e2779 100644 --- a/profiler/profiler/src/profiler.cpp +++ b/profiler/profiler/src/profiler.cpp @@ -271,6 +271,43 @@ static HRESULT GetClassNameFromClassId(ICorProfilerInfo *info, ClassID classId, return S_OK; } +static HRESULT GetClassSizeFromClassId(ICorProfilerInfo2 *info, ClassID classId, ULONG *pulClassSize) { + ModuleID moduleId; + mdTypeDef mdClass; + ClassID parentClassId; + + HRESULT hr = info->GetClassIDInfo2(classId, &moduleId, &mdClass, &parentClassId, 0, nullptr, nullptr); + if (parentClassId) { + ULONG parentClassSize = 0; + hr = GetClassSizeFromClassId(info, parentClassId, &parentClassSize); + *pulClassSize += parentClassSize; + } + + if (hr != S_OK) + return hr; + + IMetaDataImport * pIMetaDataImport; + hr = info->GetModuleMetaData(moduleId, CorOpenFlags::ofRead, IID_IMetaDataImport, + (LPUNKNOWN *)&pIMetaDataImport); + if (hr != S_OK) + return hr; + + DWORD packSize; + ULONG cMax = 128; + COR_FIELD_OFFSET rFieldOffset[cMax]; + ULONG cFieldOffset; + + ULONG classSize = 0; + hr = pIMetaDataImport->GetClassLayout(mdClass, &packSize, rFieldOffset, cMax, &cFieldOffset, &classSize); + *pulClassSize += classSize; + + if (hr != S_OK) + return hr; + + pIMetaDataImport->Release(); + return S_OK; +} + void encodeWChar(WCHAR *orig, char *encoded) { int i = 0; while (orig[i] != 0) { @@ -310,6 +347,7 @@ void OnFunctionEnter(FunctionIDOrClientID functionID, encodeWChar(szMethodName, methodName); PushShadowStack(functionID.functionID, className, methodName); + info->Release(); } void OnFunctionLeave(FunctionIDOrClientID functionID, @@ -325,7 +363,7 @@ HRESULT STDMETHODCALLTYPE Profiler::Initialize(IUnknown *pICorProfilerInfoUnk) { info->SetEventMask( COR_PRF_MONITOR_ENTERLEAVE | COR_PRF_ENABLE_FUNCTION_ARGS | COR_PRF_ENABLE_FUNCTION_RETVAL | COR_PRF_ENABLE_FRAME_INFO | - COR_PRF_ENABLE_STACK_SNAPSHOT | + COR_PRF_ENABLE_STACK_SNAPSHOT | COR_PRF_MONITOR_CLASS_LOADS | COR_PRF_ENABLE_OBJECT_ALLOCATED | COR_PRF_MONITOR_OBJECT_ALLOCATED | COR_PRF_MONITOR_GC); info->SetEnterLeaveFunctionHooks3WithInfo(OnFunctionEnter, OnFunctionLeave, NULL); @@ -411,6 +449,20 @@ HRESULT STDMETHODCALLTYPE Profiler::ClassLoadStarted(ClassID classId) { HRESULT STDMETHODCALLTYPE Profiler::ClassLoadFinished(ClassID classId, HRESULT hrStatus) { + + if (hrStatus != S_OK) + return S_OK; + ICorProfilerInfo2 *info; + HRESULT hr = g_pICorProfilerInfoUnknown->QueryInterface(IID_ICorProfilerInfo2, + (void **)&info); + WCHAR wszClassName[MAX_NAME_LENGTH + 1]; + GetClassNameFromClassId(info, classId, wszClassName); + char className[MAX_NAME_LENGTH + 1]; + encodeWChar(wszClassName, className); + uint32_t classSize = 0; + GetClassSizeFromClassId(info, classId, &classSize); + heaptrack_loadclass(reinterpret_cast(classId), classSize, className); + info->Release(); return S_OK; } @@ -579,6 +631,8 @@ HRESULT STDMETHODCALLTYPE if (hr == S_OK) PopShadowStack(); + info->Release(); + return S_OK; } @@ -591,11 +645,48 @@ HRESULT STDMETHODCALLTYPE Profiler::ObjectsAllocatedByClass(ULONG cClassCount, HRESULT STDMETHODCALLTYPE Profiler::ObjectReferences(ObjectID objectId, ClassID classId, ULONG cObjectRefs, ObjectID objectRefIds[]) { + HRESULT hr; + ICorProfilerInfo *info; + hr = g_pICorProfilerInfoUnknown->QueryInterface(IID_ICorProfilerInfo, + (void **)&info); + if (hr != S_OK) + return hr; + + for (int i = 0; i < cObjectRefs; ++i) { + ClassID subjClass; + hr = info->GetClassFromObject(objectRefIds[i], &subjClass); + + // We still want the best estimate, even though something went wrong. + if (hr != S_OK) + continue; + + heaptrack_add_object_dep((void*)objectId, (void*)classId, (void*)objectRefIds[i], (void*)subjClass); + } + info->Release(); return S_OK; } HRESULT STDMETHODCALLTYPE Profiler::RootReferences(ULONG cRootRefs, ObjectID rootRefIds[]) { + HRESULT hr; + ICorProfilerInfo *info; + hr = g_pICorProfilerInfoUnknown->QueryInterface(IID_ICorProfilerInfo, + (void **)&info); + + if (hr != S_OK) + return hr; + + for (int i = 0; i < cRootRefs; ++i) { + ClassID rootClass; + hr = info->GetClassFromObject(rootRefIds[i], &rootClass); + + // We still want the best estimate, even though something went wrong. + if (hr != S_OK) + continue; + + heaptrack_gcroot((void *) rootRefIds[i], (void*)rootClass); + } + info->Release(); return S_OK; } @@ -723,7 +814,7 @@ HRESULT STDMETHODCALLTYPE heaptrack_gcmarksurvived((void *) ranges[i].rangeStart, (unsigned long) ranges[i].rangeLength, NULL); } - + info->Release(); return S_OK; } diff --git a/profiler/profiler/src/stackentry.h b/profiler/profiler/src/stackentry.h index 9e457c0..48bba03 100644 --- a/profiler/profiler/src/stackentry.h +++ b/profiler/profiler/src/stackentry.h @@ -19,5 +19,8 @@ extern "C" void heaptrack_objectallocate(void *objectId, unsigned long objectSiz extern "C" void heaptrack_startgc(); extern "C" void heaptrack_gcmarksurvived(void *rangeStart, unsigned long rangeLength, void *rangeMovedTo); extern "C" void heaptrack_finishgc(); +extern "C" void heaptrack_add_object_dep(void *keyObjectId, void *keyClassId, void *valObjectId, void *valClassId); +extern "C" void heaptrack_loadclass(void *classId, unsigned long classSize, char *className); +extern "C" void heaptrack_gcroot(void *objectId, void *rootClass); #endif // STACKENTRY_H diff --git a/src/analyze/accumulatedtracedata.cpp b/src/analyze/accumulatedtracedata.cpp index 420ed89..28b3c46 100644 --- a/src/analyze/accumulatedtracedata.cpp +++ b/src/analyze/accumulatedtracedata.cpp @@ -68,6 +68,8 @@ AccumulatedTraceData::AccumulatedTraceData() allocations.reserve(16384); stopIndices.reserve(4); opNewIpIndices.reserve(16); + classInfos.reserve(4096); + objectTreeNodes.reserve(16384); } const string& AccumulatedTraceData::stringify(const StringIndex stringId) const @@ -778,6 +780,25 @@ bool AccumulatedTraceData::read(istream& in, const ParsePass pass) } else if (reader.mode() == 'I') { // system information reader >> systemInfo.pageSize; reader >> systemInfo.pages; + } else if (reader.mode() == 'e') { // object dependency + if (pass != FirstPass) { + continue; + } + ObjectTreeNode node; + reader >> node.gcNum; + reader >> node.numChildren; + reader >> node.objectPtr; + reader >> node.classIndex; + reader >> node.allocIndex; + objectTreeNodes.push_back(node); + } else if (reader.mode() == 'C') { // class info + if (pass != FirstPass) { + continue; + } + ClassInfo classInfo; + reader >> classInfo.classIndex; + reader >> classInfo.size; + classInfos.push_back(classInfo); } else { cerr << "failed to parse line: " << reader.line() << endl; } diff --git a/src/analyze/accumulatedtracedata.h b/src/analyze/accumulatedtracedata.h index 399c71c..88a7949 100644 --- a/src/analyze/accumulatedtracedata.h +++ b/src/analyze/accumulatedtracedata.h @@ -87,6 +87,21 @@ struct Allocation : public AllocationData TraceIndex traceIndex; }; +struct ClassInfo +{ + ClassIndex classIndex; + uint64_t size = 0; +}; + +struct ObjectTreeNode +{ + uint64_t gcNum; + uint64_t numChildren; + uint64_t objectPtr; + ClassIndex classIndex; + AllocationIndex allocIndex; +}; + /** * Information for a single call to an allocation function. */ @@ -352,8 +367,9 @@ struct AccumulatedTraceData std::vector traces; std::vector strings; std::vector opNewIpIndices; - std::vector allocationInfos; + std::vector classInfos; + std::vector objectTreeNodes; AddressRangesMap addressRangeInfos; diff --git a/src/analyze/gui/CMakeLists.txt b/src/analyze/gui/CMakeLists.txt index 4a32334..e734554 100644 --- a/src/analyze/gui/CMakeLists.txt +++ b/src/analyze/gui/CMakeLists.txt @@ -21,7 +21,9 @@ set(SRCFILES gui.cpp mainwindow.cpp treemodel.cpp + objecttreemodel.cpp treeproxy.cpp + objecttreeproxy.cpp costdelegate.cpp parser.cpp flamegraph.cpp diff --git a/src/analyze/gui/mainwindow.cpp b/src/analyze/gui/mainwindow.cpp index 16f1404..1b0aeec 100644 --- a/src/analyze/gui/mainwindow.cpp +++ b/src/analyze/gui/mainwindow.cpp @@ -41,7 +41,9 @@ #include "stacksmodel.h" #include "topproxy.h" #include "treemodel.h" +#include "objecttreemodel.h" #include "treeproxy.h" +#include "objecttreeproxy.h" #include "gui_config.h" @@ -156,6 +158,18 @@ void setupTreeModel(TreeModel* model, QTreeView* view, CostDelegate* costDelegat addContextMenu(view, TreeModel::LocationRole); } +void setupObjectTreeModel(ObjectTreeModel* model, QTreeView* view, QLineEdit* filterClass, QComboBox* filterGC) { + auto proxy = new ObjectTreeProxy(ObjectTreeModel::ClassNameColumn, ObjectTreeModel::GCNumColumn, model); + proxy->setSourceModel(model); + proxy->setSortRole(ObjectTreeModel::SortRole); + + view->setModel(proxy); + view->hideColumn(ObjectTreeModel::GCNumColumn); + QObject::connect(filterClass, &QLineEdit::textChanged, proxy, &ObjectTreeProxy::setNameFilter); + QObject::connect(filterGC, static_cast(&QComboBox::currentIndexChanged), + proxy, &ObjectTreeProxy::setGCFilter); +} + void setupCallerCalle(CallerCalleeModel* model, QTreeView* view, CostDelegate* costDelegate, QLineEdit* filterFunction, QLineEdit* filterFile, QLineEdit* filterModule) { @@ -228,15 +242,17 @@ MainWindow::MainWindow(QWidget* parent) auto bottomUpModelFilterOutLeaves = new TreeModel(this); auto topDownModel = new TreeModel(this); auto callerCalleeModel = new CallerCalleeModel(this); + auto objectTreeModel = new ObjectTreeModel(this); connect(this, &MainWindow::clearData, bottomUpModelFilterOutLeaves, &TreeModel::clearData); connect(this, &MainWindow::clearData, topDownModel, &TreeModel::clearData); connect(this, &MainWindow::clearData, callerCalleeModel, &CallerCalleeModel::clearData); connect(this, &MainWindow::clearData, m_ui->flameGraphTab, &FlameGraph::clearData); + connect(this, &MainWindow::clearData, objectTreeModel, &ObjectTreeModel::clearData); m_ui->tabWidget->setTabEnabled(m_ui->tabWidget->indexOf(m_ui->callerCalleeTab), false); m_ui->tabWidget->setTabEnabled(m_ui->tabWidget->indexOf(m_ui->topDownTab), false); m_ui->tabWidget->setTabEnabled(m_ui->tabWidget->indexOf(m_ui->flameGraphTab), false); - + m_ui->tabWidget->setTabEnabled(m_ui->tabWidget->indexOf(m_ui->heapTab), false); connect(m_parser, &Parser::bottomUpDataAvailable, this, [=](const TreeData& data) { if (!m_diffMode) { m_ui->flameGraphTab->setBottomUpData(data); @@ -247,6 +263,18 @@ MainWindow::MainWindow(QWidget* parent) m_ui->pages->setCurrentWidget(m_ui->resultsPage); m_ui->tabWidget->setTabEnabled(m_ui->tabWidget->indexOf(m_ui->bottomUpTab), true); }); + connect(m_parser, &Parser::objectTreeBottomUpDataAvailable, this, [=](const ObjectTreeData& data) { + int maxGC = 0; + for (const ObjectRowData& row: data) { + if (maxGC < row.gcNum) + maxGC = row.gcNum; + } + for (int gc = 0; gc < maxGC; gc++) { + m_ui->filterGC->addItem(QString::number(gc+1)); + } + objectTreeModel->resetData(data); + m_ui->tabWidget->setTabEnabled(m_ui->tabWidget->indexOf(m_ui->heapTab), true); + }); connect(m_parser, (AccumulatedTraceData::isHideUnmanagedStackParts ? &Parser::bottomUpDataAvailable : &Parser::bottomUpFilterOutLeavesDataAvailable), @@ -445,6 +473,8 @@ MainWindow::MainWindow(QWidget* parent) setupCallerCalle(callerCalleeModel, m_ui->callerCalleeResults, costDelegate, m_ui->callerCalleeFilterFunction, m_ui->callerCalleeFilterFile, m_ui->callerCalleeFilterModule); + setupObjectTreeModel(objectTreeModel, m_ui->objectTreeResults, m_ui->filterClass, m_ui->filterGC); + auto validateInputFile = [this](const QString& path, bool allowEmpty) -> bool { if (path.isEmpty()) { return allowEmpty; @@ -596,7 +626,7 @@ void MainWindow::setupStacks() if (!current.isValid()) { stacksModel->clear(); } else { - auto proxy = qobject_cast(current.model()); + auto proxy = qobject_cast(current.model()); Q_ASSERT(proxy); auto leaf = proxy->mapToSource(current); stacksModel->fillFromIndex(leaf); diff --git a/src/analyze/gui/mainwindow.ui b/src/analyze/gui/mainwindow.ui index e6489c1..53563af 100644 --- a/src/analyze/gui/mainwindow.ui +++ b/src/analyze/gui/mainwindow.ui @@ -6,7 +6,7 @@ 0 0 - 1332 + 1473 896 @@ -29,10 +29,7 @@ - - KMessageWidget::Information - - + .. @@ -62,38 +59,32 @@ - + <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 - @@ -742,6 +733,44 @@ + + + Managed Heap + + + + + + + + GC # + + + + + + + + + + Search + + + + + + + + + true + + + 100 + + + + + Flame Graph @@ -813,8 +842,8 @@ 0 0 - 1332 - 30 + 1473 + 22 @@ -830,6 +859,7 @@ KMessageWidget QFrame
kmessagewidget.h
+ 1 KUrlRequester diff --git a/src/analyze/gui/objecttreemodel.cpp b/src/analyze/gui/objecttreemodel.cpp new file mode 100644 index 0000000..b3fd264 --- /dev/null +++ b/src/analyze/gui/objecttreemodel.cpp @@ -0,0 +1,210 @@ +/* + * 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 "objecttreemodel.h" + +#include +#include + +#include +#include + +#include + +namespace { + +int indexOf(const ObjectRowData* row, const ObjectTreeData& siblings) +{ + Q_ASSERT(siblings.data() <= row); + Q_ASSERT(siblings.data() + siblings.size() > row); + return row - siblings.data(); +} + +const ObjectRowData* rowAt(const ObjectTreeData& rows, int row) +{ + Q_ASSERT(rows.size() > row); + return rows.data() + row; +} + +/// @return the parent row containing @p index +const ObjectRowData* toParentRow(const QModelIndex& index) +{ + return static_cast(index.internalPointer()); +} +} + +ObjectTreeModel::ObjectTreeModel(QObject* parent) + : QAbstractItemModel(parent) +{ + qRegisterMetaType(); +} + +ObjectTreeModel::~ObjectTreeModel() +{ +} + +QVariant ObjectTreeModel::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 == InstanceCountColumn) { + return Qt::DescendingOrder; + } + } + if (role == Qt::DisplayRole) { + switch (static_cast(section)) { + case ClassNameColumn: + return i18n("Class Name"); + case InstanceCountColumn: + return i18n("Instances"); + case ShallowSizeColumn: + return i18n("Shallow Size"); + case ReferencedSizeColumn: + return i18n("Referenced Size"); + case GCNumColumn: + return i18n("GC #"); + case NUM_COLUMNS: + break; + } + } else if (role == Qt::ToolTipRole) { + switch (static_cast(section)) { + case ClassNameColumn: + return i18n("The name of the class."); + case InstanceCountColumn: + return i18n("Total number of instances in this reference chain."); + case GCNumColumn: + return i18n("GC number after which the snapshot has been taken."); + case ShallowSizeColumn: + return i18n("Total size of objects of this type in a given reference chain."); + case ReferencedSizeColumn: + return i18n("Total size of objects of this type referenced by other objects in a given reference chain."); + case NUM_COLUMNS: + break; + } + } + return {}; +} + +QVariant ObjectTreeModel::data(const QModelIndex& index, int role) const +{ + if (index.row() < 0 || index.column() < 0 || index.column() > NUM_COLUMNS) { + return {}; + } + + const auto row = toRow(index); + + if (role == Qt::DisplayRole || role == SortRole) { + switch (static_cast(index.column())) { + case ClassNameColumn: + if (row->className.empty()) + return i18n(""); + return i18n(row->className.c_str()); + case InstanceCountColumn: + return static_cast(row->allocations); + case ShallowSizeColumn: + return static_cast(row->allocated); + case ReferencedSizeColumn: + return static_cast(row->referenced); + case GCNumColumn: + return static_cast(row->gcNum); + case NUM_COLUMNS: + break; + } + } else if (role == Qt::ToolTipRole) { + QString tooltip; + QTextStream stream(&tooltip); + stream << "
";
+        stream << "
"; + return tooltip; + } + return {}; +} + +QModelIndex ObjectTreeModel::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 ObjectTreeModel::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 ObjectTreeModel::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 ObjectTreeModel::columnCount(const QModelIndex& /*parent*/) const +{ + return NUM_COLUMNS; +} + +void ObjectTreeModel::resetData(const ObjectTreeData& data) +{ + beginResetModel(); + m_data = data; + endResetModel(); +} + +void ObjectTreeModel::clearData() +{ + beginResetModel(); + m_data = {}; + endResetModel(); +} + +const ObjectRowData* ObjectTreeModel::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 ObjectTreeModel::rowOf(const ObjectRowData* row) const +{ + if (auto parent = row->parent) { + return indexOf(row, parent->children); + } else { + return indexOf(row, m_data); + } +} diff --git a/src/analyze/gui/objecttreemodel.h b/src/analyze/gui/objecttreemodel.h new file mode 100644 index 0000000..97229c1 --- /dev/null +++ b/src/analyze/gui/objecttreemodel.h @@ -0,0 +1,91 @@ +/* + * 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 + */ + +#ifndef OBJECTTREEMODEL_H +#define OBJECTTREEMODEL_H + +#include +#include + +#include +#include + +struct ObjectRowData +{ + uint64_t classIndex; + std::string className; + quint32 gcNum; + uint64_t allocations; + uint64_t allocated; + uint64_t referenced; + const ObjectRowData* parent; + QVector children; + bool operator<(const ObjectRowData& rhs) const + { + return className < rhs.className; + } +}; + +using ObjectTreeData = QVector; +Q_DECLARE_METATYPE(ObjectTreeData) + +class ObjectTreeModel : public QAbstractItemModel +{ + Q_OBJECT +public: + ObjectTreeModel(QObject* parent); + virtual ~ObjectTreeModel(); + + enum Columns + { + InstanceCountColumn, + ShallowSizeColumn, + ReferencedSizeColumn, + GCNumColumn, + ClassNameColumn, + NUM_COLUMNS + }; + + enum Roles + { + SortRole = Qt::UserRole, + MaxCostRole, + LocationRole + }; + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex& child) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; + +public slots: + void resetData(const ObjectTreeData& data); + void clearData(); + +private: + /// @return the row resembled by @p index + const ObjectRowData* toRow(const QModelIndex& index) const; + /// @return the row number of @p row in its parent + int rowOf(const ObjectRowData* row) const; + + ObjectTreeData m_data; +}; + +#endif // OBJECTTREEMODEL_H diff --git a/src/analyze/gui/objecttreeproxy.cpp b/src/analyze/gui/objecttreeproxy.cpp new file mode 100644 index 0000000..53e32e0 --- /dev/null +++ b/src/analyze/gui/objecttreeproxy.cpp @@ -0,0 +1,95 @@ +/* + * 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 "objecttreeproxy.h" + +ObjectTreeProxy::ObjectTreeProxy(int nameColumn, int gcColumn, QObject* parent) + : KRecursiveFilterProxyModel(parent) + , m_nameColumn(nameColumn) + , m_gcColumn(gcColumn) +{ +} + +ObjectTreeProxy::~ObjectTreeProxy() = default; + +void ObjectTreeProxy::setNameFilter(const QString& nameFilter) +{ + m_nameFilter = nameFilter; + invalidate(); +} + +void ObjectTreeProxy::setGCFilter(int gcFilter) +{ + m_gcFilter = gcFilter; + invalidate(); +} + +bool ObjectTreeProxy::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + // TODO: Implement some caching so that if one match is found on the first pass, we can return early results + // when the subtrees are checked by QSFPM. + if (acceptRow(sourceRow, sourceParent)) { + return true; + } + + QModelIndex source_index = sourceModel()->index(sourceRow, 0, sourceParent); + Q_ASSERT(source_index.isValid()); + bool accepted = false; + + QModelIndex parent = sourceParent; + while (parent.isValid()) { + if (acceptRow(parent.row(), parent.parent())) + return true; + parent = parent.parent(); + } + + const int numChildren = sourceModel()->rowCount(source_index); + for (int row = 0, rows = numChildren; row < rows; ++row) { + if (filterAcceptsRow(row, source_index)) { + accepted = true; + break; + } + } + + return accepted; +} + +bool ObjectTreeProxy::acceptRow(int sourceRow, const QModelIndex& sourceParent) const +{ + auto source = sourceModel(); + if (!source) { + return false; + } + + const auto& name = source->index(sourceRow, m_nameColumn, sourceParent).data().toString(); + if (name.contains(QString::fromStdString("EMPTY_MESSAGE"), Qt::CaseInsensitive)) + return false; + + if (!m_nameFilter.isEmpty()) { + if (!name.contains(m_nameFilter, Qt::CaseInsensitive)) { + return false; + } + } + if (m_gcFilter != -1) { + const int gc = source->index(sourceRow, m_gcColumn, sourceParent).data().toInt() - 1; + if (gc != m_gcFilter) { + return false; + } + } + return true; +} diff --git a/src/analyze/gui/objecttreeproxy.h b/src/analyze/gui/objecttreeproxy.h new file mode 100644 index 0000000..843fa74 --- /dev/null +++ b/src/analyze/gui/objecttreeproxy.h @@ -0,0 +1,46 @@ +/* + * 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 + */ + +#ifndef OBJECTTREEPROXY_H +#define OBJECTTREEPROXY_H + +#include + +class ObjectTreeProxy final : public KRecursiveFilterProxyModel +{ + Q_OBJECT +public: + explicit ObjectTreeProxy(int nameColumn, int gcColumn, QObject* parent = nullptr); + virtual ~ObjectTreeProxy(); + +public slots: + void setNameFilter(const QString& nameFilter); + void setGCFilter(int gcFilter); + +private: + bool acceptRow(int source_row, const QModelIndex& source_parent) const override; + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + + int m_nameColumn; + int m_gcColumn; + + QString m_nameFilter; + int m_gcFilter; +}; + +#endif // OBJECTTREEPROXY_H diff --git a/src/analyze/gui/parser.cpp b/src/analyze/gui/parser.cpp index c8993c5..107ec10 100644 --- a/src/analyze/gui/parser.cpp +++ b/src/analyze/gui/parser.cpp @@ -33,6 +33,130 @@ using namespace std; namespace { +struct ObjectNode { + ObjectNode() + : m_children(), m_objectPtr(0), m_classIndex(), m_objectSize(0), gcNum(0) + { + } + + std::vector m_children; + uint64_t m_objectPtr; + ClassIndex m_classIndex; + uint64_t m_objectSize; + uint32_t gcNum; +}; + + +struct TypeTree { + TypeTree() + : m_classIndex(), + m_parents(), + m_uniqueObjects(), + m_totalSize(0), + m_referencedSize(0), + m_gcNum(0) + { + } + + TypeTree(const TypeTree& rhs) + : m_classIndex(rhs.m_classIndex), + m_parents(), + m_uniqueObjects(rhs.m_uniqueObjects), + m_totalSize(rhs.m_totalSize), + m_referencedSize(rhs.m_referencedSize), + m_gcNum(rhs.m_gcNum) + { + for (auto& rParent: rhs.m_parents) { + auto parent = std::unique_ptr(new TypeTree(*rParent)); + m_parents.push_back(std::move(parent)); + } + } + + ~TypeTree() { + } + + static std::unique_ptr create(ObjectNode& node) { + auto TT = std::unique_ptr(new TypeTree()); + TT->m_classIndex = node.m_classIndex; + TT->m_gcNum = node.gcNum; + TT->m_uniqueObjects.insert(node.m_objectPtr); + TT->m_totalSize = node.m_objectSize; + TT->m_referencedSize = node.m_objectSize; + return TT; + } + + static std::vector> createBottomUp(ObjectNode& node) { + std::vector> result; + if (node.m_classIndex.index == 0 && node.m_children.size() == 0) + return result; + + if (node.m_children.size() == 0) { + result.push_back(std::move(create(node))); + return result; + } + + for (ObjectNode& child: node.m_children) { + auto leaves = TypeTree::createBottomUp(child); + for (auto& leaf: leaves) { + auto parent = create(node); + parent->m_referencedSize = leaf->m_referencedSize; + TypeTree* childIt = leaf.get(); + while (childIt->m_parents.size() > 0) + childIt = childIt->m_parents[0].get(); + childIt->m_parents.push_back(std::move(parent)); + result.push_back(std::move(leaf)); + auto parentCopy = create(node); + result.push_back(std::move(parentCopy)); + } + } + + return result; + } + + void mergeSubtrees() { + std::unordered_map> classCounters; + + for (auto& parent: m_parents) { + classCounters[parent->m_classIndex.index].push_back(parent.get()); + } + + std::vector> newParents; + + for (auto it = classCounters.begin(), + end = classCounters.end(); it != end; ++it) { + auto combinedParent = std::unique_ptr(new TypeTree()); + combinedParent->m_classIndex.index = it->first; + auto& subtrees = it->second; + combinedParent->m_gcNum = m_gcNum; + for (auto& parent: subtrees) { + for (auto &parentsParent: parent->m_parents) + combinedParent->m_parents.push_back(std::move(parentsParent)); + combinedParent->m_referencedSize += parent->m_referencedSize; + for (auto& uo: parent->m_uniqueObjects) { + auto pib = combinedParent->m_uniqueObjects.insert(uo); + if (pib.second) + combinedParent->m_totalSize += parent->m_totalSize; + } + + } + combinedParent->mergeSubtrees(); + + newParents.push_back(std::move(combinedParent)); + } + + m_parents.erase(m_parents.begin(), m_parents.end()); + m_parents = std::move(newParents); + } + + ClassIndex m_classIndex; + std::vector> m_parents; + std::unordered_set m_uniqueObjects; + uint64_t m_totalSize; + uint64_t m_referencedSize; + int m_gcNum; +}; + // TODO: use QString directly struct StringCache { @@ -585,6 +709,72 @@ HistogramData buildSizeHistogram(ParserData& data) } } +void setObjectParents(QVector& children, const ObjectRowData* parent) +{ + for (auto& row : children) { + row.parent = parent; + setObjectParents(row.children, &row); + } +} + +ObjectRowData objectRowDataFromTypeTree(ParserData& data, TypeTree* tree) { + ObjectRowData rowData; + rowData.gcNum = tree->m_gcNum; + rowData.classIndex = tree->m_classIndex.index; + rowData.className = data.stringify(tree->m_classIndex); + rowData.allocations = tree->m_uniqueObjects.size(); + rowData.allocated = tree->m_totalSize; + rowData.referenced = tree->m_referencedSize; + for (auto& parent: tree->m_parents) + rowData.children.push_back(objectRowDataFromTypeTree(data, parent.get())); + return rowData; +} + +ObjectNode buildObjectGraph(ParserData& data, size_t &nodeIndex) { + ObjectNode node; + ObjectTreeNode &dataNode = data.objectTreeNodes[nodeIndex]; + node.m_classIndex = dataNode.classIndex; + node.m_objectPtr = dataNode.objectPtr; + node.gcNum = dataNode.gcNum; + if (dataNode.allocIndex.index != 0) + node.m_objectSize = data.allocationInfos[dataNode.allocIndex.index].size; + else + node.m_objectSize = 0; + nodeIndex++; + for (size_t i = 0; i < dataNode.numChildren; ++i) { + if (node.gcNum != data.objectTreeNodes[nodeIndex].gcNum) + break; + node.m_children.push_back(buildObjectGraph(data, nodeIndex)); + } + return node; +} + +ObjectTreeData buildObjectTree(ParserData& data) +{ + + ObjectTreeData ret; + size_t nodeIndex = 0; + std::vector nodes; + while (nodeIndex < data.objectTreeNodes.size()) { + nodes.push_back(buildObjectGraph(data, nodeIndex)); + } + + for (auto& node: nodes) { + TypeTree root; + root.m_gcNum = node.gcNum; + root.m_parents = TypeTree::createBottomUp(node); + root.mergeSubtrees(); + + for (auto& parent: root.m_parents) { + ret.push_back(objectRowDataFromTypeTree(data, parent.get())); + } + + } + setObjectParents(ret, nullptr); + + return ret; +} + Parser::Parser(QObject* parent) : QObject(parent) { @@ -658,6 +848,11 @@ void Parser::parse(const QString& path, const QString& diffBase) emit bottomUpFilterOutLeavesDataAvailable(mergedAllocations); } + if (AllocationData::display == AllocationData::DisplayId::managed) { + const auto objectTreeBottomUpData = buildObjectTree(*data); + emit objectTreeBottomUpDataAvailable(objectTreeBottomUpData); + } + // also calculate the size histogram emit progressMessageAvailable(i18n("building size histogram...")); const auto sizeHistogram = buildSizeHistogram(*data); diff --git a/src/analyze/gui/parser.h b/src/analyze/gui/parser.h index fb9af9f..7503b55 100644 --- a/src/analyze/gui/parser.h +++ b/src/analyze/gui/parser.h @@ -25,6 +25,7 @@ #include "chartmodel.h" #include "histogrammodel.h" #include "treemodel.h" +#include "objecttreemodel.h" class Parser : public QObject { @@ -49,6 +50,8 @@ signals: void allocatedChartDataAvailable(const ChartData& data); void temporaryChartDataAvailable(const ChartData& data); void sizeHistogramDataAvailable(const HistogramData& data); + void objectTreeTopDownDataAvailable(const ObjectTreeData& data); + void objectTreeBottomUpDataAvailable(const ObjectTreeData& data); void finished(); void failedToOpen(const QString& path); }; diff --git a/src/interpret/heaptrack_interpret.cpp b/src/interpret/heaptrack_interpret.cpp index 31abd86..5142f0d 100644 --- a/src/interpret/heaptrack_interpret.cpp +++ b/src/interpret/heaptrack_interpret.cpp @@ -187,6 +187,7 @@ struct AccumulatedTraceData m_backtraceStates.reserve(64); m_internedData.reserve(4096); m_encounteredIps.reserve(32768); + m_encounteredClasses.reserve(4096); } ~AccumulatedTraceData() @@ -321,6 +322,23 @@ struct AccumulatedTraceData return ipId; } + size_t addClass(const uintptr_t classPointer, const size_t classSize) { + if (!classPointer) { + return 0; + } + + auto it = m_encounteredClasses.find(classPointer); + if (it != m_encounteredClasses.end()) { + return it->second; + } + + size_t classIndex = intern(m_managedNames[classPointer]); + m_encounteredClasses.insert(it, make_pair(classPointer, classIndex)); + + fprintf(stdout, "C %zx %zx\n", classIndex, classSize); + return classIndex; + } + bool fileIsReadable(const string& path) const { return access(path.c_str(), R_OK) == 0; @@ -406,6 +424,7 @@ private: unordered_map m_managedNames; unordered_map m_internedData; unordered_map m_encounteredIps; + unordered_map m_encounteredClasses; }; } @@ -540,6 +559,7 @@ int main(int /*argc*/, char** /*argv*/) for (auto managedPtr : managedPointersSet) { auto allocation = ptrToIndex.takePointer(managedPtr); + if (!allocation.second) { cerr << "wrong trace format (unknown managed pointer) 0x" << std::hex << managedPtr << std::dec << endl; continue; @@ -663,21 +683,46 @@ int main(int /*argc*/, char** /*argv*/) --leakedAllocations; } else if (reader.mode() == 'n') { uint64_t ip; - string methodName; - if (!(reader >> ip) || !(reader >> methodName)) { + string name; + if (!(reader >> ip) || !(reader >> name)) { cerr << "failed to parse line: " << reader.line() << endl; } - if (managed_name_ids.find(methodName) == managed_name_ids.end()) { - managed_name_ids.insert(std::make_pair(methodName, 1)); + if (managed_name_ids.find(name) == managed_name_ids.end()) { + managed_name_ids.insert(std::make_pair(name, 1)); } else { - int id = ++managed_name_ids[methodName]; + int id = ++managed_name_ids[name]; - methodName.append("~"); - methodName.append(std::to_string(id)); + name.append("~"); + name.append(std::to_string(id)); } - data.addManagedNameForIP(ip, methodName); + data.addManagedNameForIP(ip, name); + } else if (reader.mode() == 'e') { + size_t gcCounter = 0; + size_t numChildren = 0; + uintptr_t objectPointer = 0; + uintptr_t classPointer = 0; + + if (!(reader >> gcCounter) || !(reader >> numChildren) || !(reader >> objectPointer) || !(reader >> classPointer)) { + cerr << "failed to parse line: " << reader.line() << endl; + continue; + } + // ensure class is encountered + const auto classId = data.addClass(classPointer, 0); + const auto objectId = ptrToIndex.peekPointer(objectPointer); + if (!objectId.second) + cerr << "unknown object id (" << objectPointer << ") here: " << reader.line() << endl; + // trace point, map current output index to parent index + fprintf(stdout, "e %zx %zx %zx %zx %zx\n", gcCounter, numChildren, objectPointer, classId, objectId.first.index); + } else if (reader.mode() == 'C') { + uintptr_t classPointer = 0; + size_t classSize = 0; + if (!(reader >> classPointer) || !(reader >> classSize)) { + cerr << "failed to parse line: " << reader.line() << endl; + continue; + } + data.addClass(classPointer, classSize); } else { fputs(reader.line().c_str(), stdout); fputc('\n', stdout); diff --git a/src/track/libheaptrack.cpp b/src/track/libheaptrack.cpp index 35a3914..02f04c2 100644 --- a/src/track/libheaptrack.cpp +++ b/src/track/libheaptrack.cpp @@ -45,10 +45,12 @@ #include #include #include +#include #include #include "tracetree.h" +#include "objectgraph.h" #include "util/config.h" #include "util/libunwind_config.h" @@ -62,6 +64,7 @@ using namespace std; unordered_set TraceTree::knownNames; +std::unordered_map ObjectGraph::m_graph; thread_local bool RecursionGuard::isActive = false; @@ -509,7 +512,10 @@ public: if (fprintf(s_data->out, "G 1\n") < 0) { writeError(); return; - } + } + + ObjectGraph graph; + graph.clear(); } void handleGCSurvivedRange(void *rangeStart, unsigned long rangeLength, void *rangeMovedTo) @@ -528,6 +534,8 @@ public: void handleFinishGC() { + static int gc_counter = 0; + gc_counter++; if (!s_data || !s_data->out) { return; } @@ -535,7 +543,20 @@ public: if (fprintf(s_data->out, "G 0\n") < 0) { writeError(); return; - } + } + + ObjectGraph graph; + graph.print(gc_counter, s_data->out); + } + + void handleLoadClass(void *classId, unsigned long classSize, char *className) { + std::string formattedName; + formattedName.append("["); + formattedName.append(className); + formattedName.append("]"); + fprintf(s_data->out, "n %" PRIxPTR " %s\n", reinterpret_cast(classId), formattedName.c_str()); + fprintf(s_data->out, "C %" PRIxPTR " %lx\n", reinterpret_cast(classId), classSize); + TraceTree::knownNames.insert(classId); } private: @@ -987,6 +1008,25 @@ void heaptrack_finishgc() { heaptrack.handleFinishGC(); } +void heaptrack_add_object_dep(void *keyObjectId, void *keyClassId, void *valObjectId, void *valClassId) { + ObjectGraph graph; + graph.index(keyObjectId, keyClassId, valObjectId, valClassId); +} + +void heaptrack_gcroot(void *objectId, void *classId) { + ObjectGraph graph; + graph.addRoot(objectId, classId); +} + +__attribute__((noinline)) +void heaptrack_loadclass(void *classId, unsigned long classSize, char *className) { + assert(!RecursionGuard::isActive); + RecursionGuard guard; + + HeapTrack heaptrack(guard); + heaptrack.handleLoadClass(classId, classSize, className); +} + void heaptrack_invalidate_module_cache() { RecursionGuard guard; diff --git a/src/track/objectgraph.h b/src/track/objectgraph.h new file mode 100644 index 0000000..4ddabd0 --- /dev/null +++ b/src/track/objectgraph.h @@ -0,0 +1,84 @@ +#ifndef OBJECTGRAPH_H +#define OBJECTGRAPH_H + +#include +#include + +class ObjectNode { +public: + ObjectNode() + : children(), objectId(nullptr), classId(nullptr), visited(false) + { + } + + std::vector children; + void *objectId; + void *classId; + bool visited; + + void print(size_t gcCounter, FILE *out) { + fprintf(out, "e %zx %zx %zx %zx\n", gcCounter, children.size(), reinterpret_cast(objectId), reinterpret_cast(classId)); + for (ObjectNode* child: children) { + child->print(gcCounter, out); + } + } +}; + +class ObjectGraph { +public: + ObjectGraph() {} + + void addRoot(void* rootObjectID, void *rootClassID) { + index(nullptr, nullptr, rootObjectID, rootClassID); + } + + void index(void *keyObjectID, void *keyClassID, void *valObjectID, void *valClassID) { + auto keyIt = m_graph.find(keyObjectID); + if (keyIt == m_graph.end()) { + ObjectNode node; + node.objectId = keyObjectID; + node.classId = keyClassID; + keyIt = m_graph.insert(std::make_pair(keyObjectID, node)).first; + } + + auto valIt = m_graph.find(valObjectID); + if (valIt == m_graph.end()) { + ObjectNode node; + node.objectId = valObjectID; + node.classId = valClassID; + valIt = m_graph.insert(std::make_pair(valObjectID, node)).first; + } + + keyIt->second.children.push_back(&(valIt->second)); + } + + void eliminateLoops(ObjectNode *node) { + node->visited = true; + node->children.erase(remove_if(node->children.begin(), node->children.end(), [](const ObjectNode* child) -> bool { + return child->visited; + }), node->children.end()); + + for (size_t i = 0; i < node->children.size(); ++i) { + eliminateLoops(node->children[i]); + } + } + + void print(size_t gcCounter, FILE* out) { + auto it = m_graph.find(nullptr); + if (it == m_graph.end()) { + return; + } + + ObjectNode *node = &(it->second); + eliminateLoops(node); + node->print(gcCounter, out); + } + + void clear() { + m_graph.clear(); + } + + static std::unordered_map m_graph; +}; + +#endif // OBJECTGRAPH_H diff --git a/src/util/indices.h b/src/util/indices.h index 99e1724..a79a007 100644 --- a/src/util/indices.h +++ b/src/util/indices.h @@ -89,6 +89,9 @@ struct ModuleIndex : public StringIndex struct FunctionIndex : public StringIndex { }; +struct ClassIndex : public StringIndex +{ +}; struct FileIndex : public StringIndex { }; diff --git a/src/util/pointermap.h b/src/util/pointermap.h index b994eec..2ca6605 100644 --- a/src/util/pointermap.h +++ b/src/util/pointermap.h @@ -165,6 +165,24 @@ public: return {index, true}; } + std::pair peekPointer(const uint64_t ptr) + { + const SplitPointer pointer(ptr); + + auto mapIt = map.find(pointer.big); + if (mapIt == map.end()) { + return {{}, false}; + } + auto& indices = mapIt->second; + auto pageIt = std::lower_bound(indices.smallPtrParts.begin(), indices.smallPtrParts.end(), pointer.small); + if (pageIt == indices.smallPtrParts.end() || *pageIt != pointer.small) { + return {{}, false}; + } + auto allocationIt = indices.allocationIndices.begin() + distance(indices.smallPtrParts.begin(), pageIt); + auto index = *allocationIt; + return {index, true}; + } + private: struct Indices { -- 2.7.4