Skip to content

Commit

Permalink
Update analysis scripts for new data.
Browse files Browse the repository at this point in the history
  • Loading branch information
jyasskin committed Oct 25, 2023
1 parent f8d21d7 commit 4fdf6bd
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 67 deletions.
87 changes: 87 additions & 0 deletions analysis/repoSizes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import fs from 'node:fs/promises';
import { quantileSorted } from 'simple-statistics';
import { RepoSummary } from '../frontend/src/lib/repo-summaries.js';

async function main() {
const repoSummaries: { [name: string]: RepoSummary } = {};

for (const org of await fs.readdir('../scanner/summaries')) {
if (org.includes('.')) {
continue;
}
for (const repoJson of await fs.readdir(`../scanner/summaries/${org}/`)) {
if (!repoJson.endsWith('.json')) continue;
const repo = repoJson.replace(/\.json$/, '');
const content = await fs.readFile(`../scanner/summaries/${org}/${repoJson}`, { encoding: 'utf8' });
repoSummaries[`${org}/${repo}`] = RepoSummary.parse(JSON.parse(content));
}
}

const repoLabels: number[] = [];
const repoIssues: number[] = [];
const repoPRs: number[] = [];
const issueLabels: number[] = [];
const issueTimelineItems: number[] = [];
const issueComments: number[] = [];

for (const repo of Object.values(repoSummaries)) {
if (repo.stats) {
repoLabels.push(repo.stats.numLabels);
repoIssues.push(repo.stats.numIssues);
repoPRs.push(repo.stats.numPRs);
}
for (const issue of repo.issues) {
if (issue.stats) {
issueLabels.push(issue.stats.numLabels);
issueTimelineItems.push(issue.stats.numTimelineItems);
if (issue.stats.numComments) {
issueComments.push(issue.stats.numComments);
}
}
}
}

function cmpNumber(a: number, b: number) { return a - b; }
repoLabels.sort(cmpNumber);
repoIssues.sort(cmpNumber);
repoPRs.sort(cmpNumber);
issueLabels.sort(cmpNumber);
issueTimelineItems.sort(cmpNumber);
issueComments.sort(cmpNumber);

console.log('Distribution of the number of matching labels in a repository:');
console.log(`Median: ${quantileSorted(repoLabels, .5)}`)
console.log(`90%: ${quantileSorted(repoLabels, .9)}`)
console.log(`Max: ${Math.max(...repoLabels)}\n`)

console.log('Distribution of the number of issues in a repository:');
console.log(`Median: ${quantileSorted(repoIssues, .5)}`)
console.log(`90%: ${quantileSorted(repoIssues, .9)}`)
console.log(`Max: ${Math.max(...repoIssues)}\n`)

console.log('Distribution of the number of PRs in a repository:');
console.log(`Median: ${quantileSorted(repoPRs, .5)}`)
console.log(`90%: ${quantileSorted(repoPRs, .9)}`)
console.log(`95%: ${quantileSorted(repoPRs, .95)}`)
console.log(`99%: ${quantileSorted(repoPRs, .99)}`)
console.log(`Max: ${Math.max(...repoPRs)}\n`)

console.log('Distribution of the number of labels in an issue/PR:');
console.log(`Median: ${quantileSorted(issueLabels, .5)}`)
console.log(`90%: ${quantileSorted(issueLabels, .9)}`)
console.log(`Max: ${Math.max(...issueLabels)}\n`)

console.log('Distribution of the number of timeline items in an issue/PR:');
console.log(`Median: ${quantileSorted(issueTimelineItems, .5)}`)
console.log(`90%: ${quantileSorted(issueTimelineItems, .9)}`)
console.log(`Max: ${Math.max(...issueTimelineItems)}\n`)

console.log('Distribution of the number of comments/reviews in an issue/PR:');
console.log(`Median: ${quantileSorted(issueComments, .5)}`)
console.log(`90%: ${quantileSorted(issueComments, .9)}`)
console.log(`95%: ${quantileSorted(issueComments, .95)}`)
console.log(`99%: ${quantileSorted(issueComments, .99)}`)
console.log(`Max: ${Math.max(...issueComments)}\n`)
}

await main();
62 changes: 18 additions & 44 deletions analysis/slo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Temporal } from '@js-temporal/polyfill';
import fs from 'node:fs/promises';
import { RepoSummary } from '../frontend/src/lib/repo-summaries.js';
import { RepoSummary, SloType } from '../frontend/src/lib/repo-summaries.js';

