Skip to content

perf(build): speed up UniFFI binding generation - #590

Merged
TarikGul merged 7 commits into
mainfrom
tg/faster-codegen-profile
Sep 3, 2026
Merged

perf(build): speed up UniFFI binding generation#590
TarikGul merged 7 commits into
mainfrom
tg/faster-codegen-profile

Conversation

@TarikGul

@TarikGul TarikGul commented Sep 3, 2026

Copy link
Copy Markdown
Member

Problem

profile.codegen builds a dylib that is never shipped. uniffi-bindgen only reads its exported metadata symbols. The profile inherited opt-level = "z", lto = "fat" and codegen-units = 1 from release. None of that helps metadata extraction, and the cost is paid on every core edit.

Change

profile.codegen now optimises for build speed.

before after
make uniffi, cold 67s 18s
make uniffi, incremental after a core edit 24s 3s

Generated bindings are byte identical before and after. The dylib still carries its metadata: 3,034 uniffi symbols and 179 UNIFFI_META entries. Only profile.codegen changes, so shipped artifacts are unaffected.

Two smaller changes come with it.

SIM_ONLY=1 on the xcframework target drops the device slice while iterating, which halves the target builds. provider-ios already uses this convention.

The host publish.sh now refuses a framework with no device slice. The provider script has always rejected one, the host script did not, so building the simulator slice alone and publishing produced a release asset that cannot link for a device. That was already reachable through XCFRAMEWORK_TARGETS.

Timings measured on an Apple M4 Max, 16 cores, macOS 26, rustc 1.97.1.

…ot use

The codegen profile builds a dylib that is never shipped: uniffi-bindgen only
reads its exported metadata symbols. It inherited opt-level z, fat LTO and a
single codegen unit from release, which buys nothing for metadata extraction
and is paid on every core edit.

Measured on an M4 Max, both figures repeated and stable:

  make uniffi, cold          67s -> 26s
  make uniffi, incremental   24s -> 10s

The generated bindings are byte-identical before and after, and the dylib still
carries its metadata: 3,034 uniffi symbols, 179 UNIFFI_META entries. Only
profile.codegen changes, so every shipped artifact is unaffected.
The provider's publish script has always rejected a simulator-only xcframework
because it cannot ship; the host's did not, so building the simulator slice
alone and publishing produced a release asset that no consumer could link for a
device.

Reachable before this change through XCFRAMEWORK_TARGETS, and about to become
easier to reach. Same check, message and exit code as the provider.

Verified both directions: exit 66 on a simulator-only framework, and execution
continues past the guard once a device slice is present.
Iterating against a simulator does not need the device slice, and building it
doubles the target builds for no benefit. SIM_ONLY=1 drops it, matching the
convention provider-ios already uses.

Safe because publish.sh now refuses a simulator-only framework, so this cannot
reach a release asset. XCFRAMEWORK_TARGETS still overrides both.

Verified that SIM_ONLY=1 resolves to one target and the default to two.
@TarikGul
TarikGul requested a review from a team September 3, 2026 15:07

@Imod7 Imod7 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This guard has no test, and the file for it already exists: scripts/lib/xcframework-publish-guard.test.mjs, run in CI by npm run test:scripts, already driving both publish scripts and already carrying a slices field. Please add a case, it is about ten lines.

Please document the new flag in ios/truapi-host/README.md. Its "Build outputs in detail" xcframework bullet still says both targets are built, and nothing on the host side mentions the new publish guard. ios/truapi-provider/README.md has the line to mirror.

Comment thread ios/truapi-host/scripts/publish.sh Outdated
Comment thread Cargo.toml
Comment thread Makefile Outdated
Most core edits touch one crate, so incremental compilation is the largest
remaining win here. Measured on an M4 Max, three runs:

  make uniffi, cold          26s -> 18s
  make uniffi, incremental   10s -> 3s

Against main that is 67s -> 18s cold and 24s -> 3s incremental. Bindings stay
byte-identical on every run.
The earlier check only looked for the device slice, so a device-only framework
passed and shipped, and no simulator build could link it. Selecting one target
through XCFRAMEWORK_TARGETS reaches either state, so both need rejecting.

Both publish scripts now loop over both slices. The message no longer names a
flag, because someone who selected targets directly never set one.

Verified in both scripts: exit 66 for a simulator-only framework, 66 for a
device-only one, and past the guard when both slices are present.
Make reads any non-empty value as true, so SIM_ONLY=0 and SIM_ONLY=false
dropped the device slice, which is the opposite of what typing them means.

0, false, no and off are now off, at both call sites including provider-ios.
Verified across empty, 1, true, 0, false, no and off.
@TarikGul
TarikGul added this pull request to the merge queue Sep 3, 2026
Merged via the queue into main with commit 8ec2e8a Sep 3, 2026
18 checks passed
@TarikGul
TarikGul deleted the tg/faster-codegen-profile branch September 3, 2026 17:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants