Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / console / plugin / DarkConsoleServicesPlugin.php
blob4a26665e0abdf69c58d8929d020c327a86ad1fdd
1 <?php
3 final class DarkConsoleServicesPlugin extends DarkConsolePlugin {
5 protected $observations;
7 public function getName() {
8 return pht('Services');
11 public function getDescription() {
12 return pht('Information about services.');
15 public static function getQueryAnalyzerHeader() {
16 return 'X-Phabricator-QueryAnalyzer';
19 public static function isQueryAnalyzerRequested() {
20 if (!empty($_REQUEST['__analyze__'])) {
21 return true;
24 $header = AphrontRequest::getHTTPHeader(self::getQueryAnalyzerHeader());
25 if ($header) {
26 return true;
29 return false;
32 public function didStartup() {
33 $should_analyze = self::isQueryAnalyzerRequested();
35 if ($should_analyze) {
36 PhutilServiceProfiler::getInstance()
37 ->setCollectStackTraces(true);
40 return null;
44 /**
45 * @phutil-external-symbol class PhabricatorStartup
47 public function generateData() {
48 $should_analyze = self::isQueryAnalyzerRequested();
50 $log = PhutilServiceProfiler::getInstance()->getServiceCallLog();
51 foreach ($log as $key => $entry) {
52 $config = idx($entry, 'config', array());
53 unset($log[$key]['config']);
55 if (!$should_analyze) {
56 $log[$key]['explain'] = array(
57 'sev' => 7,
58 'size' => null,
59 'reason' => pht('Disabled'),
61 // Query analysis is disabled for this request, so don't do any of it.
62 continue;
65 if ($entry['type'] != 'query') {
66 continue;
69 // For each SELECT query, go issue an EXPLAIN on it so we can flag stuff
70 // causing table scans, etc.
71 if (preg_match('/^\s*SELECT\b/i', $entry['query'])) {
72 $conn = PhabricatorDatabaseRef::newRawConnection($entry['config']);
73 try {
74 $explain = queryfx_all(
75 $conn,
76 'EXPLAIN %Q',
77 $entry['query']);
79 $badness = 0;
80 $size = 1;
81 $reason = null;
83 foreach ($explain as $table) {
84 $size *= (int)$table['rows'];
86 switch ($table['type']) {
87 case 'index':
88 $cur_badness = 1;
89 $cur_reason = 'Index';
90 break;
91 case 'const':
92 $cur_badness = 1;
93 $cur_reason = 'Const';
94 break;
95 case 'eq_ref';
96 $cur_badness = 2;
97 $cur_reason = 'EqRef';
98 break;
99 case 'range':
100 $cur_badness = 3;
101 $cur_reason = 'Range';
102 break;
103 case 'ref':
104 $cur_badness = 3;
105 $cur_reason = 'Ref';
106 break;
107 case 'fulltext':
108 $cur_badness = 3;
109 $cur_reason = 'Fulltext';
110 break;
111 case 'ALL':
112 if (preg_match('/Using where/', $table['Extra'])) {
113 if ($table['rows'] < 256 && !empty($table['possible_keys'])) {
114 $cur_badness = 2;
115 $cur_reason = pht('Small Table Scan');
116 } else {
117 $cur_badness = 6;
118 $cur_reason = pht('TABLE SCAN!');
120 } else {
121 $cur_badness = 3;
122 $cur_reason = pht('Whole Table');
124 break;
125 default:
126 if (preg_match('/No tables used/i', $table['Extra'])) {
127 $cur_badness = 1;
128 $cur_reason = pht('No Tables');
129 } else if (preg_match('/Impossible/i', $table['Extra'])) {
130 $cur_badness = 1;
131 $cur_reason = pht('Empty');
132 } else {
133 $cur_badness = 4;
134 $cur_reason = pht("Can't Analyze");
136 break;
139 if ($cur_badness > $badness) {
140 $badness = $cur_badness;
141 $reason = $cur_reason;
145 $log[$key]['explain'] = array(
146 'sev' => $badness,
147 'size' => $size,
148 'reason' => $reason,
150 } catch (Exception $ex) {
151 $log[$key]['explain'] = array(
152 'sev' => 5,
153 'size' => null,
154 'reason' => $ex->getMessage(),
160 return array(
161 'start' => PhabricatorStartup::getStartTime(),
162 'end' => microtime(true),
163 'log' => $log,
164 'analyzeURI' => (string)$this
165 ->getRequestURI()
166 ->alter('__analyze__', true),
167 'didAnalyze' => $should_analyze,
171 public function renderPanel() {
172 $data = $this->getData();
174 $log = $data['log'];
175 $results = array();
177 $results[] = phutil_tag(
178 'div',
179 array('class' => 'dark-console-panel-header'),
180 array(
181 phutil_tag(
182 'a',
183 array(
184 'href' => $data['analyzeURI'],
185 'class' => $data['didAnalyze'] ?
186 'disabled button' : 'button button-green',
188 pht('Analyze Query Plans')),
189 phutil_tag('h1', array(), pht('Calls to External Services')),
190 phutil_tag('div', array('style' => 'clear: both;')),
193 $page_total = $data['end'] - $data['start'];
194 $totals = array();
195 $counts = array();
197 foreach ($log as $row) {
198 $totals[$row['type']] = idx($totals, $row['type'], 0) + $row['duration'];
199 $counts[$row['type']] = idx($counts, $row['type'], 0) + 1;
201 $totals['All Services'] = array_sum($totals);
202 $counts['All Services'] = array_sum($counts);
204 $totals['Entire Page'] = $page_total;
205 $counts['Entire Page'] = 0;
207 $summary = array();
208 foreach ($totals as $type => $total) {
209 $summary[] = array(
210 $type,
211 number_format($counts[$type]),
212 pht('%s us', new PhutilNumber((int)(1000000 * $totals[$type]))),
213 sprintf('%.1f%%', 100 * $totals[$type] / $page_total),
216 $summary_table = new AphrontTableView($summary);
217 $summary_table->setColumnClasses(
218 array(
220 'n',
221 'n',
222 'wide',
224 $summary_table->setHeaders(
225 array(
226 pht('Type'),
227 pht('Count'),
228 pht('Total Cost'),
229 pht('Page Weight'),
232 $results[] = $summary_table->render();
234 $rows = array();
235 foreach ($log as $row) {
237 $analysis = null;
239 switch ($row['type']) {
240 case 'query':
241 $info = $row['query'];
242 $info = wordwrap($info, 128, "\n", true);
244 if (!empty($row['explain'])) {
245 $analysis = phutil_tag(
246 'span',
247 array(
248 'class' => 'explain-sev-'.$row['explain']['sev'],
250 $row['explain']['reason']);
253 break;
254 case 'connect':
255 $info = $row['host'].':'.$row['database'];
256 break;
257 case 'exec':
258 $info = $row['command'];
259 break;
260 case 's3':
261 case 'conduit':
262 $info = $row['method'];
263 break;
264 case 'http':
265 $info = $row['uri'];
266 break;
267 default:
268 $info = '-';
269 break;
272 $offset = ($row['begin'] - $data['start']);
274 $rows[] = array(
275 $row['type'],
276 pht('+%s ms', new PhutilNumber(1000 * $offset)),
277 pht('%s us', new PhutilNumber(1000000 * $row['duration'])),
278 $info,
279 $analysis,
282 if (isset($row['trace'])) {
283 $rows[] = array(
284 null,
285 null,
286 null,
287 $row['trace'],
288 null,
293 $table = new AphrontTableView($rows);
294 $table->setColumnClasses(
295 array(
296 null,
297 'n',
298 'n',
299 'wide prewrap',
302 $table->setHeaders(
303 array(
304 pht('Event'),
305 pht('Start'),
306 pht('Duration'),
307 pht('Details'),
308 pht('Analysis'),
311 $results[] = $table->render();
313 return phutil_implode_html("\n", $results);