fix: box transition enums so Component.Content stays small (stack overflow on deep screens)#33
Open
aliksbright wants to merge 1 commit into
Open
fix: box transition enums so Component.Content stays small (stack overflow on deep screens)#33aliksbright wants to merge 1 commit into
aliksbright wants to merge 1 commit into
Conversation
Mark `ScreenTransition` and `ComponentTransition` as `indirect`. Root cause: `DSL.Model.Component.Content` was ~5.2 KB per value because the transition enums are stored inline. `ScreenTransition.Inline` embeds several sub-models by value (~1 KB) and is inlined into `Content` in multiple places — `Content.transition` (`ComponentTransition`), and `Action.transition`, which appears twice inside every `NavigationBar` via its left/right `BarAction`s. `NavigationBar` is itself inlined into `Content`, so `navigationBar` alone was 2968 B and `transition` 994 B of the 5.2 KB — on every component, even simple labels, and even though only screen roots use them. Every component's view model copies its whole `Content` by value on init (`content as? Component`, `self.component = c`, `ComponentStateManager(content:)`, state binding). In debug (`-Onone`) builds these copies aren't elided, so a moderately deep screen accumulates enough 5 KB stack copies to overflow the 1 MB main-thread stack mid-render — surfacing as `EXC_BAD_ACCESS` (code=2) at a stack address with `__chkstk_darwin` / `initializeWithCopy` on the stack. It reproduced on the onboarding paywall (scrollView + nested template cards), and looked device-specific only because it's timing/stack-depth sensitive. `indirect` moves each enum's payload to the heap, so the field becomes pointer-sized. `Content` drops from ~5.2 KB to ~1.3 KB (measured), copies become cheap, and the overflow is gone in both debug and release. No API or decoding change — `indirect` only affects memory layout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Marks
ScreenTransitionandComponentTransitionasindirect. This shrinks every component'sDSL.Model.Component.Contentvalue from ~5.2 KB to ~1.3 KB, fixing a main-thread stack overflow (EXC_BAD_ACCESS) that occurred while rendering deep screens.Symptom
Navigating to the onboarding paywall (
onboarding-04: ascrollViewwith two nested templatePlanCards) crashed withEXC_BAD_ACCESS (code=2)at a stack address, with__chkstk_darwin+initializeWithCopy for DSL.Model.Component.Contenton the stack. It looked device-specific because it's sensitive to timing and stack depth, but it reproduces deterministically.It was not recursion (render depth peaked at 4) and not a cache/DSL problem (the cached DSL was clean and identical across devices).
Root cause
Component.Contentstores the transition enums inline by value, and those enums are large:NavigationBarComponentTransition(transition)ScreenTransition.Inlineembeds many sub-models (Participant×2,Animation,ModalPresentation,InteractiveConfig, …) by value (~1 KB). It's inlined intoContentin several places:Content.transition→ComponentTransition→ScreenTransitionContent.navigationBar→NavigationBar→leftAction/rightAction(BarAction) →Action.transition→ScreenTransition← twice per componentSo
NavigationBaralone was ~3 KB, carried on every component'sContent(even simple labels) even though only screen roots use a nav bar or a screen transition.Every component's view model copies its whole
Contentby value on init (content as? Component,self.component = c,ComponentStateManager(content:), state binding). In debug (-Onone) builds these copies aren't optimized away, so a moderately deep screen accumulates enough 5 KB stack copies to exhaust the 1 MB main-thread stack mid-render.Fix
indirectonScreenTransitionandComponentTransitionmoves each enum's payload to the heap, making the field pointer-sized. This removes the ~1 KBtransitionfield and collapsesNavigationBar(its bulk was the two inlinedAction.transitionvalues), soContentdrops to ~1.3 KB. Copies become cheap and the overflow is gone in debug and release.indirectchanges only memory layout — no API, decoding, or behavior change.Verification
Reproduced on device, instrumented
MemoryLayoutto measureContentand each inline field, confirmed the 5.2 KB / 2968 B NavBar / 994 B transition breakdown, applied the change, and confirmed the paywall renders (Contentmeasured ~1.3 KB, no crash). Instrumentation was removed after diagnosis.Follow-up
App8Cloudpin to pick this up.renderComponentImplcounts path dots, not call depth — worth making call-depth-based so any future runaway degrades to an error view instead of crashing.🤖 Generated with Claude Code