@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 297 lines 7.2 kB view raw
1<?php 2 3final class MultimeterControl extends Phobject { 4 5 private static $instance; 6 7 private $events = array(); 8 private $sampleRate; 9 private $pauseDepth; 10 11 private $eventViewer; 12 private $eventContext; 13 14 private function __construct() { 15 // Private. 16 } 17 18 public static function newInstance() { 19 $instance = new MultimeterControl(); 20 21 // NOTE: We don't set the sample rate yet. This allows the multimeter to 22 // be initialized and begin recording events, then make a decision about 23 // whether the page will be sampled or not later on (once we've loaded 24 // enough configuration). 25 26 self::$instance = $instance; 27 return self::getInstance(); 28 } 29 30 public static function getInstance() { 31 return self::$instance; 32 } 33 34 public function isActive() { 35 return ($this->sampleRate !== 0) && ($this->pauseDepth == 0); 36 } 37 38 public function setSampleRate($rate) { 39 if ($rate && (mt_rand(1, $rate) == $rate)) { 40 $sample_rate = $rate; 41 } else { 42 $sample_rate = 0; 43 } 44 45 $this->sampleRate = $sample_rate; 46 47 return; 48 } 49 50 public function pauseMultimeter() { 51 $this->pauseDepth++; 52 return $this; 53 } 54 55 public function unpauseMultimeter() { 56 if (!$this->pauseDepth) { 57 throw new Exception(pht('Trying to unpause an active multimeter!')); 58 } 59 $this->pauseDepth--; 60 return $this; 61 } 62 63 64 public function newEvent($type, $label, $cost) { 65 if (!$this->isActive()) { 66 return null; 67 } 68 69 $event = id(new MultimeterEvent()) 70 ->setEventType($type) 71 ->setEventLabel($label) 72 ->setResourceCost($cost) 73 ->setEpoch(PhabricatorTime::getNow()); 74 75 $this->events[] = $event; 76 77 return $event; 78 } 79 80 public function saveEvents() { 81 if (!$this->isActive()) { 82 return; 83 } 84 85 $events = $this->events; 86 if (!$events) { 87 return; 88 } 89 90 if ($this->sampleRate === null) { 91 throw new PhutilInvalidStateException('setSampleRate'); 92 } 93 94 $this->addServiceEvents(); 95 96 // Don't sample any of this stuff. 97 $this->pauseMultimeter(); 98 99 $use_scope = AphrontWriteGuard::isGuardActive(); 100 if ($use_scope) { 101 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 102 } else { 103 AphrontWriteGuard::allowDangerousUnguardedWrites(true); 104 } 105 106 $caught = null; 107 try { 108 $this->writeEvents(); 109 } catch (Exception $ex) { 110 $caught = $ex; 111 } 112 113 if ($use_scope) { 114 unset($unguarded); 115 } else { 116 AphrontWriteGuard::allowDangerousUnguardedWrites(false); 117 } 118 119 $this->unpauseMultimeter(); 120 121 if ($caught) { 122 throw $caught; 123 } 124 } 125 126 private function writeEvents() { 127 if (PhabricatorEnv::isReadOnly()) { 128 return; 129 } 130 131 $events = $this->events; 132 133 $random = Filesystem::readRandomBytes(32); 134 $request_key = PhabricatorHash::digestForIndex($random); 135 136 $host_id = $this->loadHostID(php_uname('n')); 137 $context_id = $this->loadEventContextID($this->eventContext); 138 $viewer_id = $this->loadEventViewerID($this->eventViewer); 139 $label_map = $this->loadEventLabelIDs(mpull($events, 'getEventLabel')); 140 141 foreach ($events as $event) { 142 $event 143 ->setRequestKey($request_key) 144 ->setSampleRate($this->sampleRate) 145 ->setEventHostID($host_id) 146 ->setEventContextID($context_id) 147 ->setEventViewerID($viewer_id) 148 ->setEventLabelID($label_map[$event->getEventLabel()]) 149 ->save(); 150 } 151 } 152 153 public function setEventContext($event_context) { 154 $this->eventContext = $event_context; 155 return $this; 156 } 157 158 public function getEventContext() { 159 return $this->eventContext; 160 } 161 162 public function setEventViewer($viewer) { 163 $this->eventViewer = $viewer; 164 return $this; 165 } 166 167 private function loadHostID($host) { 168 $map = $this->loadDimensionMap(new MultimeterHost(), array($host)); 169 return idx($map, $host); 170 } 171 172 private function loadEventViewerID($viewer) { 173 $map = $this->loadDimensionMap(new MultimeterViewer(), array($viewer)); 174 return idx($map, $viewer); 175 } 176 177 private function loadEventContextID($context) { 178 $map = $this->loadDimensionMap(new MultimeterContext(), array($context)); 179 return idx($map, $context); 180 } 181 182 private function loadEventLabelIDs(array $labels) { 183 return $this->loadDimensionMap(new MultimeterLabel(), $labels); 184 } 185 186 private function loadDimensionMap(MultimeterDimension $table, array $names) { 187 $hashes = array(); 188 foreach ($names as $name) { 189 $hashes[] = PhabricatorHash::digestForIndex($name); 190 } 191 192 $objects = $table->loadAllWhere('nameHash IN (%Ls)', $hashes); 193 $map = mpull($objects, 'getID', 'getName'); 194 195 $need = array(); 196 foreach ($names as $name) { 197 if (isset($map[$name])) { 198 continue; 199 } 200 $need[$name] = $name; 201 } 202 203 foreach ($need as $name) { 204 $object = id(clone $table) 205 ->setName($name) 206 ->save(); 207 $map[$name] = $object->getID(); 208 } 209 210 return $map; 211 } 212 213 private function addServiceEvents() { 214 $events = PhutilServiceProfiler::getInstance()->getServiceCallLog(); 215 foreach ($events as $event) { 216 $type = idx($event, 'type'); 217 switch ($type) { 218 case 'exec': 219 $this->newEvent( 220 MultimeterEvent::TYPE_EXEC_TIME, 221 $label = $this->getLabelForCommandEvent($event['command']), 222 (1000000 * $event['duration'])); 223 break; 224 } 225 } 226 } 227 228 private function getLabelForCommandEvent($command) { 229 $argv = preg_split('/\s+/', $command); 230 231 $bin = array_shift($argv); 232 $bin = basename($bin); 233 $bin = trim($bin, '"\''); 234 235 // It's important to avoid leaking details about command parameters, 236 // because some may be sensitive. Given this, it's not trivial to 237 // determine which parts of a command are arguments and which parts are 238 // flags. 239 240 // Rather than try too hard for now, just whitelist some workflows that we 241 // know about and record everything else generically. Overall, this will 242 // produce labels like "pygmentize" or "git log", discarding all flags and 243 // arguments. 244 245 $workflows = array( 246 'git' => array( 247 'log' => true, 248 'for-each-ref' => true, 249 'pull' => true, 250 'clone' => true, 251 'fetch' => true, 252 'cat-file' => true, 253 'init' => true, 254 'config' => true, 255 'remote' => true, 256 'rev-parse' => true, 257 'diff' => true, 258 'ls-tree' => true, 259 ), 260 'svn' => array( 261 'log' => true, 262 'diff' => true, 263 ), 264 'hg' => array( 265 'log' => true, 266 'locate' => true, 267 'pull' => true, 268 'clone' => true, 269 'init' => true, 270 'diff' => true, 271 'cat' => true, 272 'files' => true, 273 ), 274 'svnadmin' => array( 275 'create' => true, 276 ), 277 ); 278 279 $workflow = null; 280 $candidates = idx($workflows, $bin); 281 if ($candidates) { 282 foreach ($argv as $arg) { 283 if (isset($candidates[$arg])) { 284 $workflow = $arg; 285 break; 286 } 287 } 288 } 289 290 if ($workflow) { 291 return 'bin.'.$bin.' '.$workflow; 292 } else { 293 return 'bin.'.$bin; 294 } 295 } 296 297}