Skip to content

perf(metadata): small size reductions in metadata.bin - #434

Merged
NathanWalker merged 3 commits into
mainfrom
feat/metadata-size
Aug 12, 2026
Merged

perf(metadata): small size reductions in metadata.bin#434
NathanWalker merged 3 commits into
mainfrom
feat/metadata-size

Conversation

@edusperoni

@edusperoni edusperoni commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Description

Draft. Three small, self-contained reductions in the size of metadata-<arch>.bin. Each is independent and could be dropped on its own. The larger structural changes are stacked on top of this branch: #435.

No framework filtering, and no change to what's exposed to JS.

Sizes on a full iOS 26.2 simulator SDK run, same umbrella header throughout, same declaration count (63,878 declarations from 197 modules):

bytes vs baseline
baseline (main) 12,033,254
+ shared interning map 11,930,…
+ shared empty array 11,483,216 −4.57%
+ constructorTokens elision 11,251,503 −6.50%

1. The string-interning map was split in two

BinaryTypeEncodingSerializer held its BinaryWriter by value. The writer owns the interning map, so the copy interned into a second map, and every string reachable from both the meta path and the type-encoding path was written to the heap twice. Measured: 4,519 strings stored at two offsets, and 100% of them were the cross-path case — e.g. MTRRefrigeratorAndTemperatureControlledCabinetMode…, ASAccountAuthenticationModificationController….

Fixed by making _heapWriter a reference.

2. Empty binary arrays were never shared

push_binaryArray writes a count then the elements; for an empty vector that's four zero bytes, and nothing deduplicated them. The file contained 95,253 distinct empty arrays, overwhelmingly empty protocol lists hanging off interface-reference type encodings — 106,989 array references resolved to 106,989 distinct offsets, i.e. zero sharing of any kind.

The writer now remembers the first empty array and returns that offset for every subsequent one. Offset 0 stays a valid null sentinel: the heap reserves a marker byte at position 0 (metaFile.h:46), so no real array can land there.

These two compound — sharing the writer also merges the empty-array interning — which is why they save 550 KB together against ~490 KB measured separately.

3. constructorTokens is stored even when empty

57,933 of 60,589 methods have no constructor tokens. The string costs nothing (one interned empty copy), but the 4-byte pointer is paid by every method.

It is the trailing field of MethodMeta, which has no subclass, so the slot is now omitted and gated on a new MethodHasConstructorTokens flag (bit 9); MethodMeta::constructorTokens() returns "" without touching the slot when the flag is clear. PropertyMeta::save already conditionally omits its getter/setter pointers, so this follows existing precedent. Method records are only ever reached through ArrayOfPtrTo<MethodMeta> — arrays of offsets, never contiguous structs — so a variable-size record is safe.

This one also fixes a latent flag-mask bug. serializeMember masked with 0b11111000, clearing the 3 type bits and everything from bit 8 up — silently discarding any member flag stored there, including the HasDemangledName that serializeBase had just set. Widened to ~0b111. It is inert today (no method or property in the SDK carries a demangled name — I checked all 29,561 and 20,603 of them), but it had to be fixed before bit 9 was usable.

This is the only one of the three that changes the runtime reader.

Verification

Both files rendered to a canonical, offset-independent form — every interface, protocol, method, property, struct, enum, function, var and module, with all names, flags, type encodings and constructor tokens resolved and sorted — then diffed:

188,518 lines rendered, 0 lines differ

The renderer auto-detects which on-disk format a file uses (legacy always-present tokens slot vs. flagged slot) and compares the resulting string, so a method with no tokens renders identically under both. Flag bits 7/8/9 are excluded from the comparison because they describe how a record is stored rather than what it means; names and token values are compared directly instead.

Note on the diff size

Most of the changed lines are clang-format reindenting the BinaryFlags and MetaFlags enum blocks from 4-space to 2-space — the pre-commit hook formats staged hunks, and adding one enumerator marks the whole enum as touched. Reviewing with whitespace ignored (-w) reduces this to about 30 lines and makes it much easier to read.

Explicitly not doing

Excluding frameworks from generation. Blacklisting Matter alone is worth 3.87 MB (31% of the file), and the mechanism already exists end to end — App_Resources/iOS/native-api-usage.json → the CLI writes platforms/ios/{whitelist,blacklist}.mdgbuild-step-metadata-generator.py passes them to the generator. That is an app-level decision, not a runtime one.

