diff --git a/src/applications/differential/controller/DifferentialRevisionStatsController.php b/src/applications/differential/controller/DifferentialRevisionStatsController.php index 1389ba7d66..613ba1e4b2 100644 --- a/src/applications/differential/controller/DifferentialRevisionStatsController.php +++ b/src/applications/differential/controller/DifferentialRevisionStatsController.php @@ -1,166 +1,183 @@ <?php /* * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ final class DifferentialRevisionStatsController extends DifferentialController { private $filter; private function loadRevisions($phid) { $table = new DifferentialRevision(); $conn_r = $table->establishConnection('r'); $rows = queryfx_all( $conn_r, 'SELECT revisions.* FROM %T revisions ' . 'JOIN %T comments ON comments.revisionID = revisions.id ' . 'JOIN (' . ' SELECT revisionID FROM %T WHERE objectPHID = %s ' . ' UNION ALL ' . ' SELECT id from differential_revision WHERE authorPHID = %s) rel ' . 'ON (comments.revisionID = rel.revisionID)' . 'WHERE comments.action = %s' . 'AND comments.authorPHID = %s', $table->getTableName(), id(new DifferentialComment())->getTableName(), DifferentialRevision::RELATIONSHIP_TABLE, $phid, $phid, $this->filter, $phid ); return $table->loadAllFromArray($rows); } private function loadComments($phid) { $table = new DifferentialComment(); $conn_r = $table->establishConnection('r'); $rows = queryfx_all( $conn_r, 'SELECT comments.* FROM %T comments ' . 'JOIN (' . ' SELECT revisionID FROM %T WHERE objectPHID = %s ' . ' UNION ALL ' . ' SELECT id from differential_revision WHERE authorPHID = %s) rel ' . 'ON (comments.revisionID = rel.revisionID)' . 'WHERE comments.action = %s' . 'AND comments.authorPHID = %s', $table->getTableName(), DifferentialRevision::RELATIONSHIP_TABLE, $phid, $phid, $this->filter, $phid ); return $table->loadAllFromArray($rows); } + + private function loadDiffs(array $revisions) { + if (!$revisions) { + return array(); + } + + $diff_teml = new DifferentialDiff(); + $diffs = $diff_teml->loadAllWhere( + 'revisionID in (%Ld)', + array_keys($revisions) + ); + return $diffs; + } + public function willProcessRequest(array $data) { $this->filter = idx($data, 'filter'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($request->isFormPost()) { $phid_arr = $request->getArr('view_user'); $view_target = head($phid_arr); return id(new AphrontRedirectResponse()) ->setURI($request->getRequestURI()->alter('phid', $view_target)); } $params = array_filter( array( 'phid' => $request->getStr('phid'), )); // Fill in the defaults we'll actually use for calculations if any // parameters are missing. $params += array( 'phid' => $user->getPHID(), ); $side_nav = new AphrontSideNavFilterView(); $side_nav->setBaseURI(id(new PhutilURI('/differential/stats/')) ->alter('phid', $params['phid'])); foreach (array( DifferentialAction::ACTION_CLOSE, DifferentialAction::ACTION_ACCEPT, DifferentialAction::ACTION_REJECT, DifferentialAction::ACTION_UPDATE, DifferentialAction::ACTION_COMMENT, ) as $action) { $verb = ucfirst(DifferentialAction::getActionPastTenseVerb($action)); $side_nav->addFilter($action, $verb); } $this->filter = $side_nav->selectFilter($this->filter, DifferentialAction::ACTION_CLOSE); $panels = array(); $handles = id(new PhabricatorObjectHandleData(array($params['phid']))) ->loadHandles(); $filter_form = id(new AphrontFormView()) ->setAction('/differential/stats/'.$this->filter.'/') ->setUser($user); $filter_form->appendChild( $this->renderControl($params['phid'], $handles)); $filter_form->appendChild(id(new AphrontFormSubmitControl()) ->setValue('Filter Revisions')); $side_nav->appendChild($filter_form); $comments = $this->loadComments($params['phid']); $revisions = $this->loadRevisions($params['phid']); + $diffs = $this->loadDiffs($revisions); $panel = new AphrontPanelView(); $panel->setHeader('Differential rate analysis'); $panel->appendChild( id(new DifferentialRevisionStatsView()) ->setComments($comments) + ->setFilter($this->filter) ->setRevisions($revisions) + ->setDiffs($diffs) ->setUser($user)); $panels[] = $panel; foreach ($panels as $panel) { $side_nav->appendChild($panel); } return $this->buildStandardPageResponse( $side_nav, array( 'title' => 'Differential statistics', )); } private function renderControl($view_phid, $handles) { $value = array(); if ($view_phid) { $value = array( $view_phid => $handles[$view_phid]->getFullName(), ); } return id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/users/') ->setLabel('View User') ->setName('view_user') ->setValue($value) ->setLimit(1); } } diff --git a/src/applications/differential/view/DifferentialRevisionStatsView.php b/src/applications/differential/view/DifferentialRevisionStatsView.php index 66eb51aecf..a455a1aeae 100644 --- a/src/applications/differential/view/DifferentialRevisionStatsView.php +++ b/src/applications/differential/view/DifferentialRevisionStatsView.php @@ -1,156 +1,234 @@ <?php /* * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Render some distracting statistics on revisions */ final class DifferentialRevisionStatsView extends AphrontView { private $comments; private $revisions; + private $diffs; private $user; + private $filter; public function setRevisions(array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $this->revisions = $revisions; return $this; } public function setComments(array $comments) { assert_instances_of($comments, 'DifferentialComment'); $this->comments = $comments; return $this; } + public function setDiffs(array $diffs) { + assert_instances_of($diffs, 'DifferentialDiff'); + $this->diffs = $diffs; + return $this; + } + + public function setFilter($filter) { + $this->filter = $filter; + return $this; + } + public function setUser($user) { $this->user = $user; return $this; } public function render() { $user = $this->user; if (!$user) { throw new Exception("Call setUser() before render()!"); } $id_to_revision_map = array(); foreach ($this->revisions as $rev) { $id_to_revision_map[$rev->getID()] = $rev; } $revisions_seen = array(); $dates = array(); $counts = array(); $lines = array(); - $boosts = array(); $days_with_diffs = array(); $count_active = array(); + $response_time = array(); + $response_count = array(); $now = time(); $row_array = array(); foreach (array( '1 week', '2 weeks', '3 weeks', '1 month', '2 months', '3 months', '6 months', '9 months', '1 year', '18 months', '2 years', '3 years', '4 years', '5 years', ) as $age) { $dates[$age] = strtotime($age . ' ago 23:59:59'); $counts[$age] = 0; $lines[$age] = 0; $count_active[$age] = 0; + $response_time[$age] = array(); + } + + $revision_diffs_map = mgroup($this->diffs, 'getRevisionID'); + foreach ($revision_diffs_map as $revision_id => $diffs) { + $revision_diffs_map[$revision_id] = msort($diffs, 'getID'); } foreach ($this->comments as $comment) { - $rev_date = $comment->getDateCreated(); + $comment_date = $comment->getDateCreated(); - $day = phabricator_date($rev_date, $user); + $day = phabricator_date($comment_date, $user); $old_daycount = idx($days_with_diffs, $day, 0); $days_with_diffs[$day] = $old_daycount + 1; $rev_id = $comment->getRevisionID(); if (idx($revisions_seen, $rev_id)) { - continue; + $revision_seen = true; + $rev = null; + } else { + $revision_seen = false; + $rev = $id_to_revision_map[$rev_id]; + $revisions_seen[$rev_id] = true; } - $rev = $id_to_revision_map[$rev_id]; - $revisions_seen[$rev_id] = true; foreach ($dates as $age => $cutoff) { - if ($cutoff >= $rev_date) { + if ($cutoff >= $comment_date) { continue; } - if ($rev) { - $lines[$age] += $rev->getLineCount(); + + if (!$revision_seen) { + if ($rev) { + $lines[$age] += $rev->getLineCount(); + } + $counts[$age]++; + if (!$old_daycount) { + $count_active[$age]++; + } } - $counts[$age]++; - if (!$old_daycount) { - $count_active[$age]++; + + $diffs = $revision_diffs_map[$rev_id]; + $target_diff = $this->findTargetDiff($diffs, $comment); + if ($target_diff) { + $response_time[$age][] = + $comment_date - $target_diff->getDateCreated(); } } } $old_count = 0; foreach (array_reverse($dates) as $age => $cutoff) { $weeks = ceil(($now - $cutoff) / (60 * 60 * 24)) / 7; if ($old_count == $counts[$age] && count($row_array) == 1) { unset($dates[last_key($row_array)]); $row_array = array(); } $old_count = $counts[$age]; $row_array[$age] = array( 'Revisions per week' => number_format($counts[$age] / $weeks, 2), 'Lines per week' => number_format($lines[$age] / $weeks, 1), 'Active days per week' => number_format($count_active[$age] / $weeks, 1), 'Revisions' => number_format($counts[$age]), 'Lines' => number_format($lines[$age]), 'Lines per diff' => number_format($lines[$age] / ($counts[$age] + 0.0001)), 'Active days' => number_format($count_active[$age]), ); + + switch ($this->filter) { + case DifferentialAction::ACTION_CLOSE: + case DifferentialAction::ACTION_UPDATE: + case DifferentialAction::ACTION_COMMENT: + break; + case DifferentialAction::ACTION_ACCEPT: + case DifferentialAction::ACTION_REJECT: + $count = count($response_time[$age]); + if ($count) { + rsort($response_time[$age]); + $median = $response_time[$age][round($count / 2) - 1]; + $average = array_sum($response_time[$age]) / $count; + } else { + $median = 0; + $average = 0; + } + + $row_array[$age]['Response hours (median|average)'] = + number_format($median / 3600, 1). + ' | '. + number_format($average / 3600, 1); + break; + } } $rows = array(); $row_names = array_keys(head($row_array)); foreach ($row_names as $row_name) { $rows[] = array($row_name); } foreach (array_keys($dates) as $age) { $i = 0; foreach ($row_names as $row_name) { $rows[$i][] = idx(idx($row_array, $age), $row_name, '-'); ++$i; } } $table = new AphrontTableView($rows); $table->setColumnClasses( array( 'wide pri', )); $table->setHeaders( array_merge( array( 'Metric', ), array_keys($dates))); return $table->render(); } + + private function findTargetDiff(array $diffs, + DifferentialComment $comment) { + switch ($this->filter) { + case DifferentialAction::ACTION_CLOSE: + case DifferentialAction::ACTION_UPDATE: + case DifferentialAction::ACTION_COMMENT: + return null; + case DifferentialAction::ACTION_ACCEPT: + case DifferentialAction::ACTION_REJECT: + $result = head($diffs); + foreach ($diffs as $diff) { + if ($diff->getDateCreated() >= $comment->getDateCreated()) { + break; + } + $result = $diff; + } + + return $result; + } + } }