@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class DarkConsoleServicesPlugin extends DarkConsolePlugin {
4
5 protected $observations;
6
7 public function getName() {
8 return pht('Services');
9 }
10
11 public function getDescription() {
12 return pht('Information about services.');
13 }
14
15 public static function getQueryAnalyzerHeader() {
16 return 'X-Phabricator-QueryAnalyzer';
17 }
18
19 public static function isQueryAnalyzerRequested() {
20 if (!empty($_REQUEST['__analyze__'])) {
21 return true;
22 }
23
24 $header = AphrontRequest::getHTTPHeader(self::getQueryAnalyzerHeader());
25 if ($header) {
26 return true;
27 }
28
29 return false;
30 }
31
32 public function didStartup() {
33 $should_analyze = self::isQueryAnalyzerRequested();
34
35 if ($should_analyze) {
36 PhutilServiceProfiler::getInstance()
37 ->setCollectStackTraces(true);
38 }
39
40 return null;
41 }
42
43
44 /**
45 * @phutil-external-symbol class PhabricatorStartup
46 */
47 public function generateData() {
48 $should_analyze = self::isQueryAnalyzerRequested();
49
50 $log = PhutilServiceProfiler::getInstance()->getServiceCallLog();
51 foreach ($log as $key => $entry) {
52 $config = idx($entry, 'config', array());
53 unset($log[$key]['config']);
54
55 if (!$should_analyze) {
56 $log[$key]['explain'] = array(
57 'sev' => 7,
58 'size' => null,
59 'reason' => pht('Disabled'),
60 );
61 // Query analysis is disabled for this request, so don't do any of it.
62 continue;
63 }
64
65 if ($entry['type'] != 'query') {
66 continue;
67 }
68
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']);
78
79 $badness = 0;
80 $size = 1;
81 $reason = null;
82
83 foreach ($explain as $table) {
84 $size *= (int)$table['rows'];
85
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!');
119 }
120 } else {
121 $cur_badness = 3;
122 $cur_reason = pht('Whole Table');
123 }
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");
135 }
136 break;
137 }
138
139 if ($cur_badness > $badness) {
140 $badness = $cur_badness;
141 $reason = $cur_reason;
142 }
143 }
144
145 $log[$key]['explain'] = array(
146 'sev' => $badness,
147 'size' => $size,
148 'reason' => $reason,
149 );
150 } catch (Exception $ex) {
151 $log[$key]['explain'] = array(
152 'sev' => 5,
153 'size' => null,
154 'reason' => $ex->getMessage(),
155 );
156 }
157 }
158 }
159
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,
168 );
169 }
170
171 public function renderPanel() {
172 $data = $this->getData();
173
174 $log = $data['log'];
175 $results = array();
176
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',
187 ),
188 pht('Analyze Query Plans')),
189 phutil_tag('h1', array(), pht('Calls to External Services')),
190 phutil_tag('div', array('style' => 'clear: both;')),
191 ));
192
193 $page_total = $data['end'] - $data['start'];
194 $totals = array();
195 $counts = array();
196
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;
200 }
201 $totals['All Services'] = array_sum($totals);
202 $counts['All Services'] = array_sum($counts);
203
204 $totals['Entire Page'] = $page_total;
205 $counts['Entire Page'] = 0;
206
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),
214 );
215 }
216 $summary_table = new AphrontTableView($summary);
217 $summary_table->setColumnClasses(
218 array(
219 '',
220 'n',
221 'n',
222 'wide',
223 ));
224 $summary_table->setHeaders(
225 array(
226 pht('Type'),
227 pht('Count'),
228 pht('Total Cost'),
229 pht('Page Weight'),
230 ));
231
232 $results[] = $summary_table->render();
233
234 $rows = array();
235 foreach ($log as $row) {
236
237 $analysis = null;
238
239 switch ($row['type']) {
240 case 'query':
241 $info = $row['query'];
242 $info = wordwrap($info, 128, "\n", true);
243
244 if (!empty($row['explain'])) {
245 $analysis = phutil_tag(
246 'span',
247 array(
248 'class' => 'explain-sev-'.$row['explain']['sev'],
249 ),
250 $row['explain']['reason']);
251 }
252
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;
270 }
271
272 $offset = ($row['begin'] - $data['start']);
273
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,
280 );
281
282 if (isset($row['trace'])) {
283 $rows[] = array(
284 null,
285 null,
286 null,
287 $row['trace'],
288 null,
289 );
290 }
291 }
292
293 $table = new AphrontTableView($rows);
294 $table->setColumnClasses(
295 array(
296 null,
297 'n',
298 'n',
299 'wide prewrap',
300 '',
301 ));
302 $table->setHeaders(
303 array(
304 pht('Event'),
305 pht('Start'),
306 pht('Duration'),
307 pht('Details'),
308 pht('Analysis'),
309 ));
310
311 $results[] = $table->render();
312
313 return phutil_implode_html("\n", $results);
314 }
315}