Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display explain as graph #37

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions sources/lib/Controller/PommProfilerController.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,25 @@ public function __construct(
* @return Response
*/
public function explainAction(Request $request, $token, $index_query)
{
return $this->explain($request, $token, $index_query, 'raw');
}

/**
* Controller to explain a SQL query as graph.
*
* @param $request
* @param string $token
* @param int $index_query
*
* @return Response
*/
public function graphAction(Request $request, $token, $index_query)
{
return $this->explain($request, $token, $index_query, 'json');
}

public function explain(Request $request, $token, $index_query, $format)
{
$panel = 'pomm';
$page = 'home';
Expand Down Expand Up @@ -81,11 +100,24 @@ public function explainAction(Request $request, $token, $index_query)

$query_data = $profile->getCollector($panel)->getQueries()[$index_query];

$explain = 'explain';

if ($format === 'json') {
$explain .= ' (COSTS, VERBOSE, FORMAT JSON)';
}

$explain = $this->pomm[$query_data['session_stamp']]
->getClientUsingPooler('query_manager', null)
->query(sprintf("explain %s", $query_data['sql']), $query_data['parameters']);
->query(sprintf("%s %s", $explain, $query_data['sql']), $query_data['parameters']);

if ($format === 'json') {
$template = '@Pomm/Profiler/graph.html.twig';
}
else {
$template = '@Pomm/Profiler/explain.html.twig';
}

return new Response($this->twig->render('@Pomm/Profiler/explain.html.twig', array(
return new Response($this->twig->render($template, array(
'token' => $token,
'profile' => $profile,
'collector' => $profile->getCollector($panel),
Expand Down
58 changes: 58 additions & 0 deletions sources/lib/Twig/Extension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace PommProject\SymfonyBridge\Twig;

final class Extension extends \Twig\Extension\AbstractExtension
{
public function getFunctions()
{
return array(
new \Twig\TwigFunction('color', array($this, 'color')),
);
}

/*
* https://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
*/
public function color($percent)
{
$hue = (100 - $percent) * 1.2 / 360;
$rgb = $this->hslToRgb($hue, .9, .4);
return sprintf("rgb(%d, %d, %d)", $rgb[0], $rgb[1], $rgb[2]);
}

private function hslToRgb($h, $s, $l)
{
$r = $g = $b = $l;

if ($s !== 0) {
$q = $l < 0.5 ? $l * (1 + $s) : $l + $s - $l * $s;
$p = 2 * $l - $q;
$r = $this->hue2rgb($p, $q, $h + 1 / 3);
$g = $this->hue2rgb($p, $q, $h);
$b = $this->hue2rgb($p, $q, $h - 1 / 3);
}

return array($r * 255, $g * 255, $b * 255);
}

private function hue2rgb($p, $q, $t)
{
if ($t < 0) {
$t += 1;
}
if ($t > 1) {
$t -= 1;
}
if ($t < 1 / 6) {
return $p + ($q - $p) * 6 * $t;
}
if ($t < 1 / 2) {
return $q;
}
if ($t < 2 / 3) {
return $p + ($q - $p) * (2 / 3 - $t) * 6;
}
return $p;
}
}
168 changes: 168 additions & 0 deletions views/Profiler/db.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,15 @@
<img alt="-" src="data:image/gif;base64,R0lGODlhEgASAMQSANft94TG57Hb8GS44ez1+mC24IvK6ePx+Wa44dXs92+942e54o3L6W2844/M6dnu+P/+/l614P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAABIALAAAAAASABIAQAVCoCQBTBOd6Kk4gJhGBCTPxysJb44K0qD/ER/wlxjmisZkMqBEBW5NHrMZmVKvv9hMVsO+hE0EoNAstEYGxG9heIhCADs=" style="display: none; width: 12px; height: 12px;" />
<span style="vertical-align:top">Explain query</span>
</a>]

[<a href="{{ path('_pomm_profiler_graph', { 'token': token, "index_query": i}) }}" onclick="return explain(this);" style="text-decoration: none;" title="Explains the query as graph" data-target-id="graph-{{ i }}" >
<img alt="+" src="data:image/gif;base64,R0lGODlhEgASAMQTANft99/v+Ga44bHb8ITG52S44dXs9+z1+uPx+YvK6WC24G+944/M6W28443L6dnu+Ge54v/+/l614P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAABMALAAAAAASABIAQAVS4DQBTiOd6LkwgJgeUSzHSDoNaZ4PU6FLgYBA5/vFID/DbylRGiNIZu74I0h1hNsVxbNuUV4d9SsZM2EzWe1qThVzwWFOAFCQFa1RQq6DJB4iIQA7" style="display: inline; width: 12px; height: 12px;" />
<img alt="-" src="data:image/gif;base64,R0lGODlhEgASAMQSANft94TG57Hb8GS44ez1+mC24IvK6ePx+Wa44dXs92+942e54o3L6W2844/M6dnu+P/+/l614P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAABIALAAAAAASABIAQAVCoCQBTBOd6Kk4gJhGBCTPxysJb44K0qD/ER/wlxjmisZkMqBEBW5NHrMZmVKvv9hMVsO+hE0EoNAstEYGxG9heIhCADs=" style="display: none; width: 12px; height: 12px;" />
<span style="vertical-align:top">Explain graph</span>
</a>]

<div id="explain-{{ i }}" class="loading"></div>
<div id="graph-{{ i }}" class="loading"></div>
{% endif %}
</td>
</tr>
Expand Down Expand Up @@ -209,5 +217,165 @@
code.explain{
display: block;
}

.explain {
line-height: 1.5;
}

table tbody .explain div {
margin: 0;
}

.explain a {
color: #007BFF;
text-decoration: none;
background-color: transparent;
}

.explain .progress {
background-color: #E9ECEF;
border-radius: .25rem;
font-size: .75rem;
height: 1rem;
overflow: hidden;
}

.explain .progress-bar {
color: black;
}

.explain .text-muted {
color: #6C757D !important;
}

.explain .close {
cursor: pointer;
color: #000;
text-shadow: 0 1px 0 #FFF;
opacity: .5;
font-size: 1.5rem;
font-weight: 700;
line-height: 1;
float: right;
}

.explain ul {
position: relative;
padding: 1em 0;
white-space: nowrap;
margin: 0 auto;
text-align: center;
}

.explain ul::after {
content: '';
display: table;
clear: both;
}

.explain ul::before {
content: '';
position: absolute;
top: 0;
left: 50%;
border-left: 1px solid #DEDEDE;
width: 0;
height: 1em;
}

.explain li {
display: inline-block;
vertical-align: top;
text-align: center;
list-style-type: none;
position: relative;
padding: 1em .5em 0 .5em;
margin: 0 -4px 0 -4px;
}

.explain li::before, .explain li::after {
content: '';
position: absolute;
top: 0;
right: 50%;
border-top: 1px solid #DEDEDE;
width: 50%;
height: 1em;
}

.explain li::after {
right: auto;
left: 50%;
border-left: 1px solid #DEDEDE;
}

.explain li:only-child::after, .explain li:only-child::before {
display: none;
}

.explain li:only-child {
padding-top: 0;
}

.explain li:first-child::before, .explain li:last-child::after {
border: 0 none;
}

.explain li:last-child::before {
border-right: 1px solid #DEDEDE;
border-radius: 0 5px 0 0;
}

.explain li:first-child::after {
border-radius: 5px 0 0 0;
}

.explain li .node {
border: 1px solid #DEDEDE;
padding: .5em .75em;
text-decoration: none;
display: inline-block;
border-radius: 5px;
color: #333;
position: relative;
top: 1px;
box-shadow: 1px 1px 3px 0px rgba(0, 0, 0, 0.1);
}

.explain th, .explain td {
padding: .75rem;
border-top: 1px solid #E0E0E0;
}

.explain td:last-child {
white-space: normal;
}

.explain .rows {
font-size: .75rem;
text-align: left;
}

.explain .detail {
display: none;
text-align: left;
}

.explain .detail:target {
z-index: 2;
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .5);
}

.explain .detail div {
width: 450px;
margin: 1em auto 0 auto;
background-color: white;
}
</style>
{% endblock %}
97 changes: 97 additions & 0 deletions views/Profiler/graph.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
{% macro progress(percent, title) %}
<div class="progress">
<div
class="progress-bar"
role="progressbar"
style="width: {{ percent }}%; background-color: {{ color(percent) }};"
ria-valuenow="{{ percent }}"
aria-valuemin="0"
aria-valuemax="100"
>{{ title }}</div>
</div>
{% endmacro %}

{% macro info(data) %}
<div class="info">
{% if data['Node Type'] == 'Sort' and data['Sort Key'] is defined %}
<span class="text-muted">by</span> {{ data['Sort Key'] | join(',') }}
{% elseif data['Node Type'] == 'Aggregate' and data['Group Key'] is defined %}
<span class="text-muted">by</span> {{ data['Group Key'] | join(',') }}
{% elseif data['Node Type'] == 'Seq Scan' or data['Node Type'] == 'Index Only Scan' %}
<span class="text-muted">on</span> {{ data['Schema'] }}.{{ data['Relation Name'] }}({{ data['Alias'] }})
{% elseif data['Node Type'] == 'Hash Join' %}
<div>
{% if data['Join Type'] is defined %}
{{ data['Join Type'] }} <span class="text-muted">join</span>
{% endif %}
</div>
<div>
{% if data['Hash Cond'] is defined %}
<span class="text-muted">on</span> {{ data['Hash Cond'] }}
{% endif %}
</div>
{% endif %}
</div>
{% endmacro %}

{% macro detail(data) %}
<div>
<a href="#" class="close">x</a>
<table class="table table-sm">
<tbody>
{% for key, value in data %}
{% if key != 'Plans' %}
<tr>
<td>{{ key }}</td>
{% if value is same as(true) %}
<td>true</td>
{% elseif value is same as(false) %}
<td>false</td>
{% else %}
<td>{{ value | join(', ') }}</td>
{% endif %}
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
{% endmacro %}

{% macro plan(root, data) %}
{% import _self as self %}

{% set id = random() %}
<li>
<div class="node">
<a href="#node-{{ id }}">{{ data['Node Type'] }}</a>
{% set percent = data['Total Cost'] / root['Total Cost'] * 100 %}

{{ self.info(data) }}
{{ self.progress(percent, "Score: " ~ data['Total Cost']) }}
<div class="rows">Rows: {{ data['Plan Rows'] }}</div>
<div class="detail" id="node-{{ id }}">
{{ self.detail(data) }}
</div>
</div>

{% if data['Plans'] is defined %}
<ul>
{% for plan in data['Plans'] %}
{{ self.plan(root, plan) }}
{% endfor %}
</ul>
{% endif %}
</li>
{% endmacro %}

{% block content %}
{% import _self as self %}

<div class="explain">
<ul>
{% set plan = explain.get(0)['QUERY PLAN'] %}
{{ self.plan(plan[0]['Plan'], plan[0]['Plan']) }}
</ul>
</div>
{% endblock %}