Does your pull request have unit tests?

Not yet — draft. The device suite has not been run on this branch. Changes 1 and 2 are byte-layout only and covered by the semantic diff above; change 3 alters reader behavior and does need a suite run before this leaves draft.

Summary by CodeRabbit

  • Bug Fixes

    • Improved metadata serialization by preserving member flags and writing constructor metadata only when available.
    • Fixed metadata reading so absent constructor information returns an empty value.
    • Improved consistency when handling repeated empty arrays.
  • Performance

    • Reduced redundant binary data and improved reuse of shared serialized strings.
  • Maintenance

    • Reformatted metadata flag declarations for improved readability.

…ding serializer

BinaryTypeEncodingSerializer held its BinaryWriter by value. The writer owns the
string-interning map, so the copy interned into a second map and every string
reachable from both the meta path and the type-encoding path was written to the
heap twice.

4519 strings were stored at two offsets; every one of them was the cross-path
case.
push_binaryArray wrote a fresh count-of-zero for every empty array, and nothing
deduplicated them. Empty protocol lists alone accounted for 95k of them.

Empty arrays carry no payload, so they can all share one offset. Offset 0 stays
the null sentinel: the heap reserves a marker byte there, so no real array can
land on it.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3526e01c-5562-40a3-acec-605344df3fab

📥 Commits

Reviewing files that changed from the base of the PR and between 3232fb5 and 3c7c91e.

📒 Files selected for processing (7)
  • NativeScript/runtime/Metadata.h
  • metadata-generator/src/Binary/binarySerializer.cpp
  • metadata-generator/src/Binary/binaryStructures.cpp
  • metadata-generator/src/Binary/binaryStructures.h
  • metadata-generator/src/Binary/binaryTypeEncodingSerializer.h
  • metadata-generator/src/Binary/binaryWriter.cpp
  • metadata-generator/src/Binary/binaryWriter.h

📝 Walkthrough

Walkthrough

The changes preserve metadata flags, conditionally serialize constructor tokens, share BinaryWriter string state, and reuse serialized empty-array data.

Changes

Metadata serialization

Layer / File(s) Summary
Metadata flag and constructor-token contracts
NativeScript/runtime/Metadata.h, metadata-generator/src/Binary/binaryStructures.h
Flag formatting changes preserve values. constructorTokens() now returns data only when MethodHasConstructorTokens is set.
Method metadata serialization
metadata-generator/src/Binary/binarySerializer.cpp, metadata-generator/src/Binary/binaryStructures.cpp
Member serialization preserves higher flag bits. Constructor tokens are written only when non-empty and flagged.
Shared writer state and empty arrays
metadata-generator/src/Binary/binaryTypeEncodingSerializer.h, metadata-generator/src/Binary/binaryWriter.h, metadata-generator/src/Binary/binaryWriter.cpp
BinaryTypeEncodingSerializer references the existing writer. BinaryWriter caches the offset for serialized empty arrays.

Estimated code review effort: 2 (Simple) | ~10 minutes

Poem

A rabbit checks the flags at night,
Tokens appear when marked just right.
One writer shares its map with care,
Empty arrays reuse their lair.
Hop, metadata bytes stay light!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: reducing the size of metadata.bin through metadata serialization optimizations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

57933 of 60589 methods carry no constructor tokens, yet every MethodMeta paid a
4-byte pointer for the field. It is the trailing field and MethodMeta has no
subclass, so it can be left out entirely and gated on a flag.

Also widens the member flag mask: it cleared bits 8 and up alongside the type
bits, which would silently discard any member flag stored there. No member in
the SDK sets bit 8 today, so this changes nothing on its own.
@edusperoni edusperoni changed the title perf(metadata-generator): shrink metadata.bin (writer-side dedup) perf(metadata): small size reductions in metadata.bin Aug 11, 2026
@edusperoni
edusperoni marked this pull request as ready for review August 11, 2026 21:59
@NathanWalker
NathanWalker merged commit ba922a7 into main Aug 12, 2026
11 of 12 checks passed
@NathanWalker
NathanWalker deleted the feat/metadata-size branch August 12, 2026 21:50
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.

2 participants