Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modularise CI workflow and validate outputs for binary size checks. #549

Merged
merged 5 commits into from
Sep 9, 2023
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
48 changes: 48 additions & 0 deletions .github/actions/build-with-patched-std/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Github composite action to build a single-source-file test binary with an
# already-checked-out version of Rust's stdlib, that will be patched with a
# given revision of the backtrace crate.

name: Build with patched std
description: >
Build a binary with a version of std that's had a specific revision of
backtrace patched in.
inputs:
backtrace-commit:
description: The git commit of backtrace to patch in to std
required: true
main-rs:
description: The (single) source code file to compile
required: true
rustc-dir:
description: The root directory of the rustc repo
required: true
outputs:
test-binary-size:
description: The size in bytes of the built test binary
value: ${{ steps.measure.outputs.test-binary-size }}
runs:
using: composite
steps:
- shell: bash
id: measure
env:
RUSTC_FLAGS: -Copt-level=3 -Cstrip=symbols
# This symlink is made by Build::new() in the bootstrap crate, using a
# symlink on Linux and a junction on Windows, so it will exist on both
# platforms.
RUSTC_BUILD_DIR: build/host
working-directory: ${{ inputs.rustc-dir }}
run: |
rm -rf "$RUSTC_BUILD_DIR/stage0-std"

(cd library/backtrace && git checkout ${{ inputs.backtrace-commit }})
git add library/backtrace

python3 x.py build library --stage 0

TEMP_BUILD_OUTPUT=$(mktemp test-binary-XXXXXXXX)
"$RUSTC_BUILD_DIR/stage0-sysroot/bin/rustc" $RUSTC_FLAGS "${{ inputs.main-rs }}" -o "$TEMP_BUILD_OUTPUT"
BINARY_SIZE=$(stat -c '%s' "$TEMP_BUILD_OUTPUT")
rm "$TEMP_BUILD_OUTPUT"

echo "test-binary-size=$BINARY_SIZE" >> "$GITHUB_OUTPUT"
111 changes: 111 additions & 0 deletions .github/actions/report-code-size-changes/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Github composite action to report on code size changes across different
# platforms.

name: Report binary size changes on PR
description: |
Report on code size changes across different platforms resulting from a PR.
The only input argument is the path to a directory containing a set of
"*.json" files (extension required), each file containing the keys:

- platform: the platform that the code size change was measured on
- reference: the size in bytes of the reference binary (base of PR)
- updated: the size in bytes of the updated binary (head of PR)

The size is reported as a comment on the PR (accessed via context).
inputs:
data-directory:
description: >
Path to directory containing size data as a set of "*.json" files.
required: true
runs:
using: composite
steps:
- name: Post a PR comment if the size has changed
uses: actions/github-script@v6
env:
DATA_DIRECTORY: ${{ inputs.data-directory }}
with:
script: |
const fs = require("fs");

const size_dir = process.env.DATA_DIRECTORY;

// Map the set of all the *.json files into an array of objects.
const globber = await glob.create(`${size_dir}/*.json`);
const files = await globber.glob();
const sizes = files.map(path => {
const contents = fs.readFileSync(path);
return JSON.parse(contents);
});

// Map each object into some text, but only if it shows any difference
// to report.
const size_reports = sizes.flatMap(size_data => {
const platform = size_data["platform"];
const reference = size_data["reference"];
const updated = size_data["updated"];

if (!(reference > 0)) {
core.setFailed(`Reference size invalid: ${reference}`);
return;
}

if (!(updated > 0)) {
core.setFailed(`Updated size invalid: ${updated}`);
return;
}

const formatter = Intl.NumberFormat("en", {
useGrouping: "always"
});

const updated_str = formatter.format(updated);
const reference_str = formatter.format(reference);

const diff = updated - reference;
const diff_pct = (updated / reference) - 1;

const diff_str = Intl.NumberFormat("en", {
useGrouping: "always",
sign: "exceptZero"
}).format(diff);

const diff_pct_str = Intl.NumberFormat("en", {
style: "percent",
useGrouping: "always",
sign: "exceptZero",
maximumFractionDigits: 2
}).format(diff_pct);

if (diff !== 0) {
// The body is created here and wrapped so "weirdly" to avoid whitespace at the start of the lines,
// which is interpreted as a code block by Markdown.
const report = `On platform \`${platform}\`:

- Original binary size: **${reference_str} B**
- Updated binary size: **${updated_str} B**
- Difference: **${diff_str} B** (${diff_pct_str})

`;

return [report];
} else {
return [];
}
});

