diff --git a/sources/lib/Controller/PommProfilerController.php b/sources/lib/Controller/PommProfilerController.php index 5226dd8..6cf8b75 100644 --- a/sources/lib/Controller/PommProfilerController.php +++ b/sources/lib/Controller/PommProfilerController.php @@ -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'; @@ -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), diff --git a/sources/lib/Twig/Extension.php b/sources/lib/Twig/Extension.php new file mode 100644 index 0000000..84d476d --- /dev/null +++ b/sources/lib/Twig/Extension.php @@ -0,0 +1,58 @@ +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; + } +} diff --git a/views/Profiler/db.html.twig b/views/Profiler/db.html.twig index 2062159..24876b4 100644 --- a/views/Profiler/db.html.twig +++ b/views/Profiler/db.html.twig @@ -79,7 +79,15 @@ - Explain query ] + + [ + + + - + Explain graph + ] +
+
{% endif %} @@ -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; + } {% endblock %} diff --git a/views/Profiler/graph.html.twig b/views/Profiler/graph.html.twig new file mode 100644 index 0000000..dd5dfde --- /dev/null +++ b/views/Profiler/graph.html.twig @@ -0,0 +1,97 @@ +{% macro progress(percent, title) %} +
+
{{ title }}
+
+{% endmacro %} + +{% macro info(data) %} +
+ {% if data['Node Type'] == 'Sort' and data['Sort Key'] is defined %} + by {{ data['Sort Key'] | join(',') }} + {% elseif data['Node Type'] == 'Aggregate' and data['Group Key'] is defined %} + by {{ data['Group Key'] | join(',') }} + {% elseif data['Node Type'] == 'Seq Scan' or data['Node Type'] == 'Index Only Scan' %} + on {{ data['Schema'] }}.{{ data['Relation Name'] }}({{ data['Alias'] }}) + {% elseif data['Node Type'] == 'Hash Join' %} +
+ {% if data['Join Type'] is defined %} + {{ data['Join Type'] }} join + {% endif %} +
+
+ {% if data['Hash Cond'] is defined %} + on {{ data['Hash Cond'] }} + {% endif %} +
+ {% endif %} +
+{% endmacro %} + +{% macro detail(data) %} +
+ x + + + {% for key, value in data %} + {% if key != 'Plans' %} + + + {% if value is same as(true) %} + + {% elseif value is same as(false) %} + + {% else %} + + {% endif %} + + {% endif %} + {% endfor %} + +
{{ key }}truefalse{{ value | join(', ') }}
+
+{% endmacro %} + +{% macro plan(root, data) %} + {% import _self as self %} + + {% set id = random() %} +
  • +
    + {{ data['Node Type'] }} + {% set percent = data['Total Cost'] / root['Total Cost'] * 100 %} + + {{ self.info(data) }} + {{ self.progress(percent, "Score: " ~ data['Total Cost']) }} +
    Rows: {{ data['Plan Rows'] }}
    +
    + {{ self.detail(data) }} +
    +
    + + {% if data['Plans'] is defined %} + + {% endif %} +
  • +{% endmacro %} + +{% block content %} + {% import _self as self %} + +
    + +
    +{% endblock %}