diff --git a/libviews/callgraphview.cpp b/libviews/callgraphview.cpp index 934d088..0c78e6c 100644 --- a/libviews/callgraphview.cpp +++ b/libviews/callgraphview.cpp @@ -1,3191 +1,3195 @@ /* This file is part of KCachegrind. Copyright (c) 2007-2016 Josef Weidendorfer KCachegrind is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /* * Callgraph View */ #include "callgraphview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "config.h" #include "globalguiconfig.h" #include "listutils.h" #define DEBUG_GRAPH 0 // CallGraphView defaults #define DEFAULT_FUNCLIMIT .05 #define DEFAULT_CALLLIMIT 1. #define DEFAULT_MAXCALLER 2 #define DEFAULT_MAXCALLEE -1 #define DEFAULT_SHOWSKIPPED false #define DEFAULT_EXPANDCYCLES false #define DEFAULT_CLUSTERGROUPS false #define DEFAULT_DETAILLEVEL 1 #define DEFAULT_LAYOUT GraphOptions::TopDown #define DEFAULT_ZOOMPOS Auto // LessThen functors as helpers for sorting of graph edges // for keyboard navigation. Sorting is done according to // the angle at which a edge spline goes out or in of a function. // Sort angles of outgoing edges (edge seen as attached to the caller) class CallerGraphEdgeLessThan { public: bool operator()(const GraphEdge* ge1, const GraphEdge* ge2) const { const CanvasEdge* ce1 = ge1->canvasEdge(); const CanvasEdge* ce2 = ge2->canvasEdge(); // sort invisible edges (ie. without matching CanvasEdge) in front + if (!ce1 && !ce2) { + // strict ordering required for std::sort + return (ge1 < ge2); + } if (!ce1) return true; if (!ce2) return false; QPolygon p1 = ce1->controlPoints(); QPolygon p2 = ce2->controlPoints(); QPoint d1 = p1.point(1) - p1.point(0); QPoint d2 = p2.point(1) - p2.point(0); double angle1 = atan2(double(d1.y()), double(d1.x())); double angle2 = atan2(double(d2.y()), double(d2.x())); return (angle1 < angle2); } }; // Sort angles of ingoing edges (edge seen as attached to the callee) class CalleeGraphEdgeLessThan { public: bool operator()(const GraphEdge* ge1, const GraphEdge* ge2) const { const CanvasEdge* ce1 = ge1->canvasEdge(); const CanvasEdge* ce2 = ge2->canvasEdge(); // sort invisible edges (ie. without matching CanvasEdge) in front if (!ce1) return true; if (!ce2) return false; QPolygon p1 = ce1->controlPoints(); QPolygon p2 = ce2->controlPoints(); QPoint d1 = p1.point(p1.count()-2) - p1.point(p1.count()-1); QPoint d2 = p2.point(p2.count()-2) - p2.point(p2.count()-1); double angle1 = atan2(double(d1.y()), double(d1.x())); double angle2 = atan2(double(d2.y()), double(d2.x())); // for ingoing edges sort according to descending angles return (angle2 < angle1); } }; // // GraphNode // GraphNode::GraphNode() { _f=0; self = incl = 0; _cn = 0; _visible = false; _lastCallerIndex = _lastCalleeIndex = -1; _lastFromCaller = true; } void GraphNode::clearEdges() { callees.clear(); callers.clear(); } CallerGraphEdgeLessThan callerGraphEdgeLessThan; CalleeGraphEdgeLessThan calleeGraphEdgeLessThan; void GraphNode::sortEdges() { std::sort(callers.begin(), callers.end(), callerGraphEdgeLessThan); std::sort(callees.begin(), callees.end(), calleeGraphEdgeLessThan); } void GraphNode::addCallee(GraphEdge* e) { if (e) callees.append(e); } void GraphNode::addCaller(GraphEdge* e) { if (e) callers.append(e); } void GraphNode::addUniqueCallee(GraphEdge* e) { if (e && (callees.count(e) == 0)) callees.append(e); } void GraphNode::addUniqueCaller(GraphEdge* e) { if (e && (callers.count(e) == 0)) callers.append(e); } void GraphNode::removeEdge(GraphEdge* e) { callers.removeAll(e); callees.removeAll(e); } double GraphNode::calleeCostSum() { double sum = 0.0; foreach(GraphEdge* e, callees) sum += e->cost; return sum; } double GraphNode::calleeCountSum() { double sum = 0.0; foreach(GraphEdge* e, callees) sum += e->count; return sum; } double GraphNode::callerCostSum() { double sum = 0.0; foreach(GraphEdge* e, callers) sum += e->cost; return sum; } double GraphNode::callerCountSum() { double sum = 0.0; foreach(GraphEdge* e, callers) sum += e->count; return sum; } TraceCall* GraphNode::visibleCaller() { if (0) qDebug("GraphNode::visibleCaller %s: last %d, count %d", qPrintable(_f->prettyName()), _lastCallerIndex, callers.count()); // can not use at(): index can be -1 (out of bounds), result is 0 then GraphEdge* e = callers.value(_lastCallerIndex); if (e && !e->isVisible()) e = 0; if (!e) { double maxCost = 0.0; GraphEdge* maxEdge = 0; for(int i = 0; iisVisible() && (e->cost > maxCost)) { maxCost = e->cost; maxEdge = e; _lastCallerIndex = i; } } e = maxEdge; } return e ? e->call() : 0; } TraceCall* GraphNode::visibleCallee() { if (0) qDebug("GraphNode::visibleCallee %s: last %d, count %d", qPrintable(_f->prettyName()), _lastCalleeIndex, callees.count()); GraphEdge* e = callees.value(_lastCalleeIndex); if (e && !e->isVisible()) e = 0; if (!e) { double maxCost = 0.0; GraphEdge* maxEdge = 0; for(int i = 0; iisVisible() && (e->cost > maxCost)) { maxCost = e->cost; maxEdge = e; _lastCalleeIndex = i; } } e = maxEdge; } return e ? e->call() : 0; } void GraphNode::setCallee(GraphEdge* e) { _lastCalleeIndex = callees.indexOf(e); _lastFromCaller = false; } void GraphNode::setCaller(GraphEdge* e) { _lastCallerIndex = callers.indexOf(e); _lastFromCaller = true; } TraceFunction* GraphNode::nextVisible() { TraceCall* c; if (_lastFromCaller) { c = nextVisibleCaller(); if (c) return c->called(true); c = nextVisibleCallee(); if (c) return c->caller(true); } else { c = nextVisibleCallee(); if (c) return c->caller(true); c = nextVisibleCaller(); if (c) return c->called(true); } return 0; } TraceFunction* GraphNode::priorVisible() { TraceCall* c; if (_lastFromCaller) { c = priorVisibleCaller(); if (c) return c->called(true); c = priorVisibleCallee(); if (c) return c->caller(true); } else { c = priorVisibleCallee(); if (c) return c->caller(true); c = priorVisibleCaller(); if (c) return c->called(true); } return 0; } TraceCall* GraphNode::nextVisibleCaller(GraphEdge* e) { int idx = e ? callers.indexOf(e) : _lastCallerIndex; idx++; while(idx < callers.size()) { if (callers[idx]->isVisible()) { _lastCallerIndex = idx; return callers[idx]->call(); } idx++; } return 0; } TraceCall* GraphNode::nextVisibleCallee(GraphEdge* e) { int idx = e ? callees.indexOf(e) : _lastCalleeIndex; idx++; while(idx < callees.size()) { if (callees[idx]->isVisible()) { _lastCalleeIndex = idx; return callees[idx]->call(); } idx++; } return 0; } TraceCall* GraphNode::priorVisibleCaller(GraphEdge* e) { int idx = e ? callers.indexOf(e) : _lastCallerIndex; idx = (idx<0) ? callers.size()-1 : idx-1; while(idx >= 0) { if (callers[idx]->isVisible()) { _lastCallerIndex = idx; return callers[idx]->call(); } idx--; } return 0; } TraceCall* GraphNode::priorVisibleCallee(GraphEdge* e) { int idx = e ? callees.indexOf(e) : _lastCalleeIndex; idx = (idx<0) ? callees.size()-1 : idx-1; while(idx >= 0) { if (callees[idx]->isVisible()) { _lastCalleeIndex = idx; return callees[idx]->call(); } idx--; } return 0; } // // GraphEdge // GraphEdge::GraphEdge() { _c=0; _from = _to = 0; _fromNode = _toNode = 0; cost = count = 0; _ce = 0; _visible = false; _lastFromCaller = true; } QString GraphEdge::prettyName() { if (_c) return _c->prettyName(); if (_from) return QObject::tr("Call(s) from %1").arg(_from->prettyName()); if (_to) return QObject::tr("Call(s) to %1").arg(_to->prettyName()); return QObject::tr("(unknown call)"); } TraceFunction* GraphEdge::visibleCaller() { if (_from) { _lastFromCaller = true; if (_fromNode) _fromNode->setCallee(this); return _from; } return 0; } TraceFunction* GraphEdge::visibleCallee() { if (_to) { _lastFromCaller = false; if (_toNode) _toNode->setCaller(this); return _to; } return 0; } TraceCall* GraphEdge::nextVisible() { TraceCall* res = 0; if (_lastFromCaller && _fromNode) { res = _fromNode->nextVisibleCallee(this); if (!res && _toNode) res = _toNode->nextVisibleCaller(this); } else if (_toNode) { res = _toNode->nextVisibleCaller(this); if (!res && _fromNode) res = _fromNode->nextVisibleCallee(this); } return res; } TraceCall* GraphEdge::priorVisible() { TraceCall* res = 0; if (_lastFromCaller && _fromNode) { res = _fromNode->priorVisibleCallee(this); if (!res && _toNode) res = _toNode->priorVisibleCaller(this); } else if (_toNode) { res = _toNode->priorVisibleCaller(this); if (!res && _fromNode) res = _fromNode->priorVisibleCallee(this); } return res; } // // GraphOptions // QString GraphOptions::layoutString(Layout l) { if (l == Circular) return QStringLiteral("Circular"); if (l == LeftRight) return QStringLiteral("LeftRight"); return QStringLiteral("TopDown"); } GraphOptions::Layout GraphOptions::layout(QString s) { if (s == QStringLiteral("Circular")) return Circular; if (s == QStringLiteral("LeftRight")) return LeftRight; return TopDown; } // // StorableGraphOptions // StorableGraphOptions::StorableGraphOptions() { // default options _funcLimit = DEFAULT_FUNCLIMIT; _callLimit = DEFAULT_CALLLIMIT; _maxCallerDepth = DEFAULT_MAXCALLER; _maxCalleeDepth = DEFAULT_MAXCALLEE; _showSkipped = DEFAULT_SHOWSKIPPED; _expandCycles = DEFAULT_EXPANDCYCLES; _detailLevel = DEFAULT_DETAILLEVEL; _layout = DEFAULT_LAYOUT; } // // GraphExporter // GraphExporter::GraphExporter() { _go = this; _tmpFile = 0; _item = 0; reset(0, 0, 0, ProfileContext::InvalidType, QString()); } GraphExporter::GraphExporter(TraceData* d, TraceFunction* f, EventType* ct, ProfileContext::Type gt, QString filename) { _go = this; _tmpFile = 0; _item = 0; reset(d, f, ct, gt, filename); } GraphExporter::~GraphExporter() { if (_item && _tmpFile) { #if DEBUG_GRAPH _tmpFile->setAutoRemove(true); #endif delete _tmpFile; } } void GraphExporter::reset(TraceData*, CostItem* i, EventType* ct, ProfileContext::Type gt, QString filename) { _graphCreated = false; _nodeMap.clear(); _edgeMap.clear(); if (_item && _tmpFile) { _tmpFile->setAutoRemove(true); delete _tmpFile; } if (i) { switch (i->type()) { case ProfileContext::Function: case ProfileContext::FunctionCycle: case ProfileContext::Call: break; default: i = 0; } } _item = i; _eventType = ct; _groupType = gt; if (!i) return; if (filename.isEmpty()) { _tmpFile = new QTemporaryFile(); //_tmpFile->setSuffix(".dot"); _tmpFile->setAutoRemove(false); _tmpFile->open(); _dotName = _tmpFile->fileName(); _useBox = true; } else { _tmpFile = 0; _dotName = filename; _useBox = false; } } void GraphExporter::setGraphOptions(GraphOptions* go) { if (go == 0) go = this; _go = go; } void GraphExporter::createGraph() { if (!_item) return; if (_graphCreated) return; _graphCreated = true; if ((_item->type() == ProfileContext::Function) ||(_item->type() == ProfileContext::FunctionCycle)) { TraceFunction* f = (TraceFunction*) _item; double incl = f->inclusive()->subCost(_eventType); _realFuncLimit = incl * _go->funcLimit(); _realCallLimit = _realFuncLimit * _go->callLimit(); buildGraph(f, 0, true, 1.0); // down to callees // set costs of function back to 0, as it will be added again GraphNode& n = _nodeMap[f]; n.self = n.incl = 0.0; buildGraph(f, 0, false, 1.0); // up to callers } else { TraceCall* c = (TraceCall*) _item; double incl = c->subCost(_eventType); _realFuncLimit = incl * _go->funcLimit(); _realCallLimit = _realFuncLimit * _go->callLimit(); // create edge TraceFunction *caller, *called; caller = c->caller(false); called = c->called(false); QPair p(caller, called); GraphEdge& e = _edgeMap[p]; e.setCall(c); e.setCaller(p.first); e.setCallee(p.second); e.cost = c->subCost(_eventType); e.count = c->callCount(); SubCost s = called->inclusive()->subCost(_eventType); buildGraph(called, 0, true, e.cost / s); // down to callees s = caller->inclusive()->subCost(_eventType); buildGraph(caller, 0, false, e.cost / s); // up to callers } } void GraphExporter::writeDot(QIODevice* device) { if (!_item) return; QFile* file = 0; QTextStream* stream = 0; if (device) stream = new QTextStream(device); else { if (_tmpFile) stream = new QTextStream(_tmpFile); else { file = new QFile(_dotName); if ( !file->open(QIODevice::WriteOnly ) ) { qDebug() << "Can not write dot file '"<< _dotName << "'"; delete file; return; } stream = new QTextStream(file); } } if (!_graphCreated) createGraph(); /* Generate dot format... * When used for the CallGraphView (in contrast to "Export Callgraph..."), * the labels are only dummy placeholders to reserve space for our own * drawings. */ *stream << "digraph \"callgraph\" {\n"; if (_go->layout() == LeftRight) { *stream << QStringLiteral(" rankdir=LR;\n"); } else if (_go->layout() == Circular) { TraceFunction *f = 0; switch (_item->type()) { case ProfileContext::Function: case ProfileContext::FunctionCycle: f = (TraceFunction*) _item; break; case ProfileContext::Call: f = ((TraceCall*)_item)->caller(true); break; default: break; } if (f) *stream << QStringLiteral(" center=F%1;\n").arg((qptrdiff)f, 0, 16); *stream << QStringLiteral(" overlap=false;\n splines=true;\n"); } // for clustering QMap > nLists; GraphNodeMap::Iterator nit; for (nit = _nodeMap.begin(); nit != _nodeMap.end(); ++nit ) { GraphNode& n = *nit; if (n.incl <= _realFuncLimit) continue; // for clustering: get cost item group of function TraceCostItem* g; TraceFunction* f = n.function(); switch (_groupType) { case ProfileContext::Object: g = f->object(); break; case ProfileContext::Class: g = f->cls(); break; case ProfileContext::File: g = f->file(); break; case ProfileContext::FunctionCycle: g = f->cycle(); break; default: g = 0; break; } nLists[g].append(&n); } QMap >::Iterator lit; int cluster = 0; for (lit = nLists.begin(); lit != nLists.end(); ++lit, cluster++) { QList& l = lit.value(); TraceCostItem* i = lit.key(); if (_go->clusterGroups() && i) { QString iabr = GlobalConfig::shortenSymbol(i->prettyName()); // escape quotation marks in symbols to avoid invalid dot syntax iabr.replace("\"", "\\\""); *stream << QStringLiteral("subgraph \"cluster%1\" { label=\"%2\";\n") .arg(cluster).arg(iabr); } foreach(GraphNode* np, l) { TraceFunction* f = np->function(); QString abr = GlobalConfig::shortenSymbol(f->prettyName()); // escape quotation marks to avoid invalid dot syntax abr.replace("\"", "\\\""); *stream << QStringLiteral(" F%1 [").arg((qptrdiff)f, 0, 16); if (_useBox) { // we want a minimal size for cost display if ((int)abr.length() < 8) abr = abr + QString(8 - abr.length(),'_'); // make label 3 lines for CallGraphView *stream << QStringLiteral("shape=box,label=\"** %1 **\\n**\\n%2\"];\n") .arg(abr) .arg(SubCost(np->incl).pretty()); } else *stream << QStringLiteral("label=\"%1\\n%2\"];\n") .arg(abr) .arg(SubCost(np->incl).pretty()); } if (_go->clusterGroups() && i) *stream << QStringLiteral("}\n"); } GraphEdgeMap::Iterator eit; for (eit = _edgeMap.begin(); eit != _edgeMap.end(); ++eit ) { GraphEdge& e = *eit; if (e.cost < _realCallLimit) continue; if (!_go->expandCycles()) { // do not show inner cycle calls if (e.call()->inCycle()>0) continue; } GraphNode& from = _nodeMap[e.from()]; GraphNode& to = _nodeMap[e.to()]; e.setCallerNode(&from); e.setCalleeNode(&to); if ((from.incl <= _realFuncLimit) ||(to.incl <= _realFuncLimit)) continue; // remove dumped edges from n.callers/n.callees from.removeEdge(&e); to.removeEdge(&e); *stream << QStringLiteral(" F%1 -> F%2 [weight=%3") .arg((qptrdiff)e.from(), 0, 16) .arg((qptrdiff)e.to(), 0, 16) .arg((long)log(log(e.cost))); if (_go->detailLevel() ==1) { *stream << QStringLiteral(",label=\"%1 (%2x)\"") .arg(SubCost(e.cost).pretty()) .arg(SubCost(e.count).pretty()); } else if (_go->detailLevel() ==2) *stream << QStringLiteral(",label=\"%3\\n%4 x\"") .arg(SubCost(e.cost).pretty()) .arg(SubCost(e.count).pretty()); *stream << QStringLiteral("];\n"); } if (_go->showSkipped()) { // Create sum-edges for skipped edges GraphEdge* e; double costSum, countSum; for (nit = _nodeMap.begin(); nit != _nodeMap.end(); ++nit ) { GraphNode& n = *nit; if (n.incl <= _realFuncLimit) continue; // add edge for all skipped callers if cost sum is high enough costSum = n.callerCostSum(); countSum = n.callerCountSum(); if (costSum > _realCallLimit) { QPair p(0, n.function()); e = &(_edgeMap[p]); e->setCallee(p.second); e->cost = costSum; e->count = countSum; *stream << QStringLiteral(" R%1 [shape=point,label=\"\"];\n") .arg((qptrdiff)n.function(), 0, 16); *stream << QStringLiteral(" R%1 -> F%2 [label=\"%3\\n%4 x\",weight=%5];\n") .arg((qptrdiff)n.function(), 0, 16) .arg((qptrdiff)n.function(), 0, 16) .arg(SubCost(costSum).pretty()) .arg(SubCost(countSum).pretty()) .arg((int)log(costSum)); } // add edge for all skipped callees if cost sum is high enough costSum = n.calleeCostSum(); countSum = n.calleeCountSum(); if (costSum > _realCallLimit) { QPair p(n.function(), 0); e = &(_edgeMap[p]); e->setCaller(p.first); e->cost = costSum; e->count = countSum; *stream << QStringLiteral(" S%1 [shape=point,label=\"\"];\n") .arg((qptrdiff)n.function(), 0, 16); *stream << QStringLiteral(" F%1 -> S%2 [label=\"%3\\n%4 x\",weight=%5];\n") .arg((qptrdiff)n.function(), 0, 16) .arg((qptrdiff)n.function(), 0, 16) .arg(SubCost(costSum).pretty()) .arg(SubCost(countSum).pretty()) .arg((int)log(costSum)); } } } // clear edges here completely. // Visible edges are inserted again on parsing in CallGraphView::refresh for (nit = _nodeMap.begin(); nit != _nodeMap.end(); ++nit ) { GraphNode& n = *nit; n.clearEdges(); } *stream << "}\n"; if (!device) { if (_tmpFile) { stream->flush(); _tmpFile->seek(0); } else { file->close(); delete file; } } delete stream; } void GraphExporter::sortEdges() { GraphNodeMap::Iterator nit; for (nit = _nodeMap.begin(); nit != _nodeMap.end(); ++nit ) { GraphNode& n = *nit; n.sortEdges(); } } TraceFunction* GraphExporter::toFunc(QString s) { if (s[0] != 'F') return 0; bool ok; TraceFunction* f = (TraceFunction*) s.midRef(1).toULongLong(&ok, 16); if (!ok) return 0; return f; } GraphNode* GraphExporter::node(TraceFunction* f) { if (!f) return 0; GraphNodeMap::Iterator it = _nodeMap.find(f); if (it == _nodeMap.end()) return 0; return &(*it); } GraphEdge* GraphExporter::edge(TraceFunction* f1, TraceFunction* f2) { GraphEdgeMap::Iterator it = _edgeMap.find(qMakePair(f1, f2)); if (it == _edgeMap.end()) return 0; return &(*it); } /** * We do a DFS and do not stop on already visited nodes/edges, * but add up costs. We only stop if limits/max depth is reached. * * For a node/edge, it can happen that the first time visited the * cost will below the limit, so the search is stopped. * If on a further visit of the node/edge the limit is reached, * we use the whole node/edge cost and continue search. */ void GraphExporter::buildGraph(TraceFunction* f, int depth, bool toCallees, double factor) { #if DEBUG_GRAPH qDebug() << "buildGraph(" << f->prettyName() << "," << d << "," << factor << ") [to " << (toCallees ? "Callees":"Callers") << "]"; #endif double oldIncl = 0.0; GraphNode& n = _nodeMap[f]; if (n.function() == 0) { n.setFunction(f); } else oldIncl = n.incl; double incl = f->inclusive()->subCost(_eventType) * factor; n.incl += incl; n.self += f->subCost(_eventType) * factor; if (0) qDebug(" Added Incl. %f, now %f", incl, n.incl); // A negative depth limit means "unlimited" int maxDepth = toCallees ? _go->maxCalleeDepth() : _go->maxCallerDepth(); // Never go beyound a depth of 100 if ((maxDepth < 0) || (maxDepth>100)) maxDepth = 100; if (depth >= maxDepth) { if (0) qDebug(" Cutoff, max depth reached"); return; } // if we just reached the limit by summing, do a DFS // from here with full incl. cost because of previous cutoffs if ((n.incl >= _realFuncLimit) && (oldIncl < _realFuncLimit)) incl = n.incl; if (f->cycle()) { // for cycles members, we never stop on first visit, but always on 2nd // note: a 2nd visit never should happen, as we do not follow inner-cycle // calls if (oldIncl > 0.0) { if (0) qDebug(" Cutoff, 2nd visit to Cycle Member"); // and takeback cost addition, as it is added twice n.incl = oldIncl; n.self -= f->subCost(_eventType) * factor; return; } } else if (incl <= _realFuncLimit) { if (0) qDebug(" Cutoff, below limit"); return; } TraceFunction* f2; // on entering a cycle, only go the FunctionCycle TraceCallList l = toCallees ? f->callings(false) : f->callers(false); foreach(TraceCall* call, l) { f2 = toCallees ? call->called(false) : call->caller(false); double count = call->callCount() * factor; double cost = call->subCost(_eventType) * factor; // ignore function calls with absolute cost < 3 per call // No: This would skip a lot of functions e.g. with L2 cache misses // if (count>0.0 && (cost/count < 3)) continue; double oldCost = 0.0; QPair p(toCallees ? f : f2, toCallees ? f2 : f); GraphEdge& e = _edgeMap[p]; if (e.call() == 0) { e.setCall(call); e.setCaller(p.first); e.setCallee(p.second); } else oldCost = e.cost; e.cost += cost; e.count += count; if (0) qDebug(" Edge to %s, added cost %f, now %f", qPrintable(f2->prettyName()), cost, e.cost); // if this call goes into a FunctionCycle, we also show the real call if (f2->cycle() == f2) { TraceFunction* realF; realF = toCallees ? call->called(true) : call->caller(true); QPair realP(toCallees ? f : realF, toCallees ? realF : f); GraphEdge& e = _edgeMap[realP]; if (e.call() == 0) { e.setCall(call); e.setCaller(realP.first); e.setCallee(realP.second); } e.cost += cost; e.count += count; } // - do not do a DFS on calls in recursion/cycle if (call->inCycle()>0) continue; if (call->isRecursion()) continue; if (toCallees) n.addUniqueCallee(&e); else n.addUniqueCaller(&e); // if we just reached the call limit (=func limit by summing, do a DFS // from here with full incl. cost because of previous cutoffs if ((e.cost >= _realCallLimit) && (oldCost < _realCallLimit)) cost = e.cost; if ((cost <= 0) || (cost <= _realCallLimit)) { if (0) qDebug(" Edge Cutoff, limit not reached"); continue; } SubCost s; if (call->inCycle()) s = f2->cycle()->inclusive()->subCost(_eventType); else s = f2->inclusive()->subCost(_eventType); SubCost v = call->subCost(_eventType); // Never recurse if s or v is 0 (can happen with bogus input) if ((v == 0) || (s== 0)) continue; buildGraph(f2, depth+1, toCallees, factor * v / s); } } // // PannerView // PanningView::PanningView(QWidget * parent) : QGraphicsView(parent) { _movingZoomRect = false; // FIXME: Why does this not work? viewport()->setFocusPolicy(Qt::NoFocus); } void PanningView::setZoomRect(const QRectF& r) { _zoomRect = r; viewport()->update(); } void PanningView::drawForeground(QPainter * p, const QRectF&) { if (!_zoomRect.isValid()) return; QColor red(Qt::red); QPen pen(red.dark()); pen.setWidthF(2.0 / matrix().m11()); p->setPen(pen); QColor c(red.dark()); c.setAlphaF(0.05); p->setBrush(QBrush(c)); p->drawRect(QRectF(_zoomRect.x(), _zoomRect.y(), _zoomRect.width()-1, _zoomRect.height()-1)); } void PanningView::mousePressEvent(QMouseEvent* e) { QPointF sPos = mapToScene(e->pos()); if (_zoomRect.isValid()) { if (!_zoomRect.contains(sPos)) emit zoomRectMoved(sPos.x() - _zoomRect.center().x(), sPos.y() - _zoomRect.center().y()); _movingZoomRect = true; _lastPos = sPos; } } void PanningView::mouseMoveEvent(QMouseEvent* e) { QPointF sPos = mapToScene(e->pos()); if (_movingZoomRect) { emit zoomRectMoved(sPos.x() - _lastPos.x(), sPos.y() - _lastPos.y()); _lastPos = sPos; } } void PanningView::mouseReleaseEvent(QMouseEvent*) { _movingZoomRect = false; emit zoomRectMoveFinished(); } // // CanvasNode // CanvasNode::CanvasNode(CallGraphView* v, GraphNode* n, int x, int y, int w, int h) : QGraphicsRectItem(QRect(x, y, w, h)), _node(n), _view(v) { setPosition(0, DrawParams::TopCenter); setPosition(1, DrawParams::BottomCenter); updateGroup(); if (!_node || !_view) return; if (_node->function()) setText(0, _node->function()->prettyName()); ProfileCostArray* totalCost; if (GlobalConfig::showExpanded()) { if (_view->activeFunction()) { if (_view->activeFunction()->cycle()) totalCost = _view->activeFunction()->cycle()->inclusive(); else totalCost = _view->activeFunction()->inclusive(); } else totalCost = (ProfileCostArray*) _view->activeItem(); } else totalCost = ((TraceItemView*)_view)->data(); double total = totalCost->subCost(_view->eventType()); double inclP = 100.0 * n->incl/ total; if (GlobalConfig::showPercentage()) setText(1, QStringLiteral("%1 %") .arg(inclP, 0, 'f', GlobalConfig::percentPrecision())); else setText(1, SubCost(n->incl).pretty()); setPixmap(1, percentagePixmap(25, 10, (int)(inclP+.5), Qt::blue, true)); setToolTip(QStringLiteral("%1 (%2)").arg(text(0)).arg(text(1))); } void CanvasNode::setSelected(bool s) { StoredDrawParams::setSelected(s); update(); } void CanvasNode::updateGroup() { if (!_view || !_node) return; QColor c = GlobalGUIConfig::functionColor(_view->groupType(), _node->function()); setBackColor(c); update(); } void CanvasNode::paint(QPainter* p, const QStyleOptionGraphicsItem* option, QWidget*) { QRect r = rect().toRect(), origRect = r; r.setRect(r.x()+1, r.y()+1, r.width()-2, r.height()-2); RectDrawing d(r); d.drawBack(p, this); r.setRect(r.x()+2, r.y()+2, r.width()-4, r.height()-4); #if 0 if (StoredDrawParams::selected() && _view->hasFocus()) { _view->style().drawPrimitive( QStyle::PE_FocusRect, &p, r, _view->colorGroup()); } #endif // draw afterwards to always get a frame even when zoomed p->setPen(StoredDrawParams::selected() ? Qt::red : Qt::black); p->drawRect(QRect(origRect.x(), origRect.y(), origRect.width()-1, origRect.height()-1)); #if QT_VERSION >= 0x040600 if (option->levelOfDetailFromTransform(p->transform()) < .5) return; #else if (option->levelOfDetail < .5) return; #endif d.setRect(r); d.drawField(p, 0, this); d.drawField(p, 1, this); } // // CanvasEdgeLabel // CanvasEdgeLabel::CanvasEdgeLabel(CallGraphView* v, CanvasEdge* ce, int x, int y, int w, int h) : QGraphicsRectItem(QRect(x, y, w, h)), _ce(ce), _view(v), _percentage(0.0) { GraphEdge* e = ce->edge(); if (!e) return; setPosition(1, DrawParams::BottomCenter); ProfileCostArray* totalCost; if (GlobalConfig::showExpanded()) { if (_view->activeFunction()) { if (_view->activeFunction()->cycle()) totalCost = _view->activeFunction()->cycle()->inclusive(); else totalCost = _view->activeFunction()->inclusive(); } else totalCost = (ProfileCostArray*) _view->activeItem(); } else totalCost = ((TraceItemView*)_view)->data(); double total = totalCost->subCost(_view->eventType()); double inclP = 100.0 * e->cost/ total; if (GlobalConfig::showPercentage()) setText(1, QStringLiteral("%1 %") .arg(inclP, 0, 'f', GlobalConfig::percentPrecision())); else setText(1, SubCost(e->cost).pretty()); int pixPos = 1; if (((TraceItemView*)_view)->data()->maxCallCount() > 0) { setPosition(0, DrawParams::TopCenter); SubCost count((e->count < 1.0) ? 1.0 : e->count); setText(0, QStringLiteral("%1 x").arg(count.pretty())); pixPos = 0; setToolTip(QStringLiteral("%1 (%2)").arg(text(0)).arg(text(1))); } setPixmap(pixPos, percentagePixmap(25, 10, (int)(inclP+.5), Qt::blue, true)); _percentage = inclP; if (_percentage > 100.0) _percentage = 100.0; if (e->call() && (e->call()->isRecursion() || e->call()->inCycle())) { QFontMetrics fm(font()); QPixmap p = QIcon::fromTheme(QStringLiteral("edit-undo")).pixmap(fm.height()); setPixmap(pixPos, p); // replace percentage pixmap } } void CanvasEdgeLabel::paint(QPainter* p, const QStyleOptionGraphicsItem* option, QWidget*) { // draw nothing in PanningView #if QT_VERSION >= 0x040600 if (option->levelOfDetailFromTransform(p->transform()) < .5) return; #else if (option->levelOfDetail < .5) return; #endif QRect r = rect().toRect(); RectDrawing d(r); d.drawField(p, 0, this); d.drawField(p, 1, this); } // // CanvasEdgeArrow CanvasEdgeArrow::CanvasEdgeArrow(CanvasEdge* ce) : _ce(ce) {} void CanvasEdgeArrow::paint(QPainter* p, const QStyleOptionGraphicsItem *, QWidget *) { p->setRenderHint(QPainter::Antialiasing); p->setBrush(_ce->isSelected() ? Qt::red : Qt::black); p->drawPolygon(polygon(), Qt::OddEvenFill); } // // CanvasEdge // CanvasEdge::CanvasEdge(GraphEdge* e) : _edge(e) { _label = 0; _arrow = 0; _thickness = 0; setFlag(QGraphicsItem::ItemIsSelectable); } void CanvasEdge::setLabel(CanvasEdgeLabel* l) { _label = l; if (l) { QString tip = QStringLiteral("%1 (%2)").arg(l->text(0)).arg(l->text(1)); setToolTip(tip); if (_arrow) _arrow->setToolTip(tip); _thickness = log(l->percentage()); if (_thickness < .9) _thickness = .9; } } void CanvasEdge::setArrow(CanvasEdgeArrow* a) { _arrow = a; if (a && _label) a->setToolTip(QStringLiteral("%1 (%2)") .arg(_label->text(0)).arg(_label->text(1))); } void CanvasEdge::setSelected(bool s) { QGraphicsItem::setSelected(s); update(); } void CanvasEdge::setControlPoints(const QPolygon& pa) { _points = pa; QPainterPath path; path.moveTo(pa[0]); for (int i = 1; i < pa.size(); i += 3) path.cubicTo(pa[i], pa[(i + 1) % pa.size()], pa[(i + 2) % pa.size()]); setPath(path); } void CanvasEdge::paint(QPainter* p, const QStyleOptionGraphicsItem* option, QWidget*) { p->setRenderHint(QPainter::Antialiasing); qreal levelOfDetail; #if QT_VERSION >= 0x040600 levelOfDetail = option->levelOfDetailFromTransform(p->transform()); #else levelOfDetail = option->levelOfDetail; #endif QPen mypen = pen(); mypen.setWidthF(1.0/levelOfDetail * _thickness); p->setPen(mypen); p->drawPath(path()); if (isSelected()) { mypen.setColor(Qt::red); mypen.setWidthF(1.0/levelOfDetail * _thickness/2.0); p->setPen(mypen); p->drawPath(path()); } } // // CanvasFrame // QPixmap* CanvasFrame::_p = 0; CanvasFrame::CanvasFrame(CanvasNode* n) { if (!_p) { int d = 5; float v1 = 130.0f, v2 = 10.0f, v = v1, f = 1.03f; // calculate pix size QRect r(0, 0, 30, 30); while (v>v2) { r.setRect(r.x()-d, r.y()-d, r.width()+2*d, r.height()+2*d); v /= f; } _p = new QPixmap(r.size()); _p->fill(Qt::white); QPainter p(_p); p.setPen(Qt::NoPen); r.translate(-r.x(), -r.y()); while (vrect().center().x() - _p->width()/2, n->rect().center().y() - _p->height()/2, _p->width(), _p->height()) ); } void CanvasFrame::paint(QPainter* p, const QStyleOptionGraphicsItem* option, QWidget*) { qreal levelOfDetail; #if QT_VERSION >= 0x040600 levelOfDetail = option->levelOfDetailFromTransform(p->transform()); #else levelOfDetail = option->levelOfDetail; #endif if (levelOfDetail < .5) { QRadialGradient g(rect().center(), rect().width()/3); g.setColorAt(0.0, Qt::gray); g.setColorAt(1.0, Qt::white); p->setBrush(QBrush(g)); p->setPen(Qt::NoPen); p->drawRect(rect()); return; } p->drawPixmap(int( rect().x()),int( rect().y()), *_p ); } // // CallGraphView // CallGraphView::CallGraphView(TraceItemView* parentView, QWidget* parent, const QString& name) : QGraphicsView(parent), TraceItemView(parentView) { setObjectName(name); _zoomPosition = DEFAULT_ZOOMPOS; _lastAutoPosition = TopLeft; _scene = 0; _xMargin = _yMargin = 0; _panningView = new PanningView(this); _panningZoom = 1; _selectedNode = 0; _selectedEdge = 0; _isMoving = false; _exporter.setGraphOptions(this); _panningView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); _panningView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); _panningView->raise(); _panningView->hide(); setFocusPolicy(Qt::StrongFocus); setAttribute(Qt::WA_NoSystemBackground, true); connect(_panningView, &PanningView::zoomRectMoved, this, &CallGraphView::zoomRectMoved); connect(_panningView, &PanningView::zoomRectMoveFinished, this, &CallGraphView::zoomRectMoveFinished); this->setWhatsThis(whatsThis() ); // tooltips... //_tip = new CallGraphTip(this); _renderProcess = 0; _prevSelectedNode = 0; connect(&_renderTimer, &QTimer::timeout, this, &CallGraphView::showRenderWarning); } CallGraphView::~CallGraphView() { clear(); delete _panningView; } QString CallGraphView::whatsThis() const { return tr("Call Graph around active Function" "

