QmlProfiler: Make TimeDisplay and TimeMarks declarative

Create the vertical elements only once and stretch them over the whole
vertical space, but keep all horizontal elements distinct per model.
Also, add horizontal elements to the labels group and rename that
accordingly, so that we don't need a third Flickable or ListView

Change-Id: I4fe3bd526767e8ff5d0ebcd70e9343f65fcc787f
Reviewed-by: Kai Koehne <kai.koehne@theqtcompany.com>
This commit is contained in:
Ulf Hermann
2014-11-07 18:13:31 +01:00
parent 75e4bd7900
commit bc4586ffbe
4 changed files with 307 additions and 259 deletions

View File

@@ -124,14 +124,6 @@ Item {
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
Rectangle {
height: 1
width: parent.width
color: "#999999"
anchors.bottom: parent.bottom
z: 2
}
Column { Column {
id: column id: column
property QtObject parentModel: model property QtObject parentModel: model

View File

@@ -105,7 +105,6 @@ Rectangle {
zoomSlider.externalUpdate = true; zoomSlider.externalUpdate = true;
zoomSlider.value = zoomSlider.minimumValue; zoomSlider.value = zoomSlider.minimumValue;
overview.clear(); overview.clear();
timeDisplay.clear();
} }
function propagateSelection(newModel, newItem) { function propagateSelection(newModel, newItem) {
@@ -190,46 +189,64 @@ Rectangle {
Keys.onReleased: shiftPressed = false; Keys.onReleased: shiftPressed = false;
Flickable { Flickable {
id: labelsflick id: categories
flickableDirection: Flickable.VerticalFlick flickableDirection: Flickable.VerticalFlick
interactive: false interactive: false
anchors.top: buttonsBar.bottom anchors.top: buttonsBar.bottom
anchors.bottom: overview.top anchors.bottom: overview.top
anchors.left: parent.left anchors.left: parent.left
width: labels.width anchors.right: parent.right
contentY: flick.contentY contentY: flick.contentY
// reserve some more space than needed to prevent weird effects when resizing // reserve some more space than needed to prevent weird effects when resizing
contentHeight: labels.height + height contentHeight: categoryContent.height + height
Rectangle {
id: labels
anchors.left: parent.left
width: 150
color: root.color
height: col.height
Column {
id: col
// Dispatch the cursor shape to all labels. When dragging the DropArea receiving // Dispatch the cursor shape to all labels. When dragging the DropArea receiving
// the drag events is not necessarily related to the MouseArea receiving the mouse // the drag events is not necessarily related to the MouseArea receiving the mouse
// events, so we can't use the drag events to determine the cursor shape. // events, so we can't use the drag events to determine the cursor shape.
property bool dragging: false property bool dragging: false
Column {
id: categoryContent
anchors.left: parent.left
anchors.right: parent.right
DelegateModel { DelegateModel {
id: labelsModel id: labelsModel
// As we cannot retrieve items by visible index we keep an array of row counts here,
// for the time marks to draw the row backgrounds in the right colors.
property var rowCounts: new Array(qmlProfilerModelProxy.models.length)
function updateRowCount(visualIndex, rowCount) {
if (rowCounts[visualIndex] !== rowCount) {
rowCounts[visualIndex] = rowCount;
// Array don't "change" if entries change. We have to signal manually.
rowCountsChanged();
}
}
model: qmlProfilerModelProxy.models model: qmlProfilerModelProxy.models
delegate: CategoryLabel { delegate: Rectangle {
color: root.color
anchors.left: parent.left
anchors.right: parent.right
property int visualIndex: DelegateModel.itemsIndex
height: label.visible ? label.height : 0
CategoryLabel {
id: label
model: modelData model: modelData
mockup: qmlProfilerModelProxy.height == 0 mockup: qmlProfilerModelProxy.height === 0
visualIndex: DelegateModel.itemsIndex visualIndex: parent.visualIndex
dragging: col.dragging dragging: categories.dragging
reverseSelect: root.shiftPressed reverseSelect: root.shiftPressed
onDragStarted: col.dragging = true onDragStarted: categories.dragging = true
onDragStopped: col.dragging = false onDragStopped: categories.dragging = false
draggerParent: labels draggerParent: categories
dragOffset: y width: 150
dragOffset: parent.y
onDropped: { onDropped: {
timelineModel.items.move(sourceIndex, targetIndex); timelineModel.items.move(sourceIndex, targetIndex);
@@ -251,8 +268,36 @@ Rectangle {
zoomControl.rangeStart, zoomControl.rangeStart,
root.selectedModel === index ? root.selectedItem : -1)); root.selectedModel === index ? root.selectedItem : -1));
} }
}
TimeMarks {
id: timeMarks
model: modelData
mockup: qmlProfilerModelProxy.height === 0
anchors.right: parent.right
anchors.left: label.right
anchors.top: parent.top
anchors.bottom: parent.bottom
property int visualIndex: parent.visualIndex
// Quite a mouthful, but works fine: Add up all the row counts up to the one
// for this visual index and check if the result is even or odd.
startOdd: (labelsModel.rowCounts.slice(0, visualIndex).reduce(
function(prev, rows) {return prev + rows}, 0) % 2) === 0
onRowCountChanged: labelsModel.updateRowCount(visualIndex, rowCount)
onVisualIndexChanged: labelsModel.updateRowCount(visualIndex, rowCount)
}
Rectangle {
visible: label.visible
opacity: parent.y == 0 ? 0 : 1
color: "#B0B0B0"
height: 1
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
}
} }
} }
@@ -260,17 +305,25 @@ Rectangle {
model: labelsModel model: labelsModel
} }
} }
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: categoryContent.bottom
height: 1
color: "#B0B0B0"
} }
} }
// border between labels and timeline TimeDisplay {
Rectangle { id: timeDisplay
id: labelsborder
anchors.left: labelsflick.right
anchors.top: parent.top anchors.top: parent.top
anchors.left: buttonsBar.right
anchors.right: parent.right
anchors.bottom: overview.top anchors.bottom: overview.top
width: 1 zoomer: zoomControl
color: "#858585" contentX: flick.contentX
clip: true
} }
ButtonsBar { ButtonsBar {
@@ -297,21 +350,12 @@ Rectangle {
onLockChanged: selectionLocked = !lockButtonChecked(); onLockChanged: selectionLocked = !lockButtonChecked();
} }
TimeDisplay {
id: timeDisplay
anchors.top: parent.top
anchors.left: labelsborder.right
anchors.right: parent.right
height: 24
}
Flickable { Flickable {
id: flick id: flick
contentHeight: labels.height contentHeight: categoryContent.height
contentWidth: zoomControl.windowDuration * width / Math.max(1, zoomControl.rangeDuration) contentWidth: zoomControl.windowDuration * width / Math.max(1, zoomControl.rangeDuration)
flickableDirection: Flickable.HorizontalAndVerticalFlick flickableDirection: Flickable.HorizontalAndVerticalFlick
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
clip:true
// ScrollView will try to deinteractivate it. We don't want that // ScrollView will try to deinteractivate it. We don't want that
// as the horizontal flickable is interactive, too. We do occasionally // as the horizontal flickable is interactive, too. We do occasionally
@@ -375,9 +419,6 @@ Rectangle {
signal clearChildren signal clearChildren
signal select(int modelIndex, int eventIndex) signal select(int modelIndex, int eventIndex)
// As we cannot retrieve items by visible index we keep an array of row counts here,
// for the time marks to draw the row backgrounds in the right colors.
property var rowCounts: new Array(qmlProfilerModelProxy.models.length)
DelegateModel { DelegateModel {
id: timelineModel id: timelineModel
@@ -386,35 +427,8 @@ Rectangle {
id: spacer id: spacer
height: modelData.height height: modelData.height
width: flick.contentWidth width: flick.contentWidth
property int rowCount: (modelData.empty || modelData.hidden) ?
0 : modelData.rowCount
property int visualIndex: DelegateModel.itemsIndex property int visualIndex: DelegateModel.itemsIndex
function updateRowParentCount() {
if (timelineView.rowCounts[visualIndex] !== rowCount) {
timelineView.rowCounts[visualIndex] = rowCount;
// Array don't "change" if entries change. We have to signal manually.
timelineView.rowCountsChanged();
}
}
onRowCountChanged: updateRowParentCount()
onVisualIndexChanged: updateRowParentCount()
TimeMarks {
model: modelData
id: backgroundMarks
anchors.fill: renderer
startTime: zoomControl.rangeStart
endTime: zoomControl.rangeEnd
// Quite a mouthful, but works fine: Add up all the row counts up to the one
// for this visual index and check if the result is even or odd.
startOdd: (timelineView.rowCounts.slice(0, spacer.visualIndex).reduce(
function(prev, rows) {return prev + rows}, 0) % 2) === 0
onStartOddChanged: requestPaint()
}
TimelineRenderer { TimelineRenderer {
id: renderer id: renderer
model: modelData model: modelData
@@ -543,8 +557,8 @@ Rectangle {
ScrollView { ScrollView {
id: scroller id: scroller
contentItem: flick contentItem: flick
anchors.left: labelsborder.right anchors.left: buttonsBar.right
anchors.top: timeDisplay.bottom anchors.top: categories.top
anchors.bottom: overview.top anchors.bottom: overview.top
anchors.right: parent.right anchors.right: parent.right
} }
@@ -565,12 +579,20 @@ Rectangle {
notes: qmlProfilerModelProxy.notes notes: qmlProfilerModelProxy.notes
} }
Rectangle {
anchors.left: buttonsBar.right
anchors.bottom: overview.top
anchors.top: parent.top
width: 1
color: "#B0B0B0"
}
Rectangle { Rectangle {
id: filterMenu id: filterMenu
color: "#9b9b9b" color: "#9b9b9b"
enabled: buttonsBar.enabled enabled: buttonsBar.enabled
visible: false visible: false
width: labels.width width: buttonsBar.width
anchors.left: parent.left anchors.left: parent.left
anchors.top: buttonsBar.bottom anchors.top: buttonsBar.bottom
height: qmlProfilerModelProxy.models.length * buttonsBar.height height: qmlProfilerModelProxy.models.length * buttonsBar.height
@@ -598,7 +620,7 @@ Rectangle {
color: "#9b9b9b" color: "#9b9b9b"
enabled: buttonsBar.enabled enabled: buttonsBar.enabled
visible: false visible: false
width: labels.width width: categoryContent.width
height: buttonsBar.height height: buttonsBar.height
anchors.left: parent.left anchors.left: parent.left
anchors.top: buttonsBar.bottom anchors.top: buttonsBar.bottom

View File

@@ -29,87 +29,44 @@
****************************************************************************/ ****************************************************************************/
import QtQuick 2.1 import QtQuick 2.1
import Monitor 1.0
Canvas { Item {
id: timeDisplay id: timeDisplay
objectName: "TimeDisplay"
contextType: "2d"
Connections { property QtObject zoomer
target: zoomControl
onRangeChanged: requestPaint();
}
onPaint: { readonly property int labelsHeight: 24
var context = (timeDisplay.context === null) ? getContext("2d") : timeDisplay.context;
context.reset(); readonly property int initialBlockLength: 120
context.fillStyle = "white";
context.fillRect(0, 0, width, height);
var totalTime = Math.max(1, zoomControl.rangeDuration); property double spacing: width / rangeDuration
var spacing = width / totalTime;
var initialBlockLength = 120; property double timePerBlock: Math.pow(2, Math.floor(Math.log(initialBlockLength / spacing) /
var timePerBlock = Math.pow(2, Math.floor( Math.log( totalTime / width * initialBlockLength ) / Math.LN2 ) ); Math.LN2))
var pixelsPerBlock = timePerBlock * spacing;
var pixelsPerSection = pixelsPerBlock / 5;
var blockCount = width / pixelsPerBlock;
var realStartTime = Math.floor(zoomControl.rangeStart / timePerBlock) * timePerBlock; property double rangeDuration: Math.max(1, Math.round(zoomer.rangeDuration))
var startPos = (zoomControl.rangeStart - realStartTime) * spacing; property double alignedWindowStart: Math.round(zoomer.windowStart - (zoomer.windowStart % timePerBlock))
property double pixelsPerBlock: timeDisplay.timePerBlock * timeDisplay.spacing
property double pixelsPerSection: pixelsPerBlock / 5
var timePerPixel = timePerBlock/pixelsPerBlock; property int contentX
property int offsetX: contentX + Math.round((zoomer.windowStart % timePerBlock) * spacing)
var initialColor = Math.floor(realStartTime/timePerBlock) % 2; readonly property var timeUnits: ["μs", "ms", "s"]
function prettyPrintTime(t, rangeDuration) {
context.fillStyle = "#000000";
context.font = "8px sans-serif";
for (var ii = 0; ii < blockCount+1; ii++) {
var x = Math.floor(ii*pixelsPerBlock - startPos);
context.fillStyle = (ii+initialColor)%2 ? "#E6E6E6":"white";
context.fillRect(x, 0, pixelsPerBlock, height);
context.strokeStyle = "#B0B0B0";
context.beginPath();
context.moveTo(x, 0);
context.lineTo(x, height);
context.stroke();
context.fillStyle = "#000000";
context.fillText(prettyPrintTime(ii*timePerBlock + realStartTime), x + 5, height/2 + 5);
}
context.strokeStyle = "#525252";
context.beginPath();
context.moveTo(0, height-1);
context.lineTo(width, height-1);
context.stroke();
}
function clear()
{
requestPaint();
}
function prettyPrintTime( t )
{
var round = 1; var round = 1;
var barrier = 1; var barrier = 1;
var units = ["μs", "ms", "s"];
for (var i = 0; i < units.length; ++i) { for (var i = 0; i < timeUnits.length; ++i) {
barrier *= 1000; barrier *= 1000;
if (zoomControl.rangeDuration < barrier) if (rangeDuration < barrier)
round *= 1000; round *= 1000;
else if (zoomControl.rangeDuration < barrier * 10) else if (rangeDuration < barrier * 10)
round *= 100; round *= 100;
else if (zoomControl.rangeDuration < barrier * 100) else if (rangeDuration < barrier * 100)
round *= 10; round *= 10;
if (t < barrier * 1000) if (t < barrier * 1000)
return Math.floor(t / (barrier / round)) / round + units[i]; return Math.floor(t / (barrier / round)) / round + timeUnits[i];
} }
t /= barrier; t /= barrier;
@@ -117,4 +74,111 @@ Canvas {
var s = Math.floor((t - m * 60) * round) / round; var s = Math.floor((t - m * 60) * round) / round;
return m + "m" + s + "s"; return m + "m" + s + "s";
} }
Item {
x: Math.floor(firstBlock * timeDisplay.pixelsPerBlock - timeDisplay.offsetX)
y: 0
id: row
property int firstBlock: timeDisplay.offsetX / timeDisplay.pixelsPerBlock
property int offset: firstBlock % repeater.model
Repeater {
id: repeater
model: Math.floor(timeDisplay.width / timeDisplay.initialBlockLength * 2 + 2)
Item {
id: column
// Changing the text in text nodes is expensive. We minimize the number of changes
// by rotating the nodes during scrolling.
property int stableIndex: row.offset > index ? repeater.model - row.offset + index :
index - row.offset
height: timeDisplay.height
y: 0
x: width * stableIndex
width: timeDisplay.pixelsPerBlock
// Manually control this. We don't want it to happen when firstBlock
// changes before stableIndex changes.
onStableIndexChanged: block = row.firstBlock + stableIndex
property int block: -1
property double blockStartTime: block * timeDisplay.timePerBlock +
timeDisplay.alignedWindowStart
Rectangle {
id: timeLabel
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: timeDisplay.labelsHeight
color: (Math.round(column.block + timeDisplay.alignedWindowStart /
timeDisplay.timePerBlock) % 2) ?
"#E6E6E6" : "white";
Text {
id: labelText
renderType: Text.NativeRendering
font.pixelSize: 8
font.family: "sans-serif"
anchors.fill: parent
anchors.leftMargin: 5
anchors.bottomMargin: 5
verticalAlignment: Text.AlignBottom
text: prettyPrintTime(column.blockStartTime, timeDisplay.rangeDuration)
}
}
Row {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: timeLabel.bottom
anchors.bottom: parent.bottom
Repeater {
model: 4
Item {
anchors.top: parent.top
anchors.bottom: parent.bottom
width: timeDisplay.pixelsPerSection
Rectangle {
color: "#CCCCCC"
width: 1
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
}
}
}
}
Rectangle {
color: "#B0B0B0"
width: 1
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
}
}
}
}
Rectangle {
height: 2
anchors.left: parent.left
anchors.right: parent.right
y: labelsHeight - 2
color: "#B0B0B0"
}
Rectangle {
height: 1
anchors.left: parent.left
anchors.right: parent.right
anchors.top: row.bottom
color: "#B0B0B0"
}
} }

View File

@@ -29,72 +29,20 @@
****************************************************************************/ ****************************************************************************/
import QtQuick 2.1 import QtQuick 2.1
import Monitor 1.0
Canvas { Item {
id: timeMarks id: timeMarks
objectName: "TimeMarks" visible: model && (mockup || (!model.hidden && !model.empty))
contextType: "2d"
property QtObject model property QtObject model
property bool startOdd property bool startOdd
property bool mockup
readonly property int scaleMinHeight: 60 readonly property int scaleMinHeight: 60
readonly property int scaleStepping: 30 readonly property int scaleStepping: 30
readonly property string units: " kMGT" readonly property string units: " kMGT"
property real startTime
property real endTime
property real timePerPixel
Connections { property int rowCount: model ? model.rowCount : 0
target: model
onHeightChanged: requestPaint()
}
onStartTimeChanged: requestPaint()
onEndTimeChanged: requestPaint()
onYChanged: requestPaint()
onHeightChanged: requestPaint()
onPaint: {
var context = (timeMarks.context === null) ? getContext("2d") : timeMarks.context;
context.reset();
drawBackgroundBars( context, region );
var totalTime = endTime - startTime;
var spacing = width / totalTime;
var initialBlockLength = 120;
var timePerBlock = Math.pow(2, Math.floor( Math.log( totalTime / width * initialBlockLength ) / Math.LN2 ) );
var pixelsPerBlock = timePerBlock * spacing;
var pixelsPerSection = pixelsPerBlock / 5;
var blockCount = width / pixelsPerBlock;
var realStartTime = Math.floor(startTime/timePerBlock) * timePerBlock;
var realStartPos = (startTime-realStartTime) * spacing;
timePerPixel = timePerBlock/pixelsPerBlock;
for (var ii = 0; ii < blockCount+1; ii++) {
var x = Math.floor(ii*pixelsPerBlock - realStartPos);
context.strokeStyle = "#B0B0B0";
context.beginPath();
context.moveTo(x, 0);
context.lineTo(x, height);
context.stroke();
context.strokeStyle = "#CCCCCC";
for (var jj=1; jj < 5; jj++) {
var xx = Math.floor(ii*pixelsPerBlock + jj*pixelsPerSection - realStartPos);
context.beginPath();
context.moveTo(xx, 0);
context.lineTo(xx, height);
context.stroke();
}
}
}
function prettyPrintScale(amount) { function prettyPrintScale(amount) {
var unitOffset = 0; var unitOffset = 0;
@@ -112,66 +60,88 @@ Canvas {
} }
} }
function drawBackgroundBars( context, region ) { Connections {
var colorIndex = startOdd; target: model
onRowHeightChanged: {
context.font = "8px sans-serif"; if (row >= 0)
rowRepeater.itemAt(row).height = height;
// separators
var cumulatedHeight = 0;
for (var row = 0; row < model.rowCount; ++row) {
// row background
var rowHeight = model.rowHeight(row)
cumulatedHeight += rowHeight;
colorIndex = !colorIndex;
if (cumulatedHeight < y - rowHeight)
continue;
context.strokeStyle = context.fillStyle = colorIndex ? "#f0f0f0" : "white";
context.fillRect(0, cumulatedHeight - rowHeight - y, width, rowHeight);
if (rowHeight >= scaleMinHeight) {
var minVal = model.rowMinValue(row);
var maxVal = model.rowMaxValue(row);
if (minVal !== maxVal) {
context.strokeStyle = context.fillStyle = "#B0B0B0";
var stepValUgly = Math.ceil((maxVal - minVal) /
Math.floor(rowHeight / scaleStepping));
// align to clean 2**x
var stepVal = 1;
while (stepValUgly >>= 1)
stepVal <<= 1;
var stepHeight = rowHeight / (maxVal - minVal);
for (var step = minVal; step <= maxVal - stepVal; step += stepVal) {
var offset = cumulatedHeight - step * stepHeight - y;
context.beginPath();
context.moveTo(0, offset);
context.lineTo(width, offset);
context.stroke();
context.fillText(prettyPrintScale(step), 5, offset - 2);
}
context.beginPath();
context.moveTo(0, cumulatedHeight - rowHeight - y);
context.lineTo(width, cumulatedHeight - rowHeight - y);
context.stroke();
context.fillText(prettyPrintScale(maxVal), 5,
cumulatedHeight - rowHeight - y + 8);
} }
} }
if (cumulatedHeight > y + height)
return; Column {
id: rows
anchors.left: parent.left
anchors.right: parent.right
Repeater {
id: rowRepeater
model: timeMarks.rowCount
Rectangle {
id: row
color: ((index + (startOdd ? 1 : 0)) % 2) ? "#f0f0f0" : "white"
anchors.left: rows.left
anchors.right: rows.right
height: timeMarks.model ? timeMarks.model.rowHeight(index) : 0
property int minVal: timeMarks.model ? timeMarks.model.rowMinValue(index) : 0
property int maxVal: timeMarks.model ? timeMarks.model.rowMaxValue(index) : 0
property int valDiff: maxVal - minVal
property bool scaleVisible: timeMarks.model && timeMarks.model.expanded &&
height > scaleMinHeight && valDiff > 0
property int stepVal: {
var ret = 1;
var ugly = Math.ceil(valDiff / Math.floor(height / scaleStepping));
while (isFinite(ugly) && ugly > 1) {
ugly >>= 1;
ret <<= 1;
}
return ret;
} }
context.strokeStyle = "#B0B0B0"; Text {
context.beginPath(); id: scaleTopLabel
context.moveTo(0, cumulatedHeight - y); renderType: Text.NativeRendering
context.lineTo(width, cumulatedHeight - y); visible: parent.scaleVisible
context.stroke(); color: "#B0B0B0"
font.pixelSize: 8
anchors.top: parent.top
anchors.leftMargin: 2
anchors.topMargin: 2
anchors.left: parent.left
text: prettyPrintScale(row.maxVal)
}
Repeater {
model: parent.scaleVisible ? row.valDiff / row.stepVal : 0
Item {
anchors.left: row.left
anchors.right: row.right
height: row.stepVal * row.height / row.valDiff
y: row.height - (index + 1) * height
visible: y > scaleTopLabel.height
Text {
renderType: Text.NativeRendering
color: "#B0B0B0"
font.pixelSize: 8
anchors.bottom: parent.bottom
anchors.bottomMargin: 2
anchors.leftMargin: 2
anchors.left: parent.left
text: prettyPrintScale(index * row.stepVal)
}
Rectangle {
height: 1
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
color: "#B0B0B0"
}
}
}
}
}
} }
} }