Files
gemini-cli/tools/gemini-cli-bot/metrics/scripts/throughput.ts
T
gemini-cli[bot] b4f49bf37a 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.
2026-05-12 16:53:13 +00:00

122 lines
3.8 KiB
TypeScript

/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*
* @license
*/
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!) {
pullRequests: search(query: "repo:$owner/$repo is:pr is:merged merged:>=${sevenDaysAgo}", type: ISSUE, first: 100) {
nodes {
... on PullRequest {
authorAssociation
mergedAt
}
}
}
issues: search(query: "repo:$owner/$repo is:issue is:closed closed:>=${sevenDaysAgo}", type: ISSUE, first: 100) {
nodes {
... on Issue {
authorAssociation
closedAt
}
}
}
}
`;
const output = execSync(
`gh api graphql -F owner=${GITHUB_OWNER} -F repo=${GITHUB_REPO} -f query='${query}'`,
{ encoding: 'utf-8' },
);
const data = JSON.parse(output).data;
const prs = data.pullRequests.nodes
.map((p: { authorAssociation: string; mergedAt: string }) => ({
association: p.authorAssociation,
date: new Date(p.mergedAt).getTime(),
}))
.sort((a: { date: number }, b: { date: number }) => a.date - b.date);
const issues = data.issues.nodes
.map((i: { authorAssociation: string; closedAt: string }) => ({
association: i.authorAssociation,
date: new Date(i.closedAt).getTime(),
}))
.sort((a: { date: number }, b: { date: number }) => a.date - b.date);
const isMaintainer = (assoc: string) =>
['MEMBER', 'OWNER', 'COLLABORATOR'].includes(assoc);
const calculateThroughput = (
items: { association: string; date: number }[],
) => {
return items.length / 7; // items per day over a fixed 7-day window
};
const prOverall = calculateThroughput(prs);
const prMaintainers = calculateThroughput(
prs.filter((i: { association: string; date: number }) =>
isMaintainer(i.association),
),
);
const prCommunity = calculateThroughput(
prs.filter(
(i: { association: string; date: number }) =>
!isMaintainer(i.association),
),
);
const issueOverall = calculateThroughput(issues);
const issueMaintainers = calculateThroughput(
issues.filter((i: { association: string; date: number }) =>
isMaintainer(i.association),
),
);
const issueCommunity = calculateThroughput(
issues.filter(
(i: { association: string; date: number }) =>
!isMaintainer(i.association),
),
);
process.stdout.write(
`throughput_pr_overall_per_day,${Math.round(prOverall * 100) / 100}\n`,
);
process.stdout.write(
`throughput_pr_maintainers_per_day,${Math.round(prMaintainers * 100) / 100}\n`,
);
process.stdout.write(
`throughput_pr_community_per_day,${Math.round(prCommunity * 100) / 100}\n`,
);
process.stdout.write(
`throughput_issue_overall_per_day,${Math.round(issueOverall * 100) / 100}\n`,
);
process.stdout.write(
`throughput_issue_maintainers_per_day,${Math.round(issueMaintainers * 100) / 100}\n`,
);
process.stdout.write(
`throughput_issue_community_per_day,${Math.round(issueCommunity * 100) / 100}\n`,
);
process.stdout.write(
`throughput_issue_overall_days_per_issue,${issueOverall > 0 ? Math.round((1 / issueOverall) * 100) / 100 : 0}\n`,
);
process.stdout.write(
`throughput_issue_maintainers_days_per_issue,${issueMaintainers > 0 ? Math.round((1 / issueMaintainers) * 100) / 100 : 0}\n`,
);
process.stdout.write(
`throughput_issue_community_days_per_issue,${issueCommunity > 0 ? Math.round((1 / issueCommunity) * 100) / 100 : 0}\n`,
);
} catch (err) {
process.stderr.write(err instanceof Error ? err.message : String(err));
process.exit(1);
}