3 final class MultimeterControl
extends Phobject
{
5 private static $instance;
7 private $events = array();
12 private $eventContext;
14 private function __construct() {
18 public static function newInstance() {
19 $instance = new MultimeterControl();
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).
26 self
::$instance = $instance;
27 return self
::getInstance();
30 public static function getInstance() {
31 return self
::$instance;
34 public function isActive() {
35 return ($this->sampleRate
!== 0) && ($this->pauseDepth
== 0);
38 public function setSampleRate($rate) {
39 if ($rate && (mt_rand(1, $rate) == $rate)) {
45 $this->sampleRate
= $sample_rate;
50 public function pauseMultimeter() {
55 public function unpauseMultimeter() {
56 if (!$this->pauseDepth
) {
57 throw new Exception(pht('Trying to unpause an active multimeter!'));
64 public function newEvent($type, $label, $cost) {
65 if (!$this->isActive()) {
69 $event = id(new MultimeterEvent())
71 ->setEventLabel($label)
72 ->setResourceCost($cost)
73 ->setEpoch(PhabricatorTime
::getNow());
75 $this->events
[] = $event;
80 public function saveEvents() {
81 if (!$this->isActive()) {
85 $events = $this->events
;
90 if ($this->sampleRate
=== null) {
91 throw new PhutilInvalidStateException('setSampleRate');
94 $this->addServiceEvents();
96 // Don't sample any of this stuff.
97 $this->pauseMultimeter();
99 $use_scope = AphrontWriteGuard
::isGuardActive();
101 $unguarded = AphrontWriteGuard
::beginScopedUnguardedWrites();
103 AphrontWriteGuard
::allowDangerousUnguardedWrites(true);
108 $this->writeEvents();
109 } catch (Exception
$ex) {
116 AphrontWriteGuard
::allowDangerousUnguardedWrites(false);
119 $this->unpauseMultimeter();
126 private function writeEvents() {
127 if (PhabricatorEnv
::isReadOnly()) {
131 $events = $this->events
;
133 $random = Filesystem
::readRandomBytes(32);
134 $request_key = PhabricatorHash
::digestForIndex($random);
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'));
141 foreach ($events as $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()])
153 public function setEventContext($event_context) {
154 $this->eventContext
= $event_context;
158 public function getEventContext() {
159 return $this->eventContext
;
162 public function setEventViewer($viewer) {
163 $this->eventViewer
= $viewer;
167 private function loadHostID($host) {
168 $map = $this->loadDimensionMap(new MultimeterHost(), array($host));
169 return idx($map, $host);
172 private function loadEventViewerID($viewer) {
173 $map = $this->loadDimensionMap(new MultimeterViewer(), array($viewer));
174 return idx($map, $viewer);
177 private function loadEventContextID($context) {
178 $map = $this->loadDimensionMap(new MultimeterContext(), array($context));
179 return idx($map, $context);
182 private function loadEventLabelIDs(array $labels) {
183 return $this->loadDimensionMap(new MultimeterLabel(), $labels);
186 private function loadDimensionMap(MultimeterDimension
$table, array $names) {
188 foreach ($names as $name) {
189 $hashes[] = PhabricatorHash
::digestForIndex($name);
192 $objects = $table->loadAllWhere('nameHash IN (%Ls)', $hashes);
193 $map = mpull($objects, 'getID', 'getName');
196 foreach ($names as $name) {
197 if (isset($map[$name])) {
200 $need[$name] = $name;
203 foreach ($need as $name) {
204 $object = id(clone $table)
207 $map[$name] = $object->getID();
213 private function addServiceEvents() {
214 $events = PhutilServiceProfiler
::getInstance()->getServiceCallLog();
215 foreach ($events as $event) {
216 $type = idx($event, 'type');
220 MultimeterEvent
::TYPE_EXEC_TIME
,
221 $label = $this->getLabelForCommandEvent($event['command']),
222 (1000000 * $event['duration']));
228 private function getLabelForCommandEvent($command) {
229 $argv = preg_split('/\s+/', $command);
231 $bin = array_shift($argv);
232 $bin = basename($bin);
233 $bin = trim($bin, '"\'');
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
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
248 'for-each-ref' => true,
280 $candidates = idx($workflows, $bin);
282 foreach ($argv as $arg) {
283 if (isset($candidates[$arg])) {
291 return 'bin.'.$bin.' '.$workflow;