diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2588,6 +2588,7 @@ 'PhabricatorCoreCreateTransaction' => 'applications/transactions/xaction/PhabricatorCoreCreateTransaction.php', 'PhabricatorCoreTransactionType' => 'applications/transactions/xaction/PhabricatorCoreTransactionType.php', 'PhabricatorCoreVoidTransaction' => 'applications/transactions/xaction/PhabricatorCoreVoidTransaction.php', + 'PhabricatorCountFact' => 'applications/fact/fact/PhabricatorCountFact.php', 'PhabricatorCountdown' => 'applications/countdown/storage/PhabricatorCountdown.php', 'PhabricatorCountdownApplication' => 'applications/countdown/application/PhabricatorCountdownApplication.php', 'PhabricatorCountdownController' => 'applications/countdown/controller/PhabricatorCountdownController.php', @@ -8078,6 +8079,7 @@ 'PhabricatorCoreCreateTransaction' => 'PhabricatorCoreTransactionType', 'PhabricatorCoreTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorCoreVoidTransaction' => 'PhabricatorModularTransactionType', + 'PhabricatorCountFact' => 'PhabricatorFact', 'PhabricatorCountdown' => array( 'PhabricatorCountdownDAO', 'PhabricatorPolicyInterface', diff --git a/src/applications/fact/controller/PhabricatorFactChartController.php b/src/applications/fact/controller/PhabricatorFactChartController.php --- a/src/applications/fact/controller/PhabricatorFactChartController.php +++ b/src/applications/fact/controller/PhabricatorFactChartController.php @@ -16,6 +16,9 @@ $key_id = id(new PhabricatorFactKeyDimension()) ->newDimensionID($fact->getKey()); + if (!$key_id) { + return new Aphront404Response(); + } $table = $fact->newDatapoint(); $conn_r = $table->establishConnection('r'); diff --git a/src/applications/fact/daemon/PhabricatorFactDaemon.php b/src/applications/fact/daemon/PhabricatorFactDaemon.php --- a/src/applications/fact/daemon/PhabricatorFactDaemon.php +++ b/src/applications/fact/daemon/PhabricatorFactDaemon.php @@ -70,20 +70,28 @@ $result = null; $datapoints = array(); + $count = 0; foreach ($iterator as $key => $object) { $phid = $object->getPHID(); $this->log(pht('Processing %s...', $phid)); - $datapoints[$phid] = $this->newDatapoints($object); - if (count($datapoints) > 1024) { + $object_datapoints = $this->newDatapoints($object); + $count += count($object_datapoints); + + $datapoints[$phid] = $object_datapoints; + + if ($count > 1024) { $this->updateDatapoints($datapoints); $datapoints = array(); + $count = 0; } + $result = $key; } - if ($datapoints) { + if ($count) { $this->updateDatapoints($datapoints); $datapoints = array(); + $count = 0; } return $result; @@ -111,7 +119,6 @@ return; } - $fact_keys = array(); $objects = array(); foreach ($map as $phid => $facts) { @@ -129,9 +136,9 @@ } $key_map = id(new PhabricatorFactKeyDimension()) - ->newDimensionMap(array_keys($fact_keys)); + ->newDimensionMap(array_keys($fact_keys), true); $object_map = id(new PhabricatorFactObjectDimension()) - ->newDimensionMap(array_keys($objects)); + ->newDimensionMap(array_keys($objects), true); $table = new PhabricatorFactIntDatapoint(); $conn = $table->establishConnection('w'); diff --git a/src/applications/fact/engine/PhabricatorFactEngine.php b/src/applications/fact/engine/PhabricatorFactEngine.php --- a/src/applications/fact/engine/PhabricatorFactEngine.php +++ b/src/applications/fact/engine/PhabricatorFactEngine.php @@ -35,4 +35,8 @@ return $this->factMap[$key]; } + final protected function getViewer() { + return PhabricatorUser::getOmnipotentUser(); + } + } diff --git a/src/applications/fact/engine/PhabricatorFactManiphestTaskEngine.php b/src/applications/fact/engine/PhabricatorFactManiphestTaskEngine.php --- a/src/applications/fact/engine/PhabricatorFactManiphestTaskEngine.php +++ b/src/applications/fact/engine/PhabricatorFactManiphestTaskEngine.php @@ -5,8 +5,77 @@ public function newFacts() { return array( + id(new PhabricatorCountFact()) + ->setKey('tasks.count.create'), + + id(new PhabricatorCountFact()) + ->setKey('tasks.open-count.create'), + id(new PhabricatorCountFact()) + ->setKey('tasks.open-count.status'), + + id(new PhabricatorCountFact()) + ->setKey('tasks.count.create.project'), + id(new PhabricatorCountFact()) + ->setKey('tasks.count.assign.project'), + id(new PhabricatorCountFact()) + ->setKey('tasks.open-count.create.project'), + id(new PhabricatorCountFact()) + ->setKey('tasks.open-count.status.project'), + id(new PhabricatorCountFact()) + ->setKey('tasks.open-count.assign.project'), + + id(new PhabricatorCountFact()) + ->setKey('tasks.count.create.owner'), + id(new PhabricatorCountFact()) + ->setKey('tasks.count.assign.owner'), + id(new PhabricatorCountFact()) + ->setKey('tasks.open-count.create.owner'), + id(new PhabricatorCountFact()) + ->setKey('tasks.open-count.status.owner'), + id(new PhabricatorCountFact()) + ->setKey('tasks.open-count.assign.owner'), + + id(new PhabricatorPointsFact()) + ->setKey('tasks.points.create'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.points.score'), + + id(new PhabricatorPointsFact()) + ->setKey('tasks.open-points.create'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.open-points.status'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.open-points.score'), + + id(new PhabricatorPointsFact()) + ->setKey('tasks.points.create.project'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.points.assign.project'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.points.score.project'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.open-points.create.project'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.open-points.status.project'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.open-points.score.project'), id(new PhabricatorPointsFact()) - ->setKey('tasks.count.open'), + ->setKey('tasks.open-points.assign.project'), + + id(new PhabricatorPointsFact()) + ->setKey('tasks.points.create.owner'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.points.assign.owner'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.points.score.owner'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.open-points.create.owner'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.open-points.status.owner'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.open-points.score.project'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.open-points.assign.owner'), ); } @@ -15,18 +84,319 @@ } public function newDatapointsForObject(PhabricatorLiskDAO $object) { + $viewer = $this->getViewer(); + + $xaction_query = PhabricatorApplicationTransactionQuery::newQueryForObject( + $object); + $xactions = $xaction_query + ->setViewer($viewer) + ->withObjectPHIDs(array($object->getPHID())) + ->execute(); + + $xactions = msortv($xactions, 'newChronologicalSortVector'); + + $old_open = false; + $old_points = 0; + $old_owner = null; + $project_map = array(); + $object_phid = $object->getPHID(); + $is_create = true; + + $specs = array(); $datapoints = array(); + foreach ($xactions as $xaction_group) { + $add_projects = array(); + $rem_projects = array(); + + $new_open = $old_open; + $new_points = $old_points; + $new_owner = $old_owner; + + // TODO: Actually group concurrent transactions. + $xaction_group = array($xaction_group); + + $group_epoch = last($xaction_group)->getDateCreated(); + foreach ($xaction_group as $xaction) { + $old_value = $xaction->getOldValue(); + $new_value = $xaction->getNewValue(); + switch ($xaction->getTransactionType()) { + case ManiphestTaskStatusTransaction::TRANSACTIONTYPE: + $new_open = !ManiphestTaskStatus::isClosedStatus($new_value); + break; + case ManiphestTaskMergedIntoTransaction::TRANSACTIONTYPE: + // When a task is merged into another task, it is changed to a + // closed status without generating a separate status transaction. + $new_open = false; + break; + case ManiphestTaskPointsTransaction::TRANSACTIONTYPE: + $new_points = (int)$xaction->getNewValue(); + break; + case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE: + $new_owner = $xaction->getNewValue(); + break; + case PhabricatorTransactions::TYPE_EDGE: + $edge_type = $xaction->getMetadataValue('edge:type'); + switch ($edge_type) { + case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST: + $record = PhabricatorEdgeChangeRecord::newFromTransaction( + $xaction); + $add_projects += array_fuse($record->getAddedPHIDs()); + $rem_projects += array_fuse($record->getRemovedPHIDs()); + break; + } + break; + } + } + + // If a project was both added and removed, moot it. + $mix_projects = array_intersect_key($add_projects, $rem_projects); + $add_projects = array_diff_key($add_projects, $mix_projects); + $rem_projects = array_diff_key($rem_projects, $mix_projects); + + $project_sets = array( + array( + 'phids' => $rem_projects, + 'scale' => -1, + ), + array( + 'phids' => $add_projects, + 'scale' => 1, + ), + ); + + if ($is_create) { + $action = 'create'; + $action_points = $new_points; + } else { + $action = 'assign'; + $action_points = $old_points; + } + + foreach ($project_sets as $project_set) { + $scale = $project_set['scale']; + foreach ($project_set['phids'] as $project_phid) { + if ($old_open) { + $specs[] = array( + "tasks.open-count.{$action}.project", + 1 * $scale, + $project_phid, + ); + + $specs[] = array( + "tasks.open-points.{$action}.project", + $action_points * $scale, + $project_phid, + ); + } + + $specs[] = array( + "tasks.count.{$action}.project", + 1 * $scale, + $project_phid, + ); + + $specs[] = array( + "tasks.points.{$action}.project", + $action_points * $scale, + $project_phid, + ); + + if ($scale < 0) { + unset($project_map[$project_phid]); + } else { + $project_map[$project_phid] = $project_phid; + } + } + } + + if ($new_owner !== $old_owner) { + $owner_sets = array( + array( + 'phid' => $old_owner, + 'scale' => -1, + ), + array( + 'phid' => $new_owner, + 'scale' => 1, + ), + ); + + foreach ($owner_sets as $owner_set) { + $owner_phid = $owner_set['phid']; + if ($owner_phid === null) { + continue; + } + + if ($old_open) { + $specs[] = array( + "tasks.open-count.{$action}.owner", + 1 * $scale, + $owner_phid, + ); + + $specs[] = array( + "tasks.open-points.{$action}.owner", + $action_points * $scale, + $owner_phid, + ); + } + + $specs[] = array( + "tasks.count.{$action}.owner", + 1 * $scale, + $owner_phid, + ); + + $specs[] = array( + "tasks.points.{$action}.owner", + $action_points * $scale, + $owner_phid, + ); + } + + $old_owner = $new_owner; + } + + if ($is_create) { + $specs[] = array( + 'tasks.count.create', + 1, + ); + $specs[] = array( + 'tasks.points.create', + $new_points, + ); + + if ($new_open) { + $specs[] = array( + 'tasks.open-count.create', + 1, + ); + $specs[] = array( + 'tasks.open-points.create', + $new_points, + ); + } + } else if ($new_open !== $old_open) { + if ($new_open) { + $scale = 1; + } else { + $scale = -1; + } + + $specs[] = array( + 'tasks.open-count.status', + 1 * $scale, + ); + + $specs[] = array( + 'tasks.open-points.status', + $action_points * $scale, + ); + + if ($new_owner !== null) { + $specs[] = array( + 'tasks.open-count.status.owner', + 1 * $scale, + $new_owner, + ); + $specs[] = array( + 'tasks.open-points.status.owner', + $action_points * $scale, + $new_owner, + ); + } + + foreach ($project_map as $project_phid) { + $specs[] = array( + 'tasks.open-count.status.project', + 1 * $scale, + $project_phid, + ); + $specs[] = array( + 'tasks.open-points.status.project', + $action_points * $scale, + $new_owner, + ); + } + + $old_open = $new_open; + } + + // The "score" facts only apply to rescoring tasks which already + // exist, so we skip them if the task is being created. + if (($new_points !== $old_points) && !$is_create) { + $delta = ($new_points - $old_points); + + $specs[] = array( + 'tasks.points.score', + $delta, + ); + + foreach ($project_map as $project_phid) { + $specs[] = array( + 'tasks.points.score.project', + $delta, + $project_phid, + ); + + if ($old_open && $new_open) { + $specs[] = array( + 'tasks.open-points.score.project', + $delta, + $project_phid, + ); + } + } + + if ($new_owner !== null) { + $specs[] = array( + 'tasks.points.score.owner', + $delta, + $new_owner, + ); + + if ($old_open && $new_open) { + $specs[] = array( + 'tasks.open-points.score.owner', + $delta, + $new_owner, + ); + } + } + + if ($old_open && $new_open) { + $specs[] = array( + 'tasks.open-points.score', + $delta, + ); + } + + $old_points = $new_points; + } + + foreach ($specs as $spec) { + $spec_key = $spec[0]; + $spec_value = $spec[1]; + + $datapoint = $this->getFact($spec_key) + ->newDatapoint(); + + $datapoint + ->setObjectPHID($object_phid) + ->setValue($spec_value) + ->setEpoch($group_epoch); - $phid = $object->getPHID(); - $type = phid_get_type($phid); + if (isset($spec[2])) { + $datapoint->setDimensionPHID($spec[2]); + } - $datapoint = $this->getFact('tasks.count.open') - ->newDatapoint(); + $datapoints[] = $datapoint; + } - $datapoints[] = $datapoint - ->setObjectPHID($phid) - ->setValue(1) - ->setEpoch($object->getDateCreated()); + $specs = array(); + $is_create = false; + } return $datapoints; } diff --git a/src/applications/fact/fact/PhabricatorCountFact.php b/src/applications/fact/fact/PhabricatorCountFact.php new file mode 100644 --- /dev/null +++ b/src/applications/fact/fact/PhabricatorCountFact.php @@ -0,0 +1,9 @@ +newDimensionMap(array($key)); - return $map[$key]; + return idx($map, $key); } - final public function newDimensionMap(array $keys) { + final public function newDimensionMap(array $keys, $create = false) { if (!$keys) { return array(); } @@ -40,6 +40,10 @@ return $map; } + if (!$create) { + return $map; + } + $sql = array(); foreach ($need as $key) { $sql[] = qsprintf( @@ -66,7 +70,7 @@ $need); $rows = ipull($rows, 'id', $column); - foreach ($keys as $key) { + foreach ($need as $key) { if (isset($rows[$key])) { $map[$key] = (int)$rows[$key]; } else { diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -260,6 +260,11 @@ return $this->oldValueHasBeenSet; } + public function newChronologicalSortVector() { + return id(new PhutilSortVector()) + ->addInt((int)$this->getDateCreated()) + ->addInt((int)$this->getID()); + } /* -( Rendering )---------------------------------------------------------- */