Add initial take on a FlameGraph based on QGraphicsView.
authorMilian Wolff <mail@milianw.de>
Fri, 21 Aug 2015 21:49:20 +0000 (23:49 +0200)
committerMilian Wolff <mail@milianw.de>
Fri, 21 Aug 2015 22:17:24 +0000 (00:17 +0200)
Many issues still, mostly due to bad scaling, i.e. we'd want
constant text size (elided if parent item is too small), and constant
item height based on text height.

gui/CMakeLists.txt
gui/flamegraph.cpp [new file with mode: 0644]
gui/flamegraph.h [new file with mode: 0644]
gui/mainwindow.cpp
gui/mainwindow.ui
gui/parser.cpp
gui/parser.h

index e0375c2..d0f288e 100644 (file)
@@ -21,6 +21,7 @@ add_executable(heaptrack_gui
     chartproxy.cpp
     modeltest.cpp
     parser.cpp
+    flamegraph.cpp
     ${UIFILES}
 )
 
diff --git a/gui/flamegraph.cpp b/gui/flamegraph.cpp
new file mode 100644 (file)
index 0000000..f3f2f07
--- /dev/null
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2015 Milian Wolff <mail@milianw.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program; if not, write to the
+ * Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "flamegraph.h"
+
+#include <cmath>
+
+#include <QVBoxLayout>
+#include <QGraphicsScene>
+#include <QStyleOption>
+#include <QGraphicsView>
+#include <QGraphicsItem>
+#include <QGraphicsSimpleTextItem>
+#include <QWheelEvent>
+#include <QEvent>
+
+#include <QElapsedTimer>
+#include <QDebug>
+
+#include <KLocalizedString>
+
+namespace {
+
+QColor color(quint64 cost, quint64 maxCost)
+{
+    const double ratio = double(cost) / maxCost;
+    return QColor::fromHsv(120 - ratio * 120, 255, 255, (-((ratio-1) * (ratio-1))) * 120 + 120);
+}
+
+/*
+// TODO: aggregate top-down instead of bottom-up to better resemble
+// other flame graphs with the culprits on top instead of on bottom
+void aggregateStack(TreeLeafItem* item, StackData* data)
+{
+    const QByteArray label = isBelowThreshold(item->label()) ? QByteArray() : functionInLabel(item->label());
+
+    Frame& frame = (*data)[label];
+    frame.cost = qMax(item->cost(), frame.cost);
+
+    foreach(TreeLeafItem* child, item->children()) {
+        aggregateStack(child, &frame.children);
+    }
+}*/
+
+class FrameGraphicsItem : public QGraphicsRectItem
+{
+public:
+    FrameGraphicsItem(const QRectF& rect, const quint64 cost, const QByteArray& function)
+        : QGraphicsRectItem(rect)
+    {
+        static const QString emptyLabel = QStringLiteral("???");
+
+        m_label = i18nc("%1: memory cost, %2: function label",
+                        "%2: %1",
+                        cost,
+                        function.isEmpty() ? emptyLabel : QString::fromUtf8(function));
+        setToolTip(m_label);
+    }
+
+    virtual void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget = 0)
+    {
+        QGraphicsRectItem::paint(painter, option, widget);
+
+        // TODO: text should always be displayed in a constant size and not zoomed
+        // TODO: items should only be scaled horizontally, not vertically
+        // TODO: items should "fit" into the view width
+        static QFontMetrics m(QFont(QStringLiteral("monospace")));
+        const int margin = 5;
+        const int width = rect().width() - 2 * margin;
+        if (width < m.averageCharWidth() * 6) {
+            return;
+        }
+        const int height = rect().height();
+
+        const QPen oldPen = painter->pen();
+        QPen pen = oldPen;
+        pen.setColor(Qt::white);
+        painter->setPen(pen);
+        painter->drawText(margin + rect().x(), rect().y(), width, height, Qt::AlignCenter | Qt::TextSingleLine, m.elidedText(m_label, Qt::ElideRight, width));
+        painter->setPen(oldPen);
+    }
+
+private:
+    QString m_label;
+};
+
+// TODO: what is the right value for maxWidth here?
+QVector<QGraphicsItem*> toGraphicsItems(const FlameGraphData::Stack& data,
+                                        const qreal x_0 = 0, const qreal y_0 = 0,
+                                        const qreal maxWidth = 800., qreal totalCostForColor = 0)
+{
+    QVector<QGraphicsItem*> ret;
+    ret.reserve(data.size());
+
+    double totalCost = 0;
+    foreach(const auto& frame, data) {
+        totalCost += frame.cost;
+    }
+    if (!totalCostForColor) {
+        totalCostForColor = totalCost;
+    }
+    qDebug() << "graphicsitem:" << totalCost << totalCostForColor;
+
+    qreal x = x_0;
+    const qreal h = 25;
+    const qreal y = y_0;
+
+    const qreal x_margin = 0;
+    const qreal y_margin = 2;
+
+    for (auto it = data.constBegin(); it != data.constEnd(); ++it) {
+        const qreal w = maxWidth * double(it.value().cost) / totalCostForColor;
+        FrameGraphicsItem* item = new FrameGraphicsItem(QRectF(x, y, w, h), it.value().cost, it.key());
+        item->setBrush(color(it.value().cost, totalCostForColor));
+        ret += toGraphicsItems(it.value().children, x, y - h - y_margin, w, totalCostForColor);
+        x += w + x_margin;
+        ret << item;
+    }
+
+    return ret;
+}
+
+}
+
+FlameGraph::FlameGraph(QWidget* parent, Qt::WindowFlags flags)
+    : QWidget(parent, flags)
+    , m_scene(new QGraphicsScene(this))
+    , m_view(new QGraphicsView(this))
+{
+    qRegisterMetaType<FlameGraphData>();
+
+    setLayout(new QVBoxLayout);
+
+    m_view->setScene(m_scene);
+    m_view->viewport()->installEventFilter(this);
+
+    layout()->addWidget(m_view);
+}
+
+FlameGraph::~FlameGraph()
+{
+
+}
+
+bool FlameGraph::eventFilter(QObject* object, QEvent* event)
+{
+    if (object == m_view->viewport() && event->type() == QEvent::Wheel) {
+        QWheelEvent* wheelEvent = static_cast<QWheelEvent*>(event);
+        if (wheelEvent->modifiers() == Qt::ControlModifier) {
+            // zoom view with Ctrl + mouse wheel
+            qreal scale = pow(1.1, double(wheelEvent->delta()) / (120.0 * 2.));
+            m_view->scale(scale, scale);
+            return true;
+        }
+
+    }
+    return QObject::eventFilter(object, event);
+}
+
+void FlameGraph::setData(const FlameGraphData& data)
+{
+    m_data = data;
+    m_scene->clear();
+
+    qDebug() << "Evaluating flame graph";
+    QElapsedTimer t; t.start();
+
+    foreach(QGraphicsItem* item, toGraphicsItems(data.stack)) {
+        m_scene->addItem(item);
+    }
+
+    qDebug() << m_scene->itemsBoundingRect() << m_scene->sceneRect() << m_view->rect() << m_view->contentsRect();
+    m_view->fitInView( m_scene->itemsBoundingRect(), Qt::KeepAspectRatio );
+    // TODO: what is the correct scale value here?! without it, the contents in the view are teeny tiny!
+    m_view->scale(5, 5);
+
+    qDebug() << "took me: " << t.elapsed();
+}
diff --git a/gui/flamegraph.h b/gui/flamegraph.h
new file mode 100644 (file)
index 0000000..4ca0fdf
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2015 Milian Wolff <mail@milianw.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program; if not, write to the
+ * Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef FLAMEGRAPH_H
+#define FLAMEGRAPH_H
+
+#include <QWidget>
+#include <QMap>
+
+class QGraphicsScene;
+class QGraphicsView;
+
+struct FlameGraphData
+{
+    struct Frame {
+        quint64 cost;
+        QMap<QByteArray, Frame> children;
+    };
+
+    using Stack = QMap<QByteArray, Frame>;
+
+    Stack stack;
+};
+
+Q_DECLARE_METATYPE(FlameGraphData);
+
+class FlameGraph : public QWidget
+{
+    Q_OBJECT
+public:
+    FlameGraph(QWidget* parent = 0, Qt::WindowFlags flags = 0);
+    ~FlameGraph();
+
+    void setData(const FlameGraphData& data);
+
+protected:
+    virtual bool eventFilter(QObject* object, QEvent* event);
+
+private:
+    QGraphicsScene* m_scene;
+    QGraphicsView* m_view;
+    FlameGraphData m_data;
+};
+
+#endif // FLAMEGRAPH_H
index 228a8b0..6a00f85 100644 (file)
@@ -67,6 +67,8 @@ MainWindow::MainWindow(QWidget* parent)
             m_chartModel, &ChartModel::resetData);
     connect(m_parser, &Parser::summaryAvailable,
             m_ui->summary, &QLabel::setText);
