Skip to content
Closed
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
15 changes: 15 additions & 0 deletions .github/routines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,18 @@ routines:
timezone: Australia/Sydney
mode: propose
enabled: true
# Reads the Measurements .github/workflows/perf.yml stores on refs/notes/perf.
# It judges nothing until a Benchmark has ten usable Measurements, so the
# first several runs will report coverage and file nothing.
#
# Disabled until harlan-zw/harlan-agent-kit#355 merges. The deployed Skill
# treats any count movement as a Regression without it, so it would judge
# every count Benchmark against semantics perf/benchmarks.json contradicts.
# Flip enabled back to true once that pull request lands.
- name: perf-review
on:
schedule:
- cron: '0 11 * * 1'
timezone: Australia/Sydney
mode: report
enabled: false
182 changes: 182 additions & 0 deletions .github/workflows/perf-comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
name: Perf Comment

# Posts the report Perf Pull Request produced. It runs after that workflow, so
# it holds the write token and the pull request's own code never does. Every
# value it trusts is checked against the workflow run before it is used.
on:
workflow_run:
workflows:
- Perf Pull Request
types:
- completed

concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_repository.id }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true

permissions: {}

jobs:
comment:
if: >-
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
permissions:
actions: read
pull-requests: write
runs-on: [self-hosted, linux, x64, harlan-desktop-ci]
steps:
- name: Resolve the pull request
id: pull-request
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const run = context.payload.workflow_run
const headOwner = run.head_repository?.owner?.login
if (!headOwner || !run.head_branch) {
core.setFailed('The workflow run is missing its head repository or branch')
return
}

const { data: pullRequests } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${headOwner}:${run.head_branch}`,
per_page: 100,
})
const matches = pullRequests.filter(pullRequest =>
pullRequest.state === 'open'
&& pullRequest.head.sha === run.head_sha
&& pullRequest.head.repo?.full_name === run.head_repository.full_name
&& pullRequest.base.repo.full_name === `${context.repo.owner}/${context.repo.repo}`,
)

if (matches.length !== 1) {
core.setFailed(`Expected one open pull request for ${run.head_sha}, found ${matches.length}`)
return
}

core.setOutput('number', matches[0].number)

- name: Download the report
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: perf-report
path: ${{ runner.temp }}/perf-report
github-token: ${{ github.token }}
run-id: ${{ github.event.workflow_run.id }}

- name: Check the report before trusting it
env:
COMMENT_PATH: ${{ runner.temp }}/perf-comment.md
EXPECTED_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
EXPECTED_PR_NUMBER: ${{ steps.pull-request.outputs.number }}
REPORT_DIR: ${{ runner.temp }}/perf-report
run: |
set -euo pipefail
report="$REPORT_DIR/report.md"
pr_number="$REPORT_DIR/pr-number.txt"
head_sha="$REPORT_DIR/head-sha.txt"

for file in "$report" "$pr_number" "$head_sha"; do
if [ ! -f "$file" ] || [ -L "$file" ]; then
echo "::error::Missing or invalid report file: $file"
exit 1
fi
done

if [ "$(wc -c < "$pr_number")" -gt 16 ] || [ "$(wc -c < "$head_sha")" -gt 64 ]; then
echo "::error::Report metadata is too large"
exit 1
fi

if ! grep -Eq '^[0-9]+$' "$pr_number" \
|| [ "$(tr -d '\r\n' < "$pr_number")" != "$EXPECTED_PR_NUMBER" ]; then
echo "::error::The report names a different pull request than the workflow run"
exit 1
fi

if [ "$(tr -d '\r\n' < "$head_sha")" != "$EXPECTED_HEAD_SHA" ]; then
echo "::error::The report names a different head commit than the workflow run"
exit 1
fi

size=$(wc -c < "$report")
if [ "$size" -lt 1 ] || [ "$size" -gt 131072 ]; then
echo "::error::The report must be between 1 byte and 128 KiB"
exit 1
fi

if [ "$(head -n 1 "$report")" != '### ⚡ Perf' ]; then
echo "::error::The report has an unexpected format"
exit 1
fi

if [ -e "$COMMENT_PATH" ] || [ -L "$COMMENT_PATH" ]; then
echo "::error::Refusing to overwrite the checked comment path"
exit 1
fi
# A benchmark id is repository content, so it could carry a mention.
sed 's/@/@<!-- -->/g' "$report" > "$COMMENT_PATH"

- name: Post the comment
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
COMMENT_PATH: ${{ runner.temp }}/perf-comment.md
PR_NUMBER: ${{ steps.pull-request.outputs.number }}
with:
script: |
const { readFileSync } = require('node:fs')

// GitHub returns transient 5xx, 429, and network errors. Retry
// those. Anything else fails the step at once.
async function withRetry(label, call) {
for (let attempt = 1; ; attempt++) {
try {
return await call()
}
catch (error) {
const status = error.status
const transient = status === undefined || status === 429 || status >= 500
if (!transient || attempt >= 5)
throw error
const wait = Math.min(2000 * 2 ** (attempt - 1), 30000)
core.warning(`${label} hit a transient error (${error.message}). Retrying in ${wait}ms.`)
await new Promise(resolve => setTimeout(resolve, wait))
}
}
}

const { owner, repo } = context.repo
const issueNumber = Number(process.env.PR_NUMBER)
const marker = '<!-- perf-report -->'
const body = `${readFileSync(process.env.COMMENT_PATH, 'utf8')}\n${marker}`

const comments = await withRetry('issues.listComments', () =>
github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: issueNumber,
per_page: 100,
}),
)
const existing = comments.find(comment => comment.body?.includes(marker))

if (!existing) {
await withRetry('issues.createComment', () =>
github.rest.issues.createComment({ owner, repo, issue_number: issueNumber, body }),
)
core.info(`Posted the perf comment on #${issueNumber}`)
return
}

