Commit bf4dfd5e authored by Christiaan Janssen's avatar Christiaan Janssen
Browse files

QmlProfiler: optimized timeline display



Change-Id: I0d7cf110356ef5f805b81a5fc39dca3870765ea3
Reviewed-by: default avatarKai Koehne <kai.koehne@nokia.com>
parent 38ad15a7
......@@ -95,6 +95,11 @@ struct QmlEventStartTimeData {
};
struct QmlEventTypeCount {
QList <int> eventIds;
int nestingCount;
};
// used by quicksort
bool compareEndTimes(const QmlEventEndTimeData &t1, const QmlEventEndTimeData &t2)
{
......@@ -173,6 +178,8 @@ public:
QV8EventDescriptions m_v8EventList;
QHash<int, QV8EventData *> m_v8parents;
QHash<int, QmlEventTypeCount *> m_typeCounts;
qint64 m_traceEndTime;
// file to load
......@@ -211,8 +218,13 @@ void QmlProfilerEventList::clear()
d->m_v8EventList.clear();
d->m_v8parents.clear();
foreach (QmlEventTypeCount *typeCount, d->m_typeCounts.values())
delete typeCount;
d->m_typeCounts.clear();
d->m_traceEndTime = 0;
emit countChanged();
emit dataClear();
}
QList <QmlEventData *> QmlProfilerEventList::getEventDescriptions() const
......@@ -429,6 +441,26 @@ void QmlProfilerEventList::compileStatistics()
}
}
// generate numeric ids
int ndx = 0;
foreach (QmlEventData *binding, d->m_eventDescriptions.values()) {
binding->numericHash = ndx++;
}
// collect type counts
foreach (const QmlEventStartTimeData &eventStartData, d->m_startTimeSortedList) {
int typeNumber = eventStartData.description->eventType;
if (!d->m_typeCounts.contains(typeNumber)) {
d->m_typeCounts[typeNumber] = new QmlEventTypeCount;
d->m_typeCounts[typeNumber]->nestingCount = 0;
}
if (eventStartData.nestingLevel > d->m_typeCounts[typeNumber]->nestingCount) {
d->m_typeCounts[typeNumber]->nestingCount = eventStartData.nestingLevel;
}
if (!d->m_typeCounts[typeNumber]->eventIds.contains(eventStartData.description->numericHash))
d->m_typeCounts[typeNumber]->eventIds << eventStartData.description->numericHash;
}
// continue postprocess
postProcess();
}
......@@ -592,6 +624,7 @@ void QmlProfilerEventList::computeNestingDepth()
void QmlProfilerEventList::postProcess()
{
// Todo: collapse all this
switch (d->m_parsingStatus) {
case GettingDataStatus: {
setParsingStatus(SortingListsStatus);
......@@ -691,6 +724,37 @@ int QmlProfilerEventList::findFirstIndex(qint64 startTime) const
return ndx;
}
int QmlProfilerEventList::findFirstIndexNoParents(qint64 startTime) const
{
int candidate = -1;
// in the "endtime" list, find the first event that ends after startTime
if (d->m_endTimeSortedList.isEmpty())
return 0; // -1
if (d->m_endTimeSortedList.length() == 1 || d->m_endTimeSortedList.first().endTime >= startTime)
candidate = 0;
else
if (d->m_endTimeSortedList.last().endTime <= startTime)
return 0; // -1
if (candidate == -1) {
int fromIndex = 0;
int toIndex = d->m_endTimeSortedList.count()-1;
while (toIndex - fromIndex > 1) {
int midIndex = (fromIndex + toIndex)/2;
if (d->m_endTimeSortedList[midIndex].endTime < startTime)
fromIndex = midIndex;
else
toIndex = midIndex;
}
candidate = toIndex;
}
int ndx = d->m_endTimeSortedList[candidate].startTimeIndex;
return ndx;
}
int QmlProfilerEventList::findLastIndex(qint64 endTime) const
{
// in the "starttime" list, find the last event that starts before endtime
......@@ -1122,5 +1186,31 @@ QString QmlProfilerEventList::getDetails(int index) const {
return d->m_startTimeSortedList[index].description->details;
}
int QmlProfilerEventList::getHash(int index) const {
return d->m_startTimeSortedList[index].description->numericHash;
}
int QmlProfilerEventList::uniqueEventsOfType(int type) const {
if (!d->m_typeCounts.contains(type))
return 1;
return d->m_typeCounts[type]->eventIds.count();
}
int QmlProfilerEventList::maxNestingForType(int type) const {
if (!d->m_typeCounts.contains(type))
return 1;
return d->m_typeCounts[type]->nestingCount;
}
QString QmlProfilerEventList::eventTextForType(int type, int index) const {
if (!d->m_typeCounts.contains(type))
return QString();
return d->m_eventDescriptions.values().at(d->m_typeCounts[type]->eventIds[index])->details;
}
int QmlProfilerEventList::eventPosInType(int index) const {
int eventType = d->m_startTimeSortedList[index].description->eventType;
return d->m_typeCounts[eventType]->eventIds.indexOf(d->m_startTimeSortedList[index].description->numericHash);
}
} // namespace QmlJsDebugClient
......@@ -58,6 +58,7 @@ struct QMLJSDEBUGCLIENT_EXPORT QmlEventData
double timePerCall;
double percentOfTime;
qint64 medianTime;
int numericHash;
};
struct QMLJSDEBUGCLIENT_EXPORT QV8EventData
......@@ -99,6 +100,7 @@ public:
const QV8EventDescriptions& getV8Events() const;
int findFirstIndex(qint64 startTime) const;
int findFirstIndexNoParents(qint64 startTime) const;
int findLastIndex(qint64 endTime) const;
Q_INVOKABLE qint64 firstTimeMark() const;
Q_INVOKABLE qint64 lastTimeMark() const;
......@@ -117,6 +119,13 @@ public:
Q_INVOKABLE QString getFilename(int index) const;
Q_INVOKABLE int getLine(int index) const;
Q_INVOKABLE QString getDetails(int index) const;
Q_INVOKABLE int getHash(int index) const;
// per-type data
Q_INVOKABLE int uniqueEventsOfType(int type) const;
Q_INVOKABLE int maxNestingForType(int type) const;
Q_INVOKABLE QString eventTextForType(int type, int index) const;
Q_INVOKABLE int eventPosInType(int index) const;
Q_INVOKABLE qint64 traceStartTime() const;
Q_INVOKABLE qint64 traceEndTime() const;
......@@ -127,6 +136,7 @@ signals:
void countChanged();
void parsingStatusChanged();
void error(const QString &error);
void dataClear();
public slots:
void clear();
......
......@@ -34,12 +34,53 @@ import QtQuick 1.0
Item {
property alias text: txt.text
property bool expanded: false
property int typeIndex: index
height: 50
width: 150 //### required, or ignored by positioner
property variant descriptions: [text]
height: root.singleRowHeight
width: 150
onExpandedChanged: {
var rE = labels.rowExpanded;
rE[typeIndex] = expanded;
labels.rowExpanded = rE;
backgroundMarks.requestPaint();
view.rowExpanded(typeIndex, expanded);
updateHeight();
}
Component.onCompleted: {
updateHeight();
}
function updateHeight() {
height = root.singleRowHeight *
(expanded ? qmlEventList.uniqueEventsOfType(typeIndex) : qmlEventList.maxNestingForType(typeIndex));
}
Connections {
target: qmlEventList
onDataReady: {
var desc=[];
for (var i=0; i<qmlEventList.uniqueEventsOfType(typeIndex); i++)
desc[i] = qmlEventList.eventTextForType(typeIndex, i);
// special case: empty
if (desc.length == 1 && desc[0]=="")
desc[0] = text;
descriptions = desc;
updateHeight();
}
onDataClear: {
descriptions = [text];
updateHeight();
}
}
Text {
id: txt;
id: txt
visible: !expanded
x: 5
font.pixelSize: 12
color: "#232323"
......@@ -52,4 +93,31 @@ Item {
color: "#cccccc"
anchors.bottom: parent.bottom
}
Column {
visible: expanded
Repeater {
model: descriptions.length
Text {
height: root.singleRowHeight
x: 5
width: 140
text: descriptions[index]
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
}
}
Image {
source: expanded ? "arrow_down.png" : "arrow_right.png"
x: parent.width - 12
y: 2
MouseArea {
anchors.fill: parent
onClicked: {
expanded = !expanded;
}
}
}
}
......@@ -39,13 +39,17 @@ Rectangle {
// ***** properties
property int candidateHeight: 0
height: Math.max( candidateHeight, labels.rowCount * 50 + 2 )
height: Math.max( candidateHeight, labels.height + 2 )
property int singleRowHeight: 30
property bool dataAvailable: true
property int eventCount: 0
property real progress: 0
property bool mouseOverSelection : true
property alias selectionLocked : view.selectionLocked
signal updateLockButton
property alias selectedItem: view.selectedItem
property variant names: [ qsTr("Painting"), qsTr("Compiling"), qsTr("Creating"), qsTr("Binding"), qsTr("Handling Signal")]
property variant colors : [ "#99CCB3", "#99CCCC", "#99B3CC", "#9999CC", "#CC99B3", "#CC99CC", "#CCCC99", "#CCB399" ]
......@@ -56,9 +60,6 @@ Rectangle {
property string fileName: ""
property int lineNumber: -1
property int selectedEventIndex : -1
property bool mouseTracking: false
property real elapsedTime
signal updateTimer
......@@ -107,7 +108,11 @@ Rectangle {
onDataReady: {
if (eventCount > 0) {
view.clearData();
view.rebuildCache();
progress = 1.0;
dataAvailable = true;
view.visible = true;
view.requestPaint();
zoomControl.setRange(0, qmlEventList.traceEndTime()/10);
}
}
}
......@@ -128,7 +133,6 @@ Rectangle {
function clearDisplay() {
clearData();
selectedEventIndex = -1;
view.visible = false;
}
......@@ -139,19 +143,11 @@ Rectangle {
}
function nextEvent() {
if (eventCount > 0) {
++selectedEventIndex;
if (selectedEventIndex >= eventCount)
selectedEventIndex = 0;
}
view.selectNext();
}
function prevEvent() {
if (eventCount > 0) {
--selectedEventIndex;
if (selectedEventIndex < 0)
selectedEventIndex = eventCount - 1;
}
view.selectPrev();
}
function updateWindowLength(absoluteFactor) {
......@@ -179,9 +175,9 @@ Rectangle {
}
var fixedPoint = (view.startTime + view.endTime) / 2;
if (root.selectedEventIndex !== -1) {
if (view.selectedItem !== -1) {
// center on selected item if it's inside the current screen
var newFixedPoint = qmlEventList.getStartTime(selectedEventIndex);
var newFixedPoint = qmlEventList.getStartTime(view.selectedItem);
if (newFixedPoint >= view.startTime && newFixedPoint < view.endTime)
fixedPoint = newFixedPoint;
}
......@@ -242,37 +238,18 @@ Rectangle {
rangeDetails.type = "";
rangeDetails.file = "";
rangeDetails.line = -1;
root.mouseOverSelection = true;
selectionHighlight.visible = false;
}
// ***** slots
onSelectedEventIndexChanged: {
if ((!mouseTracking) && eventCount > 0
&& selectedEventIndex > -1 && selectedEventIndex < eventCount) {
var windowLength = view.endTime - view.startTime;
var eventStartTime = qmlEventList.getStartTime(selectedEventIndex);
var eventEndTime = eventStartTime + qmlEventList.getDuration(selectedEventIndex);
if (eventEndTime < view.startTime || eventStartTime > view.endTime) {
var center = (eventStartTime + eventEndTime)/2;
var from = Math.min(qmlEventList.traceEndTime()-windowLength,
Math.max(0, Math.floor(center - windowLength/2)));
zoomControl.setRange(from, from + windowLength);
}
}
if (selectedEventIndex === -1)
selectionHighlight.visible = false;
}
onSelectionRangeModeChanged: {
selectionRangeControl.enabled = selectionRangeMode;
selectionRange.reset(selectionRangeMode);
}
onSelectionLockedChanged: {
updateLockButton();
}
// ***** child items
Timer {
id: elapsedTimer
......@@ -312,7 +289,7 @@ Rectangle {
anchors.left: labels.right
height: root.height
contentWidth: 0;
contentHeight: labels.rowCount * 50
contentHeight: labels.height
flickableDirection: Flickable.HorizontalFlick
onContentXChanged: {
......@@ -322,13 +299,43 @@ Rectangle {
clip:true
MouseArea {
id: selectionRangeDrag
enabled: selectionRange.ready
anchors.fill: selectionRange
drag.target: selectionRange
drag.axis: "XAxis"
drag.minimumX: 0
drag.maximumX: flick.contentWidth - selectionRange.width
onPressed: {
selectionRange.isDragging = true;
}
onReleased: {
selectionRange.isDragging = false;
}
onDoubleClicked: {
zoomControl.setRange(selectionRange.startTime, selectionRange.startTime + selectionRange.duration);
root.selectionRangeMode = false;
root.updateRangeButton();
}
}
SelectionRange {
id: selectionRange
visible: root.selectionRangeMode
height: root.height
z: 2
}
TimelineView {
id: view
eventList: qmlEventList
x: flick.contentX
width: flick.width
height: flick.contentHeight
height: root.height
property variant startX: 0
onStartXChanged: {
......@@ -349,178 +356,65 @@ Rectangle {
var newStartX = startTime * flick.width / (endTime-startTime);
if (Math.abs(newStartX - startX) >= 1)
startX = newStartX;
updateTimeline();
}
}
property real timeSpan: endTime - startTime
onTimeSpanChanged: {
if (selectedEventIndex !== -1 && selectionHighlight.visible) {
var spacing = flick.width / timeSpan;
selectionHighlight.x = (qmlEventList.getStartTime(selectedEventIndex) - qmlEventList.traceStartTime()) * spacing;
selectionHighlight.width = qmlEventList.getDuration(selectedEventIndex) * spacing;
}
}
onCachedProgressChanged: {
root.progress = 0.5 + cachedProgress * 0.5;
}
onCacheReady: {
root.progress = 1.0;
root.dataAvailable = true;
if (root.eventCount > 0) {
view.visible = true;
view.updateTimeline();
zoomControl.setRange(0, qmlEventList.traceEndTime()/10);
}
}
delegate: Rectangle {
id: obj
property color myColor: root.colors[type]
function conditionalHide() {
if (!mouseArea.containsMouse)
mouseArea.exited()
}
property int baseY: type * view.height / labels.rowCount
property int baseHeight: view.height / labels.rowCount
y: baseY + (nestingLevel-1)*(baseHeight / nestingDepth)
height: baseHeight / nestingDepth
gradient: Gradient {
GradientStop { position: 0.0; color: myColor }
GradientStop { position: 0.5; color: Qt.darker(myColor, 1.1) }
GradientStop { position: 1.0; color: myColor }
}
smooth: true
property bool componentIsCompleted: false
Component.onCompleted: {
componentIsCompleted = true;
updateDetails();
}
property bool isSelected: root.selectedEventIndex == index;
onIsSelectedChanged: {
updateDetails();
}
function updateDetails() {
if (!root.mouseTracking && componentIsCompleted) {
if (isSelected) {
enableSelected(0, 0);
}
}
}
function enableSelected(x,y) {
rangeDetails.duration = qmlEventList.getDuration(index)/1000.0;
rangeDetails.label = qmlEventList.getDetails(index);
rangeDetails.file = qmlEventList.getFilename(index);
rangeDetails.line = qmlEventList.getLine(index);
rangeDetails.type = root.names[type];
onSelectedItemChanged: {
if (selectedItem !== -1) {
// display details
rangeDetails.duration = qmlEventList.getDuration(selectedItem)/1000.0;
rangeDetails.label = qmlEventList.getDetails(selectedItem);
rangeDetails.file = qmlEventList.getFilename(selectedItem);
rangeDetails.line = qmlEventList.getLine(selectedItem);
rangeDetails.type = root.names[qmlEventList.getType(selectedItem)];
rangeDetails.visible = true;
selectionHighlight.x = obj.x;
selectionHighlight.y = obj.y;
selectionHighlight.width = width;
selectionHighlight.height = height;
selectionHighlight.visible = true;
}
// center view
var windowLength = view.endTime - view.startTime;
var eventStartTime = qmlEventList.getStartTime(selectedItem);
var eventEndTime = eventStartTime + qmlEventList.getDuration(selectedItem);
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onEntered: {
if (root.mouseOverSelection) {
root.mouseTracking = true;
root.selectedEventIndex = index;
enableSelected(mouseX, y);
root.mouseTracking = false;
}
}
onPressed: {
root.mouseTracking = true;
root.selectedEventIndex = index;
enableSelected(mouseX, y);
root.mouseTracking = false;
if (eventEndTime < view.startTime || eventStartTime > view.endTime) {
var center = (eventStartTime + eventEndTime)/2;
var from = Math.min(qmlEventList.traceEndTime()-windowLength,
Math.max(0, Math.floor(center - windowLength/2)));
root.mouseOverSelection = false;
root.gotoSourceLocation(rangeDetails.file, rangeDetails.line);
zoomControl.setRange(from, from + windowLength);
}
}
}
Rectangle {
id: selectionHighlight
color:"transparent"
border.width: 2
border.color: "blue"
z: 1
radius: 2
visible: false
}
MouseArea {
width: parent.width
height: parent.height
x: flick.contentX
onClicked: {
} else {
root.hideRangeDetails();
}
}
MouseArea {
id: selectionRangeControl
enabled: false
width: flick.width
height: root.height
x: flick.contentX
hoverEnabled: enabled
z: 2
onReleased: {
selectionRange.releasedOnCreation();
}
onPressed: {