+    connect(m_parser, &Parser::flameGraphDataAvailable,
+            m_ui->flameGraphTab, &FlameGraph::setData);
     connect(m_parser, &Parser::finished,
             this, [&] { m_ui->pages->setCurrentWidget(m_ui->resultsPage); });
 
index bee5db5..8245b9f 100644 (file)
              </widget>
             </item>
            </layout>
-           <zorder>results</zorder>
-           <zorder>filterFile</zorder>
-           <zorder>filterFunction</zorder>
-           <zorder>filterModule</zorder>
-           <zorder>widget</zorder>
-           <zorder>results</zorder>
           </widget>
           <widget class="ChartWidget" name="leakedTab">
            <attribute name="title">
             <string>Allocated</string>
            </attribute>
           </widget>
+          <widget class="FlameGraph" name="flameGraphTab">
+           <attribute name="title">
+            <string>Flame Graph</string>
+           </attribute>
+          </widget>
          </widget>
         </item>
        </layout>
    <header>chartwidget.h</header>
    <container>1</container>
   </customwidget>
+  <customwidget>
+   <class>FlameGraph</class>
+   <extends>QWidget</extends>
+   <header>flamegraph.h</header>
+   <container>1</container>
+  </customwidget>
  </customwidgets>
  <resources/>
  <connections/>
