Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ window you've covered up.
| `--headless` | off | headless caps at 60fps, so the frame-rate bench means less |
| `--timeout` | `60000` | ms a single sample may take before the run fails |
| `--skip-build` | off | re-use an existing build |
| `--include-prs` | off | record the PRs that landed since the previous result set in the run's notes (from git history; shown in the results app) |

### Query params

Expand Down
18 changes: 18 additions & 0 deletions results/app/components/env.gts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { formatDuration, msOfFrameAt, throttleLabel } from "#utils";

import type { TOC } from "@ember/component/template-only";
import type { ResultSet } from "#types";
import type { DisplayPr } from "#utils";

function first8(str: string) {
return str.slice(0, 8);
Expand Down Expand Up @@ -71,11 +72,28 @@ export const Info = <template>
</li>
{{/if}}
</ul>

{{#if @prs.length}}
<details class="pr-notes">
<summary>PRs since the previous result set ({{@prs.length}})</summary>
<ul>
{{#each @prs as |pr|}}
<li>
<a target="_blank" rel="noopener noreferrer" href={{pr.url}}>
{{pr.label}}
</a>
{{#if pr.title}}{{pr.title}}{{/if}}
</li>
{{/each}}
</ul>
</details>
{{/if}}
</div>
</template> satisfies TOC<{
date: string;
sha: string;
env: ResultSet["environment"];
cpuThrottle: number | undefined;
timing: ResultSet["timing"];
prs: DisplayPr[];
}>;
2 changes: 2 additions & 0 deletions results/app/templates/results.gts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { LinkTo } from "@ember/routing";

import { Info } from "#components/env.gts";
import { prsOf } from "#utils";

import type { TOC } from "@ember/component/template-only";
import type { Model } from "#routes/results.ts";
Expand All @@ -20,6 +21,7 @@ export default <template>
@env={{@model.data.environment}}
@cpuThrottle={{@model.data.args.CPU_THROTTLE}}
@timing={{@model.data.timing}}
@prs={{prsOf @model.data}}
/>

<div class="all-results">
Expand Down
21 changes: 20 additions & 1 deletion results/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@ export interface VersionOverride {
url: string;
}

/**
* A PR recorded in a result set's notes. The runner writes these
* (`--include-prs`) from git history between the previous result set and
* the run; hand-added entries may also be plain URL strings.
*/
export interface PullRequestNote {
url: string;
/**
* The PR title, from the merge (or squash) commit. Absent on
* hand-added entries.
*/
title?: string;
}

/**
* Small labels a run records about a framework, collected by the runner
* from `frameworks/<framework>/notes.json`.
Expand Down Expand Up @@ -107,8 +121,13 @@ export interface ResultSet {
* Optional per-framework notes, keyed by framework name. Collected by the
* runner from `frameworks/<framework>/notes.json`, e.g.
* `{ vue: { variant: "Vapor" } }`.
*
* `prs` sits alongside the framework keys: the PRs that landed between
* the previous result set and this run (see {@link PullRequestNote}).
*/
notes?: Record<string, FrameworkNotes>;
notes?: {
prs?: Array<string | PullRequestNote>;
} & Record<string, FrameworkNotes>;
environment: {
machine: {
os: {
Expand Down
26 changes: 26 additions & 0 deletions results/app/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,32 @@ export function variantOf(file: ResultSet, framework: string) {
return file.notes?.[framework]?.variant;
}

export interface DisplayPr {
url: string;
title?: string;
/**
* `#<number>` when the URL has one, the URL itself otherwise.
*/
label: string;
}

/**
* The PRs a run recorded (the ones that landed between the previous
* result set and the run), normalized for display: the runner records
* `{ url, title }`, hand-added entries are plain URL strings.
*/
export function prsOf(file: ResultSet): DisplayPr[] {
const prs = file.notes?.prs ?? [];

return prs.map((pr) => {
const url = typeof pr === "string" ? pr : pr.url;
const title = typeof pr === "string" ? undefined : pr.title;
const number = url.match(/\/pull\/(\d+)/)?.[1];

return { url, title, label: number ? `#${number}` : url };
});
}

/**
* How one framework did at one benchmark, or undefined when that run
* doesn't have the pair.
Expand Down
6 changes: 6 additions & 0 deletions src/runner/arg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const FRAMEWORK = str('--framework');
export const BENCH_NAME = str('--bench');
export const SKIP_BUILD = bool('--skip-build');
export const TIMEOUT = int('--timeout', 60_000);
export const INCLUDE_PRS = bool('--include-prs');
export const VERSION_OVERRIDES = versionOverrides();

function col1(name: string) {
Expand Down Expand Up @@ -56,6 +57,11 @@ console.log(
row(col1('--timeout'), col2(TIMEOUT), col3('ms a single sample may take')),
row(col1('--framework'), col2(FRAMEWORK), col3(`or '${ALL}'`)),
row(col1('--bench'), col2(BENCH_NAME), col3(`or '${ALL}'`)),
row(
col1('--include-prs'),
col2(INCLUDE_PRS),
col3('record PRs merged since the previous result set'),
),
...Object.entries(VERSION_OVERRIDES).map(([framework, override]) =>
row(
col1(`--${framework}`),
Expand Down
28 changes: 28 additions & 0 deletions src/runner/bench-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ import * as clack from '@clack/prompts';

import * as args from './arg.ts';
import { yyyymmdd } from './environment.ts';
import { prsSinceLastResultSet } from './prs.ts';
import { frameworks } from './repo.ts';
import {
info,
saveBenchmarkInfo,
saveNotes,
savePrNotes,
saveVersionOverrides,
} from './results.ts';

import type { PullRequestNote } from '../../results/app/types.ts';

export interface BenchmarkInfo {
/**
* The benchmark name.
Expand Down Expand Up @@ -299,6 +303,29 @@ export async function getBenchInfo() {
const selectedBenches = await getBenches();
const filePath = await getFilePath();

// resolved before the confirm below, so what will be recorded is part
// of the "does this look correct?" review
let prNotes: PullRequestNote[] = [];

if (args.INCLUDE_PRS) {
const found = await prsSinceLastResultSet(filePath);

if (found && found.prs.length > 0) {
prNotes = found.prs;

clack.log.info(
`PRs since the previous result set (${found.since}):\n` +
found.prs
.map((pr) => ` ${pr.url}${pr.title ? ` — ${pr.title}` : ''}`)
.join('\n'),
);
} else {
clack.log.warn(
`--include-prs: no PRs found since the previous result set`,
);
}
}

console.info(inspect(info, { showHidden: false, depth: null, colors: true }));
console.log(`
Results will be written to ${filePath}
Expand Down Expand Up @@ -328,6 +355,7 @@ export async function getBenchInfo() {

await saveVersionOverrides(args.VERSION_OVERRIDES, filePath);
await saveNotes(selectedFrameworks, filePath);
await savePrNotes(prNotes, filePath);

return {
apps,
Expand Down
100 changes: 100 additions & 0 deletions src/runner/prs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { readdir } from 'node:fs/promises';
import { basename } from 'node:path';

import { $ } from 'execa';

import type { PullRequestNote } from '../../results/app/types.ts';

const RESULTS_DIR = './results/public/results';

/**
* Same base the results app links shas to.
*/
const REPO_URL = 'https://github.com/NullVoxPopuli/rere-benchmark';

/**
* When the most recent result set (other than the one being written) was
* recorded. Result files are named with the run's ISO timestamp, so the
* directory listing is the history -- no need to open the files.
*/
async function previousResultSetDate(currentFilePath: string) {
const files = await readdir(RESULTS_DIR);
const current = basename(currentFilePath);

const dates = files
.filter((file) => file.endsWith('.json') && file !== current)
.map((file) => file.replace(/\.json$/, ''))
.filter((iso) => !Number.isNaN(Date.parse(iso)))
.sort();

return dates.at(-1);
}

/**
* The PRs that landed between the previous result set and now, from git
* history alone (no GitHub API):
*
* - a merge commit's subject is `Merge pull request #N from ...` and the
* first line of its body is the PR title
* - a squash-merge's subject is `The PR title (#N)`
*
* Newest first, like `git log`. Deduplicated by number, so a PR that
* appears both ways (or twice via --since's commit-date filter) is
* recorded once.
*/
export async function prsSinceLastResultSet(currentFilePath: string) {
const since = await previousResultSetDate(currentFilePath);

if (!since) return;

// NUL between subject and body, RS between commits: bodies span lines
const format = '%s%x00%b%x1e';
const { stdout } = await $`git log --since=${since} --format=${format}`;

const prs: PullRequestNote[] = [];
const seen = new Set<string>();

for (const entry of stdout.split('\x1e')) {
const [subject = '', body = ''] = entry.trim().split('\0');

const pr = fromMergeCommit(subject, body) ?? fromSquashCommit(subject);

if (!pr) continue;
if (seen.has(pr.url)) continue;

seen.add(pr.url);
prs.push(pr);
}

return { since, prs };
}

function fromMergeCommit(
subject: string,
body: string,
): PullRequestNote | undefined {
const merge = subject.match(/^Merge pull request #(?<number>\d+) from /);

if (!merge?.groups) return;

const title = body
.split('\n')
.map((line) => line.trim())
.find(Boolean);

return {
url: `${REPO_URL}/pull/${merge.groups['number']}`,
...(title ? { title } : {}),
};
}

function fromSquashCommit(subject: string): PullRequestNote | undefined {
const squash = subject.match(/^(?<title>.+) \(#(?<number>\d+)\)$/);

if (!squash?.groups) return;

return {
url: `${REPO_URL}/pull/${squash.groups['number']}`,
title: squash.groups['title'],
};
}
26 changes: 25 additions & 1 deletion src/runner/results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import {
} from '../../results/app/frameworks.ts';
import { getInfo } from './environment.ts';

import type { VersionOverride } from '../../results/app/types.ts';
import type {
PullRequestNote,
VersionOverride,
} from '../../results/app/types.ts';
import type { BenchmarkInfo } from './bench-info.ts';

const require = createRequire(import.meta.url);
Expand Down Expand Up @@ -150,6 +153,27 @@ export async function saveNotes(frameworks: string[], filePath: string) {
await write(file, filePath);
}

/**
* The PRs that landed between the previous result set and this run
* (`--include-prs`, from git history). Merged in and deduplicated by URL,
* so hand-added entries (plain URL strings) and earlier appends survive.
*/
export async function savePrNotes(prs: PullRequestNote[], filePath: string) {
if (prs.length === 0) return;

const file = await read(filePath);

const existing: Array<string | PullRequestNote> = file.notes?.prs ?? [];
const known = new Set(
existing.map((pr) => (typeof pr === 'string' ? pr : pr.url)),
);
const fresh = prs.filter((pr) => !known.has(pr.url));

file.notes = { ...file.notes, prs: existing.concat(fresh) };

await write(file, filePath);
}

async function getVersion(framework: string, bench: BenchmarkInfo) {
const dir = join('frameworks', framework, bench.app);
const manifestPath = join(dir, 'package.json');
Expand Down
Loading