Depending on configuration, this view shows " "the call graph environment of the active function. " "Note: the shown cost is only the cost which is " "spent while the active function was actually running; " "i.e. the cost shown for main() - if it is visible - should " "be the same as the cost of the active function, as that is " "the part of inclusive cost of main() spent while the active " "function was running.

" "

For cycles, blue call arrows indicate that this is an " "artificial call added for correct drawing which " "actually never happened.

" "

If the graph is larger than the widget area, an overview " "panner is shown in one edge. " "There are similar visualization options to the " "Call Treemap; the selected function is highlighted.

"); } void CallGraphView::updateSizes(QSize s) { if (!_scene) return; if (s == QSize(0, 0)) s = size(); // the part of the scene that should be visible int cWidth = (int)_scene->width() - 2*_xMargin + 100; int cHeight = (int)_scene->height() - 2*_yMargin + 100; // hide birds eye view if no overview needed if (!_data || !_activeItem || ((cWidth < s.width()) && (cHeight < s.height())) ) { _panningView->hide(); return; } // first, assume use of 1/3 of width/height (possible larger) double zoom = .33 * s.width() / cWidth; if (zoom * cHeight < .33 * s.height()) zoom = .33 * s.height() / cHeight; // fit to widget size if (cWidth * zoom > s.width()) zoom = s.width() / (double)cWidth; if (cHeight * zoom > s.height()) zoom = s.height() / (double)cHeight; // scale to never use full height/width zoom = zoom * 3/4; // at most a zoom of 1/3 if (zoom > .33) zoom = .33; if (zoom != _panningZoom) { _panningZoom = zoom; if (0) qDebug("Canvas Size: %fx%f, Content: %dx%d, Zoom: %f", _scene->width(), _scene->height(), cWidth, cHeight, zoom); QMatrix m; _panningView->setMatrix(m.scale(zoom, zoom)); // make it a little bigger to compensate for widget frame _panningView->resize(int(cWidth * zoom) + 4, int(cHeight * zoom) + 4); // update ZoomRect in panningView scrollContentsBy(0, 0); } _panningView->centerOn(_scene->width()/2, _scene->height()/2); int cvW = _panningView->width(); int cvH = _panningView->height(); int x = width()- cvW - verticalScrollBar()->width() -2; int y = height()-cvH - horizontalScrollBar()->height() -2; QPoint oldZoomPos = _panningView->pos(); QPoint newZoomPos = QPoint(0, 0); ZoomPosition zp = _zoomPosition; if (zp == Auto) { int tlCols = items(QRect(0,0, cvW,cvH)).count(); int trCols = items(QRect(x,0, cvW,cvH)).count(); int blCols = items(QRect(0,y, cvW,cvH)).count(); int brCols = items(QRect(x,y, cvW,cvH)).count(); int minCols = tlCols; zp = _lastAutoPosition; switch (zp) { case TopRight: minCols = trCols; break; case BottomLeft: minCols = blCols; break; case BottomRight: minCols = brCols; break; default: case TopLeft: minCols = tlCols; break; } if (minCols > tlCols) { minCols = tlCols; zp = TopLeft; } if (minCols > trCols) { minCols = trCols; zp = TopRight; } if (minCols > blCols) { minCols = blCols; zp = BottomLeft; } if (minCols > brCols) { minCols = brCols; zp = BottomRight; } _lastAutoPosition = zp; } switch (zp) { case TopLeft: newZoomPos = QPoint(0, 0); break; case TopRight: newZoomPos = QPoint(x, 0); break; case BottomLeft: newZoomPos = QPoint(0, y); break; case BottomRight: newZoomPos = QPoint(x, y); break; default: break; } if (newZoomPos != oldZoomPos) _panningView->move(newZoomPos); if (zp == Hide) _panningView->hide(); else _panningView->show(); } void CallGraphView::focusInEvent(QFocusEvent*) { if (!_scene) return; if (_selectedNode && _selectedNode->canvasNode()) { _selectedNode->canvasNode()->setSelected(true); // requests item update _scene->update(); } } void CallGraphView::focusOutEvent(QFocusEvent* e) { // trigger updates as in focusInEvent focusInEvent(e); } void CallGraphView::keyPressEvent(QKeyEvent* e) { if (!_scene) { e->ignore(); return; } if ((e->key() == Qt::Key_Return) ||(e->key() == Qt::Key_Space)) { if (_selectedNode) activated(_selectedNode->function()); else if (_selectedEdge && _selectedEdge->call()) activated(_selectedEdge->call()); return; } // move selected node/edge if (!(e->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier)) &&(_selectedNode || _selectedEdge)&&((e->key() == Qt::Key_Up) ||(e->key() == Qt::Key_Down)||(e->key() == Qt::Key_Left)||(e->key() == Qt::Key_Right))) { TraceFunction* f = 0; TraceCall* c = 0; // rotate arrow key meaning for LeftRight layout int key = e->key(); if (_layout == LeftRight) { switch (key) { case Qt::Key_Up: key = Qt::Key_Left; break; case Qt::Key_Down: key = Qt::Key_Right; break; case Qt::Key_Left: key = Qt::Key_Up; break; case Qt::Key_Right: key = Qt::Key_Down; break; default: break; } } if (_selectedNode) { if (key == Qt::Key_Up) c = _selectedNode->visibleCaller(); if (key == Qt::Key_Down) c = _selectedNode->visibleCallee(); if (key == Qt::Key_Right) f = _selectedNode->nextVisible(); if (key == Qt::Key_Left) f = _selectedNode->priorVisible(); } else if (_selectedEdge) { if (key == Qt::Key_Up) f = _selectedEdge->visibleCaller(); if (key == Qt::Key_Down) f = _selectedEdge->visibleCallee(); if (key == Qt::Key_Right) c = _selectedEdge->nextVisible(); if (key == Qt::Key_Left) c = _selectedEdge->priorVisible(); } if (c) selected(c); if (f) selected(f); return; } // move canvas... QPointF center = mapToScene(viewport()->rect().center()); if (e->key() == Qt::Key_Home) centerOn(center + QPointF(-_scene->width(), 0)); else if (e->key() == Qt::Key_End) centerOn(center + QPointF(_scene->width(), 0)); else if (e->key() == Qt::Key_PageUp) { QPointF dy = mapToScene(0, height()) - mapToScene(0, 0); centerOn(center + QPointF(-dy.x()/2, -dy.y()/2)); } else if (e->key() == Qt::Key_PageDown) { QPointF dy = mapToScene(0, height()) - mapToScene(0, 0); centerOn(center + QPointF(dy.x()/2, dy.y()/2)); } else if (e->key() == Qt::Key_Left) { QPointF dx = mapToScene(width(), 0) - mapToScene(0, 0); centerOn(center + QPointF(-dx.x()/10, -dx.y()/10)); } else if (e->key() == Qt::Key_Right) { QPointF dx = mapToScene(width(), 0) - mapToScene(0, 0); centerOn(center + QPointF(dx.x()/10, dx.y()/10)); } else if (e->key() == Qt::Key_Down) { QPointF dy = mapToScene(0, height()) - mapToScene(0, 0); centerOn(center + QPointF(dy.x()/10, dy.y()/10)); } else if (e->key() == Qt::Key_Up) { QPointF dy = mapToScene(0, height()) - mapToScene(0, 0); centerOn(center + QPointF(-dy.x()/10, -dy.y()/10)); } else e->ignore(); } void CallGraphView::resizeEvent(QResizeEvent* e) { QGraphicsView::resizeEvent(e); if (_scene) updateSizes(e->size()); } CostItem* CallGraphView::canShow(CostItem* i) { if (i) { switch (i->type()) { case ProfileContext::Function: case ProfileContext::FunctionCycle: case ProfileContext::Call: return i; default: break; } } return 0; } void CallGraphView::doUpdate(int changeType, bool) { // Special case ? if (changeType == eventType2Changed) return; if (changeType == selectedItemChanged) { if (!_scene) return; if (!_selectedItem) return; GraphNode* n = 0; GraphEdge* e = 0; if ((_selectedItem->type() == ProfileContext::Function) ||(_selectedItem->type() == ProfileContext::FunctionCycle)) { n = _exporter.node((TraceFunction*)_selectedItem); if (n == _selectedNode) return; } else if (_selectedItem->type() == ProfileContext::Call) { TraceCall* c = (TraceCall*)_selectedItem; e = _exporter.edge(c->caller(false), c->called(false)); if (e == _selectedEdge) return; } // unselect any selected item if (_selectedNode && _selectedNode->canvasNode()) { _selectedNode->canvasNode()->setSelected(false); } _selectedNode = 0; if (_selectedEdge && _selectedEdge->canvasEdge()) { _selectedEdge->canvasEdge()->setSelected(false); } _selectedEdge = 0; // select CanvasNode* sNode = 0; if (n && n->canvasNode()) { _selectedNode = n; _selectedNode->canvasNode()->setSelected(true); if (!_isMoving) sNode = _selectedNode->canvasNode(); } if (e && e->canvasEdge()) { _selectedEdge = e; _selectedEdge->canvasEdge()->setSelected(true); #if 0 // do not change position when selecting edge if (!_isMoving) { if (_selectedEdge->fromNode()) sNode = _selectedEdge->fromNode()->canvasNode(); if (!sNode && _selectedEdge->toNode()) sNode = _selectedEdge->toNode()->canvasNode(); } #endif } if (sNode) ensureVisible(sNode); _scene->update(); return; } if (changeType == groupTypeChanged) { if (!_scene) return; if (_clusterGroups) { refresh(); return; } QList l = _scene->items(); for (int i = 0; i < l.size(); ++i) if (l[i]->type() == CANVAS_NODE) ((CanvasNode*)l[i])->updateGroup(); _scene->update(); return; } if (changeType & dataChanged) { // invalidate old selection and graph part _exporter.reset(_data, _activeItem, _eventType, _groupType); _selectedNode = 0; _selectedEdge = 0; } refresh(); } void CallGraphView::clear() { if (!_scene) return; _panningView->setScene(0); setScene(0); delete _scene; _scene = 0; } void CallGraphView::showText(QString s) { clear(); _renderTimer.stop(); _scene = new QGraphicsScene; _scene->addSimpleText(s); centerOn(0, 0); setScene(_scene); _scene->update(); _panningView->hide(); } void CallGraphView::showRenderWarning() { QString s; if (_renderProcess) s = tr("Warning: a long lasting graph layouting is in progress.\n" "Reduce node/edge limits for speedup.\n"); else s = tr("Layouting stopped.\n"); s.append(tr("The call graph has %1 nodes and %2 edges.\n") .arg(_exporter.nodeCount()).arg(_exporter.edgeCount())); showText(s); } void CallGraphView::showRenderError(QString s) { QString err; err = tr("No graph available because the layouting process failed.\n"); if (_renderProcess) err += tr("Trying to run the following command did not work:\n" "'%1'\n").arg(_renderProcessCmdLine); err += tr("Please check that 'dot' is installed (package GraphViz)."); if (!s.isEmpty()) err += QStringLiteral("\n\n%1").arg(s); showText(err); } void CallGraphView::stopRendering() { if (!_renderProcess) return; qDebug("CallGraphView::stopRendering: Killing QProcess %p", _renderProcess); _renderProcess->kill(); // forget about this process, not interesting any longer _renderProcess->deleteLater(); _renderProcess = 0; _unparsedOutput = QString(); _renderTimer.setSingleShot(true); _renderTimer.start(200); } void CallGraphView::refresh() { // trigger start of new layouting via 'dot' if (_renderProcess) stopRendering(); // we want to keep a selected node item at the same global position _prevSelectedNode = _selectedNode; _prevSelectedPos = QPoint(-1, -1); if (_selectedNode) { QPointF center = _selectedNode->canvasNode()->rect().center(); _prevSelectedPos = mapFromScene(center); } if (!_data || !_activeItem) { showText(tr("No item activated for which to " "draw the call graph.")); return; } ProfileContext::Type t = _activeItem->type(); switch (t) { case ProfileContext::Function: case ProfileContext::FunctionCycle: case ProfileContext::Call: break; default: showText(tr("No call graph can be drawn for " "the active item.")); return; } if (1) qDebug() << "CallGraphView::refresh"; _selectedNode = 0; _selectedEdge = 0; /* * Call 'dot' asynchronoulsy in the background with the aim to * - have responsive GUI while layout task runs (potentially long!) * - notify user about a long run, using a timer * - kill long running 'dot' processes when another layout is * requested, as old data is not needed any more * * Even after killing a process, the QProcess needs some time * to make sure the process is destroyed; also, stdout data * still can be delivered after killing. Thus, there can/should be * multiple QProcess's at one time. * The QProcess we currently wait for data from is <_renderProcess> * Signals from other QProcesses are ignored with the exception of * the finished() signal, which triggers QProcess destruction. */ QString renderProgram; QStringList renderArgs; if (_layout == GraphOptions::Circular) renderProgram = QStringLiteral("twopi"); else renderProgram = QStringLiteral("dot"); renderArgs << QStringLiteral("-Tplain"); _unparsedOutput = QString(); // display warning if layouting takes > 1s _renderTimer.setSingleShot(true); _renderTimer.start(1000); _renderProcess = new QProcess(this); connect(_renderProcess, &QProcess::readyReadStandardOutput, this, &CallGraphView::readDotOutput); connect(_renderProcess, SIGNAL(error(QProcess::ProcessError)), this, SLOT(dotError())); connect(_renderProcess, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(dotExited())); _renderProcessCmdLine = renderProgram + " " + renderArgs.join(QStringLiteral(" ")); qDebug("CallGraphView::refresh: Starting process %p, '%s'", _renderProcess, qPrintable(_renderProcessCmdLine)); // _renderProcess can be set to 0 on error after start(). // thus, we use a local copy afterwards QProcess* p = _renderProcess; p->start(renderProgram, renderArgs); _exporter.reset(_data, _activeItem, _eventType, _groupType); _exporter.writeDot(p); p->closeWriteChannel(); } void CallGraphView::readDotOutput() { QProcess* p = qobject_cast(sender()); qDebug("CallGraphView::readDotOutput: QProcess %p", p); // signal from old/uninteresting process? if ((_renderProcess == 0) || (p != _renderProcess)) { p->deleteLater(); return; } _unparsedOutput.append(QString::fromLocal8Bit(_renderProcess->readAllStandardOutput())); } void CallGraphView::dotError() { QProcess* p = qobject_cast(sender()); qDebug("CallGraphView::dotError: Got %d from QProcess %p", p->error(), p); // signal from old/uninteresting process? if ((_renderProcess == 0) || (p != _renderProcess)) { p->deleteLater(); return; } showRenderError(QString::fromLocal8Bit(_renderProcess->readAllStandardError())); // not interesting any longer _renderProcess->deleteLater(); _renderProcess = 0; } void CallGraphView::dotExited() { QProcess* p = qobject_cast(sender()); qDebug("CallGraphView::dotExited: QProcess %p", p); // signal from old/uninteresting process? if ((_renderProcess == 0) || (p != _renderProcess)) { p->deleteLater(); return; } _unparsedOutput.append(QString::fromLocal8Bit(_renderProcess->readAllStandardOutput())); _renderProcess->deleteLater(); _renderProcess = 0; QString line, cmd; CanvasNode *rItem; QGraphicsEllipseItem* eItem; CanvasEdge* sItem; CanvasEdgeLabel* lItem; QTextStream* dotStream; double scale = 1.0, scaleX = 1.0, scaleY = 1.0; double dotWidth = 0, dotHeight = 0; GraphNode* activeNode = 0; GraphEdge* activeEdge = 0; _renderTimer.stop(); viewport()->setUpdatesEnabled(false); clear(); dotStream = new QTextStream(&_unparsedOutput, QIODevice::ReadOnly); // First pass to adjust coordinate scaling by node height given from dot // Normal detail level (=1) should be 3 lines using general KDE font double nodeHeight = 0.0; while(1) { line = dotStream->readLine(); if (line.isNull()) break; if (line.isEmpty()) continue; QTextStream lineStream(&line, QIODevice::ReadOnly); lineStream >> cmd; if (cmd != QLatin1String("node")) continue; QString s, h; lineStream >> s /*name*/ >> s /*x*/ >> s /*y*/ >> s /*width*/ >> h /*height*/; nodeHeight = h.toDouble(); break; } if (nodeHeight > 0.0) { scaleY = (8 + (1 + 2 * _detailLevel) * fontMetrics().height()) / nodeHeight; scaleX = 80; } dotStream->seek(0); int lineno = 0; while (1) { line = dotStream->readLine(); if (line.isNull()) break; lineno++; if (line.isEmpty()) continue; QTextStream lineStream(&line, QIODevice::ReadOnly); lineStream >> cmd; if (0) qDebug("%s:%d - line '%s', cmd '%s'", qPrintable(_exporter.filename()), lineno, qPrintable(line), qPrintable(cmd)); if (cmd == QLatin1String("stop")) break; if (cmd == QLatin1String("graph")) { QString dotWidthString, dotHeightString; // scale will not be used lineStream >> scale >> dotWidthString >> dotHeightString; dotWidth = dotWidthString.toDouble(); dotHeight = dotHeightString.toDouble(); if (!_scene) { int w = (int)(scaleX * dotWidth); int h = (int)(scaleY * dotHeight); // We use as minimum canvas size the desktop size. // Otherwise, the canvas would have to be resized on widget resize. _xMargin = 50; if (w < QApplication::desktop()->width()) _xMargin += (QApplication::desktop()->width()-w)/2; _yMargin = 50; if (h < QApplication::desktop()->height()) _yMargin += (QApplication::desktop()->height()-h)/2; _scene = new QGraphicsScene( 0.0, 0.0, qreal(w+2*_xMargin), qreal(h+2*_yMargin)); // Change background color for call graph from default system color to // white. It has to blend into the gradient for the selected function. _scene->setBackgroundBrush(Qt::white); #if DEBUG_GRAPH qDebug() << qPrintable(_exporter.filename()) << ":" << lineno << " - graph (" << dotWidth << " x " << dotHeight << ") => (" << w << " x " << h << ")"; #endif } else qDebug() << "Ignoring 2nd 'graph' from dot (" << _exporter.filename() << ":"<< lineno << ")"; continue; } if ((cmd != QLatin1String("node")) && (cmd != QLatin1String("edge"))) { qDebug() << "Ignoring unknown command '"<< cmd << "' from dot ("<< _exporter.filename() << ":"<< lineno << ")"; continue; } if (_scene == 0) { qDebug() << "Ignoring '"<< cmd << "' without 'graph' from dot ("<< _exporter.filename() << ":"<< lineno << ")"; continue; } if (cmd == QLatin1String("node")) { // x, y are centered in node QString nodeName, nodeX, nodeY, nodeWidth, nodeHeight; double x, y, width, height; lineStream >> nodeName >> nodeX >> nodeY >> nodeWidth >> nodeHeight; x = nodeX.toDouble(); y = nodeY.toDouble(); width = nodeWidth.toDouble(); height = nodeHeight.toDouble(); GraphNode* n = _exporter.node(_exporter.toFunc(nodeName)); int xx = (int)(scaleX * x + _xMargin); int yy = (int)(scaleY * (dotHeight - y)+ _yMargin); int w = (int)(scaleX * width); int h = (int)(scaleY * height); #if DEBUG_GRAPH qDebug() << _exporter.filename() << ":" << lineno << " - node '" << nodeName << "' ( " << x << "/" << y << " - " << width << "x" << height << " ) => (" << xx-w/2 << "/" << yy-h/2 << " - " << w << "x" << h << ")" << endl; #endif // Unnamed nodes with collapsed edges (with 'R' and 'S') if (nodeName[0] == 'R'|| nodeName[0] == 'S') { w = 10, h = 10; eItem = new QGraphicsEllipseItem( QRectF(xx-w/2, yy-h/2, w, h) ); _scene->addItem(eItem); eItem->setBrush(Qt::gray); eItem->setZValue(1.0); eItem->show(); continue; } if (!n) { qDebug("Warning: Unknown function '%s' ?!", qPrintable(nodeName)); continue; } n->setVisible(true); rItem = new CanvasNode(this, n, xx-w/2, yy-h/2, w, h); // limit symbol space to a maximal number of lines depending on detail level if (_detailLevel>0) rItem->setMaxLines(0, 2*_detailLevel); _scene->addItem(rItem); n->setCanvasNode(rItem); if (n) { if (n->function() == activeItem()) activeNode = n; if (n->function() == selectedItem()) _selectedNode = n; rItem->setSelected(n == _selectedNode); } rItem->setZValue(1.0); rItem->show(); continue; } // edge QString node1Name, node2Name, label, edgeX, edgeY; double x, y; QPolygon poly; int points, i; lineStream >> node1Name >> node2Name >> points; GraphEdge* e = _exporter.edge(_exporter.toFunc(node1Name), _exporter.toFunc(node2Name)); if (!e) { qDebug() << "Unknown edge '"<< node1Name << "'-'"<< node2Name << "' from dot ("<< _exporter.filename() << ":"<< lineno << ")"; continue; } e->setVisible(true); if (e->fromNode()) e->fromNode()->addCallee(e); if (e->toNode()) e->toNode()->addCaller(e); if (0) qDebug(" Edge with %d points:", points); poly.resize(points); for (i=0; i> edgeX >> edgeY; x = edgeX.toDouble(); y = edgeY.toDouble(); int xx = (int)(scaleX * x + _xMargin); int yy = (int)(scaleY * (dotHeight - y)+ _yMargin); if (0) qDebug(" P %d: ( %f / %f ) => ( %d / %d)", i, x, y, xx, yy); poly.setPoint(i, xx, yy); } if (i < points) { qDebug("CallGraphView: Can not read %d spline points (%s:%d)", points, qPrintable(_exporter.filename()), lineno); continue; } // calls into/out of cycles are special: make them blue QColor arrowColor = Qt::black; TraceFunction* caller = e->fromNode() ? e->fromNode()->function() : 0; TraceFunction* called = e->toNode() ? e->toNode()->function() : 0; if ( (caller && (caller->cycle() == caller)) || (called && (called->cycle() == called)) ) arrowColor = Qt::blue; sItem = new CanvasEdge(e); _scene->addItem(sItem); e->setCanvasEdge(sItem); sItem->setControlPoints(poly); // width of pen will be adjusted in CanvasEdge::paint() sItem->setPen(QPen(arrowColor)); sItem->setZValue(0.5); sItem->show(); if (e->call() == selectedItem()) _selectedEdge = e; if (e->call() == activeItem()) activeEdge = e; sItem->setSelected(e == _selectedEdge); // Arrow head QPoint arrowDir; int indexHead = -1; // check if head is at start of spline... // this is needed because dot always gives points from top to bottom CanvasNode* fromNode = e->fromNode() ? e->fromNode()->canvasNode() : 0; if (fromNode) { QPointF toCenter = fromNode->rect().center(); qreal dx0 = poly.point(0).x() - toCenter.x(); qreal dy0 = poly.point(0).y() - toCenter.y(); qreal dx1 = poly.point(points-1).x() - toCenter.x(); qreal dy1 = poly.point(points-1).y() - toCenter.y(); if (dx0*dx0+dy0*dy0 > dx1*dx1+dy1*dy1) { // start of spline is nearer to call target node indexHead=-1; while (arrowDir.isNull() && (indexHead1)) { indexHead--; arrowDir = poly.point(indexHead) - poly.point(indexHead-1); } } if (!arrowDir.isNull()) { // arrow around pa.point(indexHead) with direction arrowDir arrowDir *= 10.0/sqrt(double(arrowDir.x()*arrowDir.x() + arrowDir.y()*arrowDir.y())); QPolygonF a; a << QPointF(poly.point(indexHead) + arrowDir); a << QPointF(poly.point(indexHead) + QPoint(arrowDir.y()/2, -arrowDir.x()/2)); a << QPointF(poly.point(indexHead) + QPoint(-arrowDir.y()/2, arrowDir.x()/2)); if (0) qDebug(" Arrow: ( %f/%f, %f/%f, %f/%f)", a[0].x(), a[0].y(), a[1].x(), a[1].y(), a[2].x(), a[1].y()); CanvasEdgeArrow* aItem = new CanvasEdgeArrow(sItem); _scene->addItem(aItem); aItem->setPolygon(a); aItem->setBrush(arrowColor); aItem->setZValue(1.5); aItem->show(); sItem->setArrow(aItem); } if (lineStream.atEnd()) continue; // parse quoted label QChar c; lineStream >> c; while (c.isSpace()) lineStream >> c; if (c != '\"') { lineStream >> label; label = c + label; } else { lineStream >> c; while (!c.isNull() && (c != '\"')) { //if (c == '\\') lineStream >> c; label += c; lineStream >> c; } } lineStream >> edgeX >> edgeY; x = edgeX.toDouble(); y = edgeY.toDouble(); int xx = (int)(scaleX * x + _xMargin); int yy = (int)(scaleY * (dotHeight - y)+ _yMargin); if (0) qDebug(" Label '%s': ( %f / %f ) => ( %d / %d)", qPrintable(label), x, y, xx, yy); // Fixed Dimensions for Label: 100 x 40 int w = 100; int h = _detailLevel * 20; lItem = new CanvasEdgeLabel(this, sItem, xx-w/2, yy-h/2, w, h); _scene->addItem(lItem); // edge labels above nodes lItem->setZValue(1.5); sItem->setLabel(lItem); if (h>0) lItem->show(); } delete dotStream; // for keyboard navigation _exporter.sortEdges(); if (!_scene) { _scene = new QGraphicsScene; QString s = tr("Error running the graph layouting tool.\n"); s += tr("Please check that 'dot' is installed (package GraphViz)."); _scene->addSimpleText(s); centerOn(0, 0); } else if (!activeNode && !activeEdge) { QString s = tr("There is no call graph available for function\n" "\t'%1'\n" "because it has no cost of the selected event type.") .arg(_activeItem->name()); _scene->addSimpleText(s); centerOn(0, 0); } _panningView->setScene(_scene); setScene(_scene); // if we do not have a selection, or the old selection is not // in visible graph, make active function selected for this view if ((!_selectedNode || !_selectedNode->canvasNode()) && (!_selectedEdge || !_selectedEdge->canvasEdge())) { if (activeNode) { _selectedNode = activeNode; _selectedNode->canvasNode()->setSelected(true); } else if (activeEdge) { _selectedEdge = activeEdge; _selectedEdge->canvasEdge()->setSelected(true); } } CanvasNode* sNode = 0; if (_selectedNode) sNode = _selectedNode->canvasNode(); else if (_selectedEdge) { if (_selectedEdge->fromNode()) sNode = _selectedEdge->fromNode()->canvasNode(); if (!sNode && _selectedEdge->toNode()) sNode = _selectedEdge->toNode()->canvasNode(); } if (sNode) { if (_prevSelectedNode) { QPointF prevPos = mapToScene(_prevSelectedPos); if (rect().contains(_prevSelectedPos)) { QPointF wCenter = mapToScene(viewport()->rect().center()); centerOn(sNode->rect().center() + wCenter - prevPos); } else ensureVisible(sNode); } else centerOn(sNode); } if (activeNode) { CanvasNode* cn = activeNode->canvasNode(); CanvasFrame* f = new CanvasFrame(cn); _scene->addItem(f); f->setPos(cn->pos()); f->setZValue(-1); } _panningZoom = 0; updateSizes(); _scene->update(); viewport()->setUpdatesEnabled(true); delete _renderProcess; _renderProcess = 0; } // Called by QAbstractScrollArea to notify about scrollbar changes void CallGraphView::scrollContentsBy(int dx, int dy) { // call QGraphicsView implementation QGraphicsView::scrollContentsBy(dx, dy); QPointF topLeft = mapToScene(QPoint(0, 0)); QPointF bottomRight = mapToScene(QPoint(width(), height())); QRectF z(topLeft, bottomRight); if (0) qDebug("CallGraphView::scrollContentsBy(dx %d, dy %d) - to (%f,%f - %f,%f)", dx, dy, topLeft.x(), topLeft.y(), bottomRight.x(), bottomRight.y()); _panningView->setZoomRect(z); } void CallGraphView::zoomRectMoved(qreal dx, qreal dy) { //FIXME if (leftMargin()>0) dx = 0; //FIXME if (topMargin()>0) dy = 0; QScrollBar *hBar = horizontalScrollBar(); QScrollBar *vBar = verticalScrollBar(); hBar->setValue(hBar->value() + int(dx)); vBar->setValue(vBar->value() + int(dy)); } void CallGraphView::zoomRectMoveFinished() { if (_zoomPosition == Auto) updateSizes(); } void CallGraphView::mousePressEvent(QMouseEvent* e) { // clicking on the viewport sets focus setFocus(); // activate scroll mode on left click if (e->button() == Qt::LeftButton) _isMoving = true; QGraphicsItem* i = itemAt(e->pos()); if (i) { if (i->type() == CANVAS_NODE) { GraphNode* n = ((CanvasNode*)i)->node(); if (0) qDebug("CallGraphView: Got Node '%s'", qPrintable(n->function()->prettyName())); selected(n->function()); } // redirect from label / arrow to edge if (i->type() == CANVAS_EDGELABEL) i = ((CanvasEdgeLabel*)i)->canvasEdge(); if (i->type() == CANVAS_EDGEARROW) i = ((CanvasEdgeArrow*)i)->canvasEdge(); if (i->type() == CANVAS_EDGE) { GraphEdge* e = ((CanvasEdge*)i)->edge(); if (0) qDebug("CallGraphView: Got Edge '%s'", qPrintable(e->prettyName())); if (e->call()) selected(e->call()); } } _lastPos = e->pos(); } void CallGraphView::mouseMoveEvent(QMouseEvent* e) { if (_isMoving) { QPoint delta = e->pos() - _lastPos; QScrollBar *hBar = horizontalScrollBar(); QScrollBar *vBar = verticalScrollBar(); hBar->setValue(hBar->value() - delta.x()); vBar->setValue(vBar->value() - delta.y()); _lastPos = e->pos(); } } void CallGraphView::mouseReleaseEvent(QMouseEvent*) { _isMoving = false; if (_zoomPosition == Auto) updateSizes(); } void CallGraphView::mouseDoubleClickEvent(QMouseEvent* e) { QGraphicsItem* i = itemAt(e->pos()); if (i == 0) return; if (i->type() == CANVAS_NODE) { GraphNode* n = ((CanvasNode*)i)->node(); if (0) qDebug("CallGraphView: Double Clicked on Node '%s'", qPrintable(n->function()->prettyName())); activated(n->function()); } // redirect from label / arrow to edge if (i->type() == CANVAS_EDGELABEL) i = ((CanvasEdgeLabel*)i)->canvasEdge(); if (i->type() == CANVAS_EDGEARROW) i = ((CanvasEdgeArrow*)i)->canvasEdge(); if (i->type() == CANVAS_EDGE) { GraphEdge* e = ((CanvasEdge*)i)->edge(); if (e->call()) { if (0) qDebug("CallGraphView: Double Clicked On Edge '%s'", qPrintable(e->call()->prettyName())); activated(e->call()); } } } // helper functions for context menu: // submenu builders and trigger handlers QAction* CallGraphView::addCallerDepthAction(QMenu* m, QString s, int d) { QAction* a; a = m->addAction(s); a->setData(d); a->setCheckable(true); a->setChecked(_maxCallerDepth == d); return a; } QMenu* CallGraphView::addCallerDepthMenu(QMenu* menu) { QAction* a; QMenu* m; m = menu->addMenu(tr("Caller Depth")); a = addCallerDepthAction(m, tr("Unlimited"), -1); a->setEnabled(_funcLimit>0.005); m->addSeparator(); addCallerDepthAction(m, tr("Depth 0", "None"), 0); addCallerDepthAction(m, tr("max. 1"), 1); addCallerDepthAction(m, tr("max. 2"), 2); addCallerDepthAction(m, tr("max. 5"), 5); addCallerDepthAction(m, tr("max. 10"), 10); addCallerDepthAction(m, tr("max. 15"), 15); connect(m, &QMenu::triggered, this, &CallGraphView::callerDepthTriggered ); return m; } void CallGraphView::callerDepthTriggered(QAction* a) { _maxCallerDepth = a->data().toInt(0); refresh(); } QAction* CallGraphView::addCalleeDepthAction(QMenu* m, QString s, int d) { QAction* a; a = m->addAction(s); a->setData(d); a->setCheckable(true); a->setChecked(_maxCalleeDepth == d); return a; } QMenu* CallGraphView::addCalleeDepthMenu(QMenu* menu) { QAction* a; QMenu* m; m = menu->addMenu(tr("Callee Depth")); a = addCalleeDepthAction(m, tr("Unlimited"), -1); a->setEnabled(_funcLimit>0.005); m->addSeparator(); addCalleeDepthAction(m, tr("Depth 0", "None"), 0); addCalleeDepthAction(m, tr("max. 1"), 1); addCalleeDepthAction(m, tr("max. 2"), 2); addCalleeDepthAction(m, tr("max. 5"), 5); addCalleeDepthAction(m, tr("max. 10"), 10); addCalleeDepthAction(m, tr("max. 15"), 15); connect(m, &QMenu::triggered, this, &CallGraphView::calleeDepthTriggered ); return m; } void CallGraphView::calleeDepthTriggered(QAction* a) { _maxCalleeDepth = a->data().toInt(0); refresh(); } QAction* CallGraphView::addNodeLimitAction(QMenu* m, QString s, double l) { QAction* a; a = m->addAction(s); a->setData(l); a->setCheckable(true); a->setChecked(_funcLimit == l); return a; } QMenu* CallGraphView::addNodeLimitMenu(QMenu* menu) { QAction* a; QMenu* m; m = menu->addMenu(tr("Min. Node Cost")); a = addNodeLimitAction(m, tr("No Minimum"), 0.0); // Unlimited node cost easily produces huge graphs such that 'dot' // would need a long time to layout. For responsiveness, we only allow // for unlimited node cost if a caller and callee depth limit is set. a->setEnabled((_maxCallerDepth>=0) && (_maxCalleeDepth>=0)); m->addSeparator(); addNodeLimitAction(m, tr("50 %"), .5); addNodeLimitAction(m, tr("20 %"), .2); addNodeLimitAction(m, tr("10 %"), .1); addNodeLimitAction(m, tr("5 %"), .05); addNodeLimitAction(m, tr("2 %"), .02); addNodeLimitAction(m, tr("1 %"), .01); connect(m, &QMenu::triggered, this, &CallGraphView::nodeLimitTriggered ); return m; } void CallGraphView::nodeLimitTriggered(QAction* a) { _funcLimit = a->data().toDouble(0); refresh(); } QAction* CallGraphView::addCallLimitAction(QMenu* m, QString s, double l) { QAction* a; a = m->addAction(s); a->setData(l); a->setCheckable(true); a->setChecked(_callLimit == l); return a; } QMenu* CallGraphView::addCallLimitMenu(QMenu* menu) { QMenu* m; m = menu->addMenu(tr("Min. Call Cost")); addCallLimitAction(m, tr("Same as Node"), 1.0); // xgettext: no-c-format addCallLimitAction(m, tr("50 % of Node"), .5); // xgettext: no-c-format addCallLimitAction(m, tr("20 % of Node"), .2); // xgettext: no-c-format addCallLimitAction(m, tr("10 % of Node"), .1); connect(m, &QMenu::triggered, this, &CallGraphView::callLimitTriggered ); return m; } void CallGraphView::callLimitTriggered(QAction* a) { _callLimit = a->data().toDouble(0); refresh(); } QAction* CallGraphView::addZoomPosAction(QMenu* m, QString s, CallGraphView::ZoomPosition p) { QAction* a; a = m->addAction(s); a->setData((int)p); a->setCheckable(true); a->setChecked(_zoomPosition == p); return a; } QMenu* CallGraphView::addZoomPosMenu(QMenu* menu) { QMenu* m = menu->addMenu(tr("Birds-eye View")); addZoomPosAction(m, tr("Top Left"), TopLeft); addZoomPosAction(m, tr("Top Right"), TopRight); addZoomPosAction(m, tr("Bottom Left"), BottomLeft); addZoomPosAction(m, tr("Bottom Right"), BottomRight); addZoomPosAction(m, tr("Automatic"), Auto); addZoomPosAction(m, tr("Hide"), Hide); connect(m, &QMenu::triggered, this, &CallGraphView::zoomPosTriggered ); return m; } void CallGraphView::zoomPosTriggered(QAction* a) { _zoomPosition = (ZoomPosition) a->data().toInt(0); updateSizes(); } QAction* CallGraphView::addLayoutAction(QMenu* m, QString s, GraphOptions::Layout l) { QAction* a; a = m->addAction(s); a->setData((int)l); a->setCheckable(true); a->setChecked(_layout == l); return a; } QMenu* CallGraphView::addLayoutMenu(QMenu* menu) { QMenu* m = menu->addMenu(tr("Layout")); addLayoutAction(m, tr("Top to Down"), TopDown); addLayoutAction(m, tr("Left to Right"), LeftRight); addLayoutAction(m, tr("Circular"), Circular); connect(m, &QMenu::triggered, this, &CallGraphView::layoutTriggered ); return m; } void CallGraphView::layoutTriggered(QAction* a) { _layout = (Layout) a->data().toInt(0); refresh(); } void CallGraphView::contextMenuEvent(QContextMenuEvent* e) { _isMoving = false; QGraphicsItem* i = itemAt(e->pos()); QMenu popup; TraceFunction *f = 0, *cycle = 0; TraceCall* c = 0; QAction* activateFunction = 0; QAction* activateCycle = 0; QAction* activateCall = 0; if (i) { if (i->type() == CANVAS_NODE) { GraphNode* n = ((CanvasNode*)i)->node(); if (0) qDebug("CallGraphView: Menu on Node '%s'", qPrintable(n->function()->prettyName())); f = n->function(); cycle = f->cycle(); QString name = f->prettyName(); QString menuStr = tr("Go to '%1'") .arg(GlobalConfig::shortenSymbol(name)); activateFunction = popup.addAction(menuStr); if (cycle && (cycle != f)) { name = GlobalConfig::shortenSymbol(cycle->prettyName()); activateCycle = popup.addAction(tr("Go to '%1'").arg(name)); } popup.addSeparator(); } // redirect from label / arrow to edge if (i->type() == CANVAS_EDGELABEL) i = ((CanvasEdgeLabel*)i)->canvasEdge(); if (i->type() == CANVAS_EDGEARROW) i = ((CanvasEdgeArrow*)i)->canvasEdge(); if (i->type() == CANVAS_EDGE) { GraphEdge* e = ((CanvasEdge*)i)->edge(); if (0) qDebug("CallGraphView: Menu on Edge '%s'", qPrintable(e->prettyName())); c = e->call(); if (c) { QString name = c->prettyName(); QString menuStr = tr("Go to '%1'") .arg(GlobalConfig::shortenSymbol(name)); activateCall = popup.addAction(menuStr); popup.addSeparator(); } } } QAction* stopLayout = 0; if (_renderProcess) { stopLayout = popup.addAction(tr("Stop Layouting")); popup.addSeparator(); } addGoMenu(&popup); popup.addSeparator(); QMenu* epopup = popup.addMenu(tr("Export Graph")); QAction* exportAsDot = epopup->addAction(tr("As DOT file...")); QAction* exportAsImage = epopup->addAction(tr("As Image...")); popup.addSeparator(); QMenu* gpopup = popup.addMenu(tr("Graph")); addCallerDepthMenu(gpopup); addCalleeDepthMenu(gpopup); addNodeLimitMenu(gpopup); addCallLimitMenu(gpopup); gpopup->addSeparator(); QAction* toggleSkipped; toggleSkipped = gpopup->addAction(tr("Arrows for Skipped Calls")); toggleSkipped->setCheckable(true); toggleSkipped->setChecked(_showSkipped); QAction* toggleExpand; toggleExpand = gpopup->addAction(tr("Inner-cycle Calls")); toggleExpand->setCheckable(true); toggleExpand->setChecked(_expandCycles); QAction* toggleCluster; toggleCluster = gpopup->addAction(tr("Cluster Groups")); toggleCluster->setCheckable(true); toggleCluster->setChecked(_clusterGroups); QMenu* vpopup = popup.addMenu(tr("Visualization")); QAction* layoutCompact = vpopup->addAction(tr("Compact")); layoutCompact->setCheckable(true); layoutCompact->setChecked(_detailLevel == 0); QAction* layoutNormal = vpopup->addAction(tr("Normal")); layoutNormal->setCheckable(true); layoutNormal->setChecked(_detailLevel == 1); QAction* layoutTall = vpopup->addAction(tr("Tall")); layoutTall->setCheckable(true); layoutTall->setChecked(_detailLevel == 2); addLayoutMenu(&popup); addZoomPosMenu(&popup); QAction* a = popup.exec(e->globalPos()); if (a == activateFunction) activated(f); else if (a == activateCycle) activated(cycle); else if (a == activateCall) activated(c); else if (a == stopLayout) stopRendering(); else if (a == exportAsDot) { TraceFunction* f = activeFunction(); if (!f) return; QString n; n = QFileDialog::getSaveFileName(this, tr("Export Graph As DOT file"), QString(), tr("Graphviz (*.dot)")); if (!n.isEmpty()) { GraphExporter ge(TraceItemView::data(), f, eventType(), groupType(), n); ge.setGraphOptions(this); ge.writeDot(); } } else if (a == exportAsImage) { // write current content of canvas as image to file if (!_scene) return; QString n; n = QFileDialog::getSaveFileName(this, tr("Export Graph As Image"), QString(), tr("Images (*.png *.jpg)")); if (!n.isEmpty()) { QRect r = _scene->sceneRect().toRect(); QPixmap pix(r.width(), r.height()); QPainter p(&pix); _scene->render( &p ); pix.save(n); } } else if (a == toggleSkipped) { _showSkipped = !_showSkipped; refresh(); } else if (a == toggleExpand) { _expandCycles = !_expandCycles; refresh(); } else if (a == toggleCluster) { _clusterGroups = !_clusterGroups; refresh(); } else if (a == layoutCompact) { _detailLevel = 0; refresh(); } else if (a == layoutNormal) { _detailLevel = 1; refresh(); } else if (a == layoutTall) { _detailLevel = 2; refresh(); } } CallGraphView::ZoomPosition CallGraphView::zoomPos(QString s) { if (s == QStringLiteral("TopLeft")) return TopLeft; if (s == QStringLiteral("TopRight")) return TopRight; if (s == QStringLiteral("BottomLeft")) return BottomLeft; if (s == QStringLiteral("BottomRight")) return BottomRight; if (s == QStringLiteral("Automatic")) return Auto; if (s == QStringLiteral("Hide")) return Hide; return DEFAULT_ZOOMPOS; } QString CallGraphView::zoomPosString(ZoomPosition p) { if (p == TopLeft) return QStringLiteral("TopLeft"); if (p == TopRight) return QStringLiteral("TopRight"); if (p == BottomLeft) return QStringLiteral("BottomLeft"); if (p == BottomRight) return QStringLiteral("BottomRight"); if (p == Auto) return QStringLiteral("Automatic"); if (p == Hide) return QStringLiteral("Hide"); return QString(); } void CallGraphView::restoreOptions(const QString& prefix, const QString& postfix) { ConfigGroup* g = ConfigStorage::group(prefix, postfix); _maxCallerDepth = g->value(QStringLiteral("MaxCaller"), DEFAULT_MAXCALLER).toInt(); _maxCalleeDepth = g->value(QStringLiteral("MaxCallee"), DEFAULT_MAXCALLEE).toInt(); _funcLimit = g->value(QStringLiteral("FuncLimit"), DEFAULT_FUNCLIMIT).toDouble(); _callLimit = g->value(QStringLiteral("CallLimit"), DEFAULT_CALLLIMIT).toDouble(); _showSkipped = g->value(QStringLiteral("ShowSkipped"), DEFAULT_SHOWSKIPPED).toBool(); _expandCycles = g->value(QStringLiteral("ExpandCycles"), DEFAULT_EXPANDCYCLES).toBool(); _clusterGroups = g->value(QStringLiteral("ClusterGroups"), DEFAULT_CLUSTERGROUPS).toBool(); _detailLevel = g->value(QStringLiteral("DetailLevel"), DEFAULT_DETAILLEVEL).toInt(); _layout = GraphOptions::layout(g->value(QStringLiteral("Layout"), layoutString(DEFAULT_LAYOUT)).toString()); _zoomPosition = zoomPos(g->value(QStringLiteral("ZoomPosition"), zoomPosString(DEFAULT_ZOOMPOS)).toString()); delete g; } void CallGraphView::saveOptions(const QString& prefix, const QString& postfix) { ConfigGroup* g = ConfigStorage::group(prefix + postfix); g->setValue(QStringLiteral("MaxCaller"), _maxCallerDepth, DEFAULT_MAXCALLER); g->setValue(QStringLiteral("MaxCallee"), _maxCalleeDepth, DEFAULT_MAXCALLEE); g->setValue(QStringLiteral("FuncLimit"), _funcLimit, DEFAULT_FUNCLIMIT); g->setValue(QStringLiteral("CallLimit"), _callLimit, DEFAULT_CALLLIMIT); g->setValue(QStringLiteral("ShowSkipped"), _showSkipped, DEFAULT_SHOWSKIPPED); g->setValue(QStringLiteral("ExpandCycles"), _expandCycles, DEFAULT_EXPANDCYCLES); g->setValue(QStringLiteral("ClusterGroups"), _clusterGroups, DEFAULT_CLUSTERGROUPS); g->setValue(QStringLiteral("DetailLevel"), _detailLevel, DEFAULT_DETAILLEVEL); g->setValue(QStringLiteral("Layout"), layoutString(_layout), layoutString(DEFAULT_LAYOUT)); g->setValue(QStringLiteral("ZoomPosition"), zoomPosString(_zoomPosition), zoomPosString(DEFAULT_ZOOMPOS)); delete g; } diff --git a/libviews/instrview.cpp b/libviews/instrview.cpp index 6031d3e..b93cb7f 100644 --- a/libviews/instrview.cpp +++ b/libviews/instrview.cpp @@ -1,1184 +1,1190 @@ /* This file is part of KCachegrind. Copyright (c) 2003-2016 Josef Weidendorfer KCachegrind is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /* * Instruction View */ #include "instrview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "config.h" #include "globalconfig.h" #include "instritem.h" // InstrView defaults #define DEFAULT_SHOWHEXCODE true // Helpers // check environment variables QProcessEnvironment env; static QString getSysRoot() { if (env.isEmpty()) env = QProcessEnvironment::systemEnvironment(); return env.value(QStringLiteral("SYSROOT")); } static QString getObjDump() { if (env.isEmpty()) env = QProcessEnvironment::systemEnvironment(); return env.value(QStringLiteral("OBJDUMP"), QStringLiteral("objdump")); } // parsing output of 'objdump' static Addr parseAddr(char* buf) { Addr addr; uint pos = 0; // check for instruction line: * ":" * while(buf[pos]==' ' || buf[pos]=='\t') pos++; int digits = addr.set(buf + pos); if ((digits==0) || (buf[pos+digits] != ':')) return Addr(0); return addr; } static bool isHexDigit(char c) { return (c >='0' && c <='9') || (c >='a' && c <='f'); } /** * Parses a line from objdump assembly output, returning false for * a line without an assembly instruction. Otherwise, it sets the * output parameters addr, code, mnemonic, operands. */ static bool parseLine(const char* buf, Addr& addr, QString& code, QString& mnemonic, QString& operands) { uint pos, start; // check for instruction line: * ":" * pos = 0; while(buf[pos]==' ' || buf[pos]=='\t') pos++; int digits = addr.set(buf + pos); pos += digits; if ((digits==0) || (buf[pos] != ':')) return false; // further parsing of objdump output... pos++; while(buf[pos]==' ' || buf[pos]=='\t') pos++; // check for hex code, patterns "xx "* / "xxxx "* / "xxxxxxxx" // (with "x" being a lower case hex digit) start = pos; while(1) { if (! isHexDigit(buf[pos])) break; if (! isHexDigit(buf[pos+1])) break; if (buf[pos+2] == ' ') { pos += 3; continue; } if (! isHexDigit(buf[pos+2])) break; if (! isHexDigit(buf[pos+3])) break; if (buf[pos+4] == ' ') { pos += 5; continue; } if (! isHexDigit(buf[pos+4])) break; if (! isHexDigit(buf[pos+5])) break; if (! isHexDigit(buf[pos+6])) break; if (! isHexDigit(buf[pos+7])) break; if (buf[pos+8] != ' ') break; pos += 9; } if (pos <= start) return false; code = QString::fromLatin1(buf + start, pos - start - 1); // skip whitespace while(buf[pos]==' ' || buf[pos]=='\t') pos++; // check for mnemonic start = pos; while(buf[pos] && buf[pos]!=' ' && buf[pos]!='\t') pos++; mnemonic = QString::fromLatin1(buf + start, pos - start); // skip whitespace while(buf[pos]==' '|| buf[pos]=='\t') pos++; // last part are the operands int operandsLen = strlen(buf + pos); // ignore a newline at end if ((operandsLen>0) && (buf[pos + operandsLen - 1] == '\n')) operandsLen--; // maximal 50 chars if (operandsLen > 50) operands = QString::fromLatin1(buf + pos, 47) + QStringLiteral("..."); else operands = QString::fromLatin1(buf+pos, operandsLen); if (0) qDebug("For 0x%s: Code '%s', Mnemonic '%s', Operands '%s'", qPrintable(addr.toString()), qPrintable(code), qPrintable(mnemonic), qPrintable(operands)); return true; } // // InstrView // InstrView::InstrView(TraceItemView* parentView, QWidget* parent) : QTreeWidget(parent), TraceItemView(parentView) { _showHexCode = DEFAULT_SHOWHEXCODE; _lastHexCodeWidth = 50; _inSelectionUpdate = false; _arrowLevels = 0; QStringList headerLabels; headerLabels << tr( "#" ) << tr( "Cost" ) << tr( "Cost 2" ) << QString() << tr( "Hex" ) << QString() // Mnenomic << tr( "Assembly Instructions" ) << tr( "Source Position" ); setHeaderLabels(headerLabels); setRootIsDecorated(false); setAllColumnsShowFocus(true); setUniformRowHeights(true); // collapsing call/jump lines by double-click is confusing setExpandsOnDoubleClick(false); // sorting will be enabled after refresh() sortByColumn(0, Qt::AscendingOrder); header()->setSortIndicatorShown(false); setItemDelegate(new InstrItemDelegate(this)); setWhatsThis( whatsThis() ); connect( this, &QTreeWidget::currentItemChanged, this, &InstrView::selectedSlot ); setContextMenuPolicy(Qt::CustomContextMenu); connect( this, &QWidget::customContextMenuRequested, this, &InstrView::context); connect(this, &QTreeWidget::itemDoubleClicked, this, &InstrView::activatedSlot); connect(header(), &QHeaderView::sectionClicked, this, &InstrView::headerClicked); this->setWhatsThis( whatsThis()); } QString InstrView::whatsThis() const { return tr( "Annotated Machine Code" "

