Skip to content

Commit

Permalink
ci: A manual command for comparing contract sizes (#1048)
Browse files Browse the repository at this point in the history
  • Loading branch information
uint authored Jul 30, 2023
1 parent 7d2f8eb commit de975ed
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 169 deletions.
60 changes: 60 additions & 0 deletions .github/workflows/compare_sizes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Compare contract sizes with master
on:
issue_comment:
types:
- created
jobs:
generate_report:
runs-on: ubuntu-latest
if: ${{ (github.event.issue.pull_request) && (github.event.comment.body == '/compare') }}
permissions:
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Checkout Pull Request
run: hub pr checkout ${{ github.event.issue.number }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Cache intermediate artifacts
uses: actions/cache@v3
env:
cache-name: compare-sizes
with:
path: |
docker-target
/home/runner/work/.cargo/git
/home/runner/work/.cargo/registry
# TODO: add an additional key here, e.g. the Rust toolchain version used
key: ${{ runner.os }}-build-${{ env.cache-name }}
# restore-keys: |
# ${{ runner.os }}-build-${{ env.cache-name }}
- name: Generate report
run: |
yes | pip install GitPython docker appdirs
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
echo "REPORT<<$EOF" >> "$GITHUB_ENV"
ci/compare_sizes/compare_sizes.py --cargo-cache-dir /home/runner/work/.cargo >> "$GITHUB_ENV"
echo "$EOF" >> "$GITHUB_ENV"
- name: Submit report
if: ${{ success() }}
uses: actions/github-script@v4
with:
script: |
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: process.env.REPORT,
});
- name: Notify about failure
if: ${{ failure() }}
uses: actions/github-script@v4
with:
script: |
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Failed to generate size comparison report! See the failed CI job for more details.',
});
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ neardev
.idea
.vscode
**/.DS_Store

# Python
__pycache__
16 changes: 16 additions & 0 deletions ci/compare_sizes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Compare example contract sizes

This is a script to compare example contract sizes between the current non-master branch and `master`, and then produce a markdown report.

# Usage

The script is mostly triggered in PRs by posting `/compare` in a comment. For details, check out [the workflow](../../.github/workflows/compare_sizes.yml).

It's also possible to test it locally like so:

``` sh
# from the root dir
ci/compare_sizes/compare_sizes.py
```

When run like this, the script maintains a build cache in the user app directories appropriate for the host platform.
27 changes: 27 additions & 0 deletions ci/compare_sizes/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import os
import sys


class Cache:
def __init__(self, dir):
self._dir = dir
print(f"Cache directory: {dir}", file=sys.stderr)
os.makedirs(self.registry, exist_ok=True)
os.makedirs(self.git, exist_ok=True)
os.makedirs(self.target, exist_ok=True)

@property
def root(self):
return self._dir

@property
def registry(self):
return os.path.join(self.root, "registry")

@property
def git(self):
return os.path.join(self.root, "git")

@property
def target(self):
return os.path.join(self.root, "target")
77 changes: 77 additions & 0 deletions ci/compare_sizes/compare_sizes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env python3

# Requires:
# `pip install GitPython docker appdirs`
import argparse
import os
import sys
from cache import Cache
from appdirs import AppDirs

from project_instance import ProjectInstance


def common_entries(*dcts):
if not dcts:
return
for i in set(dcts[0]).intersection(*dcts[1:]):
yield (i,) + tuple(d[i] for d in dcts)


def list_dirs(path):
entries = map(lambda p: os.path.join(path, p), os.listdir(path))
return filter(os.path.isdir, entries)


def report(master, this_branch):
def diff(old, new):
diff = (new - old) / old

return "{0:+.0%}".format(diff)

header = """# Contract size report
Sizes are given in bytes.
| contract | master | this branch | difference |
| - | - | - | - |"""

combined = [
(name, master, branch, diff(master, branch))
for name, master, branch in common_entries(master, this_branch)
]
combined.sort(key=lambda el: el[0])
rows = [f"| {name} | {old} | {new} | {diff} |" for name, old, new, diff in combined]

return "\n".join([header, *rows])


def main():
parser = argparse.ArgumentParser(
prog="compare_sizes",
description="compare example contract sizes between current branch and master",
)
parser.add_argument("-c", "--cargo-cache-dir")
args = parser.parse_args()

default_cache_dir = os.path.join(
AppDirs("near_sdk_dev_cache", "near").user_data_dir,
"contract_build",
)
cache_dir = args.cargo_cache_dir if args.cargo_cache_dir else default_cache_dir
cache = Cache(cache_dir)

this_file = os.path.abspath(os.path.realpath(__file__))
project_root = os.path.dirname(os.path.dirname(os.path.dirname(this_file)))

cur_branch = ProjectInstance(project_root)

with cur_branch.branch("master") as master:
cur_sizes = cur_branch.sizes(cache)
master_sizes = master.sizes(cache)

print(report(master_sizes, cur_sizes))


if __name__ == "__main__":
main()
79 changes: 79 additions & 0 deletions ci/compare_sizes/project_instance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import docker
import os
import glob
import subprocess
import tempfile
import shutil
import sys
import platform
from contextlib import contextmanager
from git import Repo


class ProjectInstance:
def __init__(self, root_dir):
self._root_dir = root_dir

@contextmanager
def branch(self, branch):
repo = Repo(self._root_dir)

try:
with tempfile.TemporaryDirectory() as tempdir:
repo.git.worktree("add", tempdir, branch)
branch_project = ProjectInstance(tempdir)

yield branch_project
finally:
repo.git.worktree("prune")

@property
def _examples_dir(self):
return os.path.join(self._root_dir, "examples")

def _build_artifact(self, artifact, cache):
client = docker.from_env()
tag = "latest-arm64" if platform.machine() == "ARM64" else "latest-amd64"
image = f"nearprotocol/contract-builder:{tag}"

client.containers.run(
image,
"./build.sh",
mounts=[
docker.types.Mount("/host", self._root_dir, type="bind"),
docker.types.Mount(
"/usr/local/cargo/registry", cache.registry, type="bind"
),
docker.types.Mount("/usr/local/cargo/git", cache.git, type="bind"),
docker.types.Mount("/target", cache.target, type="bind"),
],
working_dir=f"/host/examples/{artifact.name}",
cap_add=["SYS_PTRACE"],
security_opt=["seccomp=unconfined"],
remove=True,
user=os.getuid(),
environment={
"RUSTFLAGS": "-C link-arg=-s",
"CARGO_TARGET_DIR": "/target",
},
)

@property
def _examples(self):
examples = filter(os.DirEntry.is_dir, os.scandir(self._examples_dir))

# build "status-message" first, as it's a dependency of some other examples
return sorted(examples, key=lambda x: x.name != "status-message")

def build_artifacts(self, cache):
for example in self._examples:
print(f"Building {example.name}...", file=sys.stderr)
self._build_artifact(example, cache)

def sizes(self, cache):
self.build_artifacts(cache)

artifact_paths = glob.glob(self._examples_dir + "/*/res/*.wasm")
return {
os.path.basename(path): os.stat(path).st_size for path in artifact_paths
}
3 changes: 1 addition & 2 deletions examples/adder/res/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@
*

# Except this to keep the directory
!.gitignore
!adder_abi.json
!.gitignore
Loading

0 comments on commit de975ed

Please sign in to comment.