// If there are any size changes to report, format a comment and post
// it.
if (size_reports.length > 0) {
const comment_sizes = size_reports.join("");
const body = `Code size changes for a hello-world Rust program linked with libstd with backtrace:

${comment_sizes}`;

github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body
});
}
172 changes: 120 additions & 52 deletions .github/workflows/check-binary-size.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,75 +9,143 @@ on:
branches:
- master

# Both the "measure" and "report" jobs need to know this.
env:
SIZE_DATA_DIR: sizes

# Responsibility is divided between two jobs "measure" and "report", so that the
# job that builds (and potentnially runs) untrusted code does not have PR write
# permission, and vice-versa.
jobs:
test:
measure:
name: Check binary size
runs-on: ubuntu-latest
strategy:
matrix:
platform: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.platform }}
permissions:
pull-requests: write
contents: read
env:
# This cannot be used as a context variable in the 'uses' key later. If it
# changes, update those steps too.
BACKTRACE_DIR: backtrace
RUSTC_DIR: rustc
TEST_MAIN_RS: foo.rs
BASE_COMMIT: ${{ github.event.pull_request.base.sha }}
HEAD_COMMIT: ${{ github.event.pull_request.head.sha }}
SIZE_DATA_FILE: size-${{ strategy.job-index }}.json
steps:
- name: Print info
shell: bash
run: |
echo "Current SHA: ${{ github.event.pull_request.head.sha }}"
echo "Base SHA: ${{ github.event.pull_request.base.sha }}"
echo "Current SHA: $HEAD_COMMIT"
echo "Base SHA: $BASE_COMMIT"
# Note: the backtrace source that's cloned here is NOT the version to be
# patched in to std. It's cloned here to access the Github action for
# building and measuring the test binary.
- name: Clone backtrace to access Github action
uses: actions/checkout@v3
with:
path: ${{ env.BACKTRACE_DIR }}
- name: Clone Rustc
uses: actions/checkout@v3
with:
repository: rust-lang/rust
fetch-depth: 1
- name: Fetch backtrace
run: git submodule update --init library/backtrace
- name: Create hello world program that uses backtrace
run: printf "fn main() { panic!(); }" > foo.rs
- name: Build binary with base version of backtrace
path: ${{ env.RUSTC_DIR }}
- name: Set up std repository and backtrace submodule for size test
shell: bash
working-directory: ${{ env.RUSTC_DIR }}
env:
PR_SOURCE_REPO: ${{ github.event.pull_request.head.repo.full_name }}
run: |
printf "[llvm]\ndownload-ci-llvm = true\n\n[rust]\nincremental = false\n" > config.toml
# Bootstrap config
cat <<EOF > config.toml
[llvm]
download-ci-llvm = true
[rust]
incremental = false
EOF

# Test program source
cat <<EOF > $TEST_MAIN_RS
fn main() {
panic!();
}
EOF

git submodule update --init library/backtrace

