From f6ddcb7278b9653cf13f2fd9d9c1e9081ee18b10 Mon Sep 17 00:00:00 2001 From: Tomoya0k Date: Tue, 23 Jun 2026 20:27:40 -0600 Subject: [PATCH] perf: measure and document compiled WASM size --- .github/workflows/wasm-size.yml | 49 ++++++++ Makefile | 7 +- README.md | 21 +++- docs/build.md | 37 ++++++ scripts/measure_sizes.sh | 205 ++++++++++++++++++++++++++++++++ scripts/size_baseline.json | 21 ++++ 6 files changed, 336 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/wasm-size.yml create mode 100644 docs/build.md create mode 100644 scripts/measure_sizes.sh create mode 100644 scripts/size_baseline.json diff --git a/.github/workflows/wasm-size.yml b/.github/workflows/wasm-size.yml new file mode 100644 index 0000000..d715c6a --- /dev/null +++ b/.github/workflows/wasm-size.yml @@ -0,0 +1,49 @@ +name: WASM size + +on: + pull_request: + push: + branches: + - main + +jobs: + contract-size: + name: Check contract WASM sizes + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust targets + run: | + rustup update stable + rustup default stable + rustup target add wasm32-unknown-unknown + rustup target add wasm32v1-none + + - name: Install Stellar CLI + run: | + curl -L https://github.com/stellar/stellar-cli/releases/download/v27.0.0/stellar-cli-27.0.0-x86_64-unknown-linux-gnu.tar.gz -o /tmp/stellar-cli.tar.gz + mkdir -p /tmp/stellar-cli + tar -xzf /tmp/stellar-cli.tar.gz -C /tmp/stellar-cli + sudo install /tmp/stellar-cli/stellar /usr/local/bin/stellar + stellar --version + + - name: Install wasm-opt + run: | + sudo apt-get update + sudo apt-get install -y binaryen + wasm-opt --version + + - name: Check WASM sizes + run: bash scripts/measure_sizes.sh --baseline scripts/size_baseline.json --fail-on-increase + + - name: Upload size report + if: always() + uses: actions/upload-artifact@v4 + with: + name: wasm-size-report + path: | + target/sizes.json + target/wasm32-unknown-unknown/release/optimized/*.wasm diff --git a/Makefile b/Makefile index b70f813..1622523 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ include mx/deploy.mk include mx/upgrade.mk include mx/tokens.mk -.PHONY: all help clean +.PHONY: all help clean update-size-baseline all: build test @@ -22,6 +22,7 @@ help: @printf '%s\n' ' make lint' @printf '%s\n' ' make test' @printf '%s\n' ' make build' + @printf '%s\n' ' make update-size-baseline' @printf '%s\n' ' make smoke-prices' @printf '%s\n' '' @printf '%s\n' 'Deploy and upgrade:' @@ -43,3 +44,7 @@ help: clean: cargo clean rm -rf .deployed + +update-size-baseline: + bash scripts/measure_sizes.sh + cp target/sizes.json scripts/size_baseline.json diff --git a/README.md b/README.md index 2e568b4..4700e77 100644 --- a/README.md +++ b/README.md @@ -346,14 +346,29 @@ rustup target add wasm32-unknown-unknown ### 2. Stellar CLI ```bash -# Install from crates.io with wasm optimiser enabled -cargo install --locked stellar-cli --features opt +# Install from crates.io +cargo install --locked stellar-cli # Verify stellar --version ``` -### 3. Docker (optional — required for local node) +### 3. wasm-opt + +`wasm-opt` is required for contract size measurement and CI size checks. + +```bash +# macOS +brew install binaryen + +# Ubuntu/Debian +sudo apt-get update && sudo apt-get install -y binaryen + +# Verify +wasm-opt --version +``` + +### 4. Docker (optional — required for local node) [Install Docker Desktop](https://docs.docker.com/get-docker/) if you want to run a fully local Stellar node instead of using testnet. diff --git a/docs/build.md b/docs/build.md new file mode 100644 index 0000000..bf38558 --- /dev/null +++ b/docs/build.md @@ -0,0 +1,37 @@ +# Build + +## Contract size budget + +Soroban contracts are deployed as WASM, and smaller binaries are cheaper to upload, easier to keep within network limits, and less likely to regress when dependencies or generated bindings change. Size tracking keeps those changes visible during review. + +Current optimized contract sizes: + + +| Contract | WASM size (bytes) | Δ from baseline | +| --- | ---: | ---: | +| adl_handler | 15715 | - | +| data_store | 11405 | - | +| deposit_handler | 18273 | - | +| deposit_vault | 3081 | - | +| exchange_router | 22672 | - | +| fee_handler | 9315 | - | +| liquidation_handler | 15471 | - | +| market_factory | 11640 | - | +| market_token | 7212 | - | +| oracle | 13008 | - | +| order_handler | 42897 | - | +| order_vault | 3604 | - | +| reader | 37622 | - | +| referral_storage | 6727 | - | +| role_store | 5393 | - | +| test_faucet | 4202 | - | +| test_token | 6750 | - | +| withdrawal_handler | 14824 | - | +| withdrawal_vault | 2763 | - | + + +The baseline is stored in `scripts/size_baseline.json` and must be updated manually after intentional size changes by running: + +```bash +make update-size-baseline +``` diff --git a/scripts/measure_sizes.sh b/scripts/measure_sizes.sh new file mode 100644 index 0000000..0d52520 --- /dev/null +++ b/scripts/measure_sizes.sh @@ -0,0 +1,205 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + printf '%s\n' "Usage: $0 [--baseline ] [--fail-on-increase]" +} + +baseline="" +fail_on_increase="0" +while [ "$#" -gt 0 ]; do + case "$1" in + --baseline) + if [ "$#" -lt 2 ]; then + usage >&2 + exit 2 + fi + baseline="$2" + shift 2 + ;; + --fail-on-increase) + fail_on_increase="1" + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + usage >&2 + exit 2 + ;; + esac +done + +if ! command -v stellar >/dev/null 2>&1; then + printf '%s\n' "error: stellar CLI is required" >&2 + exit 1 +fi + +if ! command -v wasm-opt >/dev/null 2>&1; then + printf '%s\n' "error: wasm-opt is required" >&2 + exit 1 +fi + +json_tool="" +if command -v python3 >/dev/null 2>&1 && python3 -c 'import json' >/dev/null 2>&1; then + json_tool="python3" +elif command -v python >/dev/null 2>&1 && python -c 'import json' >/dev/null 2>&1; then + json_tool="python" +elif command -v node >/dev/null 2>&1 && node -e 'JSON.parse("{}")' >/dev/null 2>&1; then + json_tool="node" +else + printf '%s\n' "error: python3, python, or node is required" >&2 + exit 1 +fi + +if [ -n "$baseline" ] && [ ! -f "$baseline" ]; then + printf 'error: baseline file not found: %s\n' "$baseline" >&2 + exit 1 +fi + +release_dir="target/wasm32-unknown-unknown/release" +mkdir -p "$release_dir" +rm -f "$release_dir"/*.wasm + +if stellar contract build --help | grep -q -- '--release'; then + stellar contract build --release --out-dir "$release_dir" +else + stellar contract build --profile release --out-dir "$release_dir" +fi + +optimized_dir="$release_dir/optimized" +mkdir -p "$optimized_dir" target + +shopt -s nullglob +wasm_files=("$release_dir"/*.wasm) +if [ "${#wasm_files[@]}" -eq 0 ]; then + printf 'error: no WASM files found in %s\n' "$release_dir" >&2 + exit 1 +fi + +rm -f "$optimized_dir"/*.wasm +for wasm in "${wasm_files[@]}"; do + wasm-opt -O3 "$wasm" -o "$optimized_dir/$(basename "$wasm")" +done + +optimized_files=("$optimized_dir"/*.wasm) +if [ "$json_tool" = "node" ]; then + node - "$baseline" "$fail_on_increase" "target/sizes.json" "${optimized_files[@]}" <<'JS' +const fs = require("fs"); +const path = require("path"); + +const baselinePath = process.argv[2]; +const failOnIncrease = process.argv[3] === "1"; +const outputPath = process.argv[4]; +const wasmPaths = process.argv.slice(5); + +let baseline = null; +if (baselinePath) { + baseline = JSON.parse(fs.readFileSync(baselinePath, "utf8")); +} + +const sizes = {}; +for (const wasmPath of wasmPaths) { + sizes[path.basename(wasmPath, ".wasm")] = fs.statSync(wasmPath).size; +} + +const sortedSizes = Object.fromEntries( + Object.entries(sizes).sort(([left], [right]) => left.localeCompare(right)) +); +fs.mkdirSync(path.dirname(outputPath), { recursive: true }); +fs.writeFileSync(outputPath, `${JSON.stringify(sortedSizes, null, 2)}\n`); + +function deltaText(contract, size) { + if (baseline === null) { + return "-"; + } + const oldSize = baseline[contract]; + if (oldSize === undefined) { + return "n/a"; + } + const diff = size - Number(oldSize); + const pct = Number(oldSize) === 0 ? "n/a" : `${((diff / Number(oldSize)) * 100).toFixed(2).replace(/^(?!-)/, "+")}%`; + return `${diff >= 0 ? "+" : ""}${diff} (${pct})`; +} + +console.log("| Contract | WASM size (bytes) | Δ from baseline |"); +console.log("| --- | ---: | ---: |"); +const increases = []; +for (const [contract, size] of Object.entries(sortedSizes)) { + console.log(`| ${contract} | ${size} | ${deltaText(contract, size)} |`); + if (baseline !== null && baseline[contract] !== undefined) { + const diff = size - Number(baseline[contract]); + if (diff > 0) { + increases.push(`${contract}: +${diff} bytes`); + } + } +} + +if (failOnIncrease && increases.length > 0) { + console.error(""); + console.error("WASM size increased against baseline:"); + for (const increase of increases) { + console.error(`- ${increase}`); + } + process.exit(1); +} +JS +else + "$json_tool" - "$baseline" "$fail_on_increase" "target/sizes.json" "${optimized_files[@]}" <<'PY' +import json +import os +import sys + +baseline_path = sys.argv[1] +fail_on_increase = sys.argv[2] == "1" +output_path = sys.argv[3] +wasm_paths = sys.argv[4:] + +baseline = None +if baseline_path: + with open(baseline_path, "r", encoding="utf-8") as fh: + baseline = json.load(fh) + +sizes = { + os.path.splitext(os.path.basename(path))[0]: os.path.getsize(path) + for path in wasm_paths +} + +os.makedirs(os.path.dirname(output_path), exist_ok=True) +with open(output_path, "w", encoding="utf-8") as fh: + json.dump(dict(sorted(sizes.items())), fh, indent=2) + fh.write("\n") + +def delta_text(contract, size): + if baseline is None: + return "-" + old_size = baseline.get(contract) + if old_size is None: + return "n/a" + diff = size - int(old_size) + if old_size == 0: + pct = "n/a" + else: + pct = f"{(diff / int(old_size)) * 100:+.2f}%" + return f"{diff:+d} ({pct})" + +print("| Contract | WASM size (bytes) | Δ from baseline |") +print("| --- | ---: | ---: |") +increases = [] +for contract, size in sorted(sizes.items()): + print(f"| {contract} | {size} | {delta_text(contract, size)} |") + if baseline is not None and contract in baseline: + diff = size - int(baseline[contract]) + if diff > 0: + increases.append(f"{contract}: +{diff} bytes") + +if fail_on_increase and increases: + print("", file=sys.stderr) + print("WASM size increased against baseline:", file=sys.stderr) + for increase in increases: + print(f"- {increase}", file=sys.stderr) + sys.exit(1) +PY +fi diff --git a/scripts/size_baseline.json b/scripts/size_baseline.json new file mode 100644 index 0000000..c609e4d --- /dev/null +++ b/scripts/size_baseline.json @@ -0,0 +1,21 @@ +{ + "adl_handler": 15715, + "data_store": 11405, + "deposit_handler": 18273, + "deposit_vault": 3081, + "exchange_router": 22672, + "fee_handler": 9315, + "liquidation_handler": 15471, + "market_factory": 11640, + "market_token": 7212, + "oracle": 13008, + "order_handler": 42897, + "order_vault": 3604, + "reader": 37622, + "referral_storage": 6727, + "role_store": 5393, + "test_faucet": 4202, + "test_token": 6750, + "withdrawal_handler": 14824, + "withdrawal_vault": 2763 +}