2016-07-01 17:06:51 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
abstract class PhabricatorObjectGraph
|
|
|
|
extends AbstractDirectedGraph {
|
|
|
|
|
|
|
|
private $viewer;
|
|
|
|
private $edges = array();
|
Don't load the entire graph for tasks
Summary:
Ref T4788. As it turns out, our tasks are very tightly connected.
Instead of loading every parent/child task, then every parent/child of those tasks, etc., etc., only load tasks in the "same direction" that we're already heading.
For example, we load children of children, but not parents of children. And we load parents of parents, but not children of parents.
Basically we only go "up" and "down" now, but not "out" as much. This should reduce the gigantic multiple-thousand-node graphs currently shown in the UI.
I still discover the whole graph for revisiosn, because I think it's probably more useful and always much smaller. That might need adjustment too, though.
Test Plan: Seems fine locally??
Reviewers: chad
Reviewed By: chad
Maniphest Tasks: T4788
Differential Revision: https://secure.phabricator.com/D16218
2016-07-01 20:37:01 +02:00
|
|
|
private $edgeReach = array();
|
2016-07-01 17:06:51 +02:00
|
|
|
private $seedPHID;
|
|
|
|
private $objects;
|
Don't load the entire graph for tasks
Summary:
Ref T4788. As it turns out, our tasks are very tightly connected.
Instead of loading every parent/child task, then every parent/child of those tasks, etc., etc., only load tasks in the "same direction" that we're already heading.
For example, we load children of children, but not parents of children. And we load parents of parents, but not children of parents.
Basically we only go "up" and "down" now, but not "out" as much. This should reduce the gigantic multiple-thousand-node graphs currently shown in the UI.
I still discover the whole graph for revisiosn, because I think it's probably more useful and always much smaller. That might need adjustment too, though.
Test Plan: Seems fine locally??
Reviewers: chad
Reviewed By: chad
Maniphest Tasks: T4788
Differential Revision: https://secure.phabricator.com/D16218
2016-07-01 20:37:01 +02:00
|
|
|
private $loadEntireGraph = false;
|
2016-07-14 06:07:34 +02:00
|
|
|
private $limit;
|
2016-07-28 22:54:10 +02:00
|
|
|
private $adjacent;
|
2016-07-01 17:06:51 +02:00
|
|
|
|
|
|
|
public function setViewer(PhabricatorUser $viewer) {
|
|
|
|
$this->viewer = $viewer;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getViewer() {
|
|
|
|
if (!$this->viewer) {
|
|
|
|
throw new PhutilInvalidStateException('setViewer');
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->viewer;
|
|
|
|
}
|
|
|
|
|
2016-07-14 06:07:34 +02:00
|
|
|
public function setLimit($limit) {
|
|
|
|
$this->limit = $limit;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getLimit() {
|
|
|
|
return $this->limit;
|
|
|
|
}
|
|
|
|
|
2016-07-28 22:54:10 +02:00
|
|
|
final public function setRenderOnlyAdjacentNodes($adjacent) {
|
|
|
|
$this->adjacent = $adjacent;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
final public function getRenderOnlyAdjacentNodes() {
|
|
|
|
return $this->adjacent;
|
|
|
|
}
|
|
|
|
|
2016-07-01 17:06:51 +02:00
|
|
|
abstract protected function getEdgeTypes();
|
|
|
|
abstract protected function getParentEdgeType();
|
|
|
|
abstract protected function newQuery();
|
|
|
|
abstract protected function newTableRow($phid, $object, $trace);
|
|
|
|
abstract protected function newTable(AphrontTableView $table);
|
2016-07-01 21:53:41 +02:00
|
|
|
abstract protected function isClosed($object);
|
2016-07-01 17:06:51 +02:00
|
|
|
|
2016-07-28 22:54:10 +02:00
|
|
|
protected function newEllipsisRow() {
|
|
|
|
return array(
|
|
|
|
'...',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2016-07-01 17:06:51 +02:00
|
|
|
final public function setSeedPHID($phid) {
|
|
|
|
$this->seedPHID = $phid;
|
Don't load the entire graph for tasks
Summary:
Ref T4788. As it turns out, our tasks are very tightly connected.
Instead of loading every parent/child task, then every parent/child of those tasks, etc., etc., only load tasks in the "same direction" that we're already heading.
For example, we load children of children, but not parents of children. And we load parents of parents, but not children of parents.
Basically we only go "up" and "down" now, but not "out" as much. This should reduce the gigantic multiple-thousand-node graphs currently shown in the UI.
I still discover the whole graph for revisiosn, because I think it's probably more useful and always much smaller. That might need adjustment too, though.
Test Plan: Seems fine locally??
Reviewers: chad
Reviewed By: chad
Maniphest Tasks: T4788
Differential Revision: https://secure.phabricator.com/D16218
2016-07-01 20:37:01 +02:00
|
|
|
$this->edgeReach[$phid] = array_fill_keys($this->getEdgeTypes(), true);
|
2016-07-01 17:06:51 +02:00
|
|
|
|
|
|
|
return $this->addNodes(
|
|
|
|
array(
|
|
|
|
'<seed>' => array($phid),
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
2016-07-28 20:45:42 +02:00
|
|
|
final public function getSeedPHID() {
|
|
|
|
return $this->seedPHID;
|
|
|
|
}
|
|
|
|
|
2016-07-01 17:06:51 +02:00
|
|
|
final public function isEmpty() {
|
|
|
|
return (count($this->getNodes()) <= 2);
|
|
|
|
}
|
|
|
|
|
2016-07-14 06:07:34 +02:00
|
|
|
final public function isOverLimit() {
|
|
|
|
$limit = $this->getLimit();
|
|
|
|
|
|
|
|
if (!$limit) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (count($this->edgeReach) > $limit);
|
|
|
|
}
|
|
|
|
|
2016-07-01 17:06:51 +02:00
|
|
|
final public function getEdges($type) {
|
Don't load the entire graph for tasks
Summary:
Ref T4788. As it turns out, our tasks are very tightly connected.
Instead of loading every parent/child task, then every parent/child of those tasks, etc., etc., only load tasks in the "same direction" that we're already heading.
For example, we load children of children, but not parents of children. And we load parents of parents, but not children of parents.
Basically we only go "up" and "down" now, but not "out" as much. This should reduce the gigantic multiple-thousand-node graphs currently shown in the UI.
I still discover the whole graph for revisiosn, because I think it's probably more useful and always much smaller. That might need adjustment too, though.
Test Plan: Seems fine locally??
Reviewers: chad
Reviewed By: chad
Maniphest Tasks: T4788
Differential Revision: https://secure.phabricator.com/D16218
2016-07-01 20:37:01 +02:00
|
|
|
$edges = idx($this->edges, $type, array());
|
|
|
|
|
|
|
|
// Remove any nodes which we never reached. We can get these when loading
|
|
|
|
// only part of the graph: for example, they point at other subtasks of
|
|
|
|
// parents or other parents of subtasks.
|
|
|
|
$nodes = $this->getNodes();
|
|
|
|
foreach ($edges as $src => $dsts) {
|
|
|
|
foreach ($dsts as $key => $dst) {
|
|
|
|
if (!isset($nodes[$dst])) {
|
|
|
|
unset($edges[$src][$key]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $edges;
|
|
|
|
}
|
|
|
|
|
|
|
|
final public function setLoadEntireGraph($load_entire_graph) {
|
|
|
|
$this->loadEntireGraph = $load_entire_graph;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
final public function getLoadEntireGraph() {
|
|
|
|
return $this->loadEntireGraph;
|
2016-07-01 17:06:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
final protected function loadEdges(array $nodes) {
|
2016-07-14 06:07:34 +02:00
|
|
|
if ($this->isOverLimit()) {
|
|
|
|
return array_fill_keys($nodes, array());
|
|
|
|
}
|
|
|
|
|
2016-07-01 17:06:51 +02:00
|
|
|
$edge_types = $this->getEdgeTypes();
|
|
|
|
|
|
|
|
$query = id(new PhabricatorEdgeQuery())
|
|
|
|
->withSourcePHIDs($nodes)
|
|
|
|
->withEdgeTypes($edge_types);
|
|
|
|
|
|
|
|
$query->execute();
|
|
|
|
|
Don't load the entire graph for tasks
Summary:
Ref T4788. As it turns out, our tasks are very tightly connected.
Instead of loading every parent/child task, then every parent/child of those tasks, etc., etc., only load tasks in the "same direction" that we're already heading.
For example, we load children of children, but not parents of children. And we load parents of parents, but not children of parents.
Basically we only go "up" and "down" now, but not "out" as much. This should reduce the gigantic multiple-thousand-node graphs currently shown in the UI.
I still discover the whole graph for revisiosn, because I think it's probably more useful and always much smaller. That might need adjustment too, though.
Test Plan: Seems fine locally??
Reviewers: chad
Reviewed By: chad
Maniphest Tasks: T4788
Differential Revision: https://secure.phabricator.com/D16218
2016-07-01 20:37:01 +02:00
|
|
|
$whole_graph = $this->getLoadEntireGraph();
|
|
|
|
|
2016-07-01 17:06:51 +02:00
|
|
|
$map = array();
|
|
|
|
foreach ($nodes as $node) {
|
2016-07-01 17:50:16 +02:00
|
|
|
$map[$node] = array();
|
|
|
|
|
2016-07-01 17:06:51 +02:00
|
|
|
foreach ($edge_types as $edge_type) {
|
|
|
|
$dst_phids = $query->getDestinationPHIDs(
|
|
|
|
array($node),
|
|
|
|
array($edge_type));
|
|
|
|
|
|
|
|
$this->edges[$edge_type][$node] = $dst_phids;
|
|
|
|
foreach ($dst_phids as $dst_phid) {
|
Don't load the entire graph for tasks
Summary:
Ref T4788. As it turns out, our tasks are very tightly connected.
Instead of loading every parent/child task, then every parent/child of those tasks, etc., etc., only load tasks in the "same direction" that we're already heading.
For example, we load children of children, but not parents of children. And we load parents of parents, but not children of parents.
Basically we only go "up" and "down" now, but not "out" as much. This should reduce the gigantic multiple-thousand-node graphs currently shown in the UI.
I still discover the whole graph for revisiosn, because I think it's probably more useful and always much smaller. That might need adjustment too, though.
Test Plan: Seems fine locally??
Reviewers: chad
Reviewed By: chad
Maniphest Tasks: T4788
Differential Revision: https://secure.phabricator.com/D16218
2016-07-01 20:37:01 +02:00
|
|
|
if ($whole_graph || isset($this->edgeReach[$node][$edge_type])) {
|
|
|
|
$map[$node][] = $dst_phid;
|
|
|
|
}
|
|
|
|
$this->edgeReach[$dst_phid][$edge_type] = true;
|
2016-07-01 17:06:51 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$map[$node] = array_values(array_fuse($map[$node]));
|
|
|
|
}
|
|
|
|
|
|
|
|
return $map;
|
|
|
|
}
|
|
|
|
|
|
|
|
final public function newGraphTable() {
|
|
|
|
$viewer = $this->getViewer();
|
|
|
|
|
|
|
|
$ancestry = $this->getEdges($this->getParentEdgeType());
|
|
|
|
|
2016-07-28 22:54:10 +02:00
|
|
|
$only_adjacent = $this->getRenderOnlyAdjacentNodes();
|
|
|
|
if ($only_adjacent) {
|
|
|
|
$adjacent = array(
|
|
|
|
$this->getSeedPHID() => $this->getSeedPHID(),
|
|
|
|
);
|
|
|
|
|
|
|
|
foreach ($this->getEdgeTypes() as $edge_type) {
|
|
|
|
$map = $this->getEdges($edge_type);
|
|
|
|
$direct = idx($map, $this->getSeedPHID(), array());
|
|
|
|
$adjacent += array_fuse($direct);
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($ancestry as $key => $list) {
|
|
|
|
if (!isset($adjacent[$key])) {
|
|
|
|
unset($ancestry[$key]);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($list as $list_key => $item) {
|
|
|
|
if (!isset($adjacent[$item])) {
|
|
|
|
unset($ancestry[$key][$list_key]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-07-01 17:06:51 +02:00
|
|
|
$objects = $this->newQuery()
|
|
|
|
->setViewer($viewer)
|
|
|
|
->withPHIDs(array_keys($ancestry))
|
|
|
|
->execute();
|
|
|
|
$objects = mpull($objects, null, 'getPHID');
|
|
|
|
|
|
|
|
$order = id(new PhutilDirectedScalarGraph())
|
|
|
|
->addNodes($ancestry)
|
2019-06-20 21:13:28 +02:00
|
|
|
->getNodesInTopologicalOrder();
|
2016-07-01 17:06:51 +02:00
|
|
|
|
|
|
|
$ancestry = array_select_keys($ancestry, $order);
|
|
|
|
|
|
|
|
$traces = id(new PHUIDiffGraphView())
|
|
|
|
->renderGraph($ancestry);
|
|
|
|
|
|
|
|
$ii = 0;
|
|
|
|
$rows = array();
|
|
|
|
$rowc = array();
|
2016-07-28 22:54:10 +02:00
|
|
|
|
|
|
|
if ($only_adjacent) {
|
|
|
|
$rows[] = $this->newEllipsisRow();
|
|
|
|
$rowc[] = 'more';
|
|
|
|
}
|
|
|
|
|
2016-07-01 17:06:51 +02:00
|
|
|
foreach ($ancestry as $phid => $ignored) {
|
|
|
|
$object = idx($objects, $phid);
|
|
|
|
$rows[] = $this->newTableRow($phid, $object, $traces[$ii++]);
|
|
|
|
|
2016-07-01 21:53:41 +02:00
|
|
|
$classes = array();
|
2016-07-01 17:06:51 +02:00
|
|
|
if ($phid == $this->seedPHID) {
|
2016-07-01 21:53:41 +02:00
|
|
|
$classes[] = 'highlighted';
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($object) {
|
|
|
|
if ($this->isClosed($object)) {
|
|
|
|
$classes[] = 'closed';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($classes) {
|
|
|
|
$classes = implode(' ', $classes);
|
2016-07-01 17:06:51 +02:00
|
|
|
} else {
|
2016-07-01 21:53:41 +02:00
|
|
|
$classes = null;
|
2016-07-01 17:06:51 +02:00
|
|
|
}
|
2016-07-01 21:53:41 +02:00
|
|
|
|
|
|
|
$rowc[] = $classes;
|
2016-07-01 17:06:51 +02:00
|
|
|
}
|
2016-07-28 22:54:10 +02:00
|
|
|
|
|
|
|
if ($only_adjacent) {
|
|
|
|
$rows[] = $this->newEllipsisRow();
|
|
|
|
$rowc[] = 'more';
|
|
|
|
}
|
2016-07-01 17:06:51 +02:00
|
|
|
|
|
|
|
$table = id(new AphrontTableView($rows))
|
2016-07-01 21:53:41 +02:00
|
|
|
->setClassName('object-graph-table')
|
2016-07-01 17:06:51 +02:00
|
|
|
->setRowClasses($rowc);
|
|
|
|
|
|
|
|
$this->objects = $objects;
|
|
|
|
|
|
|
|
return $this->newTable($table);
|
|
|
|
}
|
|
|
|
|
|
|
|
final public function getReachableObjects($edge_type) {
|
|
|
|
if ($this->objects === null) {
|
|
|
|
throw new PhutilInvalidStateException('newGraphTable');
|
|
|
|
}
|
|
|
|
|
|
|
|
$graph = $this->getEdges($edge_type);
|
|
|
|
|
|
|
|
$seen = array();
|
|
|
|
$look = array($this->seedPHID);
|
|
|
|
while ($look) {
|
|
|
|
$phid = array_pop($look);
|
|
|
|
|
|
|
|
$parents = idx($graph, $phid, array());
|
|
|
|
foreach ($parents as $parent) {
|
|
|
|
if (isset($seen[$parent])) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$seen[$parent] = $parent;
|
|
|
|
$look[] = $parent;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$reachable = array();
|
|
|
|
foreach ($seen as $phid) {
|
|
|
|
if ($phid == $this->seedPHID) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$object = idx($this->objects, $phid);
|
|
|
|
if (!$object) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$reachable[] = $object;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $reachable;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|