Skip to content
Draft
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
54 changes: 54 additions & 0 deletions .github/workflows/binary-size-comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Binary size comment

# Split from "Binary size" because a pull_request run from a fork gets a
# read-only token and cannot comment. workflow_run runs on the default branch
# with a write token, and never checks out the pull request's code.
on:
workflow_run:
workflows: ["Binary size"]
types: [completed]

permissions:
contents: read
pull-requests: write

jobs:
comment:
name: Comment on the pull request
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Download the report
id: download
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: binary-size-report
path: report
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Post or update the comment
if: steps.download.outcome == 'success'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
run: |
pr=$(cat report/pr-number)
significant=$(jq -r .significant report/status.json)
existing=$(gh api "repos/$REPO/issues/$pr/comments" --paginate \
--jq '.[] | select(.body | startswith("<!-- binary-size -->")) | .id' | head -1)

# An existing comment is refreshed even below the threshold, so it
# never keeps claiming a change that later pushes undid.
if [ "$significant" != "true" ] && [ -z "$existing" ]; then
echo "Change is below the reporting threshold; not commenting."
exit 0
fi

body=$(jq -Rs . < report/report.md)
if [ -n "$existing" ]; then
gh api -X PATCH "repos/$REPO/issues/comments/$existing" --input - <<< "{\"body\": $body}"
else
gh api -X POST "repos/$REPO/issues/$pr/comments" --input - <<< "{\"body\": $body}"
fi
82 changes: 82 additions & 0 deletions .github/workflows/binary-size.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Binary size

on:
pull_request:
branches: [main]
paths:
- "crates/**"
- "Cargo.toml"
- "Cargo.lock"
- "rust-toolchain.toml"
- ".github/workflows/binary-size.yml"
- "scripts/binary-size.mjs"

permissions:
contents: read

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

jobs:
size:
name: Native binary size
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
fetch-depth: 0

- name: Install Rust
uses: moonrepo/setup-rust@abb2d32350334249b178c401e5ec5836e0cd88d3 # v1.3.0
with:
cache-target: release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: latest

# Kept outside the tree so it survives the checkout of the base commit,
# which predates this script.
- name: Stage the measuring script
run: cp scripts/binary-size.mjs "$RUNNER_TEMP/binary-size.mjs"

- name: Build this PR
run: cargo build --release -p satteri-napi

- name: Measure this PR
run: node "$RUNNER_TEMP/binary-size.mjs" measure target/release/libsatteri_napi.so --out "$RUNNER_TEMP/head.json"

- name: Build the base commit
run: |
git checkout --detach ${{ github.event.pull_request.base.sha }}
cargo build --release -p satteri-napi

- name: Measure the base commit
run: node "$RUNNER_TEMP/binary-size.mjs" measure target/release/libsatteri_napi.so --out "$RUNNER_TEMP/base.json"

- name: Compare
run: |
mkdir -p "$RUNNER_TEMP/report"
echo "${{ github.event.pull_request.number }}" > "$RUNNER_TEMP/report/pr-number"
node "$RUNNER_TEMP/binary-size.mjs" compare \
"$RUNNER_TEMP/base.json" "$RUNNER_TEMP/head.json" \
--max-growth 5 \
--comment-threshold 1 \
--status "$RUNNER_TEMP/report/status.json" \
| tee "$RUNNER_TEMP/report/report.md" >> "$GITHUB_STEP_SUMMARY"

# Uploaded even when the budget check fails, so the comment still lands.
- name: Upload the report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: binary-size-report
path: ${{ runner.temp }}/report
retention-days: 1
if-no-files-found: ignore
8 changes: 8 additions & 0 deletions .sampo/changesets/shrink-native-binary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
cargo/satteri-mdxjs: patch
cargo/satteri-property-info: patch
cargo/satteri-pulldown-cmark: patch
npm/satteri: patch
---

