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) {
encodeWChar(szMethodName, methodName);
PushShadowStack(functionID.functionID, className, methodName);
+ info->Release();
}
void OnFunctionLeave(FunctionIDOrClientID functionID,
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);
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<void*>(classId), classSize, className);
+ info->Release();
return S_OK;
}
if (hr == S_OK)
PopShadowStack();
+ info->Release();
+
return S_OK;
}
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;
}
heaptrack_gcmarksurvived((void *) ranges[i].rangeStart, (unsigned long) ranges[i].rangeLength, NULL);
}
-
+ info->Release();
return S_OK;
}
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
allocations.reserve(16384);
stopIndices.reserve(4);
opNewIpIndices.reserve(16);
+ classInfos.reserve(4096);
+ objectTreeNodes.reserve(16384);
}
const string& AccumulatedTraceData::stringify(const StringIndex stringId) const
} 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;
}
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.
*/
std::vector<TraceNode> traces;
std::vector<std::string> strings;
std::vector<IpIndex> opNewIpIndices;
-
std::vector<AllocationInfo> allocationInfos;
+ std::vector<ClassInfo> classInfos;
+ std::vector<ObjectTreeNode> objectTreeNodes;
AddressRangesMap addressRangeInfos;
gui.cpp
mainwindow.cpp
treemodel.cpp
+ objecttreemodel.cpp
treeproxy.cpp
+ objecttreeproxy.cpp
costdelegate.cpp
parser.cpp
flamegraph.cpp
#include "stacksmodel.h"
#include "topproxy.h"
#include "treemodel.h"
+#include "objecttreemodel.h"
#include "treeproxy.h"
+#include "objecttreeproxy.h"
#include "gui_config.h"
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<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
+ proxy, &ObjectTreeProxy::setGCFilter);
+}
+
void setupCallerCalle(CallerCalleeModel* model, QTreeView* view, CostDelegate* costDelegate, QLineEdit* filterFunction,
QLineEdit* filterFile, QLineEdit* filterModule)
{
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);
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),
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;
if (!current.isValid()) {
stacksModel->clear();
} else {
- auto proxy = qobject_cast<const TreeProxy*>(current.model());
+ auto proxy = qobject_cast<const QSortFilterProxyModel*>(current.model());
Q_ASSERT(proxy);
auto leaf = proxy->mapToSource(current);
stacksModel->fillFromIndex(leaf);
<rect>
<x>0</x>
<y>0</y>
- <width>1332</width>
+ <width>1473</width>
<height>896</height>
</rect>
</property>
</property>
<item>
<widget class="KMessageWidget" name="messages">
- <property name="messageType">
- <enum>KMessageWidget::Information</enum>
- </property>
- <property name="icon">
+ <property name="icon" stdset="0">
<iconset theme="dialog-error">
<normaloff>.</normaloff>.</iconset>
</property>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="1" column="1">
- <widget class="KUrlRequester" name="openFile">
+ <widget class="KUrlRequester" name="openFile" native="true">
<property name="toolTip">
<string><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></string>
</property>
- <property name="filter">
+ <property name="filter" stdset="0">
<string>heaptrack.*.*.gz</string>
</property>
- <property name="placeholderText">
+ <property name="placeholderText" stdset="0">
<string>path/to/heaptrack.$APP.$PID.gz</string>
</property>
- <property name="fileDialogModality">
- <enum>Qt::WindowModal</enum>
- </property>
</widget>
</item>
<item row="2" column="1">
- <widget class="KUrlRequester" name="compareTo">
+ <widget class="KUrlRequester" name="compareTo" native="true">
<property name="toolTip">
<string><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></string>
</property>
- <property name="filter">
+ <property name="filter" stdset="0">
<string>heaptrack.*.*.gz</string>
</property>
- <property name="placeholderText">
+ <property name="placeholderText" stdset="0">
<string>path/to/heaptrack.$APP.$PID.gz</string>
</property>
- <property name="fileDialogModality">
- <enum>Qt::WindowModal</enum>
- </property>
</widget>
</item>
<item row="1" column="0">
</item>
</layout>
</widget>
+ <widget class="QWidget" name="heapTab">
+ <attribute name="title">
+ <string>Managed Heap</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_16">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>GC #</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="filterGC"/>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="filterClass">
+ <property name="placeholderText">
+ <string>Search</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QTreeView" name="objectTreeResults">
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ <attribute name="headerDefaultSectionSize">
+ <number>100</number>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </widget>
<widget class="FlameGraph" name="flameGraphTab">
<attribute name="title">
<string>Flame Graph</string>
<rect>
<x>0</x>
<y>0</y>
- <width>1332</width>
- <height>30</height>
+ <width>1473</width>
+ <height>22</height>
</rect>
</property>
<widget class="QMenu" name="menu_File">
<class>KMessageWidget</class>
<extends>QFrame</extends>
<header>kmessagewidget.h</header>
+ <container>1</container>
</customwidget>
<customwidget>
<class>KUrlRequester</class>
--- /dev/null
+/*
+ * Copyright 2015-2017 Milian Wolff <mail@milianw.de>
+ *
+ * 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 <QDebug>
+#include <QTextStream>
+
+#include <KFormat>
+#include <KLocalizedString>
+
+#include <cmath>
+
+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<const ObjectRowData*>(index.internalPointer());
+}
+}
+
+ObjectTreeModel::ObjectTreeModel(QObject* parent)
+ : QAbstractItemModel(parent)
+{
+ qRegisterMetaType<ObjectTreeData>();
+}
+
+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<Columns>(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<Columns>(section)) {
+ case ClassNameColumn:
+ return i18n("<qt>The name of the class.</qt>");
+ case InstanceCountColumn:
+ return i18n("<qt>Total number of instances in this reference chain.</qt>");
+ case GCNumColumn:
+ return i18n("<qt>GC number after which the snapshot has been taken.</qt>");
+ case ShallowSizeColumn:
+ return i18n("<qt>Total size of objects of this type in a given reference chain.</qt>");
+ case ReferencedSizeColumn:
+ return i18n("<qt>Total size of objects of this type referenced by other objects in a given reference chain.</qt>");
+ 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<Columns>(index.column())) {
+ case ClassNameColumn:
+ if (row->className.empty())
+ return i18n("<gcroot>");
+ return i18n(row->className.c_str());
+ case InstanceCountColumn:
+ return static_cast<qint64>(row->allocations);
+ case ShallowSizeColumn:
+ return static_cast<qint64>(row->allocated);
+ case ReferencedSizeColumn:
+ return static_cast<qint64>(row->referenced);
+ case GCNumColumn:
+ return static_cast<quint64>(row->gcNum);
+ case NUM_COLUMNS:
+ break;
+ }
+ } else if (role == Qt::ToolTipRole) {
+ QString tooltip;
+ QTextStream stream(&tooltip);
+ stream << "<qt><pre style='font-family:monospace;'>";
+ stream << "</pre></qt>";
+ 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<void*>(reinterpret_cast<const void*>(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<void*>(reinterpret_cast<const void*>(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);
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-2017 Milian Wolff <mail@milianw.de>
+ *
+ * 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 <QAbstractItemModel>
+#include <QVector>
+
+#include <KFormat>
+#include <string.h>
+
+struct ObjectRowData
+{
+ uint64_t classIndex;
+ std::string className;
+ quint32 gcNum;
+ uint64_t allocations;
+ uint64_t allocated;
+ uint64_t referenced;
+ const ObjectRowData* parent;
+ QVector<ObjectRowData> children;
+ bool operator<(const ObjectRowData& rhs) const
+ {
+ return className < rhs.className;
+ }
+};
+
+using ObjectTreeData = QVector<ObjectRowData>;
+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
--- /dev/null
+/*
+ * Copyright 2015-2017 Milian Wolff <mail@milianw.de>
+ *
+ * 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;
+}
--- /dev/null
+/*
+ * Copyright 2015-2017 Milian Wolff <mail@milianw.de>
+ *
+ * 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 <KRecursiveFilterProxyModel>
+
+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
namespace {
+struct ObjectNode {
+ ObjectNode()
+ : m_children(), m_objectPtr(0), m_classIndex(), m_objectSize(0), gcNum(0)
+ {
+ }
+
+ std::vector<ObjectNode> 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<TypeTree>(new TypeTree(*rParent));
+ m_parents.push_back(std::move(parent));
+ }
+ }
+
+ ~TypeTree() {
+ }
+
+ static std::unique_ptr<TypeTree> create(ObjectNode& node) {
+ auto TT = std::unique_ptr<TypeTree>(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<std::unique_ptr<TypeTree>> createBottomUp(ObjectNode& node) {
+ std::vector<std::unique_ptr<TypeTree>> 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<uint64_t,
+ std::vector<TypeTree*>> classCounters;
+
+ for (auto& parent: m_parents) {
+ classCounters[parent->m_classIndex.index].push_back(parent.get());
+ }
+
+ std::vector<std::unique_ptr<TypeTree>> newParents;
+
+ for (auto it = classCounters.begin(),
+ end = classCounters.end(); it != end; ++it) {
+ auto combinedParent = std::unique_ptr<TypeTree>(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<std::unique_ptr<TypeTree>> m_parents;
+ std::unordered_set<uint32_t> m_uniqueObjects;
+ uint64_t m_totalSize;
+ uint64_t m_referencedSize;
+ int m_gcNum;
+};
+
// TODO: use QString directly
struct StringCache
{
}
}
+void setObjectParents(QVector<ObjectRowData>& 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<ObjectNode> 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)
{
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);
#include "chartmodel.h"
#include "histogrammodel.h"
#include "treemodel.h"
+#include "objecttreemodel.h"
class Parser : public QObject
{
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);
};
m_backtraceStates.reserve(64);
m_internedData.reserve(4096);
m_encounteredIps.reserve(32768);
+ m_encounteredClasses.reserve(4096);
}
~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;
unordered_map<uintptr_t, string> m_managedNames;
unordered_map<string, size_t> m_internedData;
unordered_map<uintptr_t, size_t> m_encounteredIps;
+ unordered_map<uintptr_t, size_t> m_encounteredClasses;
};
}
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;
--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);
#include <string>
#include <thread>
#include <unordered_set>
+#include <unordered_map>
#include <boost/algorithm/string/replace.hpp>
#include "tracetree.h"
+#include "objectgraph.h"
#include "util/config.h"
#include "util/libunwind_config.h"
using namespace std;
unordered_set<Trace::ip_t> TraceTree::knownNames;
+std::unordered_map<void*, ObjectNode> ObjectGraph::m_graph;
thread_local bool RecursionGuard::isActive = false;
if (fprintf(s_data->out, "G 1\n") < 0) {
writeError();
return;
- }
+ }
+
+ ObjectGraph graph;
+ graph.clear();
}
void handleGCSurvivedRange(void *rangeStart, unsigned long rangeLength, void *rangeMovedTo)
void handleFinishGC()
{
+ static int gc_counter = 0;
+ gc_counter++;
if (!s_data || !s_data->out) {
return;
}
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<uintptr_t>(classId), formattedName.c_str());
+ fprintf(s_data->out, "C %" PRIxPTR " %lx\n", reinterpret_cast<uintptr_t>(classId), classSize);
+ TraceTree::knownNames.insert(classId);
}
private:
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;
--- /dev/null
+#ifndef OBJECTGRAPH_H
+#define OBJECTGRAPH_H
+
+#include <vector>
+#include <unordered_map>
+
+class ObjectNode {
+public:
+ ObjectNode()
+ : children(), objectId(nullptr), classId(nullptr), visited(false)
+ {
+ }
+
+ std::vector<ObjectNode*> 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<uintptr_t>(objectId), reinterpret_cast<uintptr_t>(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<void*, ObjectNode> m_graph;
+};
+
+#endif // OBJECTGRAPH_H
struct FunctionIndex : public StringIndex
{
};
+struct ClassIndex : public StringIndex
+{
+};
struct FileIndex : public StringIndex
{
};
return {index, true};
}
+ std::pair<AllocationIndex, bool> 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
{