@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
at upstream/main 315 lines 8.2 kB view raw
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}