1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-28 17:52:43 +01:00
phorge-phorge/src/applications/phrequent/storage/PhrequentTimeBlock.php
epriestley ab3c17a2cd Emit more usable results from phrequent.tracking
Summary:
I think this pretty much does what you would expect?

The "active" item is always at the top of the stack.

Test Plan: Called `phrequent.tracking` and got reasonable results.

Reviewers: hach-que

Reviewed By: hach-que

Subscribers: epriestley

Differential Revision: https://secure.phabricator.com/D9939
2014-07-16 17:12:38 -07:00

323 lines
9 KiB
PHP

<?php
final class PhrequentTimeBlock extends Phobject {
private $events;
public function __construct(array $events) {
assert_instances_of($events, 'PhrequentUserTime');
$this->events = $events;
}
public function getTimeSpentOnObject($phid, $now) {
$slices = idx($this->getObjectTimeRanges(), $phid);
if (!$slices) {
return null;
}
return $slices->getDuration($now);
}
public function getObjectTimeRanges() {
$ranges = array();
$range_start = time();
foreach ($this->events as $event) {
$range_start = min($range_start, $event->getDateStarted());
}
$object_ranges = array();
$object_ongoing = array();
foreach ($this->events as $event) {
// First, convert each event's preempting stack into a linear timeline
// of events.
$timeline = array();
$timeline[] = array(
'event' => $event,
'at' => (int)$event->getDateStarted(),
'type' => 'start',
);
$timeline[] = array(
'event' => $event,
'at' => (int)nonempty($event->getDateEnded(), PHP_INT_MAX),
'type' => 'end',
);
$base_phid = $event->getObjectPHID();
if (!$event->getDateEnded()) {
$object_ongoing[$base_phid] = true;
}
$preempts = $event->getPreemptingEvents();
foreach ($preempts as $preempt) {
$same_object = ($preempt->getObjectPHID() == $base_phid);
$timeline[] = array(
'event' => $preempt,
'at' => (int)$preempt->getDateStarted(),
'type' => $same_object ? 'start' : 'push',
);
$timeline[] = array(
'event' => $preempt,
'at' => (int)nonempty($preempt->getDateEnded(), PHP_INT_MAX),
'type' => $same_object ? 'end' : 'pop',
);
}
// Now, figure out how much time was actually spent working on the
// object.
usort($timeline, array(__CLASS__, 'sortTimeline'));
$stack = array();
$depth = null;
// NOTE: "Strata" track the separate layers between each event tracking
// the object we care about. Events might look like this:
//
// |xxxxxxxxxxxxxxxxx|
// |yyyyyyy|
// |xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|
// 9AM 5PM
//
// ...where we care about event "x". When "y" is popped, that shouldn't
// pop the top stack -- we need to pop the stack a level down. Each
// event tracking "x" creates a new stratum, and we keep track of where
// timeline events are among the strata in order to keep stack depths
// straight.
$stratum = null;
$strata = array();
$ranges = array();
foreach ($timeline as $timeline_event) {
$id = $timeline_event['event']->getID();
$type = $timeline_event['type'];
switch ($type) {
case 'start':
$stack[] = $depth;
$depth = 0;
$stratum = count($stack);
$strata[$id] = $stratum;
$range_start = $timeline_event['at'];
break;
case 'end':
if ($strata[$id] == $stratum) {
if ($depth == 0) {
$ranges[] = array($range_start, $timeline_event['at']);
$depth = array_pop($stack);
} else {
// Here, we've prematurely ended the current stratum. Merge all
// the higher strata into it. This looks like this:
//
// V
// V
// |zzzzzzzz|
// |xxxxx|
// |yyyyyyyyyyyyy|
// |xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|
$depth = array_pop($stack) + $depth;
}
} else {
// Here, we've prematurely ended a deeper stratum. Merge higher
// stata. This looks like this:
//
// V
// V
// |aaaaaaa|
// |xxxxxxxxxxxxxxxxxxx|
// |zzzzzzzzzzzzz|
// |xxxxxxx|
// |yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy|
// |xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|
$extra = $stack[$strata[$id]];
unset($stack[$strata[$id] - 1]);
$stack = array_values($stack);
$stack[$strata[$id] - 1] += $extra;
}
// Regardless of how we got here, we need to merge down any higher
// strata.
$target = $strata[$id];
foreach ($strata as $strata_id => $id_stratum) {
if ($id_stratum >= $target) {
$strata[$strata_id]--;
}
}
$stratum = count($stack);
unset($strata[$id]);
break;
case 'push':
$strata[$id] = $stratum;
if ($depth == 0) {
$ranges[] = array($range_start, $timeline_event['at']);
}
$depth++;
break;
case 'pop':
if ($strata[$id] == $stratum) {
$depth--;
if ($depth == 0) {
$range_start = $timeline_event['at'];
}
} else {
$stack[$strata[$id]]--;
}
unset($strata[$id]);
break;
}
}
// Filter out ranges with an indefinite start time. These occur when
// popping the stack when there are multiple ongoing events.
foreach ($ranges as $key => $range) {
if ($range[0] == PHP_INT_MAX) {
unset($ranges[$key]);
}
}
$object_ranges[$base_phid][] = $ranges;
}
// Collapse all the ranges so we don't double-count time.
foreach ($object_ranges as $phid => $ranges) {
$object_ranges[$phid] = self::mergeTimeRanges(array_mergev($ranges));
}
foreach ($object_ranges as $phid => $ranges) {
foreach ($ranges as $key => $range) {
if ($range[1] == PHP_INT_MAX) {
$ranges[$key][1] = null;
}
}
$object_ranges[$phid] = new PhrequentTimeSlices(
$phid,
isset($object_ongoing[$phid]),
$ranges);
}
// Reorder the ranges to be more stack-like, so the first item is the
// top of the stack.
$object_ranges = array_reverse($object_ranges, $preserve_keys = true);
return $object_ranges;
}
/**
* Returns the current list of work.
*/
public function getCurrentWorkStack($now, $include_inactive = false) {
$ranges = $this->getObjectTimeRanges();
$results = array();
$active = null;
foreach ($ranges as $phid => $slices) {
if (!$include_inactive) {
if (!$slices->getIsOngoing()) {
continue;
}
}
$results[] = array(
'phid' => $phid,
'time' => $slices->getDuration($now),
'ongoing' => $slices->getIsOngoing(),
);
}
return $results;
}
/**
* Merge a list of time ranges (pairs of `<start, end>` epochs) so that no
* elements overlap. For example, the ranges:
*
* array(
* array(50, 150),
* array(100, 175),
* );
*
* ...are merged to:
*
* array(
* array(50, 175),
* );
*
* This is used to avoid double-counting time on objects which had timers
* started multiple times.
*
* @param list<pair<int, int>> List of possibly overlapping time ranges.
* @return list<pair<int, int>> Nonoverlapping time ranges.
*/
public static function mergeTimeRanges(array $ranges) {
$ranges = isort($ranges, 0);
$result = array();
$current = null;
foreach ($ranges as $key => $range) {
if ($current === null) {
$current = $range;
continue;
}
if ($range[0] <= $current[1]) {
$current[1] = max($range[1], $current[1]);
continue;
}
$result[] = $current;
$current = $range;
}
$result[] = $current;
return $result;
}
/**
* Sort events in timeline order. Notably, for events which occur on the same
* second, we want to process end events after start events.
*/
public static function sortTimeline(array $u, array $v) {
// If these events occur at different times, ordering is obvious.
if ($u['at'] != $v['at']) {
return ($u['at'] < $v['at']) ? -1 : 1;
}
$u_end = ($u['type'] == 'end' || $u['type'] == 'pop');
$v_end = ($v['type'] == 'end' || $v['type'] == 'pop');
$u_id = $u['event']->getID();
$v_id = $v['event']->getID();
if ($u_end == $v_end) {
// These are both start events or both end events. Sort them by ID.
if (!$u_end) {
return ($u_id < $v_id) ? -1 : 1;
} else {
return ($u_id < $v_id) ? 1 : -1;
}
} else {
// Sort them (start, end) if they're the same event, and (end, start)
// otherwise.
if ($u_id == $v_id) {
return $v_end ? -1 : 1;
} else {
return $v_end ? 1 : -1;
}
}
return 0;
}
}