feature #30450 [Profiler] Render the performance graph with SVG (Tom32i)
This PR was squashed before being merged into the 4.3-dev branch (closes #30450).
Discussion
----------
[Profiler] Render the performance graph with SVG
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | Part 1, 3 and 4 of #27262
| License | MIT
| Doc PR | n/a
Following a suggestion by @ogizanagi in #27262,
Here's a proposal to render the Request Graph, from the performance profiler panel, with SVG instead of canvas.
Some benefits of the SVG format:
- The text labels are searchable and can be selected.
- It renders well on high DPI monitors.
- [Colors and text styles](https://github.com/symfony/symfony/issues/27262#issuecomment-388868068) can be defined with CSS just like the rest of the page.
In addition, SVG allow us to consider (and easily implement) interactives features such as:
- Zoom in and time navigation (thanks to the viewport).
- Highlight hovered line (or other DOM related events).
Preview:
![screenshot_2019-03-08 symfony profiler 1](https://user-images.githubusercontent.com/1846873/54036727-a33f4300-41bc-11e9-8be7-b1de10d4afd9.png)
Filtered events example:
![capture d ecran 2019-03-08 a 17 22 47](https://user-images.githubusercontent.com/1846873/54041039-00d88d00-41c7-11e9-9590-23e809415c34.png)
### Progress :
- [x] Render request events in SVG
- [x] Show labels with duration and memory
- [x] Show specific markers at start / end of lines
- [x] Re-render graph when window resize
- [x] Re-render graph when threshold change.
- [x] Generate graph legend with only existing categories (part 1. of #27262 )
- [x] Show sub-request area with hatched pattern
- [x] Allow to hide categories by clicking them on the legend (part 3. of #27262 )
- [x] Handle text overflow on long labels.
- [x] Ensure JS code is compatible with all supported browsers (used [classes](https://caniuse.com/#feat=es6-class) and [arrow functions](https://caniuse.com/#feat=arrow-functions).
- ~Add left-padding to sub-request graph?~
Commits
-------
a69a718ec9
[Profiler] Render the performance graph with SVG
This commit is contained in:
commit
0762d2d9ac
@ -1,6 +1,11 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
4.3.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Replaced the canvas performance graph renderer with an SVG renderer
|
||||||
|
|
||||||
4.1.0
|
4.1.0
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
@ -0,0 +1,105 @@
|
|||||||
|
/* Variables */
|
||||||
|
|
||||||
|
.sf-profiler-timeline {
|
||||||
|
--color-default: #777;
|
||||||
|
--color-section: #999;
|
||||||
|
--color-event-listener: #00B8F5;
|
||||||
|
--color-template: #66CC00;
|
||||||
|
--color-doctrine: #FF6633;
|
||||||
|
--color-messenger-middleware: #BDB81E;
|
||||||
|
--color-controller-argument-value-resolver: #8c5de6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Legend */
|
||||||
|
|
||||||
|
.sf-profiler-timeline .legends .timeline-category {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
border-left: 1em solid transparent;
|
||||||
|
line-height: 1em;
|
||||||
|
margin: 0 1em 0 0;
|
||||||
|
padding: 0 0.5em;
|
||||||
|
display: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-profiler-timeline .legends .timeline-category.active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-profiler-timeline .legends .timeline-category.present {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-profiler-timeline .legends .{{ classnames.default|raw }} { border-color: var(--color-default); }
|
||||||
|
.sf-profiler-timeline .legends .{{ classnames.section|raw }} { border-color: var(--color-section); }
|
||||||
|
.sf-profiler-timeline .legends .{{ classnames.event_listener|raw }} { border-color: var(--color-event-listener); }
|
||||||
|
.sf-profiler-timeline .legends .{{ classnames.template|raw }} { border-color: var(--color-template); }
|
||||||
|
.sf-profiler-timeline .legends .{{ classnames.doctrine|raw }} { border-color: var(--color-doctrine); }
|
||||||
|
.sf-profiler-timeline .legends .{{ classnames['messenger.middleware']|raw }} { border-color: var(--color-messenger-middleware); }
|
||||||
|
.sf-profiler-timeline .legends .{{ classnames['controller.argument_value_resolver']|raw }} { border-color: var(--color-controller-argument-value-resolver); }
|
||||||
|
|
||||||
|
.timeline-graph {
|
||||||
|
margin: 1em 0;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--table-background);
|
||||||
|
border: 1px solid var(--table-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
|
||||||
|
.timeline-graph .timeline-label {
|
||||||
|
font-family: var(--font-sans-serif);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-graph .timeline-label .timeline-sublabel {
|
||||||
|
margin-left: 1em;
|
||||||
|
fill: var(--color-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-graph .timeline-subrequest,
|
||||||
|
.timeline-graph .timeline-border {
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--table-border);
|
||||||
|
stroke-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-graph .timeline-subrequest {
|
||||||
|
fill: url(#subrequest);
|
||||||
|
fill-opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-subrequest-pattern {
|
||||||
|
fill: var(--table-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Timeline periods */
|
||||||
|
|
||||||
|
.timeline-graph .timeline-period {
|
||||||
|
stroke-width: 0;
|
||||||
|
}
|
||||||
|
.timeline-graph .{{ classnames.default|raw }} .timeline-period {
|
||||||
|
fill: var(--color-default);
|
||||||
|
}
|
||||||
|
.timeline-graph .{{ classnames.section|raw }} .timeline-period {
|
||||||
|
fill: var(--color-section);
|
||||||
|
}
|
||||||
|
.timeline-graph .{{ classnames.event_listener|raw }} .timeline-period {
|
||||||
|
fill: var(--color-event-listener);
|
||||||
|
}
|
||||||
|
.timeline-graph .{{ classnames.template|raw }} .timeline-period {
|
||||||
|
fill: var(--color-template);
|
||||||
|
}
|
||||||
|
.timeline-graph .{{ classnames.doctrine|raw }} .timeline-period {
|
||||||
|
fill: var(--color-doctrine);
|
||||||
|
}
|
||||||
|
.timeline-graph .{{ classnames['messenger.middleware']|raw }} .timeline-period {
|
||||||
|
fill: var(--color-messenger-middleware);
|
||||||
|
}
|
||||||
|
.timeline-graph .{{ classnames['controller.argument_value_resolver']|raw }} .timeline-period {
|
||||||
|
fill: var(--color-controller-argument-value-resolver);
|
||||||
|
}
|
@ -2,22 +2,18 @@
|
|||||||
|
|
||||||
{% import _self as helper %}
|
{% import _self as helper %}
|
||||||
|
|
||||||
{% if colors is not defined %}
|
{% set classnames = {
|
||||||
{% set colors = {
|
'default': 'timeline-category-default',
|
||||||
'default': '#777',
|
'section': 'timeline-category-section',
|
||||||
'section': '#999',
|
'event_listener': 'timeline-category-event-listener',
|
||||||
'event_listener': '#00B8F5',
|
'template': 'timeline-category-template',
|
||||||
'template': '#66CC00',
|
'doctrine': 'timeline-category-doctrine',
|
||||||
'doctrine': '#FF6633',
|
'messenger.middleware': 'timeline-category-messenger-middleware',
|
||||||
'messenger.middleware': '#BDB81E',
|
'controller.argument_value_resolver': 'timeline-category-controller-argument-value-resolver',
|
||||||
'controller.argument_value_resolver': '#8c5de6',
|
} %}
|
||||||
} %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block toolbar %}
|
{% block toolbar %}
|
||||||
{% set has_time_events = collector.events|length > 0 %}
|
{% set has_time_events = collector.events|length > 0 %}
|
||||||
|
|
||||||
{% set total_time = has_time_events ? '%.0f'|format(collector.duration) : 'n/a' %}
|
{% set total_time = has_time_events ? '%.0f'|format(collector.duration) : 'n/a' %}
|
||||||
{% set initialization_time = collector.events|length ? '%.0f'|format(collector.inittime) : 'n/a' %}
|
{% set initialization_time = collector.events|length ? '%.0f'|format(collector.inittime) : 'n/a' %}
|
||||||
{% set status_color = has_time_events and collector.duration > 1000 ? 'yellow' : '' %}
|
{% set status_color = has_time_events and collector.duration > 1000 ? 'yellow' : '' %}
|
||||||
@ -110,7 +106,7 @@
|
|||||||
<form id="timeline-control" action="" method="get">
|
<form id="timeline-control" action="" method="get">
|
||||||
<input type="hidden" name="panel" value="time">
|
<input type="hidden" name="panel" value="time">
|
||||||
<label for="threshold">Threshold</label>
|
<label for="threshold">Threshold</label>
|
||||||
<input type="number" size="3" name="threshold" id="threshold" value="3" min="0"> ms
|
<input type="number" name="threshold" id="threshold" value="1" min="0" placeholder="1.1"> ms
|
||||||
<span class="help">(timeline only displays events with a duration longer than this threshold)</span>
|
<span class="help">(timeline only displays events with a duration longer than this threshold)</span>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -128,7 +124,7 @@
|
|||||||
</h3>
|
</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{ helper.display_timeline('timeline_' ~ token, colors) }}
|
{{ helper.display_timeline(token, classnames, collector.events, collector.events.__section__.origin) }}
|
||||||
|
|
||||||
{% if profile.children|length %}
|
{% if profile.children|length %}
|
||||||
<p class="help">Note: sections with a striped background correspond to sub-requests.</p>
|
<p class="help">Note: sections with a striped background correspond to sub-requests.</p>
|
||||||
@ -142,389 +138,34 @@
|
|||||||
<small>{{ events.__section__.duration }} ms</small>
|
<small>{{ events.__section__.duration }} ms</small>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
{{ helper.display_timeline('timeline_' ~ child.token, colors) }}
|
{{ helper.display_timeline(child.token, classnames, events, collector.events.__section__.origin) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<script>{% autoescape 'js' %}//<![CDATA[
|
<svg id="timeline-template" width="0" height="0">
|
||||||
/**
|
<defs>
|
||||||
* In-memory key-value cache manager
|
<pattern id="subrequest" class="timeline-subrequest-pattern" patternUnits="userSpaceOnUse" width="20" height="20" viewBox="0 0 40 40">
|
||||||
*/
|
<path d="M0 40L40 0H20L0 20M40 40V20L20 40"/>
|
||||||
var cache = new function() {
|
</pattern>
|
||||||
"use strict";
|
</defs>
|
||||||
var dict = {};
|
</svg>
|
||||||
|
<style type="text/css">
|
||||||
this.get = function(key) {
|
{% include '@WebProfiler/Collector/time.css.twig' with classnames %}
|
||||||
return dict.hasOwnProperty(key)
|
</style>
|
||||||
? dict[key]
|
<script>
|
||||||
: null;
|
{% include '@WebProfiler/Collector/time.js' %}
|
||||||
};
|
</script>
|
||||||
|
|
||||||
this.set = function(key, value) {
|
|
||||||
dict[key] = value;
|
|
||||||
|
|
||||||
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";
|
|
||||||
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 = '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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCssVarValue(varName)
|
|
||||||
{
|
|
||||||
return getComputedStyle(document.querySelector('body')).getPropertyValue(varName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 milliseconds)
|
|
||||||
* @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"),
|
|
||||||
scaleRatio,
|
|
||||||
devicePixelRatio;
|
|
||||||
|
|
||||||
// Filter events whose total time is below the threshold.
|
|
||||||
drawableEvents = request.events.filter(function(event) {
|
|
||||||
return event.duration >= threshold;
|
|
||||||
});
|
|
||||||
|
|
||||||
canvasHeight += gapPerEvent * drawableEvents.length;
|
|
||||||
|
|
||||||
// For retina displays so text and boxes will be crisp
|
|
||||||
devicePixelRatio = window.devicePixelRatio == "undefined" ? 1 : window.devicePixelRatio;
|
|
||||||
scaleRatio = devicePixelRatio / 1;
|
|
||||||
|
|
||||||
canvas.width = width * scaleRatio;
|
|
||||||
canvas.height = canvasHeight * scaleRatio;
|
|
||||||
|
|
||||||
canvas.style.width = width + 'px';
|
|
||||||
canvas.style.height = canvasHeight + 'px';
|
|
||||||
|
|
||||||
ctx.scale(scaleRatio, scaleRatio);
|
|
||||||
|
|
||||||
ctx.textBaseline = "middle";
|
|
||||||
ctx.lineWidth = 0;
|
|
||||||
|
|
||||||
// For each event, draw a line.
|
|
||||||
ctx.strokeStyle = getCssVarValue('--table-border');
|
|
||||||
|
|
||||||
// set the background color of the canvas
|
|
||||||
ctx.fillStyle = getCssVarValue('--table-background');
|
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
||||||
|
|
||||||
drawableEvents.forEach(function(event) {
|
|
||||||
event.periods.forEach(function(period) {
|
|
||||||
var timelineHeadPosition = x + period.start * ratio;
|
|
||||||
|
|
||||||
if (isChildEvent(event)) {
|
|
||||||
/* create a striped background dynamically */
|
|
||||||
var img = new Image();
|
|
||||||
img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKBAMAAAB/HNKOAAAAIVBMVEX////w8PDd7h7d7h7d7h7d7h7w8PDw8PDw8PDw8PDw8PAOi84XAAAAKUlEQVQImWNI71zAwMBQMYuBgY0BxExnADErGEDMTgYQE8hnAKtCZwIAlcMNSR9a1OEAAAAASUVORK5CYII=';
|
|
||||||
var pattern = ctx.createPattern(img, 'repeat');
|
|
||||||
|
|
||||||
ctx.fillStyle = pattern;
|
|
||||||
ctx.fillRect(timelineHeadPosition, 0, (period.end - period.start) * 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.start * ratio;
|
|
||||||
|
|
||||||
if (!isSectionEvent(event)) {
|
|
||||||
ctx.fillRect(timelineHeadPosition, h + 3, 2, 8);
|
|
||||||
ctx.fillRect(timelineHeadPosition, h, (period.end - period.start) * 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 = "#E0E0E0";
|
|
||||||
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 = getCssVarValue('--color-text');
|
|
||||||
ctx.font = "12px sans-serif";
|
|
||||||
text = event.name;
|
|
||||||
ms = " " + (event.duration < 1 ? event.duration : parseInt(event.duration, 10)) + " ms / " + event.memory + " MB";
|
|
||||||
if (x + event.starttime * ratio + ctx.measureText(text + ms).width > width) {
|
|
||||||
ctx.textAlign = "end";
|
|
||||||
ctx.font = "10px sans-serif";
|
|
||||||
ctx.fillStyle = getCssVarValue('--color-muted');
|
|
||||||
xc = x + event.endtime * ratio - 1;
|
|
||||||
ctx.fillText(ms, xc, h);
|
|
||||||
|
|
||||||
xc -= ctx.measureText(ms).width;
|
|
||||||
ctx.font = "12px sans-serif";
|
|
||||||
ctx.fillStyle = getCssVarValue('--color-text');
|
|
||||||
ctx.fillText(text, xc, h);
|
|
||||||
} else {
|
|
||||||
ctx.textAlign = "start";
|
|
||||||
ctx.font = "13px sans-serif";
|
|
||||||
ctx.fillStyle = getCssVarValue('--color-text');
|
|
||||||
xc = x + event.starttime * ratio + 1;
|
|
||||||
ctx.fillText(text, xc, h);
|
|
||||||
|
|
||||||
xc += ctx.measureText(text).width;
|
|
||||||
ctx.font = "11px sans-serif";
|
|
||||||
ctx.fillStyle = getCssVarValue('--color-muted');
|
|
||||||
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 = Sfjs.getPreference(_storagePrefix + 'threshold');
|
|
||||||
|
|
||||||
if (null === threshold) {
|
|
||||||
return _threshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
_threshold = parseInt(threshold);
|
|
||||||
|
|
||||||
return _threshold;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setThreshold = function(threshold)
|
|
||||||
{
|
|
||||||
_threshold = threshold;
|
|
||||||
|
|
||||||
Sfjs.setPreference(_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 = {
|
|
||||||
"max": {{ "%F"|format(collector.events.__section__.endtime) }},
|
|
||||||
"requests": [
|
|
||||||
{{ helper.dump_request_data(token, collector.events, collector.events.__section__.origin) }}
|
|
||||||
|
|
||||||
{% if profile.children|length %}
|
|
||||||
,
|
|
||||||
{% for child in profile.children %}
|
|
||||||
{{ helper.dump_request_data(child.token, child.getcollector('time').events, collector.events.__section__.origin) }}{{ loop.last ? '' : ',' }}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
window.setTimeout(function() {
|
|
||||||
canvasAutoUpdateOnThresholdChange(null);
|
|
||||||
}, 50);
|
|
||||||
|
|
||||||
//]]>{% endautoescape %}</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% macro dump_request_data(token, events, origin) %}
|
{% macro dump_request_data(token, events, origin) %}
|
||||||
{% autoescape 'js' %}
|
{% autoescape 'js' %}
|
||||||
{% from _self import dump_events %}
|
{% from _self import dump_events %}
|
||||||
{
|
{
|
||||||
"id": "{{ token }}",
|
id: "{{ token }}",
|
||||||
"left": {{ "%F"|format(events.__section__.origin - origin) }},
|
left: {{ "%F"|format(events.__section__.origin - origin) }},
|
||||||
"events": [
|
end: "{{ '%F'|format(events.__section__.endtime) }}",
|
||||||
{{ dump_events(events) }}
|
events: [ {{ dump_events(events) }} ],
|
||||||
]
|
}
|
||||||
}
|
|
||||||
{% endautoescape %}
|
{% endautoescape %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
@ -532,32 +173,45 @@
|
|||||||
{% autoescape 'js' %}
|
{% autoescape 'js' %}
|
||||||
{% for name, event in events %}
|
{% for name, event in events %}
|
||||||
{% if '__section__' != name %}
|
{% if '__section__' != name %}
|
||||||
{
|
{
|
||||||
"name": "{{ name }}",
|
name: "{{ name }}",
|
||||||
"category": "{{ event.category }}",
|
category: "{{ event.category }}",
|
||||||
"origin": {{ "%F"|format(event.origin) }},
|
origin: {{ "%F"|format(event.origin) }},
|
||||||
"starttime": {{ "%F"|format(event.starttime) }},
|
starttime: {{ "%F"|format(event.starttime) }},
|
||||||
"endtime": {{ "%F"|format(event.endtime) }},
|
endtime: {{ "%F"|format(event.endtime) }},
|
||||||
"duration": {{ "%F"|format(event.duration) }},
|
duration: {{ "%F"|format(event.duration) }},
|
||||||
"memory": {{ "%.1F"|format(event.memory / 1024 / 1024) }},
|
memory: {{ "%.1F"|format(event.memory / 1024 / 1024) }},
|
||||||
"periods": [
|
elements: {},
|
||||||
|
periods: [
|
||||||
{%- for period in event.periods -%}
|
{%- for period in event.periods -%}
|
||||||
{"start": {{ "%F"|format(period.starttime) }}, "end": {{ "%F"|format(period.endtime) }}}{{ loop.last ? '' : ', ' }}
|
{
|
||||||
|
start: {{ "%F"|format(period.starttime) }},
|
||||||
|
end: {{ "%F"|format(period.endtime) }},
|
||||||
|
duration: {{ "%F"|format(period.duration) }},
|
||||||
|
elements: {}
|
||||||
|
},
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
]
|
],
|
||||||
}{{ loop.last ? '' : ',' }}
|
},
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endautoescape %}
|
{% endautoescape %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro display_timeline(id, colors) %}
|
{% macro display_timeline(token, classnames, events, origin) %}
|
||||||
|
{% import _self as helper %}
|
||||||
<div class="sf-profiler-timeline">
|
<div class="sf-profiler-timeline">
|
||||||
<div class="legends">
|
<div id="legend-{{ token }}" class="legends"></div>
|
||||||
{% for category, color in colors %}
|
<svg id="timeline-{{ token }}" class="timeline-graph"></svg>
|
||||||
<span data-color="{{ color }}">{{ category }}</span>
|
<script>{% autoescape 'js' %}
|
||||||
{% endfor %}
|
window.addEventListener('load', function onLoad() {
|
||||||
</div>
|
new TimelineEngine(
|
||||||
<canvas width="680" height="" id="{{ id }}" class="timeline"></canvas>
|
new SvgRenderer(document.getElementById('timeline-{{ token }}')),
|
||||||
|
new Legend(document.getElementById('legend-{{ token }}'), {{ classnames|json_encode|raw }}),
|
||||||
|
document.getElementById('threshold'),
|
||||||
|
{{ helper.dump_request_data(token, events, origin) }}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
{% endautoescape %}</script>
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
@ -0,0 +1,412 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
class TimelineEngine {
|
||||||
|
/**
|
||||||
|
* @param {Renderer} renderer
|
||||||
|
* @param {Legend} legend
|
||||||
|
* @param {Element} threshold
|
||||||
|
* @param {Object} request
|
||||||
|
* @param {Number} eventHeight
|
||||||
|
* @param {Number} horizontalMargin
|
||||||
|
*/
|
||||||
|
constructor(renderer, legend, threshold, request, eventHeight = 36, horizontalMargin = 10) {
|
||||||
|
this.renderer = renderer;
|
||||||
|
this.legend = legend;
|
||||||
|
this.threshold = threshold;
|
||||||
|
this.request = request;
|
||||||
|
this.scale = renderer.width / request.end;
|
||||||
|
this.eventHeight = eventHeight;
|
||||||
|
this.horizontalMargin = horizontalMargin;
|
||||||
|
this.labelY = Math.round(this.eventHeight * 0.48);
|
||||||
|
this.periodY = Math.round(this.eventHeight * 0.66);
|
||||||
|
this.FqcnMatcher = /\\([^\\]+)$/i;
|
||||||
|
this.origin = null;
|
||||||
|
|
||||||
|
this.createEventElements = this.createEventElements.bind(this);
|
||||||
|
this.createBackground = this.createBackground.bind(this);
|
||||||
|
this.createPeriod = this.createPeriod.bind(this);
|
||||||
|
this.render = this.render.bind(this);
|
||||||
|
this.renderEvent = this.renderEvent.bind(this);
|
||||||
|
this.renderPeriod = this.renderPeriod.bind(this);
|
||||||
|
this.onResize = this.onResize.bind(this);
|
||||||
|
this.isActive = this.isActive.bind(this);
|
||||||
|
|
||||||
|
this.threshold.addEventListener('change', this.render);
|
||||||
|
this.legend.addEventListener('change', this.render);
|
||||||
|
|
||||||
|
window.addEventListener('resize', this.onResize);
|
||||||
|
|
||||||
|
this.createElements();
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
onResize() {
|
||||||
|
this.renderer.measure();
|
||||||
|
this.setScale(this.renderer.width / this.request.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
setScale(scale) {
|
||||||
|
if (scale !== this.scale) {
|
||||||
|
this.scale = scale;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createElements() {
|
||||||
|
this.origin = this.renderer.setFullVerticalLine(this.createBorder(), 0);
|
||||||
|
this.renderer.add(this.origin);
|
||||||
|
|
||||||
|
this.request.events
|
||||||
|
.filter(event => event.category === 'section')
|
||||||
|
.map(this.createBackground)
|
||||||
|
.forEach(this.renderer.add);
|
||||||
|
|
||||||
|
this.request.events
|
||||||
|
.map(this.createEventElements)
|
||||||
|
.forEach(this.renderer.add);
|
||||||
|
}
|
||||||
|
|
||||||
|
createBackground(event) {
|
||||||
|
const subrequest = event.name === '__section__.child';
|
||||||
|
const background = this.renderer.create('rect', subrequest ? 'timeline-subrequest' : 'timeline-border');
|
||||||
|
|
||||||
|
event.elements = Object.assign(event.elements || {}, { background });
|
||||||
|
|
||||||
|
return background;
|
||||||
|
}
|
||||||
|
|
||||||
|
createEventElements(event) {
|
||||||
|
const { name, category, duration, memory, periods } = event;
|
||||||
|
const border = this.renderer.setFullHorizontalLine(this.createBorder(), 0);
|
||||||
|
const lines = periods.map(period => this.createPeriod(period, category));
|
||||||
|
const label = this.createLabel(this.getShortName(name), duration, memory, periods[0]);
|
||||||
|
const title = this.renderer.createTitle(name);
|
||||||
|
const group = this.renderer.group([title, border, label].concat(lines), this.legend.getClassname(event.category));
|
||||||
|
|
||||||
|
event.elements = Object.assign(event.elements || {}, { group, label, border });
|
||||||
|
|
||||||
|
this.legend.add(event.category)
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
createLabel(name, duration, memory, period) {
|
||||||
|
const label = this.renderer.createText(name, period.start * this.scale, this.labelY, 'timeline-label');
|
||||||
|
const sublabel = this.renderer.createTspan(` ${duration} ms / ${memory} Mb`, 'timeline-sublabel');
|
||||||
|
|
||||||
|
label.appendChild(sublabel);
|
||||||
|
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
createPeriod(period, category) {
|
||||||
|
const timeline = this.renderer.createPath(null, 'timeline-period');
|
||||||
|
|
||||||
|
period.draw = category === 'section' ? this.renderer.setSectionLine : this.renderer.setPeriodLine;
|
||||||
|
period.elements = Object.assign(period.elements || {}, { timeline });
|
||||||
|
|
||||||
|
return timeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
createBorder() {
|
||||||
|
return this.renderer.createPath(null, 'timeline-border');
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive(event) {
|
||||||
|
const { duration, category } = event;
|
||||||
|
|
||||||
|
return duration >= this.threshold.value && this.legend.isActive(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const events = this.request.events.filter(this.isActive);
|
||||||
|
const width = this.renderer.width + this.horizontalMargin * 2;
|
||||||
|
const height = this.eventHeight * events.length;
|
||||||
|
|
||||||
|
// Set view box
|
||||||
|
this.renderer.setViewBox(-this.horizontalMargin, 0, width, height);
|
||||||
|
|
||||||
|
// Show 0ms origin
|
||||||
|
this.renderer.setFullVerticalLine(this.origin, 0);
|
||||||
|
|
||||||
|
// Render all events
|
||||||
|
this.request.events.forEach(event => this.renderEvent(event, events.indexOf(event)));
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEvent(event, index) {
|
||||||
|
const { name, category, duration, memory, periods, elements } = event;
|
||||||
|
const { group, label, border, background } = elements;
|
||||||
|
const visible = index >= 0;
|
||||||
|
|
||||||
|
group.setAttribute('visibility', visible ? 'visible' : 'hidden');
|
||||||
|
|
||||||
|
if (background) {
|
||||||
|
background.setAttribute('visibility', visible ? 'visible' : 'hidden');
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
const [min, max] = this.getEventLimits(event);
|
||||||
|
|
||||||
|
this.renderer.setFullRectangle(background, min * this.scale, max * this.scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
// Position the group
|
||||||
|
group.setAttribute('transform', `translate(0, ${index * this.eventHeight})`);
|
||||||
|
|
||||||
|
// Update top border
|
||||||
|
this.renderer.setFullHorizontalLine(border, 0);
|
||||||
|
|
||||||
|
// render label and ensure it doesn't escape the viewport
|
||||||
|
this.renderLabel(label, event);
|
||||||
|
|
||||||
|
// Update periods
|
||||||
|
periods.forEach(this.renderPeriod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLabel(label, event) {
|
||||||
|
const width = this.getLabelWidth(label);
|
||||||
|
const [min, max] = this.getEventLimits(event);
|
||||||
|
const alignLeft = (min * this.scale) + width <= this.renderer.width;
|
||||||
|
|
||||||
|
label.setAttribute('x', (alignLeft ? min : max) * this.scale);
|
||||||
|
label.setAttribute('text-anchor', alignLeft ? 'start' : 'end');
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPeriod(period) {
|
||||||
|
const { elements, start, duration } = period;
|
||||||
|
|
||||||
|
period.draw(elements.timeline, start * this.scale, this.periodY, Math.max(duration * this.scale, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
getLabelWidth(label) {
|
||||||
|
if (typeof label.width === 'undefined') {
|
||||||
|
label.width = label.getBBox().width;
|
||||||
|
}
|
||||||
|
|
||||||
|
return label.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEventLimits(event) {
|
||||||
|
if (typeof event.limits === 'undefined') {
|
||||||
|
const { periods } = event;
|
||||||
|
|
||||||
|
event.limits = [
|
||||||
|
periods[0].start,
|
||||||
|
periods[periods.length - 1].end
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return event.limits;
|
||||||
|
}
|
||||||
|
|
||||||
|
getShortName(name) {
|
||||||
|
const matches = this.FqcnMatcher.exec(name);
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
return matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Legend {
|
||||||
|
constructor(element, classnames) {
|
||||||
|
this.element = element;
|
||||||
|
this.classnames = classnames;
|
||||||
|
|
||||||
|
this.toggle = this.toggle.bind(this);
|
||||||
|
this.createCategory = this.createCategory.bind(this);
|
||||||
|
|
||||||
|
this.categories = Array.from(Object.keys(classnames)).map(this.createCategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
add(category) {
|
||||||
|
this.get(category).classList.add('present');
|
||||||
|
}
|
||||||
|
|
||||||
|
createCategory(category) {
|
||||||
|
const element = document.createElement('button');
|
||||||
|
|
||||||
|
element.className = `timeline-category ${this.getClassname(category)} active`;
|
||||||
|
element.innerText = category;
|
||||||
|
element.value = category;
|
||||||
|
element.type = 'button';
|
||||||
|
element.addEventListener('click', this.toggle);
|
||||||
|
|
||||||
|
this.element.appendChild(element);
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle(event) {
|
||||||
|
event.target.classList.toggle('active');
|
||||||
|
|
||||||
|
this.emit('change');
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive(category) {
|
||||||
|
return this.get(category).classList.contains('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
get(category) {
|
||||||
|
return this.categories.find(element => element.value === category);
|
||||||
|
}
|
||||||
|
|
||||||
|
getClassname(category) {
|
||||||
|
return this.classnames[category];
|
||||||
|
}
|
||||||
|
|
||||||
|
getSectionClassname() {
|
||||||
|
return this.classnames.section;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultClassname() {
|
||||||
|
return this.classnames.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStandardClassenames() {
|
||||||
|
return Array.from(Object.values(this.classnames))
|
||||||
|
.filter(className => className !== this.getSectionClassname());
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(name) {
|
||||||
|
this.element.dispatchEvent(new Event(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener(name, callback) {
|
||||||
|
this.element.addEventListener(name, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeEventListener(name, callback) {
|
||||||
|
this.element.removeEventListener(name, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SvgRenderer {
|
||||||
|
/**
|
||||||
|
* @param {SVGElement} element
|
||||||
|
*/
|
||||||
|
constructor(element) {
|
||||||
|
this.ns = 'http://www.w3.org/2000/svg';
|
||||||
|
this.width = null;
|
||||||
|
this.viewBox = {};
|
||||||
|
this.element = element;
|
||||||
|
|
||||||
|
this.add = this.add.bind(this);
|
||||||
|
|
||||||
|
this.setViewBox(0, 0, 0, 0);
|
||||||
|
this.measure();
|
||||||
|
}
|
||||||
|
|
||||||
|
setViewBox(x, y, width, height) {
|
||||||
|
this.viewBox = { x, y, width, height };
|
||||||
|
this.element.setAttribute('viewBox', `${x} ${y} ${width} ${height}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
measure() {
|
||||||
|
this.width = this.element.getBoundingClientRect().width;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(element) {
|
||||||
|
this.element.appendChild(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
group(elements, className) {
|
||||||
|
const group = this.create('g', className);
|
||||||
|
|
||||||
|
elements.forEach(element => group.appendChild(element));
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
setHorizontalLine(element, x, y, width) {
|
||||||
|
element.setAttribute('d', `M${x},${y} h${width}`);
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVerticalLine(element, x, y, height) {
|
||||||
|
element.setAttribute('d', `M${x},${y} v${height}`);
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFullHorizontalLine(element, y) {
|
||||||
|
return this.setHorizontalLine(element, this.viewBox.x, y, this.viewBox.width);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFullVerticalLine(element, x) {
|
||||||
|
return this.setVerticalLine(element, x, this.viewBox.y, this.viewBox.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFullRectangle(element, min, max) {
|
||||||
|
element.setAttribute('x', min);
|
||||||
|
element.setAttribute('y', this.viewBox.y);
|
||||||
|
element.setAttribute('width', max - min);
|
||||||
|
element.setAttribute('height', this.viewBox.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSectionLine(element, x, y, width, height = 4, markerSize = 6) {
|
||||||
|
const totalHeight = height + markerSize;
|
||||||
|
const maxMarkerWidth = Math.min(markerSize, width / 2);
|
||||||
|
const widthWithoutMarker = Math.max(0, width - (maxMarkerWidth * 2));
|
||||||
|
|
||||||
|
element.setAttribute('d', `M${x},${y + totalHeight} v${-totalHeight} h${width} v${totalHeight} l${-maxMarkerWidth} ${-markerSize} h${-widthWithoutMarker} Z`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPeriodLine(element, x, y, width, height = 4, markerWidth = 2, markerHeight = 4) {
|
||||||
|
const totalHeight = height + markerHeight;
|
||||||
|
const maxMarkerWidth = Math.min(markerWidth, width);
|
||||||
|
|
||||||
|
element.setAttribute('d', `M${x + maxMarkerWidth},${y + totalHeight} h${-maxMarkerWidth} v${-totalHeight} h${width} v${height} h${maxMarkerWidth-width}Z`);
|
||||||
|
}
|
||||||
|
|
||||||
|
createText(content, x, y, className) {
|
||||||
|
const element = this.create('text', className);
|
||||||
|
|
||||||
|
element.setAttribute('x', x);
|
||||||
|
element.setAttribute('y', y);
|
||||||
|
element.textContent = content;
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
createTspan(content, className) {
|
||||||
|
const element = this.create('tspan', className);
|
||||||
|
|
||||||
|
element.textContent = content;
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
createTitle(content) {
|
||||||
|
const element = this.create('title');
|
||||||
|
|
||||||
|
element.textContent = content;
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
createPath(path = null, className = null) {
|
||||||
|
const element = this.create('path', className);
|
||||||
|
|
||||||
|
if (path) {
|
||||||
|
element.setAttribute('d', path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
create(name, className = null) {
|
||||||
|
const element = document.createElementNS(this.ns, name);
|
||||||
|
|
||||||
|
if (className) {
|
||||||
|
element.setAttribute('class', className);
|
||||||
|
}
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
}
|
@ -836,7 +836,7 @@ tr.status-warning td {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
width: 40px;
|
width: 5em;
|
||||||
}
|
}
|
||||||
#timeline-control .help {
|
#timeline-control .help {
|
||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
@ -846,15 +846,6 @@ tr.status-warning td {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
.sf-profiler-timeline .legends span {
|
|
||||||
border-left: solid 14px;
|
|
||||||
padding: 0 10px 0 5px;
|
|
||||||
}
|
|
||||||
.sf-profiler-timeline canvas {
|
|
||||||
border: 1px solid var(--table-border);
|
|
||||||
background: var(--page-background);
|
|
||||||
margin: .5em 0;
|
|
||||||
}
|
|
||||||
.sf-profiler-timeline + p.help {
|
.sf-profiler-timeline + p.help {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user