Решил запрототипировать два представления в дополнение к стандартному Jaeger UI. Это
построение карты сервисов по трейсу;
просмотрщик логов без пиксельхантинга и разворачивания спанов.
Для Qt Widgets есть обертка в виде nbergont/qgv, а хочется сделать на Qt Quick.
Как это выглядит со стороны Qt Quick:
Flickable {
topMargin: 80
leftMargin: 80
bottomMargin: 80
rightMargin: 80
contentWidth: svcMap.width
contentHeight: svcMap.height
ServiceMap {
id: svcMap
visible: true
graph: item.graph
delegate: Rectangle {
implicitHeight: content.height + 10
implicitWidth: content.width + 10
visible: true
border.color: "black"
border.width: 1
ColumnLayout {
id: content
x: 5
y: 5
Text {
text: node.name
Layout.minimumWidth: 100
font.bold: true
}
Rectangle {
visible: node.hasEdges
height: 1
width: 10
Layout.fillWidth: true
color: "gray"
}
Repeater {
model: node.operations
Text {
text: modelData
}
}
}
MouseArea {
anchors.fill: parent
onClicked: {
nodeItem.setNode(node);
}
}
}
}
}
ServiceMap помещаем во Flickable на случай если граф не влезет в отображаемые границы. Делегат вычисляет размер исходя из содержимого, которое зависит от переданного свойства node.На стороне С++ следующая последовательность шагов:
пробежаться по вершинам и ребрам графа, создать QQuickItem со свойством node, для получения размера;
пробежаться по вершинам и ребрам графа, создать Agnode_t, Agedge_t в GraphViz, настроить параметры отображения;
выполнить расчет графа в GraphViz;
выставить вершинам и ребрам рассчитанные параметры.
Интерфейс ServiceMap, для делегата используется тип QQmlComponent:
struct ServiceMapCtx;
class ServiceMap : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(TraceGraph graph READ getGraph WRITE setGraph NOTIFY notifyGraphChanged)
Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY notifyDelegateChanged)
public:
static constexpr qreal DPI = 72.0; //https://graphviz.org/doc/info/attrs.html
explicit ServiceMap(QQuickItem *parent = nullptr);
~ServiceMap();
const TraceGraph &getGraph() const;
void setGraph(const TraceGraph &data);
QQmlComponent *delegate() const;
void setDelegate(QQmlComponent *delegate);
signals:
void notifyGraphChanged();
void notifyDelegateChanged();
private:
void makeServiceGraph();
void makeQuickNodes();
void computeLayout();
void resetGraph();
private:
std::unique_ptr<ServiceMapCtx> m_ctx;
TraceGraph m_trace;
QQmlComponent *m_delegate;
QVector<ServiceMapNode> m_nodes;
QVector<ServiceMapEdge> m_edges;
};
Немножко хелперов, GraphViz использует строки, для настройки свойств:
namespace {
struct ContextDeleter
{
void operator()(GVC_t *ctx) const
{
gvFinalize(ctx);
if (gvFreeContext(ctx) != 0) {
qWarning() << "gvFreeContext != 0";
}
}
};
struct GraphDeleter
{
void operator()(Agraph_t *graph) const
{
if (agclose(graph) != 0) {
qWarning() << "agclose != 0";
}
}
};
using GVContextPtr = std::unique_ptr<GVC_t, ContextDeleter>;
using GVGraphPtr = std::unique_ptr<Agraph_t, GraphDeleter>;
template<typename NodeType>
void setAttribute(NodeType *node, const QString &key, const QString &value)
{
char empty[] = "";
auto k = key.toLatin1();
auto v = value.toLatin1();
agsafeset(node, k.data(), v.data(), empty);
}
} // namespace
//...
struct ServiceMapCtx
{
ServiceMapCtx()
: ctx(gvContext())
, graph(agopen("service_map", Agdirected, NULL))
{
setGraphAttribute("label", "service map");
setGraphAttribute("rankdir", "LR");
setGraphAttribute("nodesep", "0.5");
//setGraphAttribute("splines", "ortho");
setNodeAttribute("shape", "box");
setEdgeAttribute("minlen", "3");
}
~ServiceMapCtx() { gvFreeLayout(ctx.get(), graph.get()); }
void setNodeAttribute(const QString &name, const QString &value)
{
if (graph) {
agattr(graph.get(), AGNODE, name.toLocal8Bit().data(), value.toLocal8Bit().data());
}
}
void setGraphAttribute(const QString &name, const QString &value)
{
if (graph) {
agattr(graph.get(), AGRAPH, name.toLocal8Bit().data(), value.toLocal8Bit().data());
}
}
void setEdgeAttribute(const QString &name, const QString &value)
{
if (graph) {
agattr(graph.get(), AGEDGE, name.toLocal8Bit().data(), value.toLocal8Bit().data());
}
}
GVContextPtr ctx;
GVGraphPtr graph;
};
ServiceMap::ServiceMap(QQuickItem *parent)
: QQuickItem(parent)
, m_ctx(std::make_unique<ServiceMapCtx>())
, m_delegate(nullptr)
{
setFlag(QQuickItem::ItemHasContents);
}
ServiceMap::~ServiceMap() {}
Для визуальных QQuickItem нужно задавать флаг QQuickItem::ItemHasContents в true, означает что item имеет детей, которых нужно отрисовывать. Создаем визуальные элементы вершин и ребер:
void ServiceMap::makeQuickNodes()
{
for (auto &node : m_nodes) {
auto creationCtx = m_delegate->creationContext();
auto ctx = new QQmlContext(creationCtx ? creationCtx : qmlContext(this));
auto item = m_delegate->beginCreate(ctx);
if (item) {
ctx->setContextProperty("node", QVariant::fromValue(node));
auto quickItem = qobject_cast<QQuickItem *>(item);
quickItem->setParentItem(this);
quickItem->setZ(1);
node.qmlObject = quickItem;
} else {
qCritical() << "failed create QQuickItem from delegate" << m_delegate->errors();
}
m_delegate->completeCreate();
}
for (auto &edge : m_edges) {
edge.qmlObject = new EdgeItem;
edge.qmlObject->setZ(2);
edge.qmlObject->setParentItem(this);
}
}
Вершина создается в несколько этапов через beginCreate/completeCreate, для установки свойства node. Создаем вершины и узлы в GraphViz, длины задаются в дюймах, при этом зашито 72 DPI:
void applySize(ServiceMapNode &node, qreal DPI)
{
if (node.qmlObject) {
auto size = node.qmlObject->size();
auto widthIn = QString::number(qreal(size.width()) / DPI);
auto heightIn = QString::number(qreal(size.height()) / DPI);
setAttribute(node.gvNode, "width", widthIn);
setAttribute(node.gvNode, "height", heightIn);
setAttribute(node.gvNode, "fixedsize", "true");
}
}
//...
auto graph = m_ctx->graph.get();
QHash<graph::Process *, Agnode_t *> nodeMap;
for (auto &node : m_nodes) {
node.gvNode = agnode(graph, NULL, true);
nodeMap.insert(node.process, node.gvNode);
applySize(node, DPI);
}
for (auto &edge : m_edges) {
auto from = nodeMap[edge.from];
auto to = nodeMap[edge.to];
edge.gvEdge = agedge(graph, from, to, NULL, TRUE);
}
Сам расчет графа осуществляется вызовом функции gvLayout из gvc.h(libgvc). В этой библиотеке содержатся функции расчета и рендеринга изображения:
if (gvLayout(m_ctx->ctx.get(), graph, "dot") != 0) {
qCritical() << "Layout render error" << agerrors() << QString::fromLocal8Bit(aglasterr());
}
Выставляем размер ServiceMap, где UR
это координаты верхнего правого угла typedef struct { pointf LL, UR; } boxf
:
qreal gvGraphHeight = GD_bb(graph).UR.y;
qreal gvGraphWidth = GD_bb(graph).UR.x;
setImplicitHeight(gvGraphHeight);
setImplicitWidth(gvGraphWidth);
Дальше нужно расставить вершины QQuickItem по координатам, который рассчитал GraphViz. В GraphViz ось Y идет снизу вверх, а в Qt сверху в низ. А координата вершины GraphViz находится в центре фигуры. Поэтому разворачиваем и смещаем координаты:
QPointF centerToOrigin(const QPointF &p, qreal width, qreal height)
{
return QPointF(p.x() - width / 2, p.y() - height / 2);
}
//...
for (auto &node : m_nodes) {
auto gvPos = ND_coord(node.gvNode);
QPoint pt(gvPos.x, gvGraphHeight - gvPos.y);
auto org = centerToOrigin(pt, node.qmlObject->width(), node.qmlObject->height());
node.qmlObject->setPosition(org);
}
С ребрами немного сложнее. Вытаскиваем точки, по которым будем рисовать линию:
for (auto &edge : m_edges) {
auto spline = ED_spl(edge.gvEdge);
QVector<QPointF> points;
if (spline->size != 0) {
bezier bez = spline->list[0];
points.reserve(bez.size);
for (int i = 0; i < bez.size; ++i) {
auto &p = bez.list[i];
points << QPointF(p.x, gvGraphHeight - p.y);
}
points << QPointF(spline->list->ep.x, gvGraphHeight - spline->list->ep.y);
}
edge.qmlObject->setPoints(points);
Осталось нарисовать объект с необычной геометрией. В документации Qt Quick Examples and Tutorials есть интересующие нас вещи. От туда потребуется примеры работы с графом сцены(Scene Graph), из которого возьмем два примера Graph и Custom Geometry. Т.к. это прототип, то для ребер написал код на выброс:
class EdgeItem : public QQuickItem
{
Q_OBJECT
QML_ELEMENT
public:
explicit EdgeItem(QQuickItem *parent = nullptr);
QSGNode *updatePaintNode(QSGNode *, UpdatePaintNodeData *) override;
void setPoints(const QVector<QPointF> &points);
private:
QVector<QPointF> m_points;
QSGGeometryNode *m_arrowNode;
};
///...
EdgeItem::EdgeItem(QQuickItem *parent)
: QQuickItem(parent)
, m_arrowNode(nullptr)
{
setFlag(ItemHasContents);
}
QSGNode *EdgeItem::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
{
if (!m_arrowNode) {
m_arrowNode = new QSGGeometryNode;
auto geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 3);
geometry->setLineWidth(1);
geometry->setDrawingMode(QSGGeometry::DrawTriangles);
m_arrowNode->setGeometry(geometry);
m_arrowNode->setFlag(QSGNode::OwnsGeometry);
auto *material = new QSGFlatColorMaterial;
material->setColor(QColor("black"));
m_arrowNode->setMaterial(material);
m_arrowNode->setFlag(QSGNode::OwnsMaterial);
geometry->allocate(3);
}
QSGGeometryNode *node = nullptr;
QSGGeometry *geometry = nullptr;
if (!oldNode) {
node = new QSGGeometryNode;
geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(),
std::max(m_points.size() - 1, qsizetype(0)));
geometry->setLineWidth(1);
geometry->setDrawingMode(QSGGeometry::DrawLineStrip);
node->setGeometry(geometry);
node->setFlag(QSGNode::OwnsGeometry);
auto *material = new QSGFlatColorMaterial;
material->setColor(QColor("black"));
node->setMaterial(material);
node->setFlag(QSGNode::OwnsMaterial);
node->appendChildNode(m_arrowNode);
} else {
node = static_cast<QSGGeometryNode *>(oldNode);
geometry = node->geometry();
geometry->allocate(m_points.size() - 1);
}
QSGGeometry::Point2D *vertices = geometry->vertexDataAsPoint2D();
for (int i = 0; i < m_points.size() - 1; ++i) {
vertices[i].set(m_points[i].x(), m_points[i].y());
}
if (!m_points.isEmpty()) {
QLineF line(m_points[m_points.size() - 2], m_points[m_points.size() - 1]);
QLineF n = line.normalVector();
QPointF o(n.dx() / 3.0, n.dy() / 3.0);
auto arrVertices = m_arrowNode->geometry()->vertexDataAsPoint2D();
arrVertices[0].set(line.p1().x() + o.x(), line.p1().y() + o.y());
arrVertices[1].set(line.p2().x(), line.p2().y());
arrVertices[2].set(line.p1().x() - o.x(), line.p1().y() - o.y());
}
node->markDirty(QSGNode::DirtyGeometry);
return node;
}
void EdgeItem::setPoints(const QVector<QPointF> &points)
{
m_points = points;
update();
}
Этого достаточно. Если задать свойство ребер setGraphAttribute("splines", "ortho")
, то получим результат:
Код доступен на RPG-18/jgv и может отличаться от кода в статье.