async function main() {
const repoSummaries: { [name: string]: RepoSummary } = {};
Expand All @@ -18,7 +18,7 @@ async function main() {
}

let totalIssues = 0;
let openIssues = 0;
let sloTypes: { [type in SloType]: number } = { "triage": 0, "none": 0, "important": 0, "urgent": 0 };
let exceededTriageSLO = 0;
let outOfTriageSLO = 0;
let outOfUrgentSLO = 0;
Expand All @@ -31,54 +31,28 @@ async function main() {
for (const repo of Object.values(repoSummaries)) {
const scannedAt = repo.cachedAt;
for (const issue of repo.issues) {
if (issue.closed_at && issue.pull_request && !issue.firstCommentLatencyMs) {
continue;
}
totalIssues++;
if (!issue.closed_at) {
openIssues++;
} else {
if (
Temporal.Duration.compare(issue.created_at.until(issue.closed_at),
urgentSLO) > 0) {
outOfUrgentSLO++;
}
if (
Temporal.Duration.compare(issue.created_at.until(issue.closed_at),
importantSLO) > 0) {
outOfImportantSLO++;
}

sloTypes[issue.whichSlo]++;

if (issue.whichSlo === "urgent" &&
Temporal.Duration.compare(issue.sloTimeUsedMs, urgentSLO) > 0) {
outOfUrgentSLO++;
}
if (issue.firstCommentLatencyMs) {
if (
Temporal.Duration.compare({ milliseconds: issue.firstCommentLatencyMs },
triageSLO) > 0) {
exceededTriageSLO++;
}
} else {
// No comments yet.
if (issue.closed_at) {
// Treat the time the issue was closed as the time it was triaged.
if (
Temporal.Duration.compare(issue.created_at.until(issue.closed_at),
triageSLO) > 0) {
exceededTriageSLO++;
}
}
else {
if (Temporal.Duration.compare(issue.created_at.until(scannedAt), triageSLO) > 0) {
outOfTriageSLO++;
}
}
if (issue.whichSlo === "important" &&
Temporal.Duration.compare(issue.sloTimeUsedMs, importantSLO) > 0) {
outOfImportantSLO++;
}
if (issue.whichSlo === "triage" &&
Temporal.Duration.compare(issue.sloTimeUsedMs, triageSLO) > 0) {
outOfTriageSLO++;
}
}
}

const closedIssues = totalIssues - openIssues;
console.log(`Of ${totalIssues} total issues, ${exceededTriageSLO} ever exceeded a ${triageSLO} triage SLO.`);
console.log(`Of ${openIssues} open issues, ${outOfTriageSLO} are out of SLO now.`);
console.log(`Of ${closedIssues} closed issues, ${outOfUrgentSLO} (${(outOfUrgentSLO / closedIssues * 100).toPrecision(2)}%) exceeded the 14-day "urgent" SLO.`);
console.log(`Of ${closedIssues} closed issues, ${outOfImportantSLO} (${(outOfImportantSLO / closedIssues * 100).toPrecision(2)}%) exceeded the 91-day "important" SLO.`);
console.log(`Of ${totalIssues} total issues, ${sloTypes["none"]} (${(sloTypes["none"] / totalIssues * 100).toPrecision(2)}%) have no SLO.`);
console.log(`Of ${sloTypes["triage"]} issues that need triage, ${outOfTriageSLO} (${(outOfTriageSLO / sloTypes["triage"] * 100).toPrecision(2)}%) are out of SLO.`);
}


await main();
41 changes: 18 additions & 23 deletions frontend/src/lib/repo-summaries.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,37 @@
import { Temporal, toTemporalInstant } from '@js-temporal/polyfill';
import { z } from 'zod';

export const SloType = z.enum(["triage", "urgent", "important", "none"]);
export type SloType = z.infer<typeof SloType>;

const durationInMs = z.number().transform(val => Temporal.Duration.from({ milliseconds: Math.round(val) }));

export const IssueSummary = z.object({
url: z.string().url(),
title: z.string(),
created_at: z.coerce.date().transform(val => toTemporalInstant.call(val)),
closed_at: z.coerce.date().transform(val => toTemporalInstant.call(val)).optional(),
createdAt: z.coerce.date().transform(val => toTemporalInstant.call(val)),
pull_request: z.object({ draft: z.boolean().default(false) }).optional(),
milestone: z.string().optional(),
labels: z.array(z.string()),
ageAtCloseMs: z.number().optional(),
firstCommentLatencyMs: z.number().optional(),
sloTimeUsedMs: durationInMs,
whichSlo: SloType,
stats: z.object({
numTimelineItems: z.number(),
numComments: z.number().optional(),
numLabels: z.number(),
}).optional(),
})
export type IssueSummary = z.infer<typeof IssueSummary>;

const durationInMs = z.number().transform(val => Temporal.Duration.from({ milliseconds: Math.round(val) }));

export const AgeStats = z.object({
count: z.number(),
mean: durationInMs,
10: durationInMs,
25: durationInMs,
50: durationInMs,
75: durationInMs,
90: durationInMs,
});
export type AgeStats = z.infer<typeof AgeStats>;

export const RepoSummary = z.object({
cachedAt: z.number().transform(val => Temporal.Instant.fromEpochMilliseconds(val)),
org: z.string(),
repo: z.string(),
issues: IssueSummary.array(),
labelsPresent: z.boolean(),
ageAtCloseMs: AgeStats.optional(),
openAgeMs: AgeStats.optional(),
firstCommentLatencyMs: AgeStats.optional(),
openFirstCommentLatencyMs: AgeStats.optional(),
closedFirstCommentLatencyMs: AgeStats.optional(),
stats: z.object({
numLabels: z.number(),
numIssues: z.number(),
numPRs: z.number(),
}).optional(),
});
export type RepoSummary = z.infer<typeof RepoSummary>;

0 comments on commit 4fdf6bd

Please sign in to comment.