From 93a118b7becce4c90aed0c873dafc46dff34bd7e Mon Sep 17 00:00:00 2001 From: Ernie Pasveer Date: Sun, 14 Jun 2026 10:58:22 -0500 Subject: [PATCH] Can't believe it worked. Initial attempt done. --- src/CMakeLists.txt | 5 + src/SeerMainWindow.cpp | 1 + src/SeerParallelStacksCommon.cpp | 215 +++++++++++ src/SeerParallelStacksCommon.h | 99 +++++ src/SeerParallelStacksGraphicsView.cpp | 402 +++++++++++++++++++++ src/SeerParallelStacksGraphicsView.h | 127 +++++++ src/SeerParallelStacksVisualizerWidget.cpp | 313 +--------------- src/SeerParallelStacksVisualizerWidget.h | 97 +---- src/SeerParallelStacksVisualizerWidget.ui | 16 +- 9 files changed, 872 insertions(+), 403 deletions(-) create mode 100644 src/SeerParallelStacksCommon.cpp create mode 100644 src/SeerParallelStacksCommon.h create mode 100644 src/SeerParallelStacksGraphicsView.cpp create mode 100644 src/SeerParallelStacksGraphicsView.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8e35d13..a8c7269 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -50,6 +50,7 @@ elseif(${QTVERSION} STREQUAL "QT5") set(CMAKE_CXX_EXTENSIONS OFF) endif() + # Define the source location of header files set(HEADER_FILES GdbMonitor.h @@ -112,6 +113,8 @@ set(HEADER_FILES SeerVarVisualizerWidget.h SeerImageVisualizerWidget.h SeerParallelStacksVisualizerWidget.h + SeerParallelStacksGraphicsView.h + SeerParallelStacksCommon.h SeerGdbMonitorWidget.h SeerRegisterValuesBrowserWidget.h SeerRegisterEditValueDialog.h @@ -227,6 +230,8 @@ set(SOURCE_FILES SeerVarVisualizerWidget.cpp SeerImageVisualizerWidget.cpp SeerParallelStacksVisualizerWidget.cpp + SeerParallelStacksGraphicsView.cpp + SeerParallelStacksCommon.cpp SeerGdbMonitorWidget.cpp SeerRegisterValuesBrowserWidget.cpp SeerRegisterEditValueDialog.cpp diff --git a/src/SeerMainWindow.cpp b/src/SeerMainWindow.cpp index bffdf05..ed408ae 100644 --- a/src/SeerMainWindow.cpp +++ b/src/SeerMainWindow.cpp @@ -205,6 +205,7 @@ SeerMainWindow::SeerMainWindow(QWidget* parent) : QMainWindow(parent) { QObject::connect(visualizerVarAction, &QAction::triggered, gdbWidget, &SeerGdbWidget::handleGdbVarVisualizer); QObject::connect(visualizerStructAction, &QAction::triggered, gdbWidget, &SeerGdbWidget::handleGdbStructVisualizer); QObject::connect(visualizerImageAction, &QAction::triggered, gdbWidget, &SeerGdbWidget::handleGdbImageVisualizer); + QObject::connect(visualizerParallelStacksAction, &QAction::triggered, gdbWidget, &SeerGdbWidget::handleGdbParallelStacksVisualizer); QObject::connect(visualizerGdbMonitorAction, &QAction::triggered, gdbWidget, &SeerGdbWidget::handleGdbMonitor); QObject::connect(gdbWidget->gdbMonitor(), &GdbMonitor::astrixTextOutput, runStatus, &SeerRunStatusIndicator::handleText); diff --git a/src/SeerParallelStacksCommon.cpp b/src/SeerParallelStacksCommon.cpp new file mode 100644 index 0000000..4fa5236 --- /dev/null +++ b/src/SeerParallelStacksCommon.cpp @@ -0,0 +1,215 @@ +// SPDX-FileCopyrightText: 2021 Ernie Pasveer +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "SeerParallelStacksVisualizerWidget.h" +#include "SeerHelpPageDialog.h" +#include "SeerUtl.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Seer { +namespace PSV { + + Frame::Frame() { + } + + Frame::Frame(const QString& text) { + + _level = Seer::parseFirst(text, "level=", '"', '"', false).toInt(); + _addr = Seer::parseFirst(text, "addr=", '"', '"', false); + _function = Seer::parseFirst(text, "func=", '"', '"', false); + _arch = Seer::parseFirst(text, "arch=", '"', '"', false); + _file = Seer::parseFirst(text, "file=", '"', '"', false); + _fullname = Seer::parseFirst(text, "fullname=", '"', '"', false); + _line = Seer::parseFirst(text, "line=", '"', '"', false).toInt(); + _type = Seer::parseFirst(text, "type=", '"', '"', false); + } + + Frame::~Frame() { + } + + int Frame::level () const { + return _level; + } + + const QString& Frame::addr () const { + return _addr; + } + + const QString& Frame::function () const { + return _function; + } + + const QString& Frame::arch () const { + return _arch; + } + + const QString& Frame::file () const { + return _file; + } + + const QString& Frame::fullname () const { + return _fullname; + } + + int Frame::line () const { + return _line; + } + + const QString& Frame::type () const { + return _type; + } + + QString Frame::toString() const { + return QString("level: %1, address: '%2', function: '%3', file: '%4', fullname: '%5'") + .arg(_level).arg(_addr).arg(_function).arg(_file).arg(_fullname); + } + + Thread::Thread () { + } + + Thread::Thread (const QString& text) { + + _id = Seer::parseFirst(text, "thread-id=", '"', '"', false).toInt(); + _target_id = Seer::parseFirst(text, "target-id=", '"', '"', false); + _name = Seer::parseFirst(text, "name=", '"', '"', false); + _current = Seer::parseFirst(text, "current=", '"', '"', false).toInt(); + + QString frames_text = Seer::parseFirst(text, "frames=", '[', ']', false); + + QStringList frame_list = Seer::parse(frames_text, "", '{', '}', false); + + // Loop through each thread. + for (const auto& frame_text : frame_list) { + Frame frame(frame_text); + _frames.push_back(frame); + } + } + + Thread::~Thread () { + } + + int Thread::id () const { + return _id; + } + + const QString& Thread::target_id () const { + return _target_id; + } + + const QString& Thread::name () const { + return _name; + } + + const QString& Thread::state () const { + return _state; + } + + int Thread::current () const { + return _current; + } + + int Thread::frameCount () const { + return _frames.size(); + } + + const Frame& Thread::frame (int i) const { + return _frames[i]; + } + + const Frames& Thread::frames () const { + return _frames; + } + + QString Thread::toString() const { + + QString result = QString("Thread %1").arg(_id); + + for (const auto &f : _frames) + result += "\n " + f.toString(); + + return result; + } + + static std::shared_ptr buildImpl( QVector &threadPtrs, const QString ¤tFunction, int depth) { + + auto node = std::make_shared(); + node->depth = depth; + node->function = currentFunction; + node->threads = threadPtrs; + + // Group threads by the function at position [-depth-1] (bottom-up). + QMap> functionThreads; + int level = -depth - 1; + + for (Thread *t : threadPtrs) { + int idx = t->frames().size() + level; // convert negative index + if (idx < 0 || idx >= t->frames().size()) + continue; + const QString &fn = t->frames()[idx].function(); + functionThreads[fn].append(t); + } + + for (auto it = functionThreads.begin(); it != functionThreads.end(); ++it) { + auto child = buildImpl(it.value(), it.key(), depth + 1); + node->children.append(child); + } + + return node; + } + + std::shared_ptr buildParallelStacks(QVector &threads) { + + QVector ptrs; + ptrs.reserve(threads.size()); + + for (auto &t : threads) + ptrs.append(&t); + + return buildImpl(ptrs, QString(), 0); + } + + // --------------------------------------------------------------- + // fillStack — flatten StackNode tree into Stack tree for graphing + // --------------------------------------------------------------- + std::shared_ptr fillStack(const std::shared_ptr &node) { + + auto stack = std::make_shared(); + stack->threadCount = static_cast(node->threads.size()); + + // Collect thread IDs for this node + for (const Thread *t : node->threads) + stack->threadIds.append(QString::number(t->id())); + + if (!node->function.isEmpty()) + stack->functions.append(node->function); + + if (node->children.size() == 1) { + // Merge single child into this stack (chain of frames). + // Keep the IDs from the leaf (most specific) node. + auto child = fillStack(node->children[0]); + stack->functions += child->functions; + stack->stacks = child->stacks; + stack->threadCount = child->threadCount; + stack->threadIds = child->threadIds; + } else { + for (const auto &childNode : node->children) { + stack->stacks.append(fillStack(childNode)); + } + } + + return stack; + } + +} // namespace PSV +} // namespace Seer diff --git a/src/SeerParallelStacksCommon.h b/src/SeerParallelStacksCommon.h new file mode 100644 index 0000000..4e6a96a --- /dev/null +++ b/src/SeerParallelStacksCommon.h @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2026 Ernie Pasveer +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace Seer { +namespace PSV { + + class Frame { + public: + Frame (); + Frame (const QString& text); + ~Frame (); + + int level () const; + const QString& addr () const; + const QString& function () const; + const QString& arch () const; + const QString& file () const; + const QString& fullname () const; + int line () const; + const QString& type () const; + QString toString () const; + + private: + int _level; + QString _addr; + QString _function; + QString _arch; + QString _file; + QString _fullname; + int _line; + QString _type; + }; + + typedef QVector Frames; + + class Thread { + public: + Thread (); + Thread (const QString& text); + ~Thread (); + + int id () const; + const QString& target_id () const; + const QString& name () const; + const QString& state () const; + int current () const; + QString toString () const; + + int frameCount () const; + const Frame& frame (int i) const; + const Frames& frames () const; + + private: + int _id; + QString _target_id; + QString _name; + QString _state; + int _current; + + Frames _frames; + }; + + typedef QVector Threads; + + typedef QVector ThreadIds; + + struct StackNode { + QString function; // empty == root + int depth = 0; + QVector threads; // non-owning pointers + QVector> children; + }; + + // Build the parallel-stacks tree from a flat list of threads. + std::shared_ptr buildParallelStacks(QVector &threads); + + // Flat "Stack" representation used when building the graph. + struct Stack { + QVector functions; + QVector> stacks; + int threadCount = 0; + QVector threadIds; // IDs of every thread in this node + }; + + std::shared_ptr fillStack(const std::shared_ptr &node); + +} // namespace PSV +} // namespace Seer + diff --git a/src/SeerParallelStacksGraphicsView.cpp b/src/SeerParallelStacksGraphicsView.cpp new file mode 100644 index 0000000..0fa7619 --- /dev/null +++ b/src/SeerParallelStacksGraphicsView.cpp @@ -0,0 +1,402 @@ +#include "SeerParallelStacksGraphicsView.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Seer { +namespace PSV { + +// ================================================================ +// StackBoxItem +// ================================================================ + +StackBoxItem::StackBoxItem(const Stack &stack, QGraphicsItem *parent) + : QGraphicsItem(parent) +{ + // Enable geometry-change notifications so itemChange() fires on setPos() + setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); + + m_headerLeft = QString("%1 Thread%2") + .arg(stack.threadCount) + .arg(stack.threadCount == 1 ? "" : "s"); + + if (!stack.threadIds.isEmpty()) { + QStringList ids = stack.threadIds; + if (ids.size() > 8) + m_headerRight = QString("[%1 … +%2]") + .arg(ids.mid(0, 8).join(", ")) + .arg(ids.size() - 8); + else + m_headerRight = "[" + ids.join(", ") + "]"; + } + + for (int i = stack.functions.size() - 1; i >= 0; --i) + m_rows.append({ stack.functions[i], Function }); + + QFont boldFont; boldFont.setBold(true); + QFontMetrics boldFm(boldFont); + QFont normFont; + QFontMetrics normFm(normFont); + + qreal headerW = boldFm.horizontalAdvance(m_headerLeft) + + boldFm.horizontalAdvance(m_headerRight) + + kHeaderGap; + qreal maxTextW = headerW; + for (const auto &row : m_rows) + maxTextW = std::max(maxTextW, (qreal)normFm.horizontalAdvance(row.text)); + + m_width = maxTextW + 2 * kPadX; + m_height = kPadY + kRowH * (1 + (int)m_rows.size()) + kPadY; +} + +QRectF StackBoxItem::boundingRect() const +{ + return QRectF(0, 0, m_width, m_height); +} + +void StackBoxItem::paint(QPainter *painter, + const QStyleOptionGraphicsItem *, + QWidget *) +{ + painter->setRenderHint(QPainter::Antialiasing); + + painter->setBrush(QColor(0xFA, 0xFA, 0xFA)); + painter->setPen(QPen(QColor(0x88, 0x88, 0x88), 1.5)); + painter->drawRoundedRect(boundingRect(), 6, 6); + + QFont boldFont; boldFont.setBold(true); + QFont normFont; + const qreal innerW = m_width - 2 * kPadX; + qreal y = kPadY; + + painter->setFont(boldFont); + painter->setPen(QColor(0x22, 0x22, 0x22)); + painter->drawText(QRectF(kPadX, y, innerW, kRowH), + Qt::AlignLeft | Qt::AlignVCenter, m_headerLeft); + if (!m_headerRight.isEmpty()) { + painter->setPen(QColor(0x1A, 0x52, 0xA8)); + painter->drawText(QRectF(kPadX, y, innerW, kRowH), + Qt::AlignRight | Qt::AlignVCenter, m_headerRight); + } + y += kRowH; + + painter->setPen(QPen(QColor(0xCC, 0xCC, 0xCC), 1)); + painter->drawLine(QPointF(0, y), QPointF(m_width, y)); + + painter->setFont(normFont); + painter->setPen(QColor(0x00, 0x7A, 0x33)); + for (const auto &row : m_rows) { + painter->drawText(QRectF(kPadX, y, innerW, kRowH), + Qt::AlignLeft | Qt::AlignVCenter, row.text); + y += kRowH; + } + + if (m_dragging) { + painter->setBrush(Qt::NoBrush); + painter->setPen(QPen(QColor(0x1A, 0x52, 0xA8), 2.0, Qt::DashLine)); + painter->drawRoundedRect(boundingRect().adjusted(1, 1, -1, -1), 6, 6); + } +} + +QPointF StackBoxItem::sceneBottom() const +{ + return mapToScene(QPointF(m_width / 2.0, m_height)); +} + +QPointF StackBoxItem::sceneTop() const +{ + return mapToScene(QPointF(m_width / 2.0, 0)); +} + +QVariant StackBoxItem::itemChange(GraphicsItemChange change, const QVariant &value) +{ + if (change == ItemPositionHasChanged) { + for (LiveEdge *e : m_edges) + e->update(); // ask each connected edge to repaint + } + return QGraphicsItem::itemChange(change, value); +} + +void StackBoxItem::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + if (event->button() == Qt::LeftButton && + event->modifiers() & Qt::ControlModifier) { + m_dragging = true; + m_dragOffset = event->pos(); + setCursor(Qt::ClosedHandCursor); + setZValue(10); + update(); + event->accept(); + } else { + QGraphicsItem::mousePressEvent(event); + } +} + +void StackBoxItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) +{ + if (m_dragging) { + setPos(mapToScene(event->pos() - m_dragOffset)); + event->accept(); + } else { + QGraphicsItem::mouseMoveEvent(event); + } +} + +void StackBoxItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + if (m_dragging && event->button() == Qt::LeftButton) { + m_dragging = false; + setCursor(Qt::ArrowCursor); + setZValue(0); + update(); + event->accept(); + } else { + QGraphicsItem::mouseReleaseEvent(event); + } +} + +// ================================================================ +// LiveEdge — redraws itself each paint() from current endpoint positions +// ================================================================ + +LiveEdge::LiveEdge(StackBoxItem *from, StackBoxItem *to, QGraphicsItem *parent) + : QGraphicsItem(parent) + , m_from(from) + , m_to(to) +{ + setZValue(-1); + // Position the edge item at the scene origin; all coordinates are scene-space. + setPos(0, 0); + + m_from->registerEdge(this); + m_to->registerEdge(this); +} + +LiveEdge::~LiveEdge() +{ + // Guard against half-destroyed scenes + if (m_from) m_from->unregisterEdge(this); + if (m_to) m_to->unregisterEdge(this); +} + +QRectF LiveEdge::boundingRect() const +{ + // Return the bounding rect of the two endpoints plus generous padding + // so the bezier and arrowhead are never clipped. + if (!m_from || !m_to) return QRectF(); + QPointF f = m_from->sceneBottom(); + QPointF t = m_to->sceneTop(); + qreal pad = kArrow + kVCtrl + 4; + return QRectF(f, t).normalized().adjusted(-pad, -pad, pad, pad); +} + +void LiveEdge::paint(QPainter *painter, + const QStyleOptionGraphicsItem *, + QWidget *) +{ + if (!m_from || !m_to) return; + + painter->setRenderHint(QPainter::Antialiasing); + + QPointF from = m_from->sceneBottom(); + QPointF to = m_to->sceneTop(); + + // Bezier: control points pull vertically toward each other + QPainterPath path; + path.moveTo(from); + path.cubicTo(from + QPointF(0, kVCtrl), + to + QPointF(0, -kVCtrl), + to); + + painter->setPen(QPen(QColor(0x55, 0x55, 0x55), 1.5)); + painter->setBrush(Qt::NoBrush); + painter->drawPath(path); + + // Arrowhead at `to` pointing downward (into the parent box) + // The tangent direction at the end of the cubic is (to - cp2) + QPointF cp2 = to + QPointF(0, -kVCtrl); + QPointF dir = to - cp2; + double len = std::hypot(dir.x(), dir.y()); + if (len < 1e-6) return; + dir /= len; // normalise + + // Perpendicular + QPointF perp(-dir.y(), dir.x()); + + QPointF a1 = to - dir * kArrow + perp * (kArrow * 0.5); + QPointF a2 = to - dir * kArrow - perp * (kArrow * 0.5); + + QPolygonF arrowHead; + arrowHead << to << a1 << a2; + painter->setPen(Qt::NoPen); + painter->setBrush(QColor(0x55, 0x55, 0x55)); + painter->drawPolygon(arrowHead); +} + +} // namespace PSV +} // namespace Seer + +// ================================================================ +// SeerParallelStacksGraphicsView +// ================================================================ + +using Seer::PSV::StackBoxItem; +using Seer::PSV::LiveEdge; + +constexpr qreal kHGap = 30.0; +constexpr qreal kVGap = 60.0; + +SeerParallelStacksGraphicsView::SeerParallelStacksGraphicsView(QWidget *parent) + : QGraphicsView(parent) + , m_scene(new QGraphicsScene(this)) +{ + setScene(m_scene); + setRenderHint(QPainter::Antialiasing); + setDragMode(QGraphicsView::ScrollHandDrag); + setTransformationAnchor(QGraphicsView::AnchorUnderMouse); + setBackgroundBrush(QColor(0xF0, 0xF0, 0xF0)); +} + +void SeerParallelStacksGraphicsView::wheelEvent(QWheelEvent *event) +{ + const double factor = event->angleDelta().y() > 0 ? 1.15 : 1.0 / 1.15; + scale(factor, factor); +} + +void SeerParallelStacksGraphicsView::setStack(const std::shared_ptr &root) +{ + m_scene->clear(); + if (!root) return; + + auto *rootPN = new PlacedNode; + buildPlacedTree(rootPN, root, nullptr); + + qreal xCursor = 0; + layoutTree(rootPN, xCursor, 0); + + // --- Align parentless items to a shared bottom row --- + // After layout, children sit above (smaller Y) their parents. + // Nodes whose PlacedNode parent has no item are the visual roots — + // they have nothing above them. We push them all to the same bottom + // Y so they form a flush baseline at the deepest point in the scene. + + // 1. Find the maximum bottom edge across every placed item. + qreal maxBottom = 0; + collectMaxBottom(rootPN, maxBottom); + + // 2. Shift each parentless item so its bottom aligns to maxBottom. + alignParentlessToBottom(rootPN, maxBottom); + + addEdges(rootPN); + deleteTree(rootPN); + + m_scene->setSceneRect(m_scene->itemsBoundingRect().adjusted(-40, -40, 40, 40)); + fitInView(m_scene->sceneRect(), Qt::KeepAspectRatio); +} + +// Recursively find the maximum bottom edge (item->y() + item->height()) in the tree. +void SeerParallelStacksGraphicsView::collectMaxBottom(PlacedNode *pn, qreal &maxBottom) +{ + if (pn->item) + maxBottom = std::max(maxBottom, pn->item->y() + pn->item->height()); + for (auto *child : pn->children) + collectMaxBottom(child, maxBottom); +} + +// Shift every parentless item (direct visual root — parent has no box) +// downward so its bottom edge sits at maxBottom. +void SeerParallelStacksGraphicsView::alignParentlessToBottom(PlacedNode *pn, qreal maxBottom) +{ + for (auto *child : pn->children) { + // child->parent == rootPN (which has no item), so child is parentless. + if (child->item) { + qreal currentBottom = child->item->y() + child->item->height(); + qreal dy = maxBottom - currentBottom; + if (std::abs(dy) > 0.5) + child->item->setPos(child->item->x(), child->item->y() + dy); + } + // Do NOT recurse — only top-level parentless nodes are shifted. + } +} + +void SeerParallelStacksGraphicsView::buildPlacedTree(PlacedNode *pn, + const std::shared_ptr &stack, + PlacedNode *parentPN) +{ + pn->stack = stack; + pn->parent = parentPN; + + if (!stack->functions.isEmpty()) { + pn->item = new StackBoxItem(*stack); + m_scene->addItem(pn->item); + } + + for (const auto &child : stack->stacks) { + auto *childPN = new PlacedNode; + buildPlacedTree(childPN, child, pn); + pn->children.append(childPN); + } +} + +void SeerParallelStacksGraphicsView::layoutTree(PlacedNode *pn, qreal &xCursor, qreal yTop) +{ + qreal itemW = pn->item ? pn->item->width() : 0; + qreal itemH = pn->item ? pn->item->height() : 0; + + if (pn->children.isEmpty()) { + pn->cx = xCursor + itemW / 2.0; + if (pn->item) { + pn->item->setPos(xCursor, yTop); + pn->cy = yTop + itemH; + } + xCursor += itemW + kHGap; + } else { + qreal childY = yTop; + qreal firstCx = -1, lastCx = -1; + + for (auto *child : pn->children) { + layoutTree(child, xCursor, childY); + if (firstCx < 0) firstCx = child->cx; + lastCx = child->cx; + } + + pn->cx = (firstCx + lastCx) / 2.0; + + qreal maxChildBottom = childY; + for (auto *child : pn->children) + maxChildBottom = std::max(maxChildBottom, child->cy); + qreal parentY = maxChildBottom + kVGap; + + if (pn->item) { + pn->item->setPos(pn->cx - itemW / 2.0, parentY); + pn->cy = parentY + itemH; + } else { + pn->cy = parentY; + } + } +} + +void SeerParallelStacksGraphicsView::addEdges(PlacedNode *pn) +{ + for (auto *child : pn->children) { + if (pn->item && child->item) { + // LiveEdge registers itself with both endpoints on construction. + // The scene takes ownership via addItem. + auto *edge = new LiveEdge(child->item, pn->item); + m_scene->addItem(edge); + } + addEdges(child); + } +} + +void SeerParallelStacksGraphicsView::deleteTree(PlacedNode *pn) +{ + for (auto *child : pn->children) + deleteTree(child); + delete pn; +} diff --git a/src/SeerParallelStacksGraphicsView.h b/src/SeerParallelStacksGraphicsView.h new file mode 100644 index 0000000..a4dd394 --- /dev/null +++ b/src/SeerParallelStacksGraphicsView.h @@ -0,0 +1,127 @@ +#pragma once +#include "SeerParallelStacksCommon.h" +#include +#include +#include +#include +#include +#include + +namespace Seer { +namespace PSV { + +class LiveEdge; // forward — StackBoxItem needs to know it + +// --------------------------------------------------------------- +// A single box in the graph: shows thread count + IDs + call frames. +// Ctrl+LMB grabs and moves the item freely within the scene. +// Moving the item automatically redraws all connected LiveEdges. +// --------------------------------------------------------------- +class StackBoxItem : public QGraphicsItem +{ +public: + explicit StackBoxItem(const Stack &stack, QGraphicsItem *parent = nullptr); + + QRectF boundingRect() const override; + void paint(QPainter *painter, + const QStyleOptionGraphicsItem *option, + QWidget *widget) override; + + qreal width() const { return m_width; } + qreal height() const { return m_height; } + + // Edge registry — called by LiveEdge on construction/destruction + void registerEdge (LiveEdge *e) { m_edges.append(e); } + void unregisterEdge(LiveEdge *e) { m_edges.removeAll(e); } + + // Bottom-centre and top-centre in scene coordinates (edge attach points) + QPointF sceneBottom() const; + QPointF sceneTop() const; + +protected: + QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; + + void mousePressEvent (QGraphicsSceneMouseEvent *event) override; + void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; + +private: + enum RowKind { Function }; + struct Row { QString text; RowKind kind; }; + + QString m_headerLeft; + QString m_headerRight; + QVector m_rows; + qreal m_width = 0; + qreal m_height = 0; + + bool m_dragging = false; + QPointF m_dragOffset; + + QVector m_edges; // non-owning + + static constexpr qreal kPadX = 12; + static constexpr qreal kPadY = 8; + static constexpr qreal kRowH = 20; + static constexpr qreal kHeaderGap = 16; +}; + +// --------------------------------------------------------------- +// A live bezier edge between two StackBoxItems. +// It redraws itself whenever either endpoint moves. +// --------------------------------------------------------------- +class LiveEdge : public QGraphicsItem +{ +public: + LiveEdge(StackBoxItem *from, StackBoxItem *to, QGraphicsItem *parent = nullptr); + ~LiveEdge() override; + + QRectF boundingRect() const override; + void paint(QPainter *painter, + const QStyleOptionGraphicsItem *option, + QWidget *widget) override; + +private: + StackBoxItem *m_from; // child (bottom anchor) + StackBoxItem *m_to; // parent (top anchor) + + static constexpr qreal kArrow = 8.0; + static constexpr qreal kVCtrl = 60.0 * 0.4; // bezier control-point stretch +}; + +} // namespace PSV +} // namespace Seer + +// --------------------------------------------------------------- +// The full graph view. Scroll-drag (no modifier) pans the canvas. +// --------------------------------------------------------------- +class SeerParallelStacksGraphicsView : public QGraphicsView +{ + Q_OBJECT +public: + explicit SeerParallelStacksGraphicsView(QWidget *parent = nullptr); + void setStack(const std::shared_ptr &root); + +protected: + void wheelEvent(QWheelEvent *event) override; + +private: + struct PlacedNode { + std::shared_ptr stack; + Seer::PSV::StackBoxItem *item = nullptr; + PlacedNode *parent = nullptr; + QVector children; + qreal cx = 0; + qreal cy = 0; + }; + + QGraphicsScene *m_scene; + + void buildPlacedTree(PlacedNode *pn, const std::shared_ptr &stack, + PlacedNode *parentPN); + void layoutTree(PlacedNode *pn, qreal &xCursor, qreal yTop); + void collectMaxBottom(PlacedNode *pn, qreal &maxBottom); + void alignParentlessToBottom(PlacedNode *pn, qreal maxBottom); + void addEdges(PlacedNode *pn); + void deleteTree(PlacedNode *pn); +}; diff --git a/src/SeerParallelStacksVisualizerWidget.cpp b/src/SeerParallelStacksVisualizerWidget.cpp index 1e96344..7ae6599 100644 --- a/src/SeerParallelStacksVisualizerWidget.cpp +++ b/src/SeerParallelStacksVisualizerWidget.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "SeerParallelStacksVisualizerWidget.h" +#include "SeerParallelStacksCommon.h" #include "SeerHelpPageDialog.h" #include "SeerUtl.h" #include @@ -17,214 +18,10 @@ #include #include -namespace Seer::PSV { - - Frame::Frame() { - } - - Frame::Frame(const QString& text) { - - _level = Seer::parseFirst(text, "level=", '"', '"', false).toInt(); - _addr = Seer::parseFirst(text, "addr=", '"', '"', false); - _function = Seer::parseFirst(text, "func=", '"', '"', false); - _arch = Seer::parseFirst(text, "arch=", '"', '"', false); - _file = Seer::parseFirst(text, "file=", '"', '"', false); - _fullname = Seer::parseFirst(text, "fullname=", '"', '"', false); - _line = Seer::parseFirst(text, "line=", '"', '"', false).toInt(); - _type = Seer::parseFirst(text, "type=", '"', '"', false); - } - - Frame::~Frame() { - } - - int Frame::level () const { - return _level; - } - - const QString& Frame::addr () const { - return _addr; - } - - const QString& Frame::function () const { - return _function; - } - - const QString& Frame::arch () const { - return _arch; - } - - const QString& Frame::file () const { - return _file; - } - - const QString& Frame::fullname () const { - return _fullname; - } - - int Frame::line () const { - return _line; - } - - const QString& Frame::type () const { - return _type; - } - - Thread::Thread () { - } - - Thread::Thread (const QString& text) { - - _id = Seer::parseFirst(text, "thread-id=", '"', '"', false).toInt(); - _target_id = Seer::parseFirst(text, "target-id=", '"', '"', false); - _name = Seer::parseFirst(text, "name=", '"', '"', false); - _current = Seer::parseFirst(text, "current=", '"', '"', false).toInt(); - - QString frames_text = Seer::parseFirst(text, "frames=", '[', ']', false); - - QStringList frame_list = Seer::parse(frames_text, "", '{', '}', false); - - // Loop through each thread. - for (const auto& frame_text : frame_list) { - Frame frame(frame_text); - _frames.push_back(frame); - } - } - - Thread::~Thread () { - } - - int Thread::id () const { - return _id; - } - - const QString& Thread::target_id () const { - return _target_id; - } - - const QString& Thread::name () const { - return _name; - } - - const QString& Thread::state () const { - return _state; - } - - int Thread::current () const { - return _current; - } - - int Thread::frameCount () const { - return _frames.size(); - } - - const Frame& Thread::frame (int i) const { - return _frames[i]; - } - - const Frames& Thread::frames () const { - return _frames; - } - - GraphNode::GraphNode (const QString& name, QObject* parent) : QObject(parent) { - - _name = name; - - qDebug() << "Creating node:" << _name; - } - - GraphNode::~GraphNode () { - - qDebug() << "Destroying node:" << _name; - } - - QString GraphNode::name () const { - - return _name; - } - - void GraphNode::addChild (GraphNode* child) { - - child->setParent(this); // QObject handles parent-child relationship - } - - GraphNode* GraphNode::getChild (int index) const { - - const QObjectList &childList = children(); - - if (index >= 0 && index < childList.size()) { - return qobject_cast(childList.at(index)); - } - - return nullptr; - } - - int GraphNode::childCount () const { - - return children().size(); - } - - void GraphNode::addFrame (const Frame& frame) { - - _frames.push_back(frame); - } - - const Frames& GraphNode::frames () const { - - return _frames; - } - - int GraphNode::frameCount () const { - - return _frames.size(); - } - - void GraphNode::addThreadId (int id) { - - _threadIds.push_back(id); - } - - const ThreadIds& GraphNode::threadIds () const { - - return _threadIds; - } - - int GraphNode::threadIdCount () const { - - return _threadIds.size(); - } - - void GraphNode::printTree (int level) const { - - { - QDebug dbg = qDebug().noquote().nospace(); - - QString indent(level * 2, ' '); - dbg << indent << "└─" << _name; - dbg << " ThreadCount: " << threadIdCount(); - dbg << " Ids: "; - for (int id : threadIds()) { - dbg << id << ", "; - } - for (auto frame : frames()) { - dbg << indent << " " << frame.function() << '\n'; - } - } - - for (QObject* child : children()) { - GraphNode* node = qobject_cast(child); - if (node) { - node->printTree(level + 1); - } - } - } -}; - - SeerParallelStacksVisualizerWidget::SeerParallelStacksVisualizerWidget (QWidget* parent) : QWidget(parent) { // Init variables. - _id = Seer::createID(); // ID for parallelstacks command. - _gnodes = 0; + _id = Seer::createID(); // ID for parallelstacks command. // Set up UI. setupUi(this); @@ -294,7 +91,10 @@ void SeerParallelStacksVisualizerWidget::handleText (const QString& text) { }else if (text.startsWith("^error,msg=\"No registers.\"")) { - imageViewer->clearImage(); + // Clear old scene. + if (QGraphicsScene* scene = graphicsView->scene()) { + scene->clear(); + } // At a stopping point, refresh. }else if (text.startsWith("*stopped,reason=\"")) { @@ -328,12 +128,16 @@ void SeerParallelStacksVisualizerWidget::handleHelpButton () { void SeerParallelStacksVisualizerWidget::handlePrintButton () { + /* XXX Implement PRINT logic on Scene. imageViewer->print(); + */ } void SeerParallelStacksVisualizerWidget::handleSaveButton () { + /* XXX Implement SAVE logic on Scene. imageViewer->saveFileDialog("/tmp/temp.png"); + */ } void SeerParallelStacksVisualizerWidget::writeSettings() { @@ -363,96 +167,15 @@ void SeerParallelStacksVisualizerWidget::resizeEvent (QResizeEvent* event) { void SeerParallelStacksVisualizerWidget::createDirectedGraph() { - // Clear old image. - imageViewer->clearImage(); - - // Create the 'gv' file. - QFile file("/tmp/xxx.gv"); - - if (file.open(QFile::WriteOnly|QFile::Text|QFile::Truncate) == false) { - qDebug() << "Can't create 'gv' file!"; + // Clear old scene. + if (QGraphicsScene* scene = graphicsView->scene()) { + scene->clear(); } - QTextStream out(&file); - - // Write 'gv' header. - out << "digraph {\n"; - - out << "\tgraph [rankdir=BT]\n"; - out << "\tnode [shape=plaintext]\n"; - - // Loop through each stack and create the graph. - for (int t=0; t<_threads.size(); t++) { - - out << "\t" << QString::number(t) << " [label=<"; - - for (int f=0; f<_threads[t].frameCount(); f++) { - - const Seer::PSV::Frame& frame = _threads[t].frame(f); - - out << ""; - } - - out << "
1 Threads
" << frame.function().toHtmlEscaped() << "
>]\n"; - } - - // Write 'gv' footer. - out << "}\n"; - - // Close 'gv' file. - file.close(); - - // Now create the pdf file. - // dot -Tpdf xxx.gv -o xxx.gv.pdf - QString program = "dot"; - QStringList arguments; - - arguments << "-Tsvg"; - arguments << "/tmp/xxx.gv"; - arguments << "-o"; - arguments << "/tmp/xxx.gv.svg"; - - int exitCode = QProcess::execute(program, arguments); - if (exitCode != 0) { - qDebug() << "Command failed with exit code:" << exitCode; - return; - } - - // View the image. - imageViewer->loadFile("/tmp/xxx.gv.svg"); - - // Delete tmp files. - QFile::remove("/tmp/xxx.gv"); - QFile::remove("/tmp/xxx.gv.svg"); - - // - // Make GraphNodes - // - - if (_gnodes != 0) { - delete _gnodes; - _gnodes = 0; - } - - _gnodes = new Seer::PSV::GraphNode("root"); - - // Loop through each stack and create the graph. - for (int t=0; t<_threads.size(); t++) { - - Seer::PSV::GraphNode* gnode = new Seer::PSV::GraphNode(QString::number(t)); - - gnode->addThreadId(_threads[t].id()); - - for (int f=0; f<_threads[t].frameCount(); f++) { - - const Seer::PSV::Frame& frame = _threads[t].frame(f); - - gnode->addFrame(frame); - } - - _gnodes->addChild(gnode); - } - - _gnodes->printTree(); + // Build parallel-stacks tree + QVector local = _threads; // mutable copy for ptr stability + auto root = Seer::PSV::buildParallelStacks(local); + auto stack = Seer::PSV::fillStack(root); + graphicsView->setStack(stack); } diff --git a/src/SeerParallelStacksVisualizerWidget.h b/src/SeerParallelStacksVisualizerWidget.h index f6543cb..eabc4d4 100644 --- a/src/SeerParallelStacksVisualizerWidget.h +++ b/src/SeerParallelStacksVisualizerWidget.h @@ -5,103 +5,9 @@ #pragma once #include -#include +#include #include "ui_SeerParallelStacksVisualizerWidget.h" -namespace Seer::PSV { - - class Frame { - public: - Frame (); - Frame (const QString& text); - ~Frame (); - - int level () const; - const QString& addr () const; - const QString& function () const; - const QString& arch () const; - const QString& file () const; - const QString& fullname () const; - int line () const; - const QString& type () const; - - private: - int _level; - QString _addr; - QString _function; - QString _arch; - QString _file; - QString _fullname; - int _line; - QString _type; - }; - - typedef QVector Frames; - - class Thread { - public: - Thread (); - Thread (const QString& text); - ~Thread (); - - int id () const; - const QString& target_id () const; - const QString& name () const; - const QString& state () const; - int current () const; - - int frameCount () const; - const Frame& frame (int i) const; - const Frames& frames () const; - - private: - int _id; - QString _target_id; - QString _name; - QString _state; - int _current; - - Frames _frames; - }; - - typedef QVector Threads; - - typedef QVector ThreadIds; - - class GraphNode : public QObject { - - Q_OBJECT - - public: - explicit GraphNode (const QString& name, QObject* parent = nullptr); - ~GraphNode (); - - QString name () const; - - void addChild (GraphNode* child); - GraphNode* getChild (int index) const; - int childCount () const; - - void addFrame (const Frame& frame); - const Frames& frames () const; - int frameCount () const; - - void addThreadId (int id); - const ThreadIds& threadIds () const; - int threadIdCount () const; - - void printTree (int level = 0) const; - - private: - QString _name; - ThreadIds _threadIds; - Frames _frames; - }; - -}; - - - class SeerParallelStacksVisualizerWidget : public QWidget, protected Ui::SeerParallelStacksVisualizerWidgetForm { Q_OBJECT @@ -132,6 +38,5 @@ class SeerParallelStacksVisualizerWidget : public QWidget, protected Ui::SeerPar int _id; Seer::PSV::Threads _threads; - Seer::PSV::GraphNode* _gnodes; }; diff --git a/src/SeerParallelStacksVisualizerWidget.ui b/src/SeerParallelStacksVisualizerWidget.ui index 47d0136..e8c0323 100644 --- a/src/SeerParallelStacksVisualizerWidget.ui +++ b/src/SeerParallelStacksVisualizerWidget.ui @@ -19,14 +19,7 @@ - - - - 0 - 100 - - - + @@ -126,10 +119,9 @@ - QImageViewer - QWidget -
QImageViewer.h
- 1 + SeerParallelStacksGraphicsView + QGraphicsView +
SeerParallelStacksGraphicsView.h