forked from qt-creator/qt-creator
Add generic FlameGraphView QML component
This allows us to reduce code duplication, but we first have to put timeline and flame graph into the same library, so that we can use the TimelineThere in FlameGraphView. Change-Id: I72b27ffb1fc5aa6baf6a23d85e5ca6c610896b8c Reviewed-by: Christian Kandeler <christian.kandeler@qt.io>
This commit is contained in:
@@ -208,7 +208,6 @@ QVariant FlameGraphModel::lookup(const FlameGraphData &stats, int role) const
|
||||
case DurationRole: return stats.duration;
|
||||
case CallCountRole: return stats.calls;
|
||||
case TimePerCallRole: return stats.duration / stats.calls;
|
||||
case TimeInPercentRole: return stats.duration * 100 / m_stackBottom.duration;
|
||||
case AllocationsRole: return stats.allocations;
|
||||
case MemoryRole: return stats.memory;
|
||||
default: break;
|
||||
@@ -321,7 +320,6 @@ QHash<int, QByteArray> FlameGraphModel::roleNames() const
|
||||
{ColumnRole, "column"},
|
||||
{NoteRole, "note"},
|
||||
{TimePerCallRole, "timePerCall"},
|
||||
{TimeInPercentRole, "timeInPercent"},
|
||||
{RangeTypeRole, "rangeType"},
|
||||
{LocationRole, "location" },
|
||||
{AllocationsRole, "allocations" },
|
||||
|
||||
@@ -68,7 +68,6 @@ public:
|
||||
ColumnRole,
|
||||
NoteRole,
|
||||
TimePerCallRole,
|
||||
TimeInPercentRole,
|
||||
RangeTypeRole,
|
||||
LocationRole,
|
||||
AllocationsRole,
|
||||
|
||||
@@ -23,20 +23,30 @@
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 1.3
|
||||
import FlameGraph 1.0
|
||||
import QmlProfilerFlameGraphModel 1.0
|
||||
import TimelineTheme 1.0
|
||||
import "../tracing/"
|
||||
|
||||
ScrollView {
|
||||
FlameGraphView {
|
||||
id: root
|
||||
|
||||
property int selectedTypeId: -1
|
||||
property int sizeRole: QmlProfilerFlameGraphModel.DurationRole
|
||||
model: flameGraphModel
|
||||
sizeRole: QmlProfilerFlameGraphModel.DurationRole
|
||||
|
||||
readonly property var trRoleNames: [
|
||||
typeIdRole: QmlProfilerFlameGraphModel.TypeIdRole
|
||||
sourceFileRole: QmlProfilerFlameGraphModel.FilenameRole
|
||||
sourceLineRole: QmlProfilerFlameGraphModel.LineRole
|
||||
sourceColumnRole: QmlProfilerFlameGraphModel.ColumnRole
|
||||
detailsTitleRole: QmlProfilerFlameGraphModel.TypeRole
|
||||
summaryRole: QmlProfilerFlameGraphModel.DetailsRole
|
||||
noteRole: QmlProfilerFlameGraphModel.NoteRole
|
||||
|
||||
modes: [
|
||||
QmlProfilerFlameGraphModel.DurationRole,
|
||||
QmlProfilerFlameGraphModel.MemoryRole,
|
||||
QmlProfilerFlameGraphModel.AllocationsRole
|
||||
]
|
||||
|
||||
trRoleNames: [
|
||||
QmlProfilerFlameGraphModel.DurationRole, qsTr("Total Time"),
|
||||
QmlProfilerFlameGraphModel.CallCountRole, qsTr("Calls"),
|
||||
QmlProfilerFlameGraphModel.DetailsRole, qsTr("Details"),
|
||||
@@ -45,311 +55,47 @@ ScrollView {
|
||||
QmlProfilerFlameGraphModel.LocationRole, qsTr("Location"),
|
||||
QmlProfilerFlameGraphModel.AllocationsRole, qsTr("Allocations"),
|
||||
QmlProfilerFlameGraphModel.MemoryRole, qsTr("Memory")
|
||||
].reduce(function(previousValue, currentValue, currentIndex, array) {
|
||||
if (currentIndex % 2 === 1)
|
||||
previousValue[array[currentIndex - 1]] = array[currentIndex];
|
||||
return previousValue;
|
||||
}, {})
|
||||
].reduce(toMap, {})
|
||||
|
||||
onSelectedTypeIdChanged: tooltip.hoveredNode = null
|
||||
details: function(flameGraph) {
|
||||
var model = [];
|
||||
if (!flameGraph.dataValid) {
|
||||
model.push(trRoleNames[QmlProfilerFlameGraphModel.DetailsRole]);
|
||||
model.push(qsTr("Various Events"));
|
||||
} else {
|
||||
function addDetail(role, format) { root.addDetail(role, format, model, flameGraph); }
|
||||
|
||||
Flickable {
|
||||
id: flickable
|
||||
contentHeight: flamegraph.height
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
addDetail(QmlProfilerFlameGraphModel.DetailsRole, detailFormats.noop);
|
||||
addDetail(QmlProfilerFlameGraphModel.CallCountRole, detailFormats.noop);
|
||||
addDetail(QmlProfilerFlameGraphModel.DurationRole, detailFormats.printTime);
|
||||
addDetail(QmlProfilerFlameGraphModel.TimePerCallRole, detailFormats.printTime);
|
||||
addDetail(QmlProfilerFlameGraphModel.LocationRole, detailFormats.noop);
|
||||
addDetail(QmlProfilerFlameGraphModel.MemoryRole, detailFormats.printMemory);
|
||||
addDetail(QmlProfilerFlameGraphModel.AllocationsRole, detailFormats.noop);
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
tooltip.selectedNode = null;
|
||||
flameGraphModel.typeSelected(-1);
|
||||
}
|
||||
onDoubleClicked: {
|
||||
tooltip.selectedNode = null;
|
||||
flameGraphModel.typeSelected(-1);
|
||||
flamegraph.resetRoot();
|
||||
summary: function(attached) {
|
||||
if (!attached.dataValid)
|
||||
return qsTr("others");
|
||||
|
||||
return attached.data(QmlProfilerFlameGraphModel.DetailsRole) + " ("
|
||||
+ attached.data(QmlProfilerFlameGraphModel.TypeRole) + ", "
|
||||
+ root.percent(root.sizeRole, attached) + "%)";
|
||||
}
|
||||
|
||||
isHighlighted: function(node) {
|
||||
function recurse(parentNode, typeId) {
|
||||
if (!parentNode)
|
||||
return false;
|
||||
if (parentNode.typeId === typeId) {
|
||||
parentNode.isHighlighted = true;
|
||||
return true;
|
||||
}
|
||||
return recurse(parentNode.parent, typeId);
|
||||
}
|
||||
|
||||
FlameGraph {
|
||||
property int delegateHeight: Math.min(60, Math.max(30, flickable.height / depth))
|
||||
property color blue: "blue"
|
||||
property color blue1: Qt.lighter(blue)
|
||||
property color blue2: Qt.rgba(0.375, 0, 1, 1)
|
||||
property color grey1: "#B0B0B0"
|
||||
property color grey2: "#A0A0A0"
|
||||
property color highlight: Theme.color(Theme.Timeline_HighlightColor)
|
||||
|
||||
function checkBindingLoop(otherTypeId) {return false;}
|
||||
|
||||
id: flamegraph
|
||||
width: parent.width
|
||||
height: Math.max(depth * delegateHeight, flickable.height)
|
||||
model: flameGraphModel
|
||||
sizeRole: root.sizeRole
|
||||
sizeThreshold: 0.002
|
||||
maximumDepth: 25
|
||||
y: flickable.height > height ? flickable.height - height : 0
|
||||
|
||||
delegate: FlameGraphDelegate {
|
||||
id: flamegraphItem
|
||||
|
||||
property int typeId: FlameGraph.data(QmlProfilerFlameGraphModel.TypeIdRole) || -1
|
||||
property bool isBindingLoop: parent.checkBindingLoop(typeId)
|
||||
|
||||
itemHeight: flamegraph.delegateHeight
|
||||
isSelected: typeId !== -1 && typeId === root.selectedTypeId
|
||||
|
||||
borderColor: {
|
||||
if (isSelected)
|
||||
return flamegraph.blue2;
|
||||
else if (tooltip.hoveredNode === flamegraphItem)
|
||||
return flamegraph.blue1;
|
||||
else if (note() !== "" || isBindingLoop)
|
||||
return flamegraph.highlight;
|
||||
else
|
||||
return flamegraph.grey1;
|
||||
}
|
||||
borderWidth: {
|
||||
if (tooltip.hoveredNode === flamegraphItem ||
|
||||
tooltip.selectedNode === flamegraphItem) {
|
||||
return 2;
|
||||
} else if (note() !== "") {
|
||||
return 3;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
onIsSelectedChanged: {
|
||||
if (isSelected && (tooltip.selectedNode === null ||
|
||||
tooltip.selectedNode.typeId !== root.selectedTypeId)) {
|
||||
tooltip.selectedNode = flamegraphItem;
|
||||
} else if (!isSelected && tooltip.selectedNode === flamegraphItem) {
|
||||
tooltip.selectedNode = null;
|
||||
}
|
||||
}
|
||||
|
||||
function checkBindingLoop(otherTypeId) {
|
||||
if (typeId === otherTypeId) {
|
||||
isBindingLoop = true;
|
||||
return true;
|
||||
} else {
|
||||
return parent.checkBindingLoop(otherTypeId);
|
||||
}
|
||||
}
|
||||
|
||||
function buildText() {
|
||||
if (!FlameGraph.dataValid)
|
||||
return "<others>";
|
||||
|
||||
return FlameGraph.data(QmlProfilerFlameGraphModel.DetailsRole) + " ("
|
||||
+ FlameGraph.data(QmlProfilerFlameGraphModel.TypeRole) + ", "
|
||||
+ Math.floor(width / flamegraph.width * 1000) / 10 + "%)";
|
||||
}
|
||||
text: textVisible ? buildText() : ""
|
||||
FlameGraph.onModelIndexChanged: {
|
||||
if (textVisible)
|
||||
text = buildText();
|
||||
|
||||
// refresh to trigger reevaluation
|
||||
if (tooltip.selectedNode == flamegraphItem) {
|
||||
var selectedNode = tooltip.selectedNode;
|
||||
tooltip.selectedNode = null;
|
||||
tooltip.selectedNode = selectedNode;
|
||||
}
|
||||
if (tooltip.hoveredNode == flamegraphItem) {
|
||||
var hoveredNode = tooltip.hoveredNode;
|
||||
tooltip.hoveredNode = null;
|
||||
tooltip.hoveredNode = hoveredNode;
|
||||
}
|
||||
}
|
||||
|
||||
onMouseEntered: {
|
||||
tooltip.hoveredNode = flamegraphItem;
|
||||
}
|
||||
|
||||
onMouseExited: {
|
||||
if (tooltip.hoveredNode === flamegraphItem) {
|
||||
// Keep the window around until something else is hovered or selected.
|
||||
if (tooltip.selectedNode === null
|
||||
|| tooltip.selectedNode.typeId !== root.selectedTypeId) {
|
||||
tooltip.selectedNode = flamegraphItem;
|
||||
}
|
||||
tooltip.hoveredNode = null;
|
||||
}
|
||||
}
|
||||
|
||||
function selectClicked() {
|
||||
if (flamegraphItem.FlameGraph.dataValid) {
|
||||
tooltip.selectedNode = flamegraphItem;
|
||||
flameGraphModel.typeSelected(flamegraphItem.FlameGraph.data(
|
||||
QmlProfilerFlameGraphModel.TypeIdRole));
|
||||
flameGraphModel.gotoSourceLocation(
|
||||
flamegraphItem.FlameGraph.data(
|
||||
QmlProfilerFlameGraphModel.FilenameRole),
|
||||
flamegraphItem.FlameGraph.data(
|
||||
QmlProfilerFlameGraphModel.LineRole),
|
||||
flamegraphItem.FlameGraph.data(
|
||||
QmlProfilerFlameGraphModel.ColumnRole));
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: selectClicked()
|
||||
onDoubleClicked: {
|
||||
selectClicked();
|
||||
flamegraph.root = FlameGraph.modelIndex;
|
||||
}
|
||||
|
||||
// Functions, not properties to limit the initial overhead when creating the nodes,
|
||||
// and because FlameGraph.data(...) cannot be notified anyway.
|
||||
function title() { return FlameGraph.data(QmlProfilerFlameGraphModel.TypeRole) || ""; }
|
||||
function note() { return FlameGraph.data(QmlProfilerFlameGraphModel.NoteRole) || ""; }
|
||||
function details() {
|
||||
var model = [];
|
||||
function addDetail(index, format) {
|
||||
model.push(trRoleNames[index]);
|
||||
model.push(format(FlameGraph.data(index)));
|
||||
}
|
||||
|
||||
function printTime(t)
|
||||
{
|
||||
if (t <= 0)
|
||||
return "0";
|
||||
if (t < 1000)
|
||||
return t + " ns";
|
||||
t = Math.floor(t / 1000);
|
||||
if (t < 1000)
|
||||
return t + " μs";
|
||||
if (t < 1e6)
|
||||
return (t / 1000) + " ms";
|
||||
return (t / 1e6) + " s";
|
||||
}
|
||||
|
||||
function noop(a) {
|
||||
return a;
|
||||
}
|
||||
|
||||
function addPercent(a) {
|
||||
return a + "%";
|
||||
}
|
||||
|
||||
function printMemory(a) {
|
||||
if (a === 0)
|
||||
return "0b";
|
||||
|
||||
var units = ["b", "kb", "Mb", "Gb"];
|
||||
var div = 1;
|
||||
for (var i = 0; i < units.length; ++i, div *= 1024) {
|
||||
if (a > div * 1024)
|
||||
continue;
|
||||
|
||||
a /= div;
|
||||
var digitsAfterDot = Math.round(3 - Math.log(a) / Math.LN10);
|
||||
var multiplier = Math.pow(10, digitsAfterDot);
|
||||
return Math.round(a * multiplier) / multiplier + units[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (!FlameGraph.dataValid) {
|
||||
model.push(qsTr("Details"));
|
||||
model.push(qsTr("Various Events"));
|
||||
} else {
|
||||
addDetail(QmlProfilerFlameGraphModel.DetailsRole, noop);
|
||||
addDetail(QmlProfilerFlameGraphModel.CallCountRole, noop);
|
||||
addDetail(QmlProfilerFlameGraphModel.DurationRole, printTime);
|
||||
addDetail(QmlProfilerFlameGraphModel.TimePerCallRole, printTime);
|
||||
addDetail(QmlProfilerFlameGraphModel.TimeInPercentRole, addPercent);
|
||||
addDetail(QmlProfilerFlameGraphModel.LocationRole, noop);
|
||||
addDetail(QmlProfilerFlameGraphModel.MemoryRole, printMemory);
|
||||
addDetail(QmlProfilerFlameGraphModel.AllocationsRole, noop);
|
||||
}
|
||||
return model;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FlameGraphDetails {
|
||||
id: tooltip
|
||||
|
||||
minimumX: 0
|
||||
maximumX: flickable.width
|
||||
minimumY: flickable.contentY
|
||||
maximumY: flickable.contentY + flickable.height
|
||||
|
||||
titleBarColor: Theme.color(Theme.Timeline_PanelHeaderColor)
|
||||
titleBarTextColor: Theme.color(Theme.PanelTextColorLight)
|
||||
contentColor: Theme.color(Theme.Timeline_PanelBackgroundColor)
|
||||
contentTextColor: Theme.color(Theme.Timeline_TextColor)
|
||||
noteTextColor: Theme.color(Theme.Timeline_HighlightColor)
|
||||
buttonHoveredColor: Theme.color(Theme.FancyToolButtonHoverColor)
|
||||
buttonSelectedColor: Theme.color(Theme.FancyToolButtonSelectedColor)
|
||||
borderWidth: 0
|
||||
|
||||
property var hoveredNode: null;
|
||||
property var selectedNode: null;
|
||||
|
||||
property var currentNode: {
|
||||
if (hoveredNode !== null)
|
||||
return hoveredNode;
|
||||
else if (selectedNode !== null)
|
||||
return selectedNode;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
onClearSelection: {
|
||||
selectedTypeId = -1;
|
||||
selectedNode = null;
|
||||
flameGraphModel.typeSelected(-1);
|
||||
}
|
||||
|
||||
dialogTitle: {
|
||||
if (currentNode)
|
||||
return currentNode.title();
|
||||
else if (flameGraphModel.rowCount() === 0)
|
||||
return qsTr("No data available");
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
||||
model: currentNode ? currentNode.details() : []
|
||||
note: currentNode ? currentNode.note() : ""
|
||||
|
||||
Connections {
|
||||
target: flameGraphModel
|
||||
onModelReset: {
|
||||
tooltip.hoveredNode = null;
|
||||
tooltip.selectedNode = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
x: flickable.width - width
|
||||
y: flickable.contentY
|
||||
|
||||
// It won't listen to anchors.margins and by default it doesn't add any margin. Great.
|
||||
width: implicitWidth + 20
|
||||
|
||||
text: qsTr("Visualize %1").arg(trRoleNames[root.sizeRole])
|
||||
|
||||
menu: Menu {
|
||||
MenuItem {
|
||||
text: trRoleNames[QmlProfilerFlameGraphModel.DurationRole]
|
||||
onTriggered: root.sizeRole = QmlProfilerFlameGraphModel.DurationRole
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: trRoleNames[QmlProfilerFlameGraphModel.MemoryRole]
|
||||
onTriggered: root.sizeRole = QmlProfilerFlameGraphModel.MemoryRole
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: trRoleNames[QmlProfilerFlameGraphModel.AllocationsRole]
|
||||
onTriggered: root.sizeRole = QmlProfilerFlameGraphModel.AllocationsRole
|
||||
}
|
||||
}
|
||||
}
|
||||
return recurse(node.parent, node.typeId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,8 +158,6 @@ void FlameGraphModelTest::testData()
|
||||
QCOMPARE(model.data(index2, FlameGraphModel::NoteRole).toString(), QString());
|
||||
QCOMPARE(model.data(index, FlameGraphModel::TimePerCallRole).toLongLong(), 20);
|
||||
QCOMPARE(model.data(index2, FlameGraphModel::TimePerCallRole).toLongLong(), 12);
|
||||
QCOMPARE(model.data(index, FlameGraphModel::TimeInPercentRole).toInt(), 62);
|
||||
QCOMPARE(model.data(index2, FlameGraphModel::TimeInPercentRole).toInt(), 37);
|
||||
QCOMPARE(model.data(index, FlameGraphModel::RangeTypeRole).toInt(),
|
||||
static_cast<int>(Javascript));
|
||||
QCOMPARE(model.data(index2, FlameGraphModel::RangeTypeRole).toInt(),
|
||||
@@ -201,7 +199,6 @@ void FlameGraphModelTest::testRoleNames()
|
||||
QCOMPARE(names[FlameGraphModel::ColumnRole], QByteArray("column"));
|
||||
QCOMPARE(names[FlameGraphModel::NoteRole], QByteArray("note"));
|
||||
QCOMPARE(names[FlameGraphModel::TimePerCallRole], QByteArray("timePerCall"));
|
||||
QCOMPARE(names[FlameGraphModel::TimeInPercentRole], QByteArray("timeInPercent"));
|
||||
QCOMPARE(names[FlameGraphModel::RangeTypeRole], QByteArray("rangeType"));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user