db.html.twig 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. {% extends request.isXmlHttpRequest ? '@WebProfiler/Profiler/ajax_layout.html.twig' : '@WebProfiler/Profiler/layout.html.twig' %}
  2. {% import _self as helper %}
  3. {% block toolbar %}
  4. {% if collector.querycount > 0 or collector.invalidEntityCount > 0 %}
  5. {% set icon %}
  6. {% set status = collector.invalidEntityCount > 0 ? 'red' : collector.querycount > 50 ? 'yellow' %}
  7. {{ include('@Doctrine/Collector/icon.svg') }}
  8. {% if collector.querycount == 0 and collector.invalidEntityCount > 0 %}
  9. <span class="sf-toolbar-value">{{ collector.invalidEntityCount }}</span>
  10. <span class="sf-toolbar-label">errors</span>
  11. {% else %}
  12. <span class="sf-toolbar-value">{{ collector.querycount }}</span>
  13. <span class="sf-toolbar-info-piece-additional-detail">
  14. <span class="sf-toolbar-label">in</span>
  15. <span class="sf-toolbar-value">{{ '%0.2f'|format(collector.time * 1000) }}</span>
  16. <span class="sf-toolbar-label">ms</span>
  17. </span>
  18. {% endif %}
  19. {% endset %}
  20. {% set text %}
  21. <div class="sf-toolbar-info-piece">
  22. <b>Database Queries</b>
  23. <span class="sf-toolbar-status {{ collector.querycount > 50 ? 'sf-toolbar-status-yellow' : '' }}">{{ collector.querycount }}</span>
  24. </div>
  25. <div class="sf-toolbar-info-piece">
  26. <b>Different statements</b>
  27. <span class="sf-toolbar-status">{{ collector.groupedQueryCount }}</span>
  28. </div>
  29. <div class="sf-toolbar-info-piece">
  30. <b>Query time</b>
  31. <span>{{ '%0.2f'|format(collector.time * 1000) }} ms</span>
  32. </div>
  33. <div class="sf-toolbar-info-piece">
  34. <b>Invalid entities</b>
  35. <span class="sf-toolbar-status {{ collector.invalidEntityCount > 0 ? 'sf-toolbar-status-red' : '' }}">{{ collector.invalidEntityCount }}</span>
  36. </div>
  37. {% if collector.cacheEnabled %}
  38. <div class="sf-toolbar-info-piece">
  39. <b>Cache hits</b>
  40. <span class="sf-toolbar-status sf-toolbar-status-green">{{ collector.cacheHitsCount }}</span>
  41. </div>
  42. <div class="sf-toolbar-info-piece">
  43. <b>Cache misses</b>
  44. <span class="sf-toolbar-status {{ collector.cacheMissesCount > 0 ? 'sf-toolbar-status-yellow' : '' }}">{{ collector.cacheMissesCount }}</span>
  45. </div>
  46. <div class="sf-toolbar-info-piece">
  47. <b>Cache puts</b>
  48. <span class="sf-toolbar-status {{ collector.cachePutsCount > 0 ? 'sf-toolbar-status-yellow' : '' }}">{{ collector.cachePutsCount }}</span>
  49. </div>
  50. {% else %}
  51. <div class="sf-toolbar-info-piece">
  52. <b>Second Level Cache</b>
  53. <span class="sf-toolbar-status">disabled</span>
  54. </div>
  55. {% endif %}
  56. {% endset %}
  57. {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status|default('') }) }}
  58. {% endif %}
  59. {% endblock %}
  60. {% block menu %}
  61. <span class="label {{ collector.invalidEntityCount > 0 ? 'label-status-error' }} {{ collector.querycount == 0 ? 'disabled' }}">
  62. <span class="icon">{{ include('@Doctrine/Collector/icon.svg') }}</span>
  63. <strong>Doctrine</strong>
  64. {% if collector.invalidEntityCount %}
  65. <span class="count">
  66. <span>{{ collector.invalidEntityCount }}</span>
  67. </span>
  68. {% endif %}
  69. </span>
  70. {% endblock %}
  71. {% block panel %}
  72. {% if 'explain' == page %}
  73. {{ render(controller('Doctrine\\Bundle\\DoctrineBundle\\Controller\\ProfilerController::explainAction', {
  74. token: token,
  75. panel: 'db',
  76. connectionName: request.query.get('connection'),
  77. query: request.query.get('query')
  78. })) }}
  79. {% else %}
  80. {{ block('queries') }}
  81. {% endif %}
  82. {% endblock %}
  83. {% block queries %}
  84. <style>
  85. .time-container { position: relative; }
  86. .time-container .nowrap { position: relative; z-index: 1; text-shadow: 0 0 2px #fff; }
  87. .time-bar { display: block; position: absolute; top: 0; left: 0; bottom: 0; background: #e0e0e0; }
  88. .sql-runnable.sf-toggle-content.sf-toggle-visible { display: flex; flex-direction: column; }
  89. .sql-runnable button { align-self: end; }
  90. </style>
  91. <h2>Query Metrics</h2>
  92. <div class="metrics">
  93. <div class="metric">
  94. <span class="value">{{ collector.querycount }}</span>
  95. <span class="label">Database Queries</span>
  96. </div>
  97. <div class="metric">
  98. <span class="value">{{ collector.groupedQueryCount }}</span>
  99. <span class="label">Different statements</span>
  100. </div>
  101. <div class="metric">
  102. <span class="value">{{ '%0.2f'|format(collector.time * 1000) }} ms</span>
  103. <span class="label">Query time</span>
  104. </div>
  105. <div class="metric">
  106. <span class="value">{{ collector.invalidEntityCount }}</span>
  107. <span class="label">Invalid entities</span>
  108. </div>
  109. {% if collector.cacheEnabled %}
  110. <div class="metric">
  111. <span class="value">{{ collector.cacheHitsCount }}</span>
  112. <span class="label">Cache hits</span>
  113. </div>
  114. <div class="metric">
  115. <span class="value">{{ collector.cacheMissesCount }}</span>
  116. <span class="label">Cache misses</span>
  117. </div>
  118. <div class="metric">
  119. <span class="value">{{ collector.cachePutsCount }}</span>
  120. <span class="label">Cache puts</span>
  121. </div>
  122. {% endif %}
  123. </div>
  124. {% set group_queries = request.query.getBoolean('group') %}
  125. {% if group_queries %}
  126. <h2>Grouped Statements</h2>
  127. <p><a href="{{ path('_profiler', { panel: 'db', token: token }) }}">Show all queries</a></p>
  128. {% else %}
  129. <h2>Queries</h2>
  130. <p><a href="{{ path('_profiler', { panel: 'db', token: token, group: true }) }}">Group similar statements</a></p>
  131. {% endif %}
  132. {% for connection, queries in collector.queries %}
  133. {% if collector.connections|length > 1 %}
  134. <h3>{{ connection }} <small>connection</small></h3>
  135. {% endif %}
  136. {% if queries is empty %}
  137. <div class="empty">
  138. <p>No database queries were performed.</p>
  139. </div>
  140. {% else %}
  141. {% if group_queries %}
  142. {% set queries = collector.groupedQueries[connection] %}
  143. {% endif %}
  144. <table class="alt queries-table">
  145. <thead>
  146. <tr>
  147. {% if group_queries %}
  148. <th class="nowrap" onclick="javascript:sortTable(this, 0, 'queries-{{ loop.index }}')" data-sort-direction="1" style="cursor: pointer;">Time<span class="text-muted">&#9660;</span></th>
  149. <th class="nowrap" onclick="javascript:sortTable(this, 1, 'queries-{{ loop.index }}')" style="cursor: pointer;">Count<span></span></th>
  150. {% else %}
  151. <th class="nowrap" onclick="javascript:sortTable(this, 0, 'queries-{{ loop.index }}')" data-sort-direction="-1" style="cursor: pointer;">#<span class="text-muted">&#9650;</span></th>
  152. <th class="nowrap" onclick="javascript:sortTable(this, 1, 'queries-{{ loop.index }}')" style="cursor: pointer;">Time<span></span></th>
  153. {% endif %}
  154. <th style="width: 100%;">Info</th>
  155. </tr>
  156. </thead>
  157. <tbody id="queries-{{ loop.index }}">
  158. {% for i, query in queries %}
  159. {% set i = group_queries ? query.index : i %}
  160. <tr id="queryNo-{{ i }}-{{ loop.parent.loop.index }}">
  161. {% if group_queries %}
  162. <td class="time-container">
  163. <span class="time-bar" style="width:{{ '%0.2f'|format(query.executionPercent) }}%"></span>
  164. <span class="nowrap">{{ '%0.2f'|format(query.executionMS * 1000) }}&nbsp;ms<br />({{ '%0.2f'|format(query.executionPercent) }}%)</span>
  165. </td>
  166. <td class="nowrap">{{ query.count }}</td>
  167. {% else %}
  168. <td class="nowrap">{{ loop.index }}</td>
  169. <td class="nowrap">{{ '%0.2f'|format(query.executionMS * 1000) }}&nbsp;ms</td>
  170. {% endif %}
  171. <td>
  172. {{ query.sql|doctrine_prettify_sql }}
  173. <div>
  174. <strong class="font-normal text-small">Parameters</strong>: {{ profiler_dump(query.params, 2) }}
  175. </div>
  176. <div class="text-small font-normal">
  177. <a href="#" class="sf-toggle link-inverse" data-toggle-selector="#formatted-query-{{ i }}-{{ loop.parent.loop.index }}" data-toggle-alt-content="Hide formatted query">View formatted query</a>
  178. {% if query.runnable %}
  179. &nbsp;&nbsp;
  180. <a href="#" class="sf-toggle link-inverse" data-toggle-selector="#original-query-{{ i }}-{{ loop.parent.loop.index }}" data-toggle-alt-content="Hide runnable query">View runnable query</a>
  181. {% endif %}
  182. {% if query.explainable %}
  183. &nbsp;&nbsp;
  184. <a class="link-inverse" href="{{ path('_profiler', { panel: 'db', token: token, page: 'explain', connection: connection, query: i }) }}" onclick="return explain(this);" data-target-id="explain-{{ i }}-{{ loop.parent.loop.index }}">Explain query</a>
  185. {% endif %}
  186. {% if query.backtrace is defined %}
  187. &nbsp;&nbsp;
  188. <a href="#" class="sf-toggle link-inverse" data-toggle-selector="#backtrace-{{ i }}-{{ loop.parent.loop.index }}" data-toggle-alt-content="Hide query backtrace">View query backtrace</a>
  189. {% endif %}
  190. </div>
  191. <div id="formatted-query-{{ i }}-{{ loop.parent.loop.index }}" class="sql-runnable hidden">
  192. {{ query.sql|doctrine_format_sql(highlight = true) }}
  193. <button class="btn btn-sm hidden" data-clipboard-text="{{ query.sql|doctrine_format_sql(highlight = false)|e('html_attr') }}">Copy</button>
  194. </div>
  195. {% if query.runnable %}
  196. <div id="original-query-{{ i }}-{{ loop.parent.loop.index }}" class="sql-runnable hidden">
  197. {% set runnable_sql = (query.sql ~ ';')|doctrine_replace_query_parameters(query.params) %}
  198. {{ runnable_sql|doctrine_prettify_sql }}
  199. <button class="btn btn-sm hidden" data-clipboard-text="{{ runnable_sql|e('html_attr') }}">Copy</button>
  200. </div>
  201. {% endif %}
  202. {% if query.explainable %}
  203. <div id="explain-{{ i }}-{{ loop.parent.loop.index }}" class="sql-explain"></div>
  204. {% endif %}
  205. {% if query.backtrace is defined %}
  206. <div id="backtrace-{{ i }}-{{ loop.parent.loop.index }}" class="hidden">
  207. <table>
  208. <thead>
  209. <tr>
  210. <th scope="col">#</th>
  211. <th scope="col">File/Call</th>
  212. </tr>
  213. </thead>
  214. <tbody>
  215. {% for trace in query.backtrace %}
  216. <tr>
  217. <td>{{ loop.index }}</td>
  218. <td>
  219. <span class="text-small">
  220. {% set line_number = trace.line|default(1) %}
  221. {% if trace.file is defined %}
  222. <a href="{{ trace.file|file_link(line_number) }}">
  223. {% endif %}
  224. {{- trace.class|default ~ (trace.class is defined ? trace.type|default('::')) -}}
  225. <span class="status-warning">{{ trace.function }}</span>
  226. {% if trace.file is defined %}
  227. </a>
  228. {% endif %}
  229. (line {{ line_number }})
  230. </span>
  231. </td>
  232. </tr>
  233. {% endfor %}
  234. </tbody>
  235. </table>
  236. </div>
  237. {% endif %}
  238. </td>
  239. </tr>
  240. {% endfor %}
  241. </tbody>
  242. </table>
  243. {% endif %}
  244. {% endfor %}
  245. <h2>Database Connections</h2>
  246. {% if not collector.connections %}
  247. <div class="empty">
  248. <p>There are no configured database connections.</p>
  249. </div>
  250. {% else %}
  251. {{ helper.render_simple_table('Name', 'Service', collector.connections) }}
  252. {% endif %}
  253. <h2>Entity Managers</h2>
  254. {% if not collector.managers %}
  255. <div class="empty">
  256. <p>There are no configured entity managers.</p>
  257. </div>
  258. {% else %}
  259. {{ helper.render_simple_table('Name', 'Service', collector.managers) }}
  260. {% endif %}
  261. <h2>Second Level Cache</h2>
  262. {% if not collector.cacheEnabled %}
  263. <div class="empty">
  264. <p>Second Level Cache is not enabled.</p>
  265. </div>
  266. {% else %}
  267. {% if not collector.cacheCounts %}
  268. <div class="empty">
  269. <p>Second level cache information is not available.</p>
  270. </div>
  271. {% else %}
  272. <div class="metrics">
  273. <div class="metric">
  274. <span class="value">{{ collector.cacheCounts.hits }}</span>
  275. <span class="label">Hits</span>
  276. </div>
  277. <div class="metric">
  278. <span class="value">{{ collector.cacheCounts.misses }}</span>
  279. <span class="label">Misses</span>
  280. </div>
  281. <div class="metric">
  282. <span class="value">{{ collector.cacheCounts.puts }}</span>
  283. <span class="label">Puts</span>
  284. </div>
  285. </div>
  286. {% if collector.cacheRegions.hits %}
  287. <h3>Number of cache hits</h3>
  288. {{ helper.render_simple_table('Region', 'Hits', collector.cacheRegions.hits) }}
  289. {% endif %}
  290. {% if collector.cacheRegions.misses %}
  291. <h3>Number of cache misses</h3>
  292. {{ helper.render_simple_table('Region', 'Misses', collector.cacheRegions.misses) }}
  293. {% endif %}
  294. {% if collector.cacheRegions.puts %}
  295. <h3>Number of cache puts</h3>
  296. {{ helper.render_simple_table('Region', 'Puts', collector.cacheRegions.puts) }}
  297. {% endif %}
  298. {% endif %}
  299. {% endif %}
  300. {% if collector.entities|length > 0 %}
  301. <h2>Entities Mapping</h2>
  302. {% for manager, classes in collector.entities %}
  303. {% if collector.managers|length > 1 %}
  304. <h3>{{ manager }} <small>entity manager</small></h3>
  305. {% endif %}
  306. {% if classes is empty %}
  307. <div class="empty">
  308. <p>No loaded entities.</p>
  309. </div>
  310. {% else %}
  311. <table>
  312. <thead>
  313. <tr>
  314. <th scope="col">Class</th>
  315. <th scope="col">Mapping errors</th>
  316. </tr>
  317. </thead>
  318. <tbody>
  319. {% for class in classes %}
  320. {% set contains_errors = collector.mappingErrors[manager] is defined and collector.mappingErrors[manager][class] is defined %}
  321. <tr class="{{ contains_errors ? 'status-error' }}">
  322. <td>{{ class }}</td>
  323. <td class="font-normal">
  324. {% if contains_errors %}
  325. <ul>
  326. {% for error in collector.mappingErrors[manager][class] %}
  327. <li>{{ error }}</li>
  328. {% endfor %}
  329. </ul>
  330. {% else %}
  331. No errors.
  332. {% endif %}
  333. </td>
  334. </tr>
  335. {% endfor %}
  336. </tbody>
  337. </table>
  338. {% endif %}
  339. {% endfor %}
  340. {% endif %}
  341. <script type="text/javascript">//<![CDATA[
  342. function explain(link) {
  343. "use strict";
  344. var targetId = link.getAttribute('data-target-id');
  345. var targetElement = document.getElementById(targetId);
  346. if (targetElement.style.display != 'block') {
  347. Sfjs.load(targetId, link.href, null, function(xhr, el) {
  348. el.innerHTML = 'An error occurred while loading the query explanation.';
  349. });
  350. targetElement.style.display = 'block';
  351. link.innerHTML = 'Hide query explanation';
  352. } else {
  353. targetElement.style.display = 'none';
  354. link.innerHTML = 'Explain query';
  355. }
  356. return false;
  357. }
  358. function sortTable(header, column, targetId) {
  359. "use strict";
  360. var direction = parseInt(header.getAttribute('data-sort-direction')) || 1,
  361. items = [],
  362. target = document.getElementById(targetId),
  363. rows = target.children,
  364. headers = header.parentElement.children,
  365. i;
  366. for (i = 0; i < rows.length; ++i) {
  367. items.push(rows[i]);
  368. }
  369. for (i = 0; i < headers.length; ++i) {
  370. headers[i].removeAttribute('data-sort-direction');
  371. if (headers[i].children.length > 0) {
  372. headers[i].children[0].innerHTML = '';
  373. }
  374. }
  375. header.setAttribute('data-sort-direction', (-1*direction).toString());
  376. header.children[0].innerHTML = direction > 0 ? '<span class="text-muted">&#9650;</span>' : '<span class="text-muted">&#9660;</span>';
  377. items.sort(function(a, b) {
  378. return direction * (parseFloat(a.children[column].innerHTML) - parseFloat(b.children[column].innerHTML));
  379. });
  380. for (i = 0; i < items.length; ++i) {
  381. Sfjs.removeClass(items[i], i % 2 ? 'even' : 'odd');
  382. Sfjs.addClass(items[i], i % 2 ? 'odd' : 'even');
  383. target.appendChild(items[i]);
  384. }
  385. }
  386. if (navigator.clipboard) {
  387. document.querySelectorAll('[data-clipboard-text]').forEach(function(button) {
  388. Sfjs.removeClass(button, 'hidden');
  389. button.addEventListener('click', function() {
  390. navigator.clipboard.writeText(button.getAttribute('data-clipboard-text'));
  391. })
  392. });
  393. }
  394. //]]></script>
  395. {% endblock %}
  396. {% macro render_simple_table(label1, label2, data) %}
  397. <table>
  398. <thead>
  399. <tr>
  400. <th scope="col" class="key">{{ label1 }}</th>
  401. <th scope="col">{{ label2 }}</th>
  402. </tr>
  403. </thead>
  404. <tbody>
  405. {% for key, value in data %}
  406. <tr>
  407. <th scope="row">{{ key }}</th>
  408. <td>{{ value }}</td>
  409. </tr>
  410. {% endfor %}
  411. </tbody>
  412. </table>
  413. {% endmacro %}