@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
1<?php
2
3final class PhabricatorDaemonConsoleController
4 extends PhabricatorDaemonController {
5
6 public function handleRequest(AphrontRequest $request) {
7 $viewer = $this->getViewer();
8
9 $window_start = (time() - (60 * 15));
10
11 // Assume daemons spend about 250ms second in overhead per task acquiring
12 // leases and doing other bookkeeping. This is probably an over-estimation,
13 // but we'd rather show that utilization is too high than too low.
14 $lease_overhead = 0.250;
15
16 $completed = id(new PhabricatorWorkerArchiveTaskQuery())
17 ->withDateModifiedSince($window_start)
18 ->execute();
19
20 $failed = id(new PhabricatorWorkerActiveTask())->loadAllWhere(
21 'failureTime > %d',
22 $window_start);
23
24 $usage_total = 0;
25 $usage_start = PHP_INT_MAX;
26
27 $completed_info = array();
28 foreach ($completed as $completed_task) {
29 $class = $completed_task->getTaskClass();
30 if (empty($completed_info[$class])) {
31 $completed_info[$class] = array(
32 'n' => 0,
33 'duration' => 0,
34 'queueTime' => 0,
35 );
36 }
37 $completed_info[$class]['n']++;
38 $duration = $completed_task->getDuration();
39 $completed_info[$class]['duration'] += $duration;
40
41 // NOTE: Duration is in microseconds, but we're just using seconds to
42 // compute utilization.
43 $usage_total += $lease_overhead + ($duration / 1000000);
44 $usage_start = min($usage_start, $completed_task->getDateModified());
45
46 $date_archived = $completed_task->getArchivedEpoch();
47 $queue_seconds = $date_archived - $completed_task->getDateCreated();
48
49 // Don't measure queue time for tasks that completed in the same
50 // epoch-second they were created in.
51 if ($queue_seconds > 0) {
52 $sec_in_us = phutil_units('1 second in microseconds');
53 $queue_us = $queue_seconds * $sec_in_us;
54 $queue_exclusive_us = $queue_us - $duration;
55 $queue_exclusive_seconds = $queue_exclusive_us / $sec_in_us;
56 $rounded = floor($queue_exclusive_seconds);
57 $completed_info[$class]['queueTime'] += $rounded;
58 }
59 }
60
61 $completed_info = isort($completed_info, 'n');
62
63 $rows = array();
64 foreach ($completed_info as $class => $info) {
65 $duration_avg = new PhutilNumber((int)($info['duration'] / $info['n']));
66 $queue_avg = new PhutilNumber((int)($info['queueTime'] / $info['n']));
67 $rows[] = array(
68 $class,
69 number_format($info['n']),
70 pht('%s us', $duration_avg),
71 pht('%s s', $queue_avg),
72 );
73 }
74
75 if ($failed) {
76 // Add the time it takes to restart the daemons. This includes a guess
77 // about other overhead of 2X.
78 $restart_delay = PhutilDaemonHandle::getWaitBeforeRestart();
79 $usage_total += $restart_delay * count($failed) * 2;
80 foreach ($failed as $failed_task) {
81 $usage_start = min($usage_start, $failed_task->getFailureTime());
82 }
83
84 $rows[] = array(
85 phutil_tag('em', array(), pht('Temporary Failures')),
86 count($failed),
87 null,
88 null,
89 );
90 }
91
92 $logs = id(new PhabricatorDaemonLogQuery())
93 ->setViewer($viewer)
94 ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE)
95 ->setAllowStatusWrites(true)
96 ->execute();
97
98 $taskmasters = 0;
99 foreach ($logs as $log) {
100 if ($log->getDaemon() == 'PhabricatorTaskmasterDaemon') {
101 $taskmasters++;
102 }
103 }
104
105 if ($taskmasters && $usage_total) {
106 // Total number of wall-time seconds the daemons have been running since
107 // the oldest event. For very short times round up to 15s so we don't
108 // render any ridiculous numbers if you reload the page immediately after
109 // restarting the daemons.
110 $available_time = $taskmasters * max(15, (time() - $usage_start));
111
112 // Percentage of those wall-time seconds we can account for, which the
113 // daemons spent doing work:
114 $used_time = ($usage_total / $available_time);
115
116 $rows[] = array(
117 phutil_tag('em', array(), pht('Queue Utilization (Approximate)')),
118 sprintf('%.1f%%', 100 * $used_time),
119 null,
120 null,
121 );
122 }
123
124 $completed_table = new AphrontTableView($rows);
125 $completed_table->setNoDataString(
126 pht('No tasks have completed in the last 15 minutes.'));
127 $completed_table->setHeaders(
128 array(
129 pht('Class'),
130 pht('Count'),
131 pht('Average Duration'),
132 pht('Average Queue Time'),
133 ));
134 $completed_table->setColumnClasses(
135 array(
136 'wide',
137 'n',
138 'n',
139 'n',
140 ));
141
142 $completed_panel = id(new PHUIObjectBoxView())
143 ->setHeaderText(pht('Recently Completed Tasks (Last 15m)'))
144 ->setTable($completed_table);
145
146 $daemon_table = id(new PhabricatorDaemonLogListView())
147 ->setUser($viewer)
148 ->setDaemonLogs($logs);
149
150 $daemon_panel = id(new PHUIObjectBoxView())
151 ->setHeaderText(pht('Active Daemons'))
152 ->setTable($daemon_table);
153
154 $tasks = id(new PhabricatorWorkerLeaseQuery())
155 ->setSkipLease(true)
156 ->withLeasedTasks(true)
157 ->setLimit(100)
158 ->execute();
159
160 $tasks_table = id(new PhabricatorDaemonTasksTableView())
161 ->setTasks($tasks)
162 ->setNoDataString(pht('No tasks are leased by workers.'));
163
164 $leased_panel = id(new PHUIObjectBoxView())
165 ->setHeaderText(pht('Leased Tasks'))
166 ->setTable($tasks_table);
167
168 $task_table = new PhabricatorWorkerActiveTask();
169 $queued = queryfx_all(
170 $task_table->establishConnection('r'),
171 'SELECT taskClass, count(*) N FROM %T GROUP BY taskClass
172 ORDER BY N DESC',
173 $task_table->getTableName());
174
175 $rows = array();
176 foreach ($queued as $row) {
177 $rows[] = array(
178 $row['taskClass'],
179 number_format($row['N']),
180 );
181 }
182
183 $queued_table = new AphrontTableView($rows);
184 $queued_table->setHeaders(
185 array(
186 pht('Class'),
187 pht('Count'),
188 ));
189 $queued_table->setColumnClasses(
190 array(
191 'wide',
192 'n',
193 ));
194 $queued_table->setNoDataString(pht('Task queue is empty.'));
195
196 $queued_panel = new PHUIObjectBoxView();
197 $queued_panel->setHeaderText(pht('Queued Tasks'));
198 $queued_panel->setTable($queued_table);
199
200 $upcoming = id(new PhabricatorWorkerLeaseQuery())
201 ->setLimit(10)
202 ->setSkipLease(true)
203 ->execute();
204
205 $upcoming_panel = id(new PHUIObjectBoxView())
206 ->setHeaderText(pht('Next In Queue'))
207 ->setTable(
208 id(new PhabricatorDaemonTasksTableView())
209 ->setTasks($upcoming)
210 ->setNoDataString(pht('Task queue is empty.')));
211
212 $triggers = id(new PhabricatorWorkerTriggerQuery())
213 ->setViewer($viewer)
214 ->setOrder(PhabricatorWorkerTriggerQuery::ORDER_EXECUTION)
215 ->withNextEventBetween(0, null)
216 ->needEvents(true)
217 ->setLimit(10)
218 ->execute();
219
220 $triggers_table = $this->buildTriggersTable($triggers);
221
222 $triggers_panel = id(new PHUIObjectBoxView())
223 ->setHeaderText(pht('Upcoming Triggers'))
224 ->setTable($triggers_table);
225
226 $crumbs = $this->buildApplicationCrumbs();
227 $crumbs->addTextCrumb(pht('Console'));
228
229 $nav = $this->buildSideNavView();
230 $nav->selectFilter('/');
231 $nav->appendChild(
232 array(
233 $crumbs,
234 $completed_panel,
235 $daemon_panel,
236 $queued_panel,
237 $leased_panel,
238 $upcoming_panel,
239 $triggers_panel,
240 ));
241
242 return $this->newPage()
243 ->setTitle(pht('Console'))
244 ->appendChild($nav);
245
246 }
247
248 private function buildTriggersTable(array $triggers) {
249 $viewer = $this->getViewer();
250
251 $rows = array();
252 foreach ($triggers as $trigger) {
253 $event = $trigger->getEvent();
254 if ($event) {
255 $last_epoch = $event->getLastEventEpoch();
256 $next_epoch = $event->getNextEventEpoch();
257 } else {
258 $last_epoch = null;
259 $next_epoch = null;
260 }
261
262 $rows[] = array(
263 $trigger->getID(),
264 $trigger->getClockClass(),
265 $trigger->getActionClass(),
266 $last_epoch ? phabricator_datetime($last_epoch, $viewer) : null,
267 $next_epoch ? phabricator_datetime($next_epoch, $viewer) : null,
268 );
269 }
270
271 return id(new AphrontTableView($rows))
272 ->setNoDataString(pht('There are no upcoming event triggers.'))
273 ->setHeaders(
274 array(
275 pht('ID'),
276 pht('Clock'),
277 pht('Action'),
278 pht('Last'),
279 pht('Next'),
280 ))
281 ->setColumnClasses(
282 array(
283 '',
284 '',
285 'wide',
286 'date',
287 'date',
288 ));
289 }
290
291}