perf(metadata): small size reductions in metadata.bin - #434
Conversation
…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.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughThe changes preserve metadata flags, conditionally serialize constructor tokens, share ChangesMetadata serialization
Estimated code review effort: 2 (Simple) | ~10 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
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. Comment |
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.
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):
main)1. The string-interning map was split in two
BinaryTypeEncodingSerializerheld itsBinaryWriterby 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
_heapWritera reference.2. Empty binary arrays were never shared
push_binaryArraywrites 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.
constructorTokensis stored even when empty57,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 newMethodHasConstructorTokensflag (bit 9);MethodMeta::constructorTokens()returns""without touching the slot when the flag is clear.PropertyMeta::savealready conditionally omits its getter/setter pointers, so this follows existing precedent. Method records are only ever reached throughArrayOfPtrTo<MethodMeta>— arrays of offsets, never contiguous structs — so a variable-size record is safe.This one also fixes a latent flag-mask bug.
serializeMembermasked with0b11111000, clearing the 3 type bits and everything from bit 8 up — silently discarding any member flag stored there, including theHasDemangledNamethatserializeBasehad 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:
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
BinaryFlagsandMetaFlagsenum 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
Matteralone 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 writesplatforms/ios/{whitelist,blacklist}.mdg→build-step-metadata-generator.pypasses 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
Performance
Maintenance