The annotated machine code list shows the " "assembly instructions of the current selected " "function together with (self) cost spent while " "executing an instruction. If this is a call " "instruction, lines with details on the " "call happening are inserted into the source: " "the cost spent inside of the call, the " "number of calls happening, and the call destination.

" "

The machine code shown is generated with " "the 'objdump' utility from the 'binutils' package.

" "

Select a line with call information to " "make the destination function of this call current.

"); } void InstrView::context(const QPoint & p) { QMenu popup; int c = columnAt(p.x()); QTreeWidgetItem* i = itemAt(p); TraceInstrCall* ic = i ? ((InstrItem*) i)->instrCall() : 0; TraceInstrJump* ij = i ? ((InstrItem*) i)->instrJump() : 0; TraceFunction* f = ic ? ic->call()->called() : 0; TraceInstr* instr = ij ? ij->instrTo() : 0; QAction* activateFunctionAction = 0; QAction* activateInstrAction = 0; if (f) { QString menuText = tr("Go to '%1'").arg(GlobalConfig::shortenSymbol(f->prettyName())); activateFunctionAction = popup.addAction(menuText); popup.addSeparator(); } else if (instr) { QString menuText = tr("Go to Address %1").arg(instr->name()); activateInstrAction = popup.addAction(menuText); popup.addSeparator(); } if ((c == 1) || (c == 2)) { addEventTypeMenu(&popup); popup.addSeparator(); } addGoMenu(&popup); popup.addSeparator(); QAction* toggleHexAction = new QAction(tr("Hex Code"), &popup); toggleHexAction->setCheckable(true); toggleHexAction->setChecked(_showHexCode); popup.addAction(toggleHexAction); QAction* a = popup.exec(mapToGlobal(p + QPoint(0,header()->height()))); if (a == activateFunctionAction) TraceItemView::activated(f); else if (a == activateInstrAction) TraceItemView::activated(instr); else if (a == toggleHexAction) { _showHexCode = !_showHexCode; // remember width when hiding if (!_showHexCode) _lastHexCodeWidth = columnWidth(4); setColumnWidths(); } } void InstrView::selectedSlot(QTreeWidgetItem *i, QTreeWidgetItem *) { if (!i) return; // programatically selected items are not signalled if (_inSelectionUpdate) return; TraceInstrCall* ic = ((InstrItem*) i)->instrCall(); TraceInstrJump* ij = ((InstrItem*) i)->instrJump(); if (!ic && !ij) { TraceInstr* instr = ((InstrItem*) i)->instr(); if (instr) { _selectedItem = instr; selected(instr); } return; } if (ic) { _selectedItem = ic; selected(ic); } else if (ij) { _selectedItem = ij; selected(ij); } } void InstrView::activatedSlot(QTreeWidgetItem* i, int) { if (!i) return; TraceInstrCall* ic = ((InstrItem*) i)->instrCall(); TraceInstrJump* ij = ((InstrItem*) i)->instrJump(); if (!ic && !ij) { TraceInstr* instr = ((InstrItem*) i)->instr(); if (instr) TraceItemView::activated(instr); return; } if (ic) { TraceFunction* f = ic->call()->called(); if (f) TraceItemView::activated(f); } else if (ij) { TraceInstr* instr = ij->instrTo(); if (instr) TraceItemView::activated(instr); } } void InstrView::keyPressEvent(QKeyEvent* event) { QTreeWidgetItem *item = currentItem(); if (item && ((event->key() == Qt::Key_Return) || (event->key() == Qt::Key_Space))) { activatedSlot(item, 0); } QTreeView::keyPressEvent(event); } CostItem* InstrView::canShow(CostItem* i) { ProfileContext::Type t = i ? i->type() : ProfileContext::InvalidType; switch(t) { case ProfileContext::Function: case ProfileContext::Instr: case ProfileContext::InstrJump: case ProfileContext::Line: return i; default: break; } return 0; } void InstrView::doUpdate(int changeType, bool) { // Special case ? if (changeType == selectedItemChanged) { if (!_selectedItem) { clearSelection(); return; } QList items = selectedItems(); InstrItem* ii = (items.count() > 0) ? (InstrItem*)items[0] : 0; if (ii) { if ((ii->instr() == _selectedItem) || (ii->instr() && (ii->instr()->line() == _selectedItem))) return; if (ii->instrCall() && (ii->instrCall()->call()->called() == _selectedItem)) return; } TraceInstrJump* ij = 0; if (_selectedItem->type() == ProfileContext::InstrJump) ij = (TraceInstrJump*) _selectedItem; QTreeWidgetItem *item, *item2; for (int i=0; iinstr() == _selectedItem) || (ii->instr() && (ii->instr()->line() == _selectedItem)) || (ij && (ij->instrTo() == ii->instr())) ) { scrollToItem(item); _inSelectionUpdate = true; setCurrentItem(item); _inSelectionUpdate = false; break; } item2 = 0; for (int j=0; ichildCount(); j++) { item2 = item->child(j); ii = (InstrItem*)item2; if (!ii->instrCall()) continue; if (ii->instrCall()->call()->called() == _selectedItem) { scrollToItem(item2); _inSelectionUpdate = true; setCurrentItem(item2); _inSelectionUpdate = false; break; } } if (item2) break; } return; } if (changeType == groupTypeChanged) { // update group colors for call lines QTreeWidgetItem *item, *item2; for (int i=0; ichildCount(); i++) { item2 = item->child(j); ((InstrItem*)item2)->updateGroup(); } } return; } // On eventTypeChanged, we can not just change the costs shown in // already existing items, as costs of 0 should make the line to not // be shown at all. So we do a full refresh. refresh(); } void InstrView::setColumnWidths() { #if QT_VERSION >= 0x050000 header()->setSectionResizeMode(4, QHeaderView::Interactive); #else header()->setResizeMode(4, QHeaderView::Interactive); #endif if (_showHexCode) { setColumnWidth(4, _lastHexCodeWidth); } else { setColumnWidth(4, 0); } } // compare functions for jump arrow drawing // helper for compare void getInstrJumpAddresses(const TraceInstrJump* ij, Addr& low, Addr& high) { low = ij->instrFrom()->addr(); high = ij->instrTo()->addr(); if (low > high) { Addr t = low; low = high; high = t; } } // sort jumps according to lower instruction address bool instrJumpLowLessThan(const TraceInstrJump* ij1, const TraceInstrJump* ij2) { Addr addr1Low, addr1High, addr2Low, addr2High; getInstrJumpAddresses(ij1, addr1Low, addr1High); getInstrJumpAddresses(ij2, addr2Low, addr2High); if (addr1Low != addr2Low) return (addr1Low < addr2Low); // jump ends come before jump starts - if (addr1Low == ij1->instrTo()->addr()) return true; - if (addr2Low == ij2->instrTo()->addr()) return false; + bool low1IsEnd = (addr1Low == ij1->instrTo()->addr()); + bool low2IsEnd = (addr2Low == ij2->instrTo()->addr()); + if (low1IsEnd && !low2IsEnd) return true; + if (low2IsEnd && !low1IsEnd) return false; + // both the low address of jump 1 and 2 are end or start return (addr1High < addr2High); } // sort jumps according to higher instruction address bool instrJumpHighLessThan(const TraceInstrJump* ij1, const TraceInstrJump* ij2) { Addr addr1Low, addr1High, addr2Low, addr2High; getInstrJumpAddresses(ij1, addr1Low, addr1High); getInstrJumpAddresses(ij2, addr2Low, addr2High); if (addr1High != addr2High) return (addr1High < addr2High); // jump ends come before jump starts - if (addr1High == ij1->instrTo()->addr()) return true; - if (addr2High == ij2->instrTo()->addr()) return false; + bool high1IsEnd = (addr1High == ij1->instrTo()->addr()); + bool high2IsEnd = (addr2High == ij2->instrTo()->addr()); + if (high1IsEnd && !high2IsEnd) return true; + if (high2IsEnd && !high1IsEnd) return false; + // both the high address of jump 1 and 2 are end or start return (addr1Low < addr2Low); } void InstrView::refresh() { int originalPosition = verticalScrollBar()->value(); clear(); setColumnWidth(0, 20); setColumnWidth(1, 50); setColumnWidth(2, _eventType2 ? 50:0); setColumnWidth(3, 0); // arrows, defaults to invisible setColumnWidth(4, 0); // hex code column setColumnWidth(5, 50); // command column setColumnWidth(6, 250); // arg column // reset to automatic sizing to get column width #if QT_VERSION >= 0x050000 header()->setSectionResizeMode(4, QHeaderView::ResizeToContents); #else header()->setResizeMode(4, QHeaderView::ResizeToContents); #endif if (_eventType) headerItem()->setText(1, _eventType->name()); if (_eventType2) headerItem()->setText(2, _eventType2->name()); _arrowLevels = 0; if (!_data || !_activeItem) return; ProfileContext::Type t = _activeItem->type(); TraceFunction* f = 0; if (t == ProfileContext::Function) f = (TraceFunction*) _activeItem; if (t == ProfileContext::Instr) { f = ((TraceInstr*)_activeItem)->function(); if (!_selectedItem) _selectedItem = _activeItem; } if (t == ProfileContext::Line) { f = ((TraceLine*)_activeItem)->functionSource()->function(); if (!_selectedItem) _selectedItem = _activeItem; } if (!f) return; // check for instruction map TraceInstrMap::Iterator itStart, it, tmpIt, itEnd; TraceInstrMap* instrMap = f->instrMap(); if (instrMap) { it = instrMap->begin(); itEnd = instrMap->end(); // get first instruction with cost of selected type while(it != itEnd) { if ((*it).hasCost(_eventType)) break; if (_eventType2 && (*it).hasCost(_eventType2)) break; ++it; } } if (!instrMap || (it == itEnd)) { new InstrItem(this, this, 1, tr("There is no instruction info in the profile data file.")); new InstrItem(this, this, 2, tr("Tip: For Callgrind, rerun with option")); new InstrItem(this, this, 3, tr(" --dump-instr=yes")); new InstrItem(this, this, 4, tr("To see (conditional) jumps, additionally specify")); new InstrItem(this, this, 5, tr(" --collect-jumps=yes")); setColumnWidths(); return; } // initialisation for arrow drawing // create sorted list of jumps (for jump arrows) _lowList.clear(); _highList.clear(); itStart = it; while(1) { TraceInstrJumpList jlist = (*it).instrJumps(); foreach(TraceInstrJump* ij, jlist) { if (ij->executedCount()==0) continue; _lowList.append(ij); _highList.append(ij); } ++it; while(it != itEnd) { if ((*it).hasCost(_eventType)) break; if (_eventType2 && (*it).hasCost(_eventType2)) break; ++it; } if (it == itEnd) break; } std::sort(_lowList.begin(), _lowList.end(), instrJumpLowLessThan); std::sort(_highList.begin(), _highList.end(), instrJumpHighLessThan); _lowListIter = _lowList.begin(); // iterators to list start _highListIter = _highList.begin(); _arrowLevels = 0; _jump.resize(0); // do multiple calls to 'objdump' if there are large gaps in addresses it = itStart; while(1) { itStart = it; while(1) { tmpIt = it; ++it; while(it != itEnd) { if ((*it).hasCost(_eventType)) break; if (_eventType2 && (*it).hasCost(_eventType2)) break; ++it; } if (it == itEnd) break; if (!(*it).addr().isInRange( (*tmpIt).addr(),10000) ) break; } // tmpIt is always last instruction with cost if (!fillInstrRange(f, itStart, ++tmpIt)) break; if (it == itEnd) break; } _lastHexCodeWidth = columnWidth(4); setColumnWidths(); if (!_eventType2) { #if QT_VERSION >= 0x050000 header()->setSectionResizeMode(2, QHeaderView::Interactive); #else header()->setResizeMode(2, QHeaderView::Interactive); #endif setColumnWidth(2, 0); } // reset to the original position - this is useful when the view // is refreshed just because we change between relative/absolute verticalScrollBar()->setValue(originalPosition); } /* This is called after adding instrItems, for each of them in * address order. _jump is the global array of valid jumps * for a line while we iterate downwards. * The existing jumps, sorted in lowList according lower address, * is iterated in the same way. */ void InstrView::updateJumpArray(Addr addr, InstrItem* ii, bool ignoreFrom, bool ignoreTo) { Addr lowAddr, highAddr; int iEnd = -1, iStart = -1; if (0) qDebug("updateJumpArray(addr 0x%s, jump to %s)", qPrintable(addr.toString()), ii->instrJump() ? qPrintable(ii->instrJump()->instrTo()->name()) : "?" ); // check for new arrows starting from here downwards while(_lowListIter != _lowList.end()) { TraceInstrJump* ij= *_lowListIter; lowAddr = ij->instrFrom()->addr(); if (ij->instrTo()->addr() < lowAddr) lowAddr = ij->instrTo()->addr(); if (lowAddr > addr) break; // if target is downwards but we draw no source, break if (ignoreFrom && (lowAddr < ij->instrTo()->addr())) break; // if source is downward but we draw no target, break if (ignoreTo && (lowAddr < ij->instrFrom()->addr())) break; // if this is another jump start, break if (ii->instrJump() && (ij != ii->instrJump())) break; #if 0 for(iStart=0;iStart<_arrowLevels;iStart++) if (_jump[iStart] && (_jump[iStart]->instrTo() == ij->instrTo())) break; #else iStart = _arrowLevels; #endif if (iStart==_arrowLevels) { for(iStart=0; iStart<_arrowLevels; ++iStart) if (_jump[iStart] == 0) break; if (iStart==_arrowLevels) { _arrowLevels++; _jump.resize(_arrowLevels); } if (0) qDebug(" new start at %d for %s", iStart, qPrintable(ij->name())); _jump[iStart] = ij; } _lowListIter++; } ii->setJumpArray(_jump); // check for active arrows ending here while(_highListIter != _highList.end()) { TraceInstrJump* ij= *_highListIter; highAddr = ij->instrFrom()->addr(); if (ij->instrTo()->addr() > highAddr) { highAddr = ij->instrTo()->addr(); if (ignoreTo) break; } else if (ignoreFrom) break; if (highAddr > addr) break; for(iEnd=0; iEnd<_arrowLevels; ++iEnd) if (_jump[iEnd] == ij) break; if (iEnd==_arrowLevels) { qDebug() << "InstrView: no jump start for end at 0x" << highAddr.toString() << " ?"; iEnd = -1; } if (0 && (iEnd>=0)) qDebug(" end %d (%s to %s)", iEnd, qPrintable(_jump[iEnd]->instrFrom()->name()), qPrintable(_jump[iEnd]->instrTo()->name())); if (0 && ij) qDebug("next end: %s to %s", qPrintable(ij->instrFrom()->name()), qPrintable(ij->instrTo()->name())); _highListIter++; if (highAddr > addr) break; else { if (iEnd>=0) _jump[iEnd] = 0; iEnd = -1; } } if (iEnd>=0) _jump[iEnd] = 0; } bool InstrView::searchFile(QString& dir, TraceObject* o) { QString filename = o->shortName(); if (QDir::isAbsolutePath(dir)) { if (QFile::exists(dir + '/' + filename)) return true; QString sysRoot = getSysRoot(); if (!sysRoot.isEmpty()) { if (!dir.startsWith('/') && !sysRoot.endsWith('/')) sysRoot += '/'; dir = sysRoot + dir; return QFile::exists(dir + '/' + filename); } return false; } QFileInfo fi(dir, filename); if (fi.exists()) { dir = fi.absolutePath(); return true; } TracePart* firstPart = _data->parts().first(); if (firstPart) { QFileInfo partFile(firstPart->name()); if (QFileInfo(partFile.absolutePath(), filename).exists()) { dir = partFile.absolutePath(); return true; } } return false; } /** * Fill up with instructions from cost range [it;itEnd[ */ bool InstrView::fillInstrRange(TraceFunction* function, TraceInstrMap::Iterator it, TraceInstrMap::Iterator itEnd) { Addr costAddr, nextCostAddr, objAddr, addr; Addr dumpStartAddr, dumpEndAddr; TraceInstrMap::Iterator costIt; bool isArm = (function->data()->architecture() == TraceData::ArchARM); // should not happen if (it == itEnd) return false; // calculate address range for call to objdump TraceInstrMap::Iterator tmpIt = itEnd; --tmpIt; nextCostAddr = (*it).addr(); if (isArm) { // for Arm: address always even (even for Thumb encoding) nextCostAddr = nextCostAddr.alignedDown(2); } dumpStartAddr = (nextCostAddr<20) ? Addr(0) : nextCostAddr -20; dumpEndAddr = (*tmpIt).addr() +20; QString dir = function->object()->directory(); if (!searchFile(dir, function->object())) { new InstrItem(this, this, 1, tr("For annotated machine code, " "the following object file is needed:")); new InstrItem(this, this, 2, QStringLiteral(" '%1'").arg(function->object()->name())); new InstrItem(this, this, 3, tr("This file can not be found.")); if (isArm) new InstrItem(this, this, 4, tr("If cross-compiled, set SYSROOT variable.")); return false; } function->object()->setDirectory(dir); // call objdump synchronously QString objfile = dir + '/' + function->object()->shortName(); QStringList objdumpArgs = QStringList() << QStringLiteral("-C") << QStringLiteral("-d") << QStringLiteral("--start-address=0x%1").arg(dumpStartAddr.toString()) << QStringLiteral("--stop-address=0x%1").arg(dumpEndAddr.toString()) << objfile; QString objdumpCmd = getObjDump() + " " + objdumpArgs.join(QStringLiteral(" ")); qDebug("Running '%s'...", qPrintable(objdumpCmd)); // and run... QProcess objdump; objdump.start(getObjDump(), objdumpArgs); if (!objdump.waitForStarted() || !objdump.waitForFinished()) { new InstrItem(this, this, 1, tr("There is an error trying to execute the command")); new InstrItem(this, this, 2, QStringLiteral(" '%1'").arg(objdumpCmd)); new InstrItem(this, this, 3, tr("Check that you have installed 'objdump'.")); new InstrItem(this, this, 4, tr("This utility can be found in the 'binutils' package.")); return false; } #define BUF_SIZE 256 char buf[BUF_SIZE]; bool inside = false, skipLineWritten = true; int readBytes = -1; int objdumpLineno = 0, dumpedLines = 0, noAssLines = 0; SubCost most = 0; TraceInstr* currInstr; InstrItem *ii, *ii2, *item = 0, *first = 0, *selected = 0; QString code, cmd, args; bool needObjAddr = true, needCostAddr = true; costAddr = 0; objAddr = 0; QList items; while (1) { if (needObjAddr) { needObjAddr = false; // read next objdump line while (1) { readBytes=objdump.readLine(buf, BUF_SIZE); if (readBytes<=0) { objAddr = 0; break; } objdumpLineno++; if (readBytes == BUF_SIZE) { qDebug("ERROR: Line %d of '%s' too long\n", objdumpLineno, qPrintable(objdumpCmd)); } else if ((readBytes>0) && (buf[readBytes-1] == '\n')) buf[readBytes-1] = 0; objAddr = parseAddr(buf); if ((objAddrdumpEndAddr)) objAddr = 0; if (objAddr != 0) break; } if (0) qDebug() << "Got ObjAddr: 0x" << objAddr.toString(); } // try to keep objAddr in [costAddr;nextCostAddr] if (needCostAddr && (nextCostAddr > 0) && ((objAddr == Addr(0)) || (objAddr >= nextCostAddr)) ) { needCostAddr = false; costIt = it; ++it; while(it != itEnd) { if ((*it).hasCost(_eventType)) break; if (_eventType2 && (*it).hasCost(_eventType2)) break; ++it; } costAddr = nextCostAddr; nextCostAddr = (it == itEnd) ? Addr(0) : (*it).addr(); if (isArm) nextCostAddr = nextCostAddr.alignedDown(2); if (0) qDebug() << "Got nextCostAddr: 0x" << nextCostAddr.toString() << ", costAddr 0x" << costAddr.toString(); } // if we have no more address from objdump, stop if (objAddr == 0) break; if ((nextCostAddr==0) || (costAddr == 0) || (objAddr < nextCostAddr)) { // next line is objAddr // this sets addr, code, cmd, args bool isAssemblyInstr = parseLine(buf, addr, code, cmd, args); Q_UNUSED(isAssemblyInstr); assert(isAssemblyInstr && (objAddr == addr)); if (costAddr == objAddr) { currInstr = &(*costIt); needCostAddr = true; } else currInstr = 0; needObjAddr = true; if (0) qDebug() << "Dump Obj Addr: 0x" << addr.toString() << " [" << cmd << " " << args << "], cost (0x" << costAddr.toString() << ", next 0x" << nextCostAddr.toString() << ")"; } else { addr = costAddr; code = cmd = QString(); args = tr("(No Instruction)"); currInstr = &(*costIt); needCostAddr = true; noAssLines++; if (0) qDebug() << "Dump Cost Addr: 0x" << addr.toString() << " (no ass), objAddr 0x" << objAddr.toString(); } // update inside if (!inside) { if (currInstr) inside = true; } else { if (0) qDebug() << "Check if 0x" << addr.toString() << " is in ]0x" << costAddr.toString() << ",0x" << (nextCostAddr - 3*GlobalConfig::noCostInside()).toString() << "[" << endl; // Suppose a average instruction len of 3 bytes if ( (addr > costAddr) && ((nextCostAddr==0) || (addr < nextCostAddr - 3*GlobalConfig::noCostInside()) )) inside = false; } int context = GlobalConfig::context(); if ( ((costAddr==0) || (addr > costAddr + 3*context)) && ((nextCostAddr==0) || (addr < nextCostAddr - 3*context)) ) { // the very last skipLine can be ommitted if ((it == itEnd) && (itEnd == function->instrMap()->end())) skipLineWritten=true; if (!skipLineWritten) { skipLineWritten = true; // a "skipping" line: print "..." instead of a line number code = cmd = QString(); args = QStringLiteral("..."); } else continue; } else skipLineWritten = false; ii = new InstrItem(this, 0, addr, inside, code, cmd, args, currInstr); items.append(ii); dumpedLines++; if (0) qDebug() << "Dumped 0x" << addr.toString() << " " << (inside ? "Inside " : "Outside") << (currInstr ? "Cost" : ""); // no calls/jumps if we have no cost for this line if (!currInstr) continue; if (!selected && ((currInstr == _selectedItem) || (currInstr->line() == _selectedItem))) selected = ii; if (!first) first = ii; if (currInstr->subCost(_eventType) > most) { item = ii; most = currInstr->subCost(_eventType); } ii->setExpanded(true); foreach(TraceInstrCall* ic, currInstr->instrCalls()) { if ((ic->subCost(_eventType)==0) && (ic->subCost(_eventType2)==0)) continue; if (ic->subCost(_eventType) > most) { item = ii; most = ic->subCost(_eventType); } ii2 = new InstrItem(this, ii, addr, currInstr, ic); if (!selected && (ic->call()->called() == _selectedItem)) selected = ii2; } foreach(TraceInstrJump* ij, currInstr->instrJumps()) { if (ij->executedCount()==0) continue; new InstrItem(this, ii, addr, currInstr, ij); } } // Resize columns with address/counts/opcode to contents #if QT_VERSION >= 0x050000 header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); header()->setSectionResizeMode(2, QHeaderView::ResizeToContents); header()->setSectionResizeMode(5, QHeaderView::ResizeToContents); #else header()->setResizeMode(0, QHeaderView::ResizeToContents); header()->setResizeMode(1, QHeaderView::ResizeToContents); header()->setResizeMode(2, QHeaderView::ResizeToContents); header()->setResizeMode(5, QHeaderView::ResizeToContents); #endif setSortingEnabled(false); addTopLevelItems(items); expandAll(); setSortingEnabled(true); // always reset to line number sort sortByColumn(0, Qt::AscendingOrder); header()->setSortIndicatorShown(false); // Reallow interactive column size change after resizing to content #if QT_VERSION >= 0x050000 header()->setSectionResizeMode(0, QHeaderView::Interactive); header()->setSectionResizeMode(1, QHeaderView::Interactive); header()->setSectionResizeMode(2, QHeaderView::Interactive); header()->setSectionResizeMode(5, QHeaderView::Interactive); #else header()->setResizeMode(0, QHeaderView::Interactive); header()->setResizeMode(1, QHeaderView::Interactive); header()->setResizeMode(2, QHeaderView::Interactive); header()->setResizeMode(5, QHeaderView::Interactive); #endif if (selected) item = selected; if (item) first = item; if (first) { scrollToItem(first); _inSelectionUpdate = true; setCurrentItem(first); _inSelectionUpdate = false; } // for arrows: go down the list according to list sorting QTreeWidgetItem *item1, *item2; for (int i=0; iaddr(), ii, true, false); for (int j=0; jchildCount(); j++) { item2 = item1->child(j); ii2 = (InstrItem*)item2; if (ii2->instrJump()) updateJumpArray(ii->addr(), ii2, false, true); else ii2->setJumpArray(_jump); } } if (arrowLevels()) setColumnWidth(3, 10 + 6*arrowLevels() + 2); else setColumnWidth(3, 0); if (noAssLines > 1) { // trace cost not matching code //~ singular There is %n cost line without machine code. //~ plural There are %n cost lines without machine code. new InstrItem(this, this, 1, tr("There are %n cost line(s) without machine code.", "", noAssLines)); new InstrItem(this, this, 2, tr("This happens because the code of")); new InstrItem(this, this, 3, QStringLiteral(" %1").arg(objfile)); new InstrItem(this, this, 4, tr("does not seem to match the profile data file.")); new InstrItem(this, this, 5, QString()); new InstrItem(this, this, 6, tr("Are you using an old profile data file or is the above mentioned")); new InstrItem(this, this, 7, tr("ELF object from an updated installation/another machine?")); new InstrItem(this, this, 8, QString()); return false; } if (dumpedLines == 0) { // no matching line read from popen new InstrItem(this, this, 1, tr("There seems to be an error trying to execute the command")); new InstrItem(this, this, 2, QStringLiteral(" '%1'").arg(objdumpCmd)); new InstrItem(this, this, 3, tr("Check that the ELF object used in the command exists.")); new InstrItem(this, this, 4, tr("Check that you have installed 'objdump'.")); new InstrItem(this, this, 5, tr("This utility can be found in the 'binutils' package.")); return false; } return true; } void InstrView::headerClicked(int col) { if (col == 0) { sortByColumn(col, Qt::AscendingOrder); } //All others but Source Text column Descending else if (col <4) { sortByColumn(col, Qt::DescendingOrder); } } void InstrView::restoreOptions(const QString& prefix, const QString& postfix) { ConfigGroup* g = ConfigStorage::group(prefix, postfix); _showHexCode = g->value(QStringLiteral("ShowHexCode"), DEFAULT_SHOWHEXCODE).toBool(); delete g; } void InstrView::saveOptions(const QString& prefix, const QString& postfix) { ConfigGroup* g = ConfigStorage::group(prefix + postfix); g->setValue(QStringLiteral("ShowHexCode"), _showHexCode, DEFAULT_SHOWHEXCODE); delete g; } diff --git a/libviews/sourceview.cpp b/libviews/sourceview.cpp index 3fdd06e..c175114 100644 --- a/libviews/sourceview.cpp +++ b/libviews/sourceview.cpp @@ -1,944 +1,950 @@ /* This file is part of KCachegrind. Copyright (c) 2011-2016 Josef Weidendorfer KCachegrind is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /* * Source View */ #include "sourceview.h" #include #include #include #include #include #include #include #include #include #include "globalconfig.h" #include "sourceitem.h" // // SourceView // SourceView::SourceView(TraceItemView* parentView, QWidget* parent) : QTreeWidget(parent), TraceItemView(parentView) { _inSelectionUpdate = false; _arrowLevels = 0; setColumnCount(5); setRootIsDecorated(false); setAllColumnsShowFocus(true); setUniformRowHeights(true); // collapsing call/jump lines by double-click is confusing setExpandsOnDoubleClick(false); QStringList headerLabels; headerLabels << tr( "#" ) << tr( "Cost" ) << tr( "Cost 2" ) << QString() << tr( "Source"); setHeaderLabels(headerLabels); // sorting will be enabled after refresh() sortByColumn(0, Qt::AscendingOrder); header()->setSortIndicatorShown(false); this->setItemDelegate(new SourceItemDelegate(this)); this->setWhatsThis( whatsThis()); connect( this, &QTreeWidget::currentItemChanged, this, &SourceView::selectedSlot); setContextMenuPolicy(Qt::CustomContextMenu); connect( this, &QWidget::customContextMenuRequested, this, &SourceView::context); connect(this, &QTreeWidget::itemDoubleClicked, this, &SourceView::activatedSlot); connect(header(), &QHeaderView::sectionClicked, this, &SourceView::headerClicked); } QString SourceView::whatsThis() const { return tr( "Annotated Source" "

The annotated source list shows the " "source lines of the current selected function " "together with (self) cost spent while executing the " "code of this source line. If there was a call " "in a source line, lines with details on the " "call happening are inserted into the source: " "the cost spent inside of the call, the " "number of calls happening, and the call destination.

" "

Select a inserted call information line to " "make the destination function current.

"); } void SourceView::context(const QPoint & p) { int c = columnAt(p.x()); QTreeWidgetItem* i = itemAt(p); QMenu popup; TraceLineCall* lc = i ? ((SourceItem*) i)->lineCall() : 0; TraceLineJump* lj = i ? ((SourceItem*) i)->lineJump() : 0; TraceFunction* f = lc ? lc->call()->called() : 0; TraceLine* line = lj ? lj->lineTo() : 0; QAction* activateFunctionAction = 0; QAction* activateLineAction = 0; if (f) { QString menuText = tr("Go to '%1'").arg(GlobalConfig::shortenSymbol(f->prettyName())); activateFunctionAction = popup.addAction(menuText); popup.addSeparator(); } else if (line) { QString menuText = tr("Go to Line %1").arg(line->name()); activateLineAction = popup.addAction(menuText); popup.addSeparator(); } if ((c == 1) || (c == 2)) { addEventTypeMenu(&popup); popup.addSeparator(); } addGoMenu(&popup); QAction* a = popup.exec(mapToGlobal(p + QPoint(0,header()->height()))); if (a == activateFunctionAction) TraceItemView::activated(f); else if (a == activateLineAction) TraceItemView::activated(line); } void SourceView::selectedSlot(QTreeWidgetItem *i, QTreeWidgetItem *) { if (!i) return; // programatically selected items are not signalled if (_inSelectionUpdate) return; TraceLineCall* lc = ((SourceItem*) i)->lineCall(); TraceLineJump* lj = ((SourceItem*) i)->lineJump(); if (!lc && !lj) { TraceLine* l = ((SourceItem*) i)->line(); if (l) { _selectedItem = l; selected(l); } return; } TraceFunction* f = lc ? lc->call()->called() : 0; if (f) { _selectedItem = f; selected(f); } else { TraceLine* line = lj ? lj->lineTo() : 0; if (line) { _selectedItem = line; selected(line); } } } void SourceView::activatedSlot(QTreeWidgetItem* i, int) { if (!i) return; TraceLineCall* lc = ((SourceItem*) i)->lineCall(); TraceLineJump* lj = ((SourceItem*) i)->lineJump(); if (!lc && !lj) { TraceLine* l = ((SourceItem*) i)->line(); if (l) TraceItemView::activated(l); return; } TraceFunction* f = lc ? lc->call()->called() : 0; if (f) TraceItemView::activated(f); else { TraceLine* line = lj ? lj->lineTo() : 0; if (line) TraceItemView::activated(line); } } void SourceView::keyPressEvent(QKeyEvent* event) { QTreeWidgetItem *item = currentItem(); if (item && ((event->key() == Qt::Key_Return) || (event->key() == Qt::Key_Space))) { activatedSlot(item, 0); } QTreeView::keyPressEvent(event); } CostItem* SourceView::canShow(CostItem* i) { ProfileContext::Type t = i ? i->type() : ProfileContext::InvalidType; switch(t) { case ProfileContext::Function: case ProfileContext::Instr: case ProfileContext::Line: return i; default: break; } return 0; } void SourceView::doUpdate(int changeType, bool) { // Special case ? if (changeType == selectedItemChanged) { if (!_selectedItem) { clearSelection(); return; } TraceLine* sLine = 0; if (_selectedItem->type() == ProfileContext::Line) sLine = (TraceLine*) _selectedItem; if (_selectedItem->type() == ProfileContext::Instr) sLine = ((TraceInstr*)_selectedItem)->line(); if ((_selectedItem->type() != ProfileContext::Function) && (sLine == 0)) return; QList items = selectedItems(); SourceItem* si = (items.count() > 0) ? (SourceItem*)items[0] : 0; if (si) { if (sLine && (si->line() == sLine)) return; if (si->lineCall() && (si->lineCall()->call()->called() == _selectedItem)) return; } QTreeWidgetItem *item, *item2; for (int i=0; iline() == sLine)) { scrollToItem(item); _inSelectionUpdate = true; setCurrentItem(item); _inSelectionUpdate = false; break; } bool foundCall = false; for (int j=0; jchildCount(); j++) { item2 = item->child(j); si = (SourceItem*)item2; if (!si->lineCall()) continue; if (si->lineCall()->call()->called() == _selectedItem) { scrollToItem(item2); _inSelectionUpdate = true; setCurrentItem(item2); _inSelectionUpdate = false; foundCall = true; break; } } if (foundCall) break; } return; } if (changeType == groupTypeChanged) { // update group colors for call lines QTreeWidgetItem *item, *item2; for (int i=0; ichildCount(); i++) { item2 = item->child(j); ((SourceItem*)item2)->updateGroup(); } } return; } // On eventTypeChanged, we can not just change the costs shown in // already existing items, as costs of 0 should make the line to not // be shown at all. So we do a full refresh. refresh(); } void SourceView::refresh() { int originalPosition = verticalScrollBar()->value(); clear(); setColumnWidth(0, 20); setColumnWidth(1, 50); setColumnWidth(2, _eventType2 ? 50:0); setColumnWidth(3, 0); // arrows, defaults to invisible if (_eventType) headerItem()->setText(1, _eventType->name()); if (_eventType2) headerItem()->setText(2, _eventType2->name()); _arrowLevels = 0; if (!_data || !_activeItem) { return; } ProfileContext::Type t = _activeItem->type(); TraceFunction* f = 0; if (t == ProfileContext::Function) f = (TraceFunction*) _activeItem; if (t == ProfileContext::Instr) { f = ((TraceInstr*)_activeItem)->function(); if (!_selectedItem) _selectedItem = ((TraceInstr*)_activeItem)->line(); } if (t == ProfileContext::Line) { f = ((TraceLine*)_activeItem)->functionSource()->function(); if (!_selectedItem) _selectedItem = _activeItem; } if (!f) return; TraceFunctionSource* mainSF = f->sourceFile(); // skip first source if there is no debug info and there are more sources // (this is for a bug in GCC 2.95.x giving unknown source for prologs) if (mainSF && (mainSF->firstLineno() == 0) && (mainSF->lastLineno() == 0) && (f->sourceFiles().count()>1) ) { // skip } else fillSourceFile(mainSF, 0); int fileno = 0; foreach(TraceFunctionSource* sf, f->sourceFiles()) { fileno++; if (sf != mainSF) fillSourceFile(sf, fileno); } if (!_eventType2) { #if QT_VERSION >= 0x050000 header()->setSectionResizeMode(2, QHeaderView::Interactive); #else header()->setResizeMode(2, QHeaderView::Interactive); #endif setColumnWidth(2, 0); } // reset to the original position - this is useful when the view // is refreshed just because we change between relative/absolute // FIXME: this overrides scrolling to selected item verticalScrollBar()->setValue(originalPosition); } /* Helper for fillSourceList: * search recursive for a file, starting from a base dir * If found, returns true and is set to the file path. */ static bool searchFileRecursive(QString& dir, const QString& name) { // we leave this in... qDebug("Checking %s/%s", qPrintable(dir), qPrintable(name)); if (QFile::exists(dir + '/' + name)) return true; // check in subdirectories QDir d(dir); d.setFilter( QDir::Dirs | QDir::NoSymLinks ); d.setSorting( QDir::Unsorted ); QStringList subdirs = d.entryList(); QStringList::const_iterator it =subdirs.constBegin(); for(; it != subdirs.constEnd(); ++it ) { if (*it == QLatin1String(".") || *it == QLatin1String("..") || *it == QLatin1String("CVS")) continue; dir = d.filePath(*it); if (searchFileRecursive(dir, name)) return true; } return false; } /* Search for a source file in different places. * If found, returns true and is set to the file path. */ bool SourceView::searchFile(QString& dir, TraceFunctionSource* sf) { QString name = sf->file()->shortName(); if (QDir::isAbsolutePath(dir)) { if (QFile::exists(dir + '/' + name)) return true; } else { /* Directory is relative. Check * - relative to cwd * - relative to path of data file */ QString base = QDir::currentPath() + '/' + dir; if (QFile::exists(base + '/' + name)) { dir = base; return true; } TracePart* firstPart = _data->parts().first(); if (firstPart) { QFileInfo partFile(firstPart->name()); if (QFileInfo(partFile.absolutePath(), name).exists()) { dir = partFile.absolutePath(); return true; } } } QStringList list = GlobalConfig::sourceDirs(_data, sf->function()->object()); QStringList::const_iterator it; for ( it = list.constBegin(); it != list.constEnd(); ++it ) { dir = *it; if (searchFileRecursive(dir, name)) return true; } return false; } void SourceView::updateJumpArray(uint lineno, SourceItem* si, bool ignoreFrom, bool ignoreTo) { uint lowLineno, highLineno; int iEnd = -1, iStart = -1; if (0) qDebug("updateJumpArray(line %d, jump to %s)", lineno, si->lineJump() ? qPrintable(si->lineJump()->lineTo()->name()) : "?" ); while(_lowListIter != _lowList.end()) { TraceLineJump* lj= *_lowListIter; lowLineno = lj->lineFrom()->lineno(); if (lj->lineTo()->lineno() < lowLineno) lowLineno = lj->lineTo()->lineno(); if (lowLineno > lineno) break; if (ignoreFrom && (lowLineno < lj->lineTo()->lineno())) break; if (ignoreTo && (lowLineno < lj->lineFrom()->lineno())) break; if (si->lineJump() && (lj != si->lineJump())) break; int asize = (int)_jump.size(); #if 0 for(iStart=0;iStartlineTo() == lj->lineTo())) break; #else iStart = asize; #endif if (iStart == asize) { for(iStart=0; iStart _arrowLevels) _arrowLevels = asize; } if (0) qDebug(" start %d (%s to %s)", iStart, qPrintable(lj->lineFrom()->name()), qPrintable(lj->lineTo()->name())); _jump[iStart] = lj; } _lowListIter++; } si->setJumpArray(_jump); while(_highListIter != _highList.end()) { TraceLineJump* lj= *_highListIter; highLineno = lj->lineFrom()->lineno(); if (lj->lineTo()->lineno() > highLineno) { highLineno = lj->lineTo()->lineno(); if (ignoreTo) break; } else if (ignoreFrom) break; if (highLineno > lineno) break; for(iEnd=0; iEnd< (int)_jump.size(); ++iEnd) if (_jump[iEnd] == lj) break; if (iEnd == (int)_jump.size()) { qDebug("LineView: no jump start for end at %x ?", highLineno); iEnd = -1; } if (0 && (iEnd>=0)) qDebug(" end %d (%s to %s)", iEnd, qPrintable(_jump[iEnd]->lineFrom()->name()), qPrintable(_jump[iEnd]->lineTo()->name())); if (0 && lj) qDebug("next end: %s to %s", qPrintable(lj->lineFrom()->name()), qPrintable(lj->lineTo()->name())); _highListIter++; if (highLineno > lineno) break; else { if (iEnd>=0) _jump[iEnd] = 0; iEnd = -1; } } if (iEnd>=0) _jump[iEnd] = 0; } // compare functions for jump arrow drawing void getJumpLines(const TraceLineJump* jump, uint& low, uint& high) { low = jump->lineFrom()->lineno(); high = jump->lineTo()->lineno(); if (low > high) { uint t = low; low = high; high = t; } } // sort jumps according to lower line number bool lineJumpLowLessThan(const TraceLineJump* jump1, const TraceLineJump* jump2) { uint line1Low, line1High, line2Low, line2High; getJumpLines(jump1, line1Low, line1High); getJumpLines(jump2, line2Low, line2High); if (line1Low != line2Low) return (line1Low < line2Low); // jump ends come before jump starts - if (line1Low == jump1->lineTo()->lineno()) return true; - if (line2Low == jump2->lineTo()->lineno()) return false; + bool low1IsEnd = (line1Low == jump1->lineTo()->lineno()); + bool low2IsEnd = (line2Low == jump2->lineTo()->lineno()); + if (low1IsEnd && !low2IsEnd) return true; + if (low2IsEnd && !low1IsEnd) return false; + // both the low line of jump 1 and 2 are end or start return (line1High < line2High); } // sort jumps according to higher line number bool lineJumpHighLessThan(const TraceLineJump* jump1, const TraceLineJump* jump2) { uint line1Low, line1High, line2Low, line2High; getJumpLines(jump1, line1Low, line1High); getJumpLines(jump2, line2Low, line2High); if (line1High != line2High) return (line1High < line2High); // jump ends come before jump starts - if (line1High == jump1->lineTo()->lineno()) return true; - if (line2High == jump2->lineTo()->lineno()) return false; + bool high1IsEnd = (line1High == jump1->lineTo()->lineno()); + bool high2IsEnd = (line2High == jump2->lineTo()->lineno()); + if (high1IsEnd && !high2IsEnd) return true; + if (high2IsEnd && !high1IsEnd) return false; + // both the high line of jump 1 and 2 are end or start return (line1Low < line2Low); } /* If sourceList is empty we set the source file name into the header, * else this code is of a inlined function, and we add "inlined from..." */ void SourceView::fillSourceFile(TraceFunctionSource* sf, int fileno) { if (!sf) return; if (0) qDebug("Selected Item %s", _selectedItem ? qPrintable(_selectedItem->name()) : "(none)"); TraceLineMap::Iterator lineIt, lineItEnd; int nextCostLineno = 0, lastCostLineno = 0; bool validSourceFile = (!sf->file()->name().isEmpty()); TraceLine* sLine = 0; if (_selectedItem) { if (_selectedItem->type() == ProfileContext::Line) sLine = (TraceLine*) _selectedItem; if (_selectedItem->type() == ProfileContext::Instr) sLine = ((TraceInstr*)_selectedItem)->line(); } if (validSourceFile) { TraceLineMap* lineMap = sf->lineMap(); if (lineMap) { lineIt = lineMap->begin(); lineItEnd = lineMap->end(); // get first line with cost of selected type while(lineIt != lineItEnd) { if (&(*lineIt) == sLine) break; if ((*lineIt).hasCost(_eventType)) break; if (_eventType2 && (*lineIt).hasCost(_eventType2)) break; ++lineIt; } nextCostLineno = (lineIt == lineItEnd) ? 0 : (*lineIt).lineno(); if (nextCostLineno<0) { qDebug() << "SourceView::fillSourceFile: Negative line number " << nextCostLineno; qDebug() << " Function '" << sf->function()->name() << "'"; qDebug() << " File '" << sf->file()->name() << "'"; nextCostLineno = 0; } } if (nextCostLineno == 0) { new SourceItem(this, this, fileno, 1, false, tr("There is no cost of current selected type associated")); new SourceItem(this, this, fileno, 2, false, tr("with any source line of this function in file")); new SourceItem(this, this, fileno, 3, false, QStringLiteral(" '%1'").arg(sf->file()->prettyName())); new SourceItem(this, this, fileno, 4, false, tr("Thus, no annotated source can be shown.")); return; } } QString filename = sf->file()->shortName(); QString dir = sf->file()->directory(); if (!dir.isEmpty()) filename = dir + '/' + filename; if (nextCostLineno>0) { // we have debug info... search for source file if (searchFile(dir, sf)) { filename = dir + '/' + sf->file()->shortName(); // no need to search again sf->file()->setDirectory(dir); } else nextCostLineno = 0; } // do it here, because the source directory could have been set before if (topLevelItemCount()==0) { if (validSourceFile && (nextCostLineno != 0)) new SourceItem(this, this, fileno, 0, true, tr("--- From '%1' ---").arg(filename)); } else { new SourceItem(this, this, fileno, 0, true, validSourceFile ? tr("--- Inlined from '%1' ---").arg(filename) : tr("--- Inlined from unknown source ---")); } if (nextCostLineno == 0) { new SourceItem(this, this, fileno, 1, false, tr("There is no source available for the following function:")); new SourceItem(this, this, fileno, 2, false, QStringLiteral(" '%1'").arg(sf->function()->prettyName())); if (sf->file()->name().isEmpty()) { new SourceItem(this, this, fileno, 3, false, tr("This is because no debug information is present.")); new SourceItem(this, this, fileno, 4, false, tr("Recompile source and redo the profile run.")); if (sf->function()->object()) { new SourceItem(this, this, fileno, 5, false, tr("The function is located in this ELF object:")); new SourceItem(this, this, fileno, 6, false, QStringLiteral(" '%1'") .arg(sf->function()->object()->prettyName())); } } else { new SourceItem(this, this, fileno, 3, false, tr("This is because its source file cannot be found:")); new SourceItem(this, this, fileno, 4, false, QStringLiteral(" '%1'").arg(sf->file()->name())); new SourceItem(this, this, fileno, 5, false, tr("Add the folder of this file to the source folder list.")); new SourceItem(this, this, fileno, 6, false, tr("The list can be found in the configuration dialog.")); } return; } // initialisation for arrow drawing // create sorted list of jumps (for jump arrows) TraceLineMap::Iterator it = lineIt, nextIt; _lowList.clear(); _highList.clear(); while(1) { nextIt = it; ++nextIt; while(nextIt != lineItEnd) { if (&(*nextIt) == sLine) break; if ((*nextIt).hasCost(_eventType)) break; if (_eventType2 && (*nextIt).hasCost(_eventType2)) break; ++nextIt; } TraceLineJumpList jlist = (*it).lineJumps(); foreach(TraceLineJump* lj, jlist) { if (lj->executedCount()==0) continue; // skip jumps to next source line with cost //if (lj->lineTo() == &(*nextIt)) continue; _lowList.append(lj); _highList.append(lj); } it = nextIt; if (it == lineItEnd) break; } std::sort(_lowList.begin(), _lowList.end(), lineJumpLowLessThan); std::sort(_highList.begin(), _highList.end(), lineJumpHighLessThan); _lowListIter = _lowList.begin(); // iterators to list start _highListIter = _highList.begin(); _jump.resize(0); char buf[160]; bool inside = false, skipLineWritten = true; int readBytes; int fileLineno = 0; SubCost most = 0; QList items; TraceLine* currLine; SourceItem *si, *si2, *item = 0, *first = 0, *selected = 0; QFile file(filename); bool fileEndReached = false; if (!file.open(QIODevice::ReadOnly)) return; while (1) { readBytes=file.readLine(buf, sizeof( buf )); if (readBytes<=0) { // for nice empty 4 lines after function with EOF buf[0] = 0; if (readBytes<0) fileEndReached = true; } if ((readBytes >0) && (buf[readBytes-1] != '\n')) { /* Something was read but not ending in newline. I.e. * - buffer was not big enough: discard rest of line, add "..." * - this is last line of file, not ending in newline * NB: checking for '\n' is enough for all systems. */ int r; char buf2[32]; bool somethingRead = false; while(1) { r = file.readLine(buf2, sizeof(buf2)); if ((r<=0) || (buf2[r-1] == '\n')) break; somethingRead = true; } if (somethingRead) { // add dots as sign that we truncated the line Q_ASSERT(readBytes>3); buf[readBytes-1] = buf[readBytes-2] = buf[readBytes-3] = '.'; } } else if ((readBytes>0) && (buf[readBytes-1] == '\n')) buf[readBytes-1] = 0; // keep fileLineno inside [lastCostLineno;nextCostLineno] fileLineno++; if (fileLineno == nextCostLineno) { currLine = &(*lineIt); // get next line with cost of selected type ++lineIt; while(lineIt != lineItEnd) { if (&(*lineIt) == sLine) break; if ((*lineIt).hasCost(_eventType)) break; if (_eventType2 && (*lineIt).hasCost(_eventType2)) break; ++lineIt; } lastCostLineno = nextCostLineno; nextCostLineno = (lineIt == lineItEnd) ? 0 : (*lineIt).lineno(); } else currLine = 0; // update inside if (!inside) { if (currLine) inside = true; } else { if ( (fileLineno > lastCostLineno) && ((nextCostLineno == 0) || (fileLineno < nextCostLineno - GlobalConfig::noCostInside()) )) inside = false; } int context = GlobalConfig::context(); if ( ((lastCostLineno==0) || (fileLineno > lastCostLineno + context)) && ((nextCostLineno==0) || (fileLineno < nextCostLineno - context))) { if ((lineIt == lineItEnd) || fileEndReached) break; if (!skipLineWritten) { skipLineWritten = true; // a "skipping" line: print "..." instead of a line number strcpy(buf,"..."); } else continue; } else skipLineWritten = false; QString s = QString(buf); if(s.size() > 0 && s.at(s.length()-1) == '\r') s = s.left(s.length()-1); si = new SourceItem(this, 0, fileno, fileLineno, inside, s, currLine); items.append(si); if (!currLine) continue; if (!selected && (currLine == sLine)) selected = si; if (!first) first = si; if (currLine->subCost(_eventType) > most) { item = si; most = currLine->subCost(_eventType); } si->setExpanded(true); foreach(TraceLineCall* lc, currLine->lineCalls()) { if ((lc->subCost(_eventType)==0) && (lc->subCost(_eventType2)==0)) continue; if (lc->subCost(_eventType) > most) { item = si; most = lc->subCost(_eventType); } si2 = new SourceItem(this, si, fileno, fileLineno, currLine, lc); if (!selected && (lc->call()->called() == _selectedItem)) selected = si2; } foreach(TraceLineJump* lj, currLine->lineJumps()) { if (lj->executedCount()==0) continue; new SourceItem(this, si, fileno, fileLineno, currLine, lj); } } file.close(); // Resize column 0 (line number) and 1/2 (cost) to contents #if QT_VERSION >= 0x050000 header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); header()->setSectionResizeMode(2, QHeaderView::ResizeToContents); #else header()->setResizeMode(0, QHeaderView::ResizeToContents); header()->setResizeMode(1, QHeaderView::ResizeToContents); header()->setResizeMode(2, QHeaderView::ResizeToContents); #endif setSortingEnabled(false); addTopLevelItems(items); this->expandAll(); setSortingEnabled(true); // always reset to line number sort sortByColumn(0, Qt::AscendingOrder); header()->setSortIndicatorShown(false); // Reallow interactive column size change after resizing to content #if QT_VERSION >= 0x050000 header()->setSectionResizeMode(0, QHeaderView::Interactive); header()->setSectionResizeMode(1, QHeaderView::Interactive); header()->setSectionResizeMode(2, QHeaderView::Interactive); #else header()->setResizeMode(0, QHeaderView::Interactive); header()->setResizeMode(1, QHeaderView::Interactive); header()->setResizeMode(2, QHeaderView::Interactive); #endif if (selected) item = selected; if (item) first = item; if (first) { scrollToItem(first); _inSelectionUpdate = true; setCurrentItem(first); _inSelectionUpdate = false; } // for arrows: go down the list according to list sorting QTreeWidgetItem *item1, *item2; for (int i=0; ilineno(), si, true, false); for (int j=0; jchildCount(); j++) { item2 = item1->child(j); si2 = (SourceItem*)item2; if (si2->lineJump()) updateJumpArray(si->lineno(), si2, false, true); else si2->setJumpArray(_jump); } } if (arrowLevels()) //fix this: setColumnWidth(3, 10 + 6*arrowLevels() + itemMargin() * 2); setColumnWidth(3, 10 + 6*arrowLevels() + 2); else setColumnWidth(3, 0); } void SourceView::headerClicked(int col) { if (col == 0) { sortByColumn(col, Qt::AscendingOrder); } //All others but Source Text column Descending else if (col !=4) { sortByColumn(col, Qt::DescendingOrder); } }