if (existing.body === body) {
core.info('The perf comment is unchanged. Leaving it alone.')
return
}

await withRetry('issues.updateComment', () =>
github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }),
)
core.info(`Updated the perf comment on #${issueNumber}`)
120 changes: 120 additions & 0 deletions .github/workflows/perf-pull-request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
name: Perf Pull Request

# Measures a pull request against its base, in one job, on one machine.
# It holds no write access and posts nothing. Perf Comment does that, from the
# artifact this job uploads.
# It is not a required check and it never blocks a merge.
on:
pull_request:
paths-ignore:
- '**/*.md'
- 'docs/**'
- LICENSE

concurrency:
group: ${{ github.workflow }}-${{ github.event.number }}
cancel-in-progress: true

permissions:
contents: read

jobs:
measure:
# Safe on the workstation pool because the repository is private; a fork
# pull request cannot reach this runner.
runs-on: [self-hosted, linux, x64, harlan-desktop-ci]
timeout-minutes: 45
steps:
- name: Check out the pull request
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false

# Installs this revision only for the heartbeat wrapper. Each measured
# side installs from its own lockfile below.
- uses: harlan-zw/harlan-nuxt/.github/actions/harlan-desktop-setup@main

# Both sides are exported from git into fresh directories. Nothing is
# restored into them: a restored skew-protection cache would copy the
# chunks of earlier deploys into the output and count them too.
- name: Export both commits
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
set -euo pipefail
mkdir "$RUNNER_TEMP/head" "$RUNNER_TEMP/base"
git archive HEAD | tar -x -C "$RUNNER_TEMP/head"
git archive "$BASE_SHA" | tar -x -C "$RUNNER_TEMP/base"

# The desktop runner sends SIGHUP after 45 seconds without output.
# Nitro can bundle longer than that, so the wrapper emits a heartbeat.
- name: Build the pull request
working-directory: ${{ runner.temp }}/head
env:
NODE_OPTIONS: --max-old-space-size=8192
run: |
pnpm install --frozen-lockfile
"$GITHUB_WORKSPACE/node_modules/.bin/tsx" "$GITHUB_WORKSPACE/scripts/run-with-heartbeat.ts" pnpm build

- name: Build the base commit
working-directory: ${{ runner.temp }}/base
env:
NODE_OPTIONS: --max-old-space-size=8192
run: |
pnpm install --frozen-lockfile
"$GITHUB_WORKSPACE/node_modules/.bin/tsx" "$GITHUB_WORKSPACE/scripts/run-with-heartbeat.ts" pnpm build

- name: Note whether the dependencies moved
id: deps
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
set -euo pipefail
# Each side installs from its own lockfile, so this does not affect
# whether the comparison holds. It tells a reader where a delta may
# have come from.
if git diff --quiet "$BASE_SHA" HEAD -- pnpm-lock.yaml; then
echo "dependencies_changed=false" >> "$GITHUB_OUTPUT"
else
echo "dependencies_changed=true" >> "$GITHUB_OUTPUT"
fi
echo "label=$(git log -1 --format='%h · %cs' "$BASE_SHA")" >> "$GITHUB_OUTPUT"

- name: Measure both builds
# Every value a pull request can influence arrives as an environment
# variable. Nothing from the event is pasted into this script.
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
DEPENDENCIES_CHANGED: ${{ steps.deps.outputs.dependencies_changed }}
run: |
node scripts/perf/run.mjs \
--head "$RUNNER_TEMP/head" \
--parent "$RUNNER_TEMP/base" \
--commit "$HEAD_SHA" \
--parent-commit "$BASE_SHA" \
--dependencies-changed "$DEPENDENCIES_CHANGED" \
--out "$RUNNER_TEMP/perf-report/measurement.json"

- name: Write the report
env:
BASE_LABEL: ${{ github.base_ref }} @ ${{ steps.deps.outputs.label }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
MEASUREMENT: ${{ runner.temp }}/perf-report/measurement.json
PR_NUMBER: ${{ github.event.pull_request.number }}
REPORT_DIR: ${{ runner.temp }}/perf-report
run: |
set -euo pipefail
node scripts/perf/report.mjs | tee "$REPORT_DIR/report.md"
cat "$REPORT_DIR/report.md" >> "$GITHUB_STEP_SUMMARY"
printf '%s\n' "$PR_NUMBER" > "$REPORT_DIR/pr-number.txt"
printf '%s\n' "$HEAD_SHA" > "$REPORT_DIR/head-sha.txt"

- name: Upload the report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: perf-report
path: ${{ runner.temp }}/perf-report
if-no-files-found: error
retention-days: 14
Loading