Summary
Scrolling a UIScrollView-hosted UIStackView on a physical device while JS mutates its labels aborts the app:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil'
App terminated due to signal 6.
Requires human touch input. No automated probe reproduces it, which is why it has gone unseen — every headless run passes.
Where it throws
The backtrace is start → main → UIApplicationMain → CFRunLoop → CA transaction commit → UIView layout → UIStackView → throw. The exception lands in the layout pass at the end of a run-loop iteration, not inside any Perry callback. Something earlier leaves a nil in a UIKit array; it detonates at commit time.
Reproduction
Device: iPhone 16 Pro, iOS 26, Release build, --target ios.
PERRY_NO_AUTO_OPTIMIZE=1 perry run ios benchmarks/ios-ui/bench.ts --device <UDID>
Scroll the list continuously during the property-updates phase (50 labels, textSetString on each every frame) or add-remove (100 rows rebuilt 20x/sec). Crash lands within a few seconds. Without scrolling, the same build runs indefinitely.
PERRY_NO_AUTO_OPTIMIZE=1 is needed because perry-stdlib currently fails to build with --no-default-features — separate issue, see below.
Not caused by the frame-metrics work (#7754)
Established by four single-variable device bisects, each with a human scrolling:
| arm |
result |
| deferred view release |
crashes |
| widget release disabled entirely |
crashes |
display link in NSDefaultRunLoopMode |
crashes |
| display-link driver disabled, original NSTimer pump |
crashes |
The last arm is decisive: with #7754's driver removed and onFrame back on the pre-PR js_frame_pump_default() path, the abort still occurs. That PR's benchmark is simply the first workload that drives enough label mutation against a scrolled stack to expose it.
Hypotheses tested and falsified
- Deallocating views inside CoreAnimation's pre-commit phase — falsified; crashes with release deferred to a quiescent point.
- Releasing widget-table entries at all — falsified; crashes with release fully disabled. An isolation probe released ~323k widgets inline with no fault.
- Running JS during
UITrackingRunLoopMode — falsified; crashes with the link in default mode, where JS is frozen during the drag.
Suggested next step
Symbolicate a full crash report (the raw frames above have no load address), and audit every path that can hand a view to a UIStackView arranged-subview array. add_child / add_child_at guard their handle lookups, so the nil likely enters via a UIKit-held reference to a view that went away, or via constraint bookkeeping in HEIGHT_CONSTRAINTS.
Summary
Scrolling a
UIScrollView-hostedUIStackViewon a physical device while JS mutates its labels aborts the app:Requires human touch input. No automated probe reproduces it, which is why it has gone unseen — every headless run passes.
Where it throws
The backtrace is
start → main → UIApplicationMain → CFRunLoop → CA transaction commit → UIView layout → UIStackView → throw. The exception lands in the layout pass at the end of a run-loop iteration, not inside any Perry callback. Something earlier leaves a nil in a UIKit array; it detonates at commit time.Reproduction
Device: iPhone 16 Pro, iOS 26, Release build,
--target ios.Scroll the list continuously during the
property-updatesphase (50 labels,textSetStringon each every frame) oradd-remove(100 rows rebuilt 20x/sec). Crash lands within a few seconds. Without scrolling, the same build runs indefinitely.PERRY_NO_AUTO_OPTIMIZE=1is needed becauseperry-stdlibcurrently fails to build with--no-default-features— separate issue, see below.Not caused by the frame-metrics work (#7754)
Established by four single-variable device bisects, each with a human scrolling:
NSDefaultRunLoopModeThe last arm is decisive: with #7754's driver removed and
onFrameback on the pre-PRjs_frame_pump_default()path, the abort still occurs. That PR's benchmark is simply the first workload that drives enough label mutation against a scrolled stack to expose it.Hypotheses tested and falsified
UITrackingRunLoopMode— falsified; crashes with the link in default mode, where JS is frozen during the drag.Suggested next step
Symbolicate a full crash report (the raw frames above have no load address), and audit every path that can hand a view to a
UIStackViewarranged-subview array.add_child/add_child_atguard their handle lookups, so the nil likely enters via a UIKit-held reference to a view that went away, or via constraint bookkeeping inHEIGHT_CONSTRAINTS.