Skip to content

perf(metadata): omit empty constructorTokens slot (reader change) - #435

Merged
edusperoni merged 1 commit into
feat/metadata-sizefrom
feat/metadata-size-runtime
Aug 11, 2026
Merged

perf(metadata): omit empty constructorTokens slot (reader change)#435
edusperoni merged 1 commit into
feat/metadata-sizefrom
feat/metadata-size-runtime

Conversation

@edusperoni

@edusperoni edusperoni commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Description

Draft. Stacked on #434 (feat/metadata-size) — review only the last commit; the base PR is generator-only, this one is for changes that also need the runtime reader to move.

Sizes on a full iOS 26.2 simulator SDK run, same umbrella header throughout:

bytes vs baseline
baseline (main) 12,033,254
after #434 (generator-only) 11,483,216 −4.57%
after this PR 11,251,503 −6.50%

This commit accounts for −231,713 B.

Current behavior

Every MethodMeta stores a 4-byte pointer to its constructor-tokens string. 57,933 of 60,589 methods have no constructor tokens — the field points at the interned empty string. The string itself costs nothing (one shared copy), but the pointer is paid 60,589 times.

New behavior

constructorTokens is the trailing field of MethodMeta, and MethodMeta has no subclass, so the slot can simply be omitted. A new flag, MethodHasConstructorTokens (bit 9), says whether it is present; MethodMeta::constructorTokens() returns "" without touching the slot when the flag is clear. PropertyMeta::save already conditionally omits its getter/setter pointers, so this follows an existing precedent in the format.

Method records are only ever reached through ArrayOfPtrTo<MethodMeta> — arrays of offsets, never contiguous structs — so a variable-size record is safe; nothing does pointer arithmetic across them.

Also in this commit: a latent flag-mask bug

serializeMember masked with 0b11111000, which clears the 3 type bits and everything from bit 8 up. That silently discards any member flag stored at bit 8 or above — including HasDemangledName, which serializeBase had just set. It is widened to ~0b111 so only the type bits are cleared.

This changes nothing on its own today: I checked all 29,561 methods and 20,603 properties in the SDK and none carries a demangled name, so bit 8 was always 0 for members. But the mask had to be fixed before bit 9 could be used at all.

Verification

Both files rendered to a canonical, offset-independent form — every entity with names, flags, type encodings and constructor tokens resolved, sorted — then diffed:

188,518 lines rendered, 0 lines differ

The renderer auto-detects which format a file uses (legacy always-present slot vs. flagged slot) and compares the resulting string, so a method with no tokens renders as "" 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 tokens are compared directly instead.

Risk

The reader change is the load-bearing part: if constructorTokens() ever reads the slot when the flag is clear, it reads whatever the generator emitted next in the heap. The accessor is the single point where that is decided, and it is the only place _constructorTokens is touched.

Only consumer of the value is ArgConverter.mm:645-647, which compares it against tokens built from a JS initializer object; "" there behaves exactly as the interned empty string did.

Note on the diff size

Roughly 85 of the ~95 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. The semantic change is about ten lines: one new flag in each enum, the conditional push_pointer, the flag assignment, the mask widening, and the accessor. Reviewing with whitespace ignored makes this much easier to read.

Does your pull request have unit tests?

Not yet — draft. The device suite has not been run on this branch. Unlike #434 this one changes reader behavior, so it does need a suite run before it leaves draft.

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.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 39ed1c12-46d5-4cf8-ae6c-409051cc3e87

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@edusperoni
edusperoni merged commit 3c7c91e into feat/metadata-size Aug 11, 2026
3 checks passed
@edusperoni
edusperoni deleted the feat/metadata-size-runtime branch August 11, 2026 17:12
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.

1 participant