Adjust the width of the timeline in the profiler dynamically when the (browser) window is resized.

This commit is contained in:
Juti Noppornpitak 2012-06-01 14:12:35 -04:00
parent 1541fe26e4
commit 06cc9ff32d
2 changed files with 338 additions and 170 deletions

View File

@ -455,4 +455,20 @@ td.main, td.menu {
background: transparent url(../images/profiler/spinner.gif) scroll no-repeat 50% 50%;
height: 30px;
display: none;
}
}
.sf-profiler-timeline .legends {
font-size: 12px;
line-height: 1.5em;
}
.sf-profiler-timeline .legends span {
border-left-width: 10px;
border-left-style: solid;
padding: 0 10px 0 5px;
}
.sf-profiler-timeline canvas {
border: 1px solid #999;
border-width: 1px 0;
}

View File

@ -37,10 +37,7 @@
{% block panel %}
<h2>Timeline</h2>
{% set threshold = app.request.query.get('threshold', 1) %}
{% set width = app.request.query.get('width', 680) %}
<form action="" method="get" style="display: inline">
<form id="timeline-control" action="" method="get">
<input type="hidden" name="panel" value="time" />
<table>
<tr>
@ -53,15 +50,7 @@
</tr>
<tr>
<th>Threshold</th>
<td><input type="number" size="3" name="threshold" value="{{ threshold }}" min="0" /> ms</td>
</tr>
<tr>
<th>Width</th>
<td><input type="range" name="width" min="0" max="1200" step="10" value="{{ width }}" onChange="drawCanvases(this.value);" /> px</td>
</tr>
<tr>
<th>&nbsp;</th>
<td><input type="submit" value="refresh" /></td>
<td><input type="number" size="3" name="threshold" value="1" min="0" /> ms</td>
</tr>
</table>
</form>
@ -71,174 +60,319 @@
<small>
- {{ collector.events.__section__.totaltime }} ms
{% if profile.parent %}
- <a href="{{ path('_profiler', { 'token': profile.parent.token, 'panel': 'time', 'threshold': threshold, 'width': width }) }}">parent</a>
- <a href="{{ path('_profiler', { 'token': profile.parent.token, 'panel': 'time' }) }}">parent</a>
{% endif %}
</small>
</h3>
{% set max = collector.events.__section__.endtime %}
{{ _self.display_timeline('timeline_' ~ token, collector.events, threshold, colors, width) }}
{{ _self.display_timeline('timeline_' ~ token, collector.events, colors) }}
{% if profile.children|length %}
{% for child in profile.children %}
{% set events = child.getcollector('time').events %}
<h3>
Sub-request "<a href="{{ path('_profiler', { 'token': child.token, 'panel': 'time', 'threshold': threshold, 'width': width }) }}">{{ child.getcollector('request').requestattributes.get('_controller') }}</a>"
Sub-request "<a href="{{ path('_profiler', { 'token': child.token, 'panel': 'time' }) }}">{{ child.getcollector('request').requestattributes.get('_controller') }}</a>"
<small> - {{ events.__section__.totaltime }} ms</small>
</h3>
{{ _self.display_timeline('timeline_' ~ child.token, events, threshold, colors, width) }}
{{ _self.display_timeline('timeline_' ~ child.token, events, colors) }}
{% endfor %}
{% endif %}
<script type="text/javascript">//<![CDATA[
function drawCanvas(request, max, threshold, width) {
/**
* In-memory key-value cache manager
*/
var cache = new function() {
"use strict";
var text,
ms,
xc,
mainEvents,
colors = {{ colors|json_encode|raw }},
space = 10.5,
ratio = (width - space * 2) / max,
height = 0,
h = space,
x = request.left * ratio + space,
canvas = document.getElementById('timeline_' + request.id),
ctx = canvas.getContext("2d");
var dict = {};
request.events = request.events.filter(function(event) {
return event.totaltime >= threshold;
});
height += 38 * request.events.length;
canvas.width = width;
ctx.textBaseline = "middle";
ctx.lineWidth = 0;
request.events.forEach(function(event) {
event.periods.forEach(function(period) {
if ('__section__.child' === event.name) {
ctx.fillStyle = colors.child_sections;
ctx.fillRect(x + period.begin * ratio, 0, (period.end - period.begin) * ratio, height);
} else if ('section' === event.category) {
ctx.beginPath();
ctx.strokeStyle = "#dfdfdf";
ctx.moveTo(x + period.begin * ratio, 0);
ctx.lineTo(x + period.begin * ratio, height);
ctx.moveTo(x + period.end * ratio, 0);
ctx.lineTo(x + period.end * ratio, height);
ctx.fill();
ctx.closePath();
ctx.stroke();
}
});
});
mainEvents = request.events.filter(function(event) {
return '__section__.child' !== event.name;
});
mainEvents.forEach(function(event) {
h += 8;
event.periods.forEach(function(period) {
if (colors[event.name]) {
ctx.fillStyle = colors[event.name];
ctx.strokeStyle = colors[event.name];
} else if (colors[event.category]) {
ctx.fillStyle = colors[event.category];
ctx.strokeStyle = colors[event.category];
} else {
ctx.fillStyle = colors['default'];
ctx.strokeStyle = colors['default'];
}
if ('section' !== event.category) {
ctx.fillRect(x + period.begin * ratio, h + 3, 2, 6);
ctx.fillRect(x + period.begin * ratio, h, (period.end - period.begin) * ratio || 2, 6);
} else {
ctx.beginPath();
ctx.moveTo(x + period.begin * ratio, h);
ctx.lineTo(x + period.begin * ratio, h + 11);
ctx.lineTo(x + period.begin * ratio + 8, h);
ctx.lineTo(x + period.begin * ratio, h);
ctx.fill();
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x + period.end * ratio, h);
ctx.lineTo(x + period.end * ratio, h + 11);
ctx.lineTo(x + period.end * ratio - 8, h);
ctx.lineTo(x + period.end * ratio, h);
ctx.fill();
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x + period.begin * ratio, h);
ctx.lineTo(x + period.end * ratio, h);
ctx.lineTo(x + period.end * ratio, h + 2);
ctx.lineTo(x + period.begin * ratio, h + 2);
ctx.lineTo(x + period.begin * ratio, h);
ctx.fill();
ctx.closePath();
ctx.stroke();
}
});
h += 30;
ctx.beginPath();
ctx.strokeStyle = "#dfdfdf";
ctx.moveTo(0, h - 10);
ctx.lineTo(width, h - 10);
ctx.closePath();
ctx.stroke();
});
h = space;
mainEvents.forEach(function(event) {
ctx.fillStyle = "#444";
ctx.font = "12px sans-serif";
text = event.name;
ms = " ~ " + (event.totaltime < 1 ? event.totaltime : parseInt(event.totaltime, 10)) + " ms";
if (x + event.starttime * ratio + ctx.measureText(text + ms).width > width) {
ctx.textAlign = "end";
ctx.font = "10px sans-serif";
xc = x + event.endtime * ratio - 1;
ctx.fillText(ms, xc, h);
xc -= ctx.measureText(ms).width;
ctx.font = "12px sans-serif";
ctx.fillText(text, xc, h);
} else {
ctx.textAlign = "start";
ctx.font = "12px sans-serif";
xc = x + event.starttime * ratio + 1;
ctx.fillText(text, xc, h);
xc += ctx.measureText(text).width;
ctx.font = "10px sans-serif";
ctx.fillText(ms, xc, h);
this.get = function(key) {
return dict.hasOwnProperty(key)
? dict[key]
: null;
}
h += 38;
});
}
this.set = function(key, value) {
dict[key] = value;
function drawCanvases(width)
return value;
}
};
/**
* Query an element with a CSS selector.
*
* @param string selector a CSS-selector-compatible query string.
*
* @return DOMElement|null
*/
function query(selector)
{
"use strict";
requests_data.requests.forEach(function(request) {
drawCanvas(request, requests_data.max, {{ threshold }}, width);
});
var key = 'SELECTOR: ' + selector;
return cache.get(key) || cache.set(key, document.querySelector(selector));
}
/**
* Canvas Manager
*/
function CanvasManager(requests, maxRequestTime) {
"use strict";
var _drawingColors = {{ colors|json_encode|raw }},
_storagePrefix = 'sf2/profiler/timeline',
_threshold = 1,
_requests = requests,
_maxRequestTime = maxRequestTime;
/**
* Check whether this event is a child event.
*
* @return true if it is.
*/
function isChildEvent(event)
{
return '__section__.child' === event.name;
}
/**
* Check whether this event is categorized in 'section'.
*
* @return true if it is.
*/
function isSectionEvent(event)
{
return 'section' === event.category;
}
/**
* Get the width of the container.
*/
function getContainerWidth()
{
return query('#collector_content h2').clientWidth;
}
/**
* Draw one canvas.
*
* @param request the request object
* @param max <subjected for removal>
* @param threshold the threshold (lower bound) of the length of the timeline (in miliseconds).
* @param width the width of the canvas.
*/
this.drawOne = function(request, max, threshold, width)
{
"use strict";
var text,
ms,
xc,
drawableEvents,
mainEvents,
elementId = 'timeline_' + request.id,
canvasHeight = 0,
gapPerEvent = 38,
colors = _drawingColors,
space = 10.5,
ratio = (width - space * 2) / max,
h = space,
x = request.left * ratio + space, // position
canvas = cache.get(elementId) || cache.set(elementId, document.getElementById(elementId)),
ctx = canvas.getContext("2d");
// Filter events whose total time is below the threshold.
drawableEvents = request.events.filter(function(event) {
return event.totaltime >= threshold;
});
canvasHeight += gapPerEvent * drawableEvents.length;
canvas.width = width;
canvas.height = canvasHeight;
ctx.textBaseline = "middle";
ctx.lineWidth = 0;
// For each event, draw a line.
ctx.strokeStyle = "#dfdfdf";
drawableEvents.forEach(function(event) {
event.periods.forEach(function(period) {
var timelineHeadPosition = x + period.begin * ratio;
if (isChildEvent(event)) {
ctx.fillStyle = colors.child_sections;
ctx.fillRect(timelineHeadPosition, 0, (period.end - period.begin) * ratio, canvasHeight);
} else if (isSectionEvent(event)) {
var timelineTailPosition = x + period.end * ratio;
ctx.beginPath();
ctx.moveTo(timelineHeadPosition, 0);
ctx.lineTo(timelineHeadPosition, canvasHeight);
ctx.moveTo(timelineTailPosition, 0);
ctx.lineTo(timelineTailPosition, canvasHeight);
ctx.fill();
ctx.closePath();
ctx.stroke();
}
});
});
// Filter for main events.
mainEvents = drawableEvents.filter(function(event) {
return ! isChildEvent(event)
});
// For each main event, draw the visual presentation of timelines.
mainEvents.forEach(function(event) {
h += 8;
// For each sub event, ...
event.periods.forEach(function(period) {
// Set the drawing style.
ctx.fillStyle = colors['default'];
ctx.strokeStyle = colors['default'];
if (colors[event.name]) {
ctx.fillStyle = colors[event.name];
ctx.strokeStyle = colors[event.name];
} else if (colors[event.category]) {
ctx.fillStyle = colors[event.category];
ctx.strokeStyle = colors[event.category];
}
// Draw the timeline
var timelineHeadPosition = x + period.begin * ratio;
if ( ! isSectionEvent(event)) {
ctx.fillRect(timelineHeadPosition, h + 3, 2, 6);
ctx.fillRect(timelineHeadPosition, h, (period.end - period.begin) * ratio || 2, 6);
} else {
var timelineTailPosition = x + period.end * ratio;
ctx.beginPath();
ctx.moveTo(timelineHeadPosition, h);
ctx.lineTo(timelineHeadPosition, h + 11);
ctx.lineTo(timelineHeadPosition + 8, h);
ctx.lineTo(timelineHeadPosition, h);
ctx.fill();
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(timelineTailPosition, h);
ctx.lineTo(timelineTailPosition, h + 11);
ctx.lineTo(timelineTailPosition - 8, h);
ctx.lineTo(timelineTailPosition, h);
ctx.fill();
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(timelineHeadPosition, h);
ctx.lineTo(timelineTailPosition, h);
ctx.lineTo(timelineTailPosition, h + 2);
ctx.lineTo(timelineHeadPosition, h + 2);
ctx.lineTo(timelineHeadPosition, h);
ctx.fill();
ctx.closePath();
ctx.stroke();
}
});
h += 30;
ctx.beginPath();
ctx.strokeStyle = "#dfdfdf";
ctx.moveTo(0, h - 10);
ctx.lineTo(width, h - 10);
ctx.closePath();
ctx.stroke();
});
h = space;
// For each event, draw the label.
mainEvents.forEach(function(event) {
ctx.fillStyle = "#444";
ctx.font = "12px sans-serif";
text = event.name;
ms = " ~ " + (event.totaltime < 1 ? event.totaltime : parseInt(event.totaltime, 10)) + " ms";
if (x + event.starttime * ratio + ctx.measureText(text + ms).width > width) {
ctx.textAlign = "end";
ctx.font = "10px sans-serif";
xc = x + event.endtime * ratio - 1;
ctx.fillText(ms, xc, h);
xc -= ctx.measureText(ms).width;
ctx.font = "12px sans-serif";
ctx.fillText(text, xc, h);
} else {
ctx.textAlign = "start";
ctx.font = "12px sans-serif";
xc = x + event.starttime * ratio + 1;
ctx.fillText(text, xc, h);
xc += ctx.measureText(text).width;
ctx.font = "10px sans-serif";
ctx.fillText(ms, xc, h);
}
h += gapPerEvent;
});
};
this.drawAll = function(width, threshold)
{
"use strict";
width = width || getContainerWidth();
threshold = threshold || this.getThreshold();
var self = this;
_requests.forEach(function(request) {
self.drawOne(request, maxRequestTime, threshold, width);
});
};
this.getThreshold = function() {
var threshold = localStorage.getItem(_storagePrefix + '/threshold');
if (threshold === null) {
return _threshold;
}
_threshold = parseInt(threshold);
return _threshold;
};
this.setThreshold = function(threshold)
{
_threshold = threshold;
localStorage.setItem(_storagePrefix + '/threshold', threshold);
return this;
};
};
function canvasAutoUpdateOnResizeAndSubmit(e) {
e.preventDefault();
canvasManager.drawAll();
}
function canvasAutoUpdateOnThresholdChange(e) {
canvasManager
.setThreshold(query('input[name="threshold"]').value)
.drawAll();
}
var requests_data = {
@ -255,7 +389,31 @@
]
};
drawCanvases({{ width }});
var canvasManager = new CanvasManager(requests_data.requests, requests_data.max);
query('input[name="threshold"]').value = canvasManager.getThreshold();
canvasManager.drawAll();
// Update the colors of legends.
var timelineLegends = document.querySelectorAll('.sf-profiler-timeline > .legends > span[data-color]');
for (var i = 0; i < timelineLegends.length; ++i) {
var timelineLegend = timelineLegends[i];
timelineLegend.style.borderLeftColor = timelineLegend.getAttribute('data-color');
}
// Bind event handlers
var elementTimelineControl = query('#timeline-control'),
elementThresholdControl = query('input[name="threshold"]');
window.onresize = canvasAutoUpdateOnResizeAndSubmit;
elementTimelineControl.onsubmit = canvasAutoUpdateOnResizeAndSubmit;
elementThresholdControl.onclick = canvasAutoUpdateOnThresholdChange;
elementThresholdControl.onchange = canvasAutoUpdateOnThresholdChange;
elementThresholdControl.onkeyup = canvasAutoUpdateOnThresholdChange;
//]]></script>
{% endblock %}
@ -289,19 +447,13 @@
{% endfor %}
{% endmacro %}
{% macro display_timeline(id, events, threshold, colors, width) %}
{% set height = 0 %}
{% for name, event in events if '__section__' != name and event.totaltime >= threshold %}
{% set height = height + 38 %}
{% endfor %}
<div>
<small>
{% macro display_timeline(id, events, colors) %}
<div class="sf-profiler-timeline">
<div class="legends">
{% for category, color in colors %}
<span style="background-color: {{ color }}">&nbsp;&nbsp;&nbsp;</span> {{ category }}&nbsp;&nbsp;
<span data-color="{{ color }}">{{ category }}</span>
{% endfor %}
</small>
</div>
<canvas width="680" height="" id="{{ id }}" class="timeline"></canvas>
</div>
<canvas width="{{ width }}" height="{{ height }}" id="{{ id }}" class="timeline"></canvas>
{% endmacro %}