Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / multimeter / data / MultimeterControl.php
blob34e03e0587296c0f2be0f7a04ce1bde04df9f037
1 <?php
3 final class MultimeterControl extends Phobject {
5 private static $instance;
7 private $events = array();
8 private $sampleRate;
9 private $pauseDepth;
11 private $eventViewer;
12 private $eventContext;
14 private function __construct() {
15 // Private.
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)) {
40 $sample_rate = $rate;
41 } else {
42 $sample_rate = 0;
45 $this->sampleRate = $sample_rate;
47 return;
50 public function pauseMultimeter() {
51 $this->pauseDepth++;
52 return $this;
55 public function unpauseMultimeter() {
56 if (!$this->pauseDepth) {
57 throw new Exception(pht('Trying to unpause an active multimeter!'));
59 $this->pauseDepth--;
60 return $this;
64 public function newEvent($type, $label, $cost) {
65 if (!$this->isActive()) {
66 return null;
69 $event = id(new MultimeterEvent())
70 ->setEventType($type)
71 ->setEventLabel($label)
72 ->setResourceCost($cost)
73 ->setEpoch(PhabricatorTime::getNow());
75 $this->events[] = $event;
77 return $event;
80 public function saveEvents() {
81 if (!$this->isActive()) {
82 return;
85 $events = $this->events;
86 if (!$events) {
87 return;
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();
100 if ($use_scope) {
101 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
102 } else {
103 AphrontWriteGuard::allowDangerousUnguardedWrites(true);
106 $caught = null;
107 try {
108 $this->writeEvents();
109 } catch (Exception $ex) {
110 $caught = $ex;
113 if ($use_scope) {
114 unset($unguarded);
115 } else {
116 AphrontWriteGuard::allowDangerousUnguardedWrites(false);
119 $this->unpauseMultimeter();
121 if ($caught) {
122 throw $caught;
126 private function writeEvents() {
127 if (PhabricatorEnv::isReadOnly()) {
128 return;
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) {
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();
153 public function setEventContext($event_context) {
154 $this->eventContext = $event_context;
155 return $this;
158 public function getEventContext() {
159 return $this->eventContext;
162 public function setEventViewer($viewer) {
163 $this->eventViewer = $viewer;
164 return $this;
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) {
187 $hashes = array();
188 foreach ($names as $name) {
189 $hashes[] = PhabricatorHash::digestForIndex($name);
192 $objects = $table->loadAllWhere('nameHash IN (%Ls)', $hashes);
193 $map = mpull($objects, 'getID', 'getName');
195 $need = array();
196 foreach ($names as $name) {
197 if (isset($map[$name])) {
198 continue;
200 $need[$name] = $name;
203 foreach ($need as $name) {
204 $object = id(clone $table)
205 ->setName($name)
206 ->save();
207 $map[$name] = $object->getID();
210 return $map;
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;
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
238 // flags.
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.
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,
260 'svn' => array(
261 'log' => true,
262 'diff' => true,
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,
274 'svnadmin' => array(
275 'create' => true,
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;
290 if ($workflow) {
291 return 'bin.'.$bin.' '.$workflow;
292 } else {
293 return 'bin.'.$bin;