|
| 1 | +-- vLLM PR cycle time breakdown |
| 2 | +-- Computes P50 and P90 (hours) for: |
| 3 | +-- 1) Time to first (human) review: PR ready -> first human review |
| 4 | +-- 2) Time to approval: first human review -> first approval |
| 5 | +-- 3) Time in merge queue: first approval -> merge time |
| 6 | +-- Notes: |
| 7 | +-- - "Ready" is derived from the first time the 'ready' label was applied. |
| 8 | +-- - Reviews excluded if state = 'DISMISSED' and if reviewer looks like a bot. |
| 9 | +-- - Human review is approximated via author_association in an allowed set and reviewer != PR author. |
| 10 | +-- - Metrics only consider merged PRs within the window [startTime, stopTime). |
| 11 | + |
| 12 | +WITH prs AS ( |
| 13 | + SELECT |
| 14 | + number AS pr_number, |
| 15 | + user.login AS author, |
| 16 | + parseDateTimeBestEffort(created_at) AS created_at_ts, |
| 17 | + parseDateTimeBestEffort(closed_at) AS merged_at_ts |
| 18 | + FROM default.pull_request |
| 19 | + WHERE |
| 20 | + dynamoKey LIKE concat({repo: String }, '%') |
| 21 | + AND state = 'closed' |
| 22 | + AND closed_at != '' |
| 23 | + AND parseDateTimeBestEffort(closed_at) >= {startTime: DateTime64(3) } |
| 24 | + AND parseDateTimeBestEffort(closed_at) < {stopTime: DateTime64(3) } |
| 25 | +), |
| 26 | + |
| 27 | +ready_events AS ( |
| 28 | + SELECT |
| 29 | + ple.pr_number, |
| 30 | + minIf(ple.event_time, lowerUTF8(ple.label_name) = 'ready' AND ple.action = 'labeled') AS first_ready_ts |
| 31 | + FROM default.pull_label_event ple |
| 32 | + WHERE |
| 33 | + ple.repo_name = {repo: String } |
| 34 | + GROUP BY ple.pr_number |
| 35 | +), |
| 36 | + |
| 37 | +reviews_raw AS ( |
| 38 | + SELECT |
| 39 | + toUInt32(extractGroups(review.'pull_request_url', 'pulls/([0-9]+)')[1]) AS pr_number, |
| 40 | + review.'user'.'login' AS reviewer, |
| 41 | + review.'state' AS state, |
| 42 | + review.'author_association' AS author_association, |
| 43 | + review.'submitted_at' AS submitted_at_ts |
| 44 | + FROM default.pull_request_review |
| 45 | + WHERE |
| 46 | + dynamoKey LIKE concat({repo: String }, '%') |
| 47 | + AND review.'submitted_at' IS NOT NULL |
| 48 | +), |
| 49 | + |
| 50 | +-- Filter to human reviews and exclude dismissed ones and bot reviewers |
| 51 | +human_reviews AS ( |
| 52 | + SELECT |
| 53 | + r.pr_number, |
| 54 | + r.reviewer, |
| 55 | + r.state, |
| 56 | + r.author_association, |
| 57 | + r.submitted_at_ts |
| 58 | + FROM reviews_raw r |
| 59 | + WHERE |
| 60 | + lowerUTF8(r.state) != 'dismissed' |
| 61 | + AND r.author_association IN ('MEMBER', 'OWNER', 'COLLABORATOR', 'CONTRIBUTOR') |
| 62 | + AND r.reviewer NOT LIKE '%[bot]' |
| 63 | + AND lowerUTF8(r.reviewer) NOT LIKE '%bot%' |
| 64 | +), |
| 65 | + |
| 66 | +first_human_review AS ( |
| 67 | + SELECT |
| 68 | + pr.pr_number, |
| 69 | + -- Define "first review" as first non-approved human review (commented/changes_requested) |
| 70 | + minIf( |
| 71 | + hr.submitted_at_ts, |
| 72 | + hr.reviewer != pr.author AND lowerUTF8(hr.state) IN ('commented','changes_requested') |
| 73 | + ) AS first_review_ts |
| 74 | + FROM prs pr |
| 75 | + LEFT JOIN human_reviews hr ON pr.pr_number = hr.pr_number |
| 76 | + GROUP BY pr.pr_number |
| 77 | +), |
| 78 | + |
| 79 | +first_approval AS ( |
| 80 | + SELECT |
| 81 | + pr.pr_number, |
| 82 | + -- Only count approvals from maintainers (exclude contributor approvals) |
| 83 | + minIf( |
| 84 | + hr.submitted_at_ts, |
| 85 | + lowerUTF8(hr.state) = 'approved' |
| 86 | + AND hr.reviewer != pr.author |
| 87 | + AND hr.author_association IN ('MEMBER','OWNER','COLLABORATOR') |
| 88 | + ) AS first_approval_ts |
| 89 | + FROM prs pr |
| 90 | + LEFT JOIN human_reviews hr ON pr.pr_number = hr.pr_number |
| 91 | + GROUP BY pr.pr_number |
| 92 | +), |
| 93 | + |
| 94 | +durations AS ( |
| 95 | + SELECT |
| 96 | + pr.pr_number, |
| 97 | + coalesce(re.first_ready_ts, pr.created_at_ts) AS ready_ts, |
| 98 | + fr.first_review_ts, |
| 99 | + fa.first_approval_ts, |
| 100 | + pr.merged_at_ts, |
| 101 | + -- Durations in hours |
| 102 | + if( |
| 103 | + fr.first_review_ts IS NULL OR fr.first_review_ts < coalesce(re.first_ready_ts, pr.created_at_ts), |
| 104 | + NULL, |
| 105 | + dateDiff('second', coalesce(re.first_ready_ts, pr.created_at_ts), fr.first_review_ts) / 3600.0 |
| 106 | + ) AS time_to_first_review_hours, |
| 107 | + |
| 108 | + if( |
| 109 | + fa.first_approval_ts IS NULL OR fr.first_review_ts IS NULL OR fa.first_approval_ts < fr.first_review_ts, |
| 110 | + NULL, |
| 111 | + dateDiff('second', fr.first_review_ts, fa.first_approval_ts) / 3600.0 |
| 112 | + ) AS time_to_approval_hours, |
| 113 | + |
| 114 | + if( |
| 115 | + fa.first_approval_ts IS NULL OR pr.merged_at_ts < fa.first_approval_ts, |
| 116 | + NULL, |
| 117 | + dateDiff('second', fa.first_approval_ts, pr.merged_at_ts) / 3600.0 |
| 118 | + ) AS time_in_merge_queue_hours |
| 119 | + FROM prs pr |
| 120 | + LEFT JOIN ready_events re ON pr.pr_number = re.pr_number |
| 121 | + LEFT JOIN first_human_review fr ON pr.pr_number = fr.pr_number |
| 122 | + LEFT JOIN first_approval fa ON pr.pr_number = fa.pr_number |
| 123 | +), |
| 124 | + |
| 125 | +filtered AS ( |
| 126 | + SELECT |
| 127 | + * |
| 128 | + FROM durations |
| 129 | + WHERE |
| 130 | + (time_to_first_review_hours IS NULL OR (time_to_first_review_hours >= 0 AND time_to_first_review_hours < 24 * 30)) |
| 131 | + AND (time_to_approval_hours IS NULL OR (time_to_approval_hours >= 0 AND time_to_approval_hours < 24 * 30)) |
| 132 | + AND (time_in_merge_queue_hours IS NULL OR (time_in_merge_queue_hours >= 0 AND time_in_merge_queue_hours < 24 * 30)) |
| 133 | +) |
| 134 | + |
| 135 | +SELECT |
| 136 | + round(quantile(0.5)(time_to_first_review_hours), 2) AS time_to_first_review_p50, |
| 137 | + round(quantile(0.9)(time_to_first_review_hours), 2) AS time_to_first_review_p90, |
| 138 | + round(quantile(0.5)(time_to_approval_hours), 2) AS time_to_approval_p50, |
| 139 | + round(quantile(0.9)(time_to_approval_hours), 2) AS time_to_approval_p90, |
| 140 | + round(quantile(0.5)(time_in_merge_queue_hours), 2) AS time_in_merge_queue_p50, |
| 141 | + round(quantile(0.9)(time_in_merge_queue_hours), 2) AS time_in_merge_queue_p90 |
| 142 | +FROM filtered |
| 143 | +-- Quantiles ignore NULLs implicitly; if a column is entirely NULL in window, result will be NULL |
| 144 | + |
0 commit comments