index 66ce6a7..b822697 100644 (file)
@@ -179,6 +179,27 @@ Parser::Parser(QObject* parent)
 
 Parser::~Parser() = default;
 
+static FlameGraphData::Stack fakeStack(int children, int recurse, int* id = 0, quint64* parentCost = 0)
+{
+    int stack_id = 1;
+    FlameGraphData::Stack data;
+    if (!id) {
+        id = &stack_id;
+    }
+    for (int i = 0; i < children; ++i) {
+        FlameGraphData::Frame frame;
+        frame.cost = (i + 1) * 2;
+        if (recurse) {
+            frame.children = fakeStack(children - 1, recurse - 1, id, &frame.cost);
+        }
+        if (parentCost) {
+            parentCost += frame.cost;
+        }
+        data[QByteArray::number((*id)++)] = frame;
+    }
+    return data;
+}
+
 void Parser::parse(const QString& path)
 {
     using namespace ThreadWeaver;
@@ -188,6 +209,8 @@ void Parser::parse(const QString& path)
         emit summaryAvailable(generateSummary(data));
         emit bottomUpDataAvailable(mergeAllocations(data));
         emit chartDataAvailable(data.chartData);
+        // TODO: implement this
+        emit flameGraphDataAvailable({fakeStack(4, 4)});
         emit finished();
     });
 }
index 9307c63..3bd4a1f 100644 (file)
@@ -24,6 +24,7 @@
 
 #include "bottomupmodel.h"
 #include "chartmodel.h"
+#include "flamegraph.h"
 
 class Parser : public QObject
 {
@@ -39,6 +40,7 @@ signals:
     void summaryAvailable(const QString& summary);
     void bottomUpDataAvailable(const BottomUpData& data);
     void chartDataAvailable(const ChartData& data);
+    void flameGraphDataAvailable(const FlameGraphData& data);
     void finished();
 };