cd library/backtrace
git remote add head-pr https://github.com/${{ github.event.pull_request.head.repo.full_name }}
git remote add head-pr "https://github.com/$PR_SOURCE_REPO"
git fetch --all
git checkout ${{ github.event.pull_request.base.sha }}
cd ../..
git add library/backtrace
python3 x.py build library --stage 0
./build/x86_64-unknown-linux-gnu/stage0-sysroot/bin/rustc -O foo.rs -o binary-reference
- name: Build binary with base version of backtrace
uses: ./backtrace/.github/actions/build-with-patched-std
with:
backtrace-commit: ${{ env.BASE_COMMIT }}
main-rs: ${{ env.TEST_MAIN_RS }}
rustc-dir: ${{ env.RUSTC_DIR }}
id: size-reference
- name: Build binary with PR version of backtrace
run: |
cd library/backtrace
git checkout ${{ github.event.pull_request.head.sha }}
cd ../..
git add library/backtrace
rm -rf build/x86_64-unknown-linux-gnu/stage0-std
python3 x.py build library --stage 0
./build/x86_64-unknown-linux-gnu/stage0-sysroot/bin/rustc -O foo.rs -o binary-updated
- name: Display binary size
run: |
ls -la binary-*
echo "SIZE_REFERENCE=$(stat -c '%s' binary-reference)" >> "$GITHUB_ENV"
echo "SIZE_UPDATED=$(stat -c '%s' binary-updated)" >> "$GITHUB_ENV"
- name: Post a PR comment if the size has changed
uses: ./backtrace/.github/actions/build-with-patched-std
with:
backtrace-commit: ${{ env.HEAD_COMMIT }}
main-rs: ${{ env.TEST_MAIN_RS }}
rustc-dir: ${{ env.RUSTC_DIR }}
id: size-updated
# There is no built-in way to "collect" all the outputs of a set of jobs
# run with a matrix strategy. Subsequent jobs that have a "needs"
# dependency on this one will be run once, when the last matrix job is
# run. Appending data to a single file within a matrix is subject to race
# conditions. So we write the size data to files with distinct names
# generated from the job index.
- name: Write sizes to file
uses: actions/github-script@v6
env:
SIZE_REFERENCE: ${{ steps.size-reference.outputs.test-binary-size }}
SIZE_UPDATED: ${{ steps.size-updated.outputs.test-binary-size }}
PLATFORM: ${{ matrix.platform }}
with:
script: |
const reference = process.env.SIZE_REFERENCE;
const updated = process.env.SIZE_UPDATED;
const diff = updated - reference;
const plus = diff > 0 ? "+" : "";
const diff_str = `${plus}${diff}B`;
const fs = require("fs");
const path = require("path");

if (diff !== 0) {
const percent = (((updated / reference) - 1) * 100).toFixed(2);
// The body is created here and wrapped so "weirdly" to avoid whitespace at the start of the lines,
// which is interpreted as a code block by Markdown.
const body = `Below is the size of a hello-world Rust program linked with libstd with backtrace.
fs.mkdirSync(process.env.SIZE_DATA_DIR, {recursive: true});

Original binary size: **${reference}B**
Updated binary size: **${updated}B**
Difference: **${diff_str}** (${percent}%)`;
const output_data = JSON.stringify({
platform: process.env.PLATFORM,
reference: process.env.SIZE_REFERENCE,
updated: process.env.SIZE_UPDATED,
});

github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body
})
}
// The "wx" flag makes this fail if the file exists, which we want,
// because there should be no collisions.
fs.writeFileSync(
path.join(process.env.SIZE_DATA_DIR, process.env.SIZE_DATA_FILE),
output_data,
{ flag: "wx" },
);
- name: Upload size data
uses: actions/upload-artifact@v3
with:
name: size-files
path: ${{ env.SIZE_DATA_DIR }}/${{ env.SIZE_DATA_FILE }}
retention-days: 1
if-no-files-found: error
report:
name: Report binary size changes
runs-on: ubuntu-latest
needs: measure
permissions:
pull-requests: write
steps:
# Clone backtrace to access Github composite actions to report size.
- uses: actions/checkout@v3
- name: Download size data
uses: actions/download-artifact@v3
with:
name: size-files
path: ${{ env.SIZE_DATA_DIR }}
- name: Analyze and report size changes
uses: ./.github/actions/report-code-size-changes
with:
data-directory: ${{ env.SIZE_DATA_DIR }}
Loading