merged branch shiroyuki/master (PR #4481)

Commits
-------

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

Discussion
----------

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

(Rework of [PR 4476](https://github.com/symfony/symfony/pull/4476))

Instead of making the developer to resize the width of the visual presentation of the timeline in the profile manually, this change is to make the profiler adjust the width of the timeline dynamically when the (browser) window is resized.

Also, this change introduce the cleaner HTML/JavaScript code and URL as the result of:

* the removal of 'width' from the query string as the width is now controlled by JavaScript.
the removal of 'threshold' from the query string as the threshold is now passed between pages via HTML5 LocalStorage.
* Please note that at the time of submitting the pull request, GitHub didn't pick up some commits to deal with the trailing white spaces.

---------------------------------------------------------------------------

by travisbot at 2012-06-01T18:30:49Z

This pull request [passes](http://travis-ci.org/symfony/symfony/builds/1501464) (merged 06cc9ff3 into 1541fe26).
This commit is contained in:
Fabien Potencier 2012-06-08 09:55:41 +02:00
commit 3c8947e3f1
2 changed files with 337 additions and 169 deletions

View File

@ -467,3 +467,19 @@ td.main, td.menu {
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 %}