Can't believe it worked. Initial attempt done.

This commit is contained in:
Ernie Pasveer
2026-06-14 10:58:22 -05:00
parent dc39015a42
commit 93a118b7be
9 changed files with 872 additions and 403 deletions
+5
View File
@@ -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
+1
View File
@@ -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);
+215
View File
@@ -0,0 +1,215 @@
// SPDX-FileCopyrightText: 2021 Ernie Pasveer <epasveer@att.net>
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "SeerParallelStacksVisualizerWidget.h"
#include "SeerHelpPageDialog.h"
#include "SeerUtl.h"
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QFileDialog>
#include <QtGui/QIntValidator>
#include <QtGui/QIcon>
#include <QtPrintSupport/QPrinter>
#include <QtPrintSupport/QPrintDialog>
#include <QtCore/QSettings>
#include <QtCore/QProcess>
#include <QtCore/QStringList>
#include <QtCore/QFile>
#include <QtCore/QDebug>
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<StackNode> buildImpl( QVector<Thread *> &threadPtrs, const QString &currentFunction, int depth) {
auto node = std::make_shared<StackNode>();
node->depth = depth;
node->function = currentFunction;
node->threads = threadPtrs;
// Group threads by the function at position [-depth-1] (bottom-up).
QMap<QString, QVector<Thread *>> 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<StackNode> buildParallelStacks(QVector<Thread> &threads) {
QVector<Thread *> 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<Stack> fillStack(const std::shared_ptr<StackNode> &node) {
auto stack = std::make_shared<Stack>();
stack->threadCount = static_cast<int>(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
+99
View File
@@ -0,0 +1,99 @@
// SPDX-FileCopyrightText: 2026 Ernie Pasveer <epasveer@att.net>
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QtWidgets/QWidget>
#include <QtCore/QVector>
#include <QWidget>
#include <QGraphicsItem>
#include <QPointF>
#include <QString>
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<Frame> 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<Thread> Threads;
typedef QVector<int> ThreadIds;
struct StackNode {
QString function; // empty == root
int depth = 0;
QVector<Thread *> threads; // non-owning pointers
QVector<std::shared_ptr<StackNode>> children;
};
// Build the parallel-stacks tree from a flat list of threads.
std::shared_ptr<StackNode> buildParallelStacks(QVector<Thread> &threads);
// Flat "Stack" representation used when building the graph.
struct Stack {
QVector<QString> functions;
QVector<std::shared_ptr<Stack>> stacks;
int threadCount = 0;
QVector<QString> threadIds; // IDs of every thread in this node
};
std::shared_ptr<Stack> fillStack(const std::shared_ptr<StackNode> &node);
} // namespace PSV
} // namespace Seer
+402
View File
@@ -0,0 +1,402 @@
#include "SeerParallelStacksGraphicsView.h"
#include <QPainter>
#include <QPainterPath>
#include <QWheelEvent>
#include <QFontMetrics>
#include <QGraphicsSceneMouseEvent>
#include <QCursor>
#include <algorithm>
#include <cmath>
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<Seer::PSV::Stack> &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<Seer::PSV::Stack> &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;
}
+127
View File
@@ -0,0 +1,127 @@
#pragma once
#include "SeerParallelStacksCommon.h"
#include <QWidget>
#include <QGraphicsView>
#include <QGraphicsScene>
#include <QGraphicsItem>
#include <QPointF>
#include <QVector>
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<Row> m_rows;
qreal m_width = 0;
qreal m_height = 0;
bool m_dragging = false;
QPointF m_dragOffset;
QVector<LiveEdge *> 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<Seer::PSV::Stack> &root);
protected:
void wheelEvent(QWheelEvent *event) override;
private:
struct PlacedNode {
std::shared_ptr<Seer::PSV::Stack> stack;
Seer::PSV::StackBoxItem *item = nullptr;
PlacedNode *parent = nullptr;
QVector<PlacedNode *> children;
qreal cx = 0;
qreal cy = 0;
};
QGraphicsScene *m_scene;
void buildPlacedTree(PlacedNode *pn, const std::shared_ptr<Seer::PSV::Stack> &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);
};
+18 -295
View File
@@ -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 <QtWidgets/QMessageBox>
@@ -17,214 +18,10 @@
#include <QtCore/QFile>
#include <QtCore/QDebug>
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<GraphNode*>(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<GraphNode*>(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=<<table BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\"><tr><td align=\"right\"><b>1 Threads</b></td></tr>";
for (int f=0; f<_threads[t].frameCount(); f++) {
const Seer::PSV::Frame& frame = _threads[t].frame(f);
out << "<tr><td align=\"left\"><font color=\"darkgreen\">" << frame.function().toHtmlEscaped() << "</font></td></tr>";
}
out << "</table>>]\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<Seer::PSV::Thread> local = _threads; // mutable copy for ptr stability
auto root = Seer::PSV::buildParallelStacks(local);
auto stack = Seer::PSV::fillStack(root);
graphicsView->setStack(stack);
}
+1 -96
View File
@@ -5,103 +5,9 @@
#pragma once
#include <QtWidgets/QWidget>
#include <QtCore/QVector>
#include <QString>
#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<Frame> 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<Thread> Threads;
typedef QVector<int> 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;
};
+4 -12
View File
@@ -19,14 +19,7 @@
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QImageViewer" name="imageViewer" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>100</verstretch>
</sizepolicy>
</property>
</widget>
<widget class="SeerParallelStacksGraphicsView" name="graphicsView"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
@@ -126,10 +119,9 @@
</widget>
<customwidgets>
<customwidget>
<class>QImageViewer</class>
<extends>QWidget</extends>
<header>QImageViewer.h</header>
<container>1</container>
<class>SeerParallelStacksGraphicsView</class>
<extends>QGraphicsView</extends>
<header location="global">SeerParallelStacksGraphicsView.h</header>
</customwidget>
</customwidgets>
<resources>