Path: blob/master/src/applications/multimeter/data/MultimeterControl.php
12256 views
<?php12final class MultimeterControl extends Phobject {34private static $instance;56private $events = array();7private $sampleRate;8private $pauseDepth;910private $eventViewer;11private $eventContext;1213private function __construct() {14// Private.15}1617public static function newInstance() {18$instance = new MultimeterControl();1920// NOTE: We don't set the sample rate yet. This allows the multimeter to21// be initialized and begin recording events, then make a decision about22// whether the page will be sampled or not later on (once we've loaded23// enough configuration).2425self::$instance = $instance;26return self::getInstance();27}2829public static function getInstance() {30return self::$instance;31}3233public function isActive() {34return ($this->sampleRate !== 0) && ($this->pauseDepth == 0);35}3637public function setSampleRate($rate) {38if ($rate && (mt_rand(1, $rate) == $rate)) {39$sample_rate = $rate;40} else {41$sample_rate = 0;42}4344$this->sampleRate = $sample_rate;4546return;47}4849public function pauseMultimeter() {50$this->pauseDepth++;51return $this;52}5354public function unpauseMultimeter() {55if (!$this->pauseDepth) {56throw new Exception(pht('Trying to unpause an active multimeter!'));57}58$this->pauseDepth--;59return $this;60}616263public function newEvent($type, $label, $cost) {64if (!$this->isActive()) {65return null;66}6768$event = id(new MultimeterEvent())69->setEventType($type)70->setEventLabel($label)71->setResourceCost($cost)72->setEpoch(PhabricatorTime::getNow());7374$this->events[] = $event;7576return $event;77}7879public function saveEvents() {80if (!$this->isActive()) {81return;82}8384$events = $this->events;85if (!$events) {86return;87}8889if ($this->sampleRate === null) {90throw new PhutilInvalidStateException('setSampleRate');91}9293$this->addServiceEvents();9495// Don't sample any of this stuff.96$this->pauseMultimeter();9798$use_scope = AphrontWriteGuard::isGuardActive();99if ($use_scope) {100$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();101} else {102AphrontWriteGuard::allowDangerousUnguardedWrites(true);103}104105$caught = null;106try {107$this->writeEvents();108} catch (Exception $ex) {109$caught = $ex;110}111112if ($use_scope) {113unset($unguarded);114} else {115AphrontWriteGuard::allowDangerousUnguardedWrites(false);116}117118$this->unpauseMultimeter();119120if ($caught) {121throw $caught;122}123}124125private function writeEvents() {126if (PhabricatorEnv::isReadOnly()) {127return;128}129130$events = $this->events;131132$random = Filesystem::readRandomBytes(32);133$request_key = PhabricatorHash::digestForIndex($random);134135$host_id = $this->loadHostID(php_uname('n'));136$context_id = $this->loadEventContextID($this->eventContext);137$viewer_id = $this->loadEventViewerID($this->eventViewer);138$label_map = $this->loadEventLabelIDs(mpull($events, 'getEventLabel'));139140foreach ($events as $event) {141$event142->setRequestKey($request_key)143->setSampleRate($this->sampleRate)144->setEventHostID($host_id)145->setEventContextID($context_id)146->setEventViewerID($viewer_id)147->setEventLabelID($label_map[$event->getEventLabel()])148->save();149}150}151152public function setEventContext($event_context) {153$this->eventContext = $event_context;154return $this;155}156157public function getEventContext() {158return $this->eventContext;159}160161public function setEventViewer($viewer) {162$this->eventViewer = $viewer;163return $this;164}165166private function loadHostID($host) {167$map = $this->loadDimensionMap(new MultimeterHost(), array($host));168return idx($map, $host);169}170171private function loadEventViewerID($viewer) {172$map = $this->loadDimensionMap(new MultimeterViewer(), array($viewer));173return idx($map, $viewer);174}175176private function loadEventContextID($context) {177$map = $this->loadDimensionMap(new MultimeterContext(), array($context));178return idx($map, $context);179}180181private function loadEventLabelIDs(array $labels) {182return $this->loadDimensionMap(new MultimeterLabel(), $labels);183}184185private function loadDimensionMap(MultimeterDimension $table, array $names) {186$hashes = array();187foreach ($names as $name) {188$hashes[] = PhabricatorHash::digestForIndex($name);189}190191$objects = $table->loadAllWhere('nameHash IN (%Ls)', $hashes);192$map = mpull($objects, 'getID', 'getName');193194$need = array();195foreach ($names as $name) {196if (isset($map[$name])) {197continue;198}199$need[$name] = $name;200}201202foreach ($need as $name) {203$object = id(clone $table)204->setName($name)205->save();206$map[$name] = $object->getID();207}208209return $map;210}211212private function addServiceEvents() {213$events = PhutilServiceProfiler::getInstance()->getServiceCallLog();214foreach ($events as $event) {215$type = idx($event, 'type');216switch ($type) {217case 'exec':218$this->newEvent(219MultimeterEvent::TYPE_EXEC_TIME,220$label = $this->getLabelForCommandEvent($event['command']),221(1000000 * $event['duration']));222break;223}224}225}226227private function getLabelForCommandEvent($command) {228$argv = preg_split('/\s+/', $command);229230$bin = array_shift($argv);231$bin = basename($bin);232$bin = trim($bin, '"\'');233234// It's important to avoid leaking details about command parameters,235// because some may be sensitive. Given this, it's not trivial to236// determine which parts of a command are arguments and which parts are237// flags.238239// Rather than try too hard for now, just whitelist some workflows that we240// know about and record everything else generically. Overall, this will241// produce labels like "pygmentize" or "git log", discarding all flags and242// arguments.243244$workflows = array(245'git' => array(246'log' => true,247'for-each-ref' => true,248'pull' => true,249'clone' => true,250'fetch' => true,251'cat-file' => true,252'init' => true,253'config' => true,254'remote' => true,255'rev-parse' => true,256'diff' => true,257'ls-tree' => true,258),259'svn' => array(260'log' => true,261'diff' => true,262),263'hg' => array(264'log' => true,265'locate' => true,266'pull' => true,267'clone' => true,268'init' => true,269'diff' => true,270'cat' => true,271'files' => true,272),273'svnadmin' => array(274'create' => true,275),276);277278$workflow = null;279$candidates = idx($workflows, $bin);280if ($candidates) {281foreach ($argv as $arg) {282if (isset($candidates[$arg])) {283$workflow = $arg;284break;285}286}287}288289if ($workflow) {290return 'bin.'.$bin.' '.$workflow;291} else {292return 'bin.'.$bin;293}294}295296}297298299