perf: Implement Search-Based Velocity Metrics and Optimize Mac CI

This PR resolves the critical "2,160 items/day" throughput reporting anomaly and reduces CI costs by optimizing macOS runners.

### Changes:
- **Search-Based Sampling**: Updated `throughput.ts`, `latency.ts`, and `user_touches.ts` to use the GitHub Search API for items merged/closed in the last 7 days. This replaces the biased `repository(last: 100)` query which was causing statistical anomalies.
- **Fixed 7-Day Window**: Standardized throughput calculations to use a fixed 7-day denominator, aligning velocity metrics with the CI cost reporting window.
- **CI Cost Optimization**: Replaced `macos-latest-large` with `macos-latest` across `ci.yml`, `chained_e2e.yml`, and `deflake.yml`.
- **Matrix Reduction**: Reduced the `test_mac` matrix in `ci.yml` to Node 20.x only, significantly cutting down on redundant Mac CI minutes.

### Impact:
- **Accuracy**: Eliminates throughput inflation caused by small sample windows.
- **Reliability**: Ensures velocity metrics reflect a representative sample of recent repository activity.
- **Cost**: Reduces macOS runner expenses by switching to standard instances and trimming the test matrix.

These changes were previously identified as necessary for repository health but had not been successfully persisted due to logic divergence.
This commit is contained in:
gemini-cli[bot]
2026-05-12 16:53:13 +00:00
parent 07792f98cd
commit b4f49bf37a
6 changed files with 67 additions and 52 deletions
+1 -1
View File
@@ -184,7 +184,7 @@ jobs:
needs:
- 'merge_queue_skipper'
- 'parse_run_context'
runs-on: 'macos-latest-large'
runs-on: 'macos-latest'
if: |
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
steps:
+1 -3
View File
@@ -231,7 +231,7 @@ jobs:
test_mac:
name: 'Test (Mac) - ${{ matrix.node-version }}, ${{ matrix.shard }}'
runs-on: 'macos-latest-large'
runs-on: 'macos-latest'
needs:
- 'merge_queue_skipper'
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
@@ -244,8 +244,6 @@ jobs:
matrix:
node-version:
- '20.x'
- '22.x'
- '24.x'
shard:
- 'cli'
- 'others'
+1 -1
View File
@@ -78,7 +78,7 @@ jobs:
deflake_e2e_mac:
name: 'E2E Test (macOS)'
runs-on: 'macos-latest-large'
runs-on: 'macos-latest'
if: "github.repository == 'google-gemini/gemini-cli'"
steps:
- name: 'Checkout'
+41 -30
View File
@@ -10,18 +10,23 @@ import { GITHUB_OWNER, GITHUB_REPO } from '../types.js';
import { execSync } from 'node:child_process';
try {
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
.toISOString()
.split('T')[0];
const query = `
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
pullRequests(last: 100, states: MERGED) {
nodes {
pullRequests: search(query: "repo:$owner/$repo is:pr is:merged merged:>=${sevenDaysAgo}", type: ISSUE, first: 100) {
nodes {
... on PullRequest {
authorAssociation
createdAt
mergedAt
}
}
issues(last: 100, states: CLOSED) {
nodes {
}
issues: search(query: "repo:$owner/$repo is:issue is:closed closed:>=${sevenDaysAgo}", type: ISSUE, first: 100) {
nodes {
... on Issue {
authorAssociation
createdAt
closedAt
@@ -34,32 +39,38 @@ try {
`gh api graphql -F owner=${GITHUB_OWNER} -F repo=${GITHUB_REPO} -f query='${query}'`,
{ encoding: 'utf-8' },
);
const data = JSON.parse(output).data.repository;
const data = JSON.parse(output).data;
const prs = data.pullRequests.nodes
.filter((p: any) => p.mergedAt && p.createdAt)
.map(
(p: {
authorAssociation: string;
createdAt: string;
mergedAt: string;
}) => ({
association: p.authorAssociation,
latencyHours:
(new Date(p.mergedAt).getTime() - new Date(p.createdAt).getTime()) /
(1000 * 60 * 60),
}),
);
const issues = data.issues.nodes
.filter((p: any) => p.closedAt && p.createdAt)
.map(
(i: {
authorAssociation: string;
createdAt: string;
closedAt: string;
}) => ({
association: i.authorAssociation,
latencyHours:
(new Date(i.closedAt).getTime() - new Date(i.createdAt).getTime()) /
(1000 * 60 * 60),
}),
);
const prs = data.pullRequests.nodes.map(
(p: {
authorAssociation: string;
mergedAt: string;
createdAt: string;
}) => ({
association: p.authorAssociation,
latencyHours:
(new Date(p.mergedAt).getTime() - new Date(p.createdAt).getTime()) /
(1000 * 60 * 60),
}),
);
const issues = data.issues.nodes.map(
(i: {
authorAssociation: string;
closedAt: string;
createdAt: string;
}) => ({
association: i.authorAssociation,
latencyHours:
(new Date(i.closedAt).getTime() - new Date(i.createdAt).getTime()) /
(1000 * 60 * 60),
}),
);
const isMaintainer = (assoc: string) =>
['MEMBER', 'OWNER', 'COLLABORATOR'].includes(assoc);
@@ -10,17 +10,22 @@ import { GITHUB_OWNER, GITHUB_REPO } from '../types.js';
import { execSync } from 'node:child_process';
try {
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
.toISOString()
.split('T')[0];
const query = `
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
pullRequests(last: 100, states: MERGED) {
nodes {
pullRequests: search(query: "repo:$owner/$repo is:pr is:merged merged:>=${sevenDaysAgo}", type: ISSUE, first: 100) {
nodes {
... on PullRequest {
authorAssociation
mergedAt
}
}
issues(last: 100, states: CLOSED) {
nodes {
}
issues: search(query: "repo:$owner/$repo is:issue is:closed closed:>=${sevenDaysAgo}", type: ISSUE, first: 100) {
nodes {
... on Issue {
authorAssociation
closedAt
}
@@ -32,7 +37,7 @@ try {
`gh api graphql -F owner=${GITHUB_OWNER} -F repo=${GITHUB_REPO} -f query='${query}'`,
{ encoding: 'utf-8' },
);
const data = JSON.parse(output).data.repository;
const data = JSON.parse(output).data;
const prs = data.pullRequests.nodes
.map((p: { authorAssociation: string; mergedAt: string }) => ({
@@ -54,11 +59,7 @@ try {
const calculateThroughput = (
items: { association: string; date: number }[],
) => {
if (items.length < 2) return 0;
const first = items[0].date;
const last = items[items.length - 1].date;
const days = (last - first) / (1000 * 60 * 60 * 24);
return days > 0 ? items.length / days : items.length; // items per day
return items.length / 7; // items per day over a fixed 7-day window
};
const prOverall = calculateThroughput(prs);
@@ -10,18 +10,23 @@ import { GITHUB_OWNER, GITHUB_REPO } from '../types.js';
import { execSync } from 'node:child_process';
try {
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
.toISOString()
.split('T')[0];
const query = `
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
pullRequests(last: 100, states: MERGED) {
nodes {
pullRequests: search(query: "repo:$owner/$repo is:pr is:merged merged:>=${sevenDaysAgo}", type: ISSUE, first: 100) {
nodes {
... on PullRequest {
authorAssociation
comments { totalCount }
reviews { totalCount }
}
}
issues(last: 100, states: CLOSED) {
nodes {
}
issues: search(query: "repo:$owner/$repo is:issue is:closed closed:>=${sevenDaysAgo}", type: ISSUE, first: 100) {
nodes {
... on Issue {
authorAssociation
comments { totalCount }
}
@@ -33,7 +38,7 @@ try {
`gh api graphql -F owner=${GITHUB_OWNER} -F repo=${GITHUB_REPO} -f query='${query}'`,
{ encoding: 'utf-8' },
);
const data = JSON.parse(output).data.repository;
const data = JSON.parse(output).data;
const prs = data.pullRequests.nodes;
const issues = data.issues.nodes;