Reduced the size of the native binaries by about 9%.
43 changes: 0 additions & 43 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ memchr = { version = "2.5", default-features = false }
oxc_allocator = "0.121.0"
oxc_ast = "0.121.0"
oxc_ast_visit = "0.121.0"
oxc_codegen = "0.121.0"
oxc_codegen = { version = "0.121.0", default-features = false }
oxc_estree = "0.121.0"
oxc_parser = "0.121.0"
oxc_parser = { version = "0.121.0", default-features = false }
oxc_span = "0.121.0"
oxc_syntax = "0.121.0"
rustc-hash = "2"
Expand Down
99 changes: 99 additions & 0 deletions crates/satteri-layout-codegen/src/emit_property_tables.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//! Emitter: pack [`property_schema`] into relocation-free lookup tables.
//!
//! A `&'static str` in a `static` table costs a relocation record and a pointer
//! slot, so rows are keyed by `(offset, length)` into one packed blob instead.

use std::fmt::Write as _;

use crate::property_schema::Row;

const HEADER_RS: &str = "//! @generated by `cargo run -p satteri-layout-codegen`. Do not edit by hand.\n\
//!\n\
//! Source of truth: `crates/satteri-layout-codegen/src/property_schema.rs`.\n\n";

/// Lays each distinct name down once, longest first so shorter ones can reuse
/// a span inside one already written.
fn pack(tables: &[&[Row]]) -> (String, Vec<(String, usize)>) {
let mut distinct: Vec<&str> = tables
.iter()
.flat_map(|rows| {
rows.iter()
.flat_map(|(name, prop, attr, _)| [*name, *prop, *attr])
})
.collect();
distinct.sort_unstable();
distinct.dedup();
distinct.sort_by_key(|s| (std::cmp::Reverse(s.len()), *s));

let mut blob = String::new();
let mut offsets: Vec<(String, usize)> = Vec::new();
for text in distinct {
let at = blob.find(text).unwrap_or_else(|| {
blob.push_str(text);
blob.len() - text.len()
});
offsets.push((text.to_string(), at));
}
offsets.sort_by(|a, b| a.0.cmp(&b.0));
(blob, offsets)
}

fn offset_of(offsets: &[(String, usize)], text: &str) -> usize {
let at = offsets
.binary_search_by(|probe| probe.0.as_str().cmp(text))
.unwrap_or_else(|_| panic!("{text} missing from the packed blob"));
offsets[at].1
}

fn table(out: &mut String, name: &str, rows: &[Row], offsets: &[(String, usize)]) {
let _ = write!(
out,
"\n/// Sorted by name for binary search.\npub(crate) static {name}: &[Row] = &[\n"
);
for (key, property, attribute, class) in rows {
let _ = writeln!(
out,
" ({}, {}, {}, {}, {}, {}, {}), // {key}",
offset_of(offsets, key),
key.len(),
offset_of(offsets, property),
property.len(),
offset_of(offsets, attribute),
attribute.len(),
class.code(),
);
}
out.push_str("];\n");
}

pub fn property_tables_rs(html: &[Row], svg: &[Row]) -> String {
let (blob, offsets) = pack(&[html, svg]);
assert!(blob.len() < u16::MAX as usize, "blob outgrew u16 offsets");
assert!(
[html, svg]
.iter()
.flat_map(|rows| rows.iter())
.all(|(k, p, a, _)| k.len() < 256 && p.len() < 256 && a.len() < 256),
"a name outgrew its u8 length"
);

let mut out = String::from(HEADER_RS);
out.push_str("/// Every distinct name, property, and attribute, overlapped where one is a\n");
out.push_str("/// substring of another.\npub(crate) static STRINGS: &str = \"");
for (i, chunk) in blob.as_bytes().chunks(96).enumerate() {
if i > 0 {
out.push_str("\\\n ");
}
out.push_str(std::str::from_utf8(chunk).expect("chunk splits a code point"));
}
out.push_str("\";\n\n");
out.push_str(
"/// `(name, property, attribute)` as `(offset, length)` pairs into [`STRINGS`],\n",
);
out.push_str("/// then the coercion class.\n");
out.push_str("pub(crate) type Row = (u16, u8, u16, u8, u16, u8, u8);\n");

table(&mut out, "HTML_TABLE", html, &offsets);
table(&mut out, "SVG_TABLE", svg, &offsets);
out
}
Loading