diff --git a/src/applications/multimeter/data/MultimeterControl.php b/src/applications/multimeter/data/MultimeterControl.php index 9d948609a6..2bc849c185 100644 --- a/src/applications/multimeter/data/MultimeterControl.php +++ b/src/applications/multimeter/data/MultimeterControl.php @@ -1,207 +1,292 @@ sampleRate !== 0) && ($this->pauseDepth == 0); } public function setSampleRate($rate) { if ($rate && (mt_rand(1, $rate) == $rate)) { $sample_rate = $rate; } else { $sample_rate = 0; } $this->sampleRate = $sample_rate; return; } public function pauseMultimeter() { $this->pauseDepth++; return $this; } public function unpauseMultimeter() { if (!$this->pauseDepth) { throw new Exception(pht('Trying to unpause an active multimeter!')); } $this->pauseDepth--; return $this; } public function newEvent($type, $label, $cost) { if (!$this->isActive()) { return null; } $event = id(new MultimeterEvent()) ->setEventType($type) ->setEventLabel($label) ->setResourceCost($cost) ->setEpoch(PhabricatorTime::getNow()); $this->events[] = $event; return $event; } public function saveEvents() { if (!$this->isActive()) { return; } $events = $this->events; if (!$events) { return; } if ($this->sampleRate === null) { throw new Exception(pht('Call setSampleRate() before saving events!')); } + $this->addServiceEvents(); + // Don't sample any of this stuff. $this->pauseMultimeter(); $use_scope = AphrontWriteGuard::isGuardActive(); if ($use_scope) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); } else { AphrontWriteGuard::allowDangerousUnguardedWrites(true); } $caught = null; try { $this->writeEvents(); } catch (Exception $ex) { $caught = $ex; } if ($use_scope) { unset($unguarded); } else { AphrontWriteGuard::allowDangerousUnguardedWrites(false); } $this->unpauseMultimeter(); if ($caught) { throw $caught; } } private function writeEvents() { $events = $this->events; $random = Filesystem::readRandomBytes(32); $request_key = PhabricatorHash::digestForIndex($random); $host_id = $this->loadHostID(php_uname('n')); $context_id = $this->loadEventContextID($this->eventContext); $viewer_id = $this->loadEventViewerID($this->eventViewer); $label_map = $this->loadEventLabelIDs(mpull($events, 'getEventLabel')); foreach ($events as $event) { $event ->setRequestKey($request_key) ->setSampleRate($this->sampleRate) ->setEventHostID($host_id) ->setEventContextID($context_id) ->setEventViewerID($viewer_id) ->setEventLabelID($label_map[$event->getEventLabel()]) ->save(); } } public function setEventContext($event_context) { $this->eventContext = $event_context; return $this; } public function getEventContext() { return $this->eventContext; } public function setEventViewer($viewer) { $this->eventViewer = $viewer; return $this; } private function loadHostID($host) { $map = $this->loadDimensionMap(new MultimeterHost(), array($host)); return idx($map, $host); } private function loadEventViewerID($viewer) { $map = $this->loadDimensionMap(new MultimeterViewer(), array($viewer)); return idx($map, $viewer); } private function loadEventContextID($context) { $map = $this->loadDimensionMap(new MultimeterContext(), array($context)); return idx($map, $context); } private function loadEventLabelIDs(array $labels) { return $this->loadDimensionMap(new MultimeterLabel(), $labels); } private function loadDimensionMap(MultimeterDimension $table, array $names) { $hashes = array(); foreach ($names as $name) { $hashes[] = PhabricatorHash::digestForIndex($name); } $objects = $table->loadAllWhere('nameHash IN (%Ls)', $hashes); $map = mpull($objects, 'getID', 'getName'); $need = array(); foreach ($names as $name) { if (isset($map[$name])) { continue; } - $need[] = $name; + $need[$name] = $name; } foreach ($need as $name) { $object = id(clone $table) ->setName($name) ->save(); $map[$name] = $object->getID(); } return $map; } + private function addServiceEvents() { + $events = PhutilServiceProfiler::getInstance()->getServiceCallLog(); + foreach ($events as $event) { + $type = idx($event, 'type'); + switch ($type) { + case 'exec': + $this->newEvent( + MultimeterEvent::TYPE_EXEC_TIME, + $label = $this->getLabelForCommandEvent($event['command']), + (1000000 * $event['duration'])); + break; + } + } + } + + private function getLabelForCommandEvent($command) { + $argv = preg_split('/\s+/', $command); + + $bin = array_shift($argv); + $bin = basename($bin); + $bin = trim($bin, '"\''); + + // It's important to avoid leaking details about command parameters, + // because some may be sensitive. Given this, it's not trivial to + // determine which parts of a command are arguments and which parts are + // flags. + + // Rather than try too hard for now, just whitelist some workflows that we + // know about and record everything else generically. Overall, this will + // produce labels like "pygmentize" or "git log", discarding all flags and + // arguments. + + $workflows = array( + 'git' => array( + 'log' => true, + 'for-each-ref' => true, + 'pull' => true, + 'clone' => true, + 'fetch' => true, + 'cat-file' => true, + 'init' => true, + 'config' => true, + 'remote' => true, + 'rev-parse' => true, + 'diff' => true, + 'ls-tree' => true, + ), + 'svn' => array( + 'log' => true, + 'diff' => true, + ), + 'hg' => array( + 'log' => true, + 'locate' => true, + 'pull' => true, + 'clone' => true, + 'init' => true, + 'diff' => true, + 'cat' => true, + ), + 'svnadmin' => array( + 'create' => true, + ), + ); + + $workflow = null; + $candidates = idx($workflows, $bin); + if ($candidates) { + foreach ($argv as $arg) { + if (isset($candidates[$arg])) { + $workflow = $arg; + break; + } + } + } + + if ($workflow) { + return 'bin.'.$bin.' '.$workflow; + } else { + return 'bin.'.$bin; + } + } + } diff --git a/src/applications/multimeter/storage/MultimeterEvent.php b/src/applications/multimeter/storage/MultimeterEvent.php index 23263a6e8d..1585640fdf 100644 --- a/src/applications/multimeter/storage/MultimeterEvent.php +++ b/src/applications/multimeter/storage/MultimeterEvent.php @@ -1,76 +1,80 @@ eventLabel = $event_label; return $this; } public function getEventLabel() { return $this->eventLabel; } public static function getEventTypeName($type) { switch ($type) { case self::TYPE_STATIC_RESOURCE: return pht('Static Resource'); case self::TYPE_REQUEST_TIME: return pht('Web Request'); + case self::TYPE_EXEC_TIME: + return pht('Subprocesses'); } return pht('Unknown ("%s")', $type); } public static function formatResourceCost( PhabricatorUser $viewer, $type, $cost) { switch ($type) { case self::TYPE_STATIC_RESOURCE: return pht('%s Req', new PhutilNumber($cost)); case self::TYPE_REQUEST_TIME: + case self::TYPE_EXEC_TIME: return pht('%s us', new PhutilNumber($cost)); } return pht('%s Unit(s)', new PhutilNumber($cost)); } protected function getConfiguration() { return array( self::CONFIG_TIMESTAMPS => false, self::CONFIG_COLUMN_SCHEMA => array( 'eventType' => 'uint32', 'resourceCost' => 'sint64', 'sampleRate' => 'uint32', 'requestKey' => 'bytes12', ), self::CONFIG_KEY_SCHEMA => array( 'key_request' => array( 'columns' => array('requestKey'), ), 'key_type' => array( 'columns' => array('eventType', 'epoch'), ), ), ) + parent::getConfiguration(); } }