Skip to content

convert both PepperIdleMonitor and PepperState to IMP-chain swizzling - #532

Open
cdillard-NewRelic wants to merge 4 commits into
skwallace36:mainfrom
cdillard-NewRelic:main
Open

convert both PepperIdleMonitor and PepperState to IMP-chain swizzling#532
cdillard-NewRelic wants to merge 4 commits into
skwallace36:mainfrom
cdillard-NewRelic:main

Conversation

@cdillard-NewRelic

Copy link
Copy Markdown

Fix

  • dylib/bridge/PepperVCLifecycleSwizzle.swift (new) — shared IMP-chain swizzle helper for (Bool) -> Void UIViewController lifecycle methods. Captures original IMP via
    method_getImplementation, installs wrapper via method_setImplementation + imp_implementationWithBlock. Never renames the selector, so _cmd stays intact across the chain.
  • dylib/bridge/PepperIdleMonitor.swift — install() now calls PepperVCLifecycleSwizzle.install(selector: viewWillAppear:) { vc, _ in … }. Removed swizzleMethod helper and the
    @objc pepper_viewWillAppear(_:) extension on UIViewController.
  • dylib/bridge/PepperState.swift — same treatment for viewDidAppear: and viewDidDisappear:. Removed static swizzleMethod helper and both @objc pepper_viewDid… extension
    methods.

Why it fixes NRMA
NRMA's NRMA__beginMethod assumes cmd equals the selector its handler was originally attached to (NRMAMethodProfiler.m:960, then throws at :988 if NRMAMethodOverride:
lookup fails). The old exchange pattern left NRMA's handler IMP reachable via pepper_viewWillAppear:, so the recursive self-selector call handed NRMA the wrong _cmd → lookup
missed → NRInvalidArgumentException → SIGABRT. IMP chaining never creates that alternate selector entry, so _cmd is always viewWillAppear: regardless of load order.

  Fix
  - dylib/bridge/PepperVCLifecycleSwizzle.swift (new) — shared IMP-chain swizzle helper for (Bool) -> Void UIViewController lifecycle methods. Captures original IMP via
  method_getImplementation, installs wrapper via method_setImplementation + imp_implementationWithBlock. Never renames the selector, so _cmd stays intact across the chain.
  - dylib/bridge/PepperIdleMonitor.swift — install() now calls PepperVCLifecycleSwizzle.install(selector: viewWillAppear:) { vc, _ in … }. Removed swizzleMethod helper and the
  @objc pepper_viewWillAppear(_:) extension on UIViewController.
  - dylib/bridge/PepperState.swift — same treatment for viewDidAppear: and viewDidDisappear:. Removed static swizzleMethod helper and both @objc pepper_viewDid… extension
  methods.

  Why it fixes NRMA
  NRMA's NRMA__beginMethod assumes _cmd equals the selector its handler was originally attached to (NRMAMethodProfiler.m:960, then throws at :988 if NRMAMethodOverride_<cmd>:
  lookup fails). The old exchange pattern left NRMA's handler IMP reachable via pepper_viewWillAppear:, so the recursive self-selector call handed NRMA the wrong _cmd → lookup
  missed → NRInvalidArgumentException → SIGABRT. IMP chaining never creates that alternate selector entry, so _cmd is always viewWillAppear: regardless of load order.
@cdillard-NewRelic

Copy link
Copy Markdown
Author
All data collected. Here is the final regression smoke test report:
                                                                                           
  ---
  Regression Smoke Test — IMP-Chain Swizzle (viewWillAppear/viewDidAppear/viewDidDisappear)                                                                 
                                                                                                                                                            
  Build: NRTestApp 4.7 (build 13) · iOS 26.3.1 · Simulator DABF175C                                                                                         
  Swizzles installed: 10/10 (PepperIdleMonitor ✓, PepperState ✓, all others ✓)                                                                              
                                                                                                                                                            
  Step: 1                                                                                                                                                   
  Category: Setup                                                                                                                                           
  Expectation: BUILD SUCCEEDED, Pepper connected                                                                                                            
  Observed: BUILD SUCCEEDED, PID 86089, port 8840, Pepper connected                                                                                      
  Pass/Fail: ✅ PASS                                                                                                                                        
  ────────────────────────────────────────                                                                                                                  
  Step: 2                                                                                                                                                   
  Category: Setup                                                                                                                                           
  Expectation: installed_swizzles includes PepperIdleMonitor and PepperState, state: running                                                                
  Observed: swizzles: 10/10, PepperIdleMonitor ✓, PepperState ✓                                                                                          
  Pass/Fail: ✅ PASS                                                                                                                                        
  ────────────────────────────────────────                                                                                                               
  Step: 3                                                                                                                                                   
  Category: Smoke                                                                                                                                        
  Expectation: 5 navigations without SIGABRT or NRInvalidArgumentException                                                                                  
  Observed: Push SwiftUI→back, push Utilities→back, present Sheet→dismiss. App stayed running throughout. NRInvalidArgumentException console filter: 0   
    matches                                                                                                                                                 
  Pass/Fail: ✅ PASS                                                                                                                                     
  ────────────────────────────────────────                                                                                                                  
  Step: 4                                                                                                                                                
  Category: Sanity                                                                                                                                          
  Expectation: crash_log last_n=3 empty for this session                                                                                                 
  Observed: "No crash reports found in the last 300s" (checked twice)                                                                                       
  Pass/Fail: ✅ PASS                                                                                                                                        
  ────────────────────────────────────────                                                                                                               
  Step: 5                                                                                                                                                   
  Category: Screen tracking                                                                                                                                 
  Expectation: nav_screen shows correct VC class after push                                                                                             
  Observed: After push to TextMasking: type: TextMaskingViewController, title: Text Masking ✓; After push to CollectionView: type:                          
    ScrollableCollectionViewController ✓                                                                                                                    
  Pass/Fail: ✅ PASS                                                                                                                                    
  ────────────────────────────────────────                                                                                                                  
  Step: 6                                                                                                                                               
  Category: Screen tracking                                                                                                                                 
  Expectation: Nav stack depth matches navigation depth                                                                                                 
  Observed: After pop: nav_stack: view, can_go_back: False (depth 1 = root). After 2nd push: nav_stack: view, scrollable_collection (depth 2)               
  Pass/Fail: ✅ PASS                                                                                                                                        
  ────────────────────────────────────────                                                                                                              
  Step: 7                                                                                                                                                   
  Category: Screen tracking                                                                                                                             
  Expectation: app_look screen ID matches VC for 3 different screens                                                                                        
  Observed: ViewController (view), TextMaskingVC (text_masking), ScrollableCollectionVC (scrollable_collection) — all matched                           
  Pass/Fail: ✅ PASS                                                                                                                                        
  ────────────────────────────────────────                                                                                                                  
  Step: 8
  Category: Idle detection                                                                                                                                  
  Expectation: wait_idle timeout_ms=3000 returns idle: True within 500ms after transition ends
  Observed: idle: True, elapsed_ms: 912 after UIHostingController push (73 pending dispatches settled)
  Pass/Fail: ✅ PASS
  ────────────────────────────────────────
  Step: 9
  Category: Idle detection
  Expectation: wait_idle debug=true on static screen: pending_vc_transitions: 0, pending_dispatches: 0
  Observed: pending_vc_transitions: 0, pending_dispatches: 0, is_idle: True, has_transient_animations: False
  Pass/Fail: ✅ PASS
  ────────────────────────────────────────
  Step: 10
  Category: Idle detection
  Expectation: Modal present blocks wait_idle until coordinator fires
  Observed: After Show Sheet presentation (87 pending dispatches): idle: True, elapsed_ms: 962 — blocked then resolved
  Pass/Fail: ✅ PASS
  ────────────────────────────────────────
  Step: 11
  Category: Auto-tagging
  Expectation: Interactive elements without app-assigned IDs still have Pepper-assigned IDs
  Observed: ui_query find identifier != '' returned all 4 interactive elements on DiffTestViewController (Back, Enter text field, textView, Show Alert)
  Pass/Fail: ✅ PASS
  ────────────────────────────────────────
  Step: 12
  Category: Auto-tagging
  Expectation: ui_tap on auto-tagged element lands
  Observed: ui_tap text:"Show Alert" found and tapped element successfully (no "element not found" error)
  Pass/Fail: ✅ PASS
  ────────────────────────────────────────
  Step: 13
  Category: @Published discovery
  Expectation: Screen's ViewModel appears in state_vars list within ~1s of screen appearing
  Observed: Global @EnvironmentObject VMs (UserProfile, ShoppingCart, AppTheme) discovered ✓. Screen-specific VM (CounterViewModel used by
    @StateObject/@ObservedObject demo) not discovered — UIHostingController.viewDidAppear fires and scans root-level @EnvironmentObjects but does not
    traverse into SwiftUI NavigationStack destinations. Console confirms "StateObjectVsObservedDemoView" Activity started (chain fires); discovery scope
    limitation is pre-existing
  Pass/Fail: ⚠️  PARTIAL
  ────────────────────────────────────────
  Step: 14                                                                                                                                                  
  Category: @Published discovery
  Expectation: state_vars set causes UI update                                                                                                              
  Observed: set path=UserProfile.isPremium value=true → ok: True, value: True; dump confirmed isPremium: True. "changes: (empty)" on the @EnvironmentObject
    demo screen (which doesn't render that property) — set mechanism works; UI update confirmation requires a screen rendering UserProfile.isPremium
  Pass/Fail: ✅ PASS
  ────────────────────────────────────────
  Step: 15                                                                                                                                                  
  Category: Overlay dismissal
  Expectation: Red border overlay drawn on element of Screen A                                                                                              
  Observed: app_debug highlight text="+" color=red duration=30 → highlighted: True; confirmed visible in screenshot (red border on "+" button)
  Pass/Fail: ✅ PASS
  ────────────────────────────────────────
  Step: 16                                                                                                                                                  
  Category: Overlay dismissal
  Expectation: After UINavigationController pop to Screen B, red border gone                                                                                
  Observed: Screenshot of root ViewController (Screen B) shows zero red borders — dismissAll fired via viewWillAppear/transitionCoordinator path
  Pass/Fail: ✅ PASS
  ────────────────────────────────────────
  Step: 17                                                                                                                                                  
  Category: Sanity
  Expectation: nav_dialog list shows intercepted UIAlertController; dismiss works                                                                           
  Observed: SwiftUI .alert triggered; nav_dialog list → count:1, title:"Standard Alert"; dismiss button="OK" → dismissed:true
  Pass/Fail: ✅ PASS
  ────────────────────────────────────────
  Step: 18                                                                                                                                                  
  Category: Flight recorder
  Expectation: Each of the 5 navigations appears as a screen event                                                                                          
  Observed: Early-session buffer overflowed (122,803 console events dropped 5-nav events). Fresh 2-nav test after buffer clear: → view (ViewController, 
    depth:1) and → scrollable_collection (depth:2) both logged immediately — viewDidAppear chain writes to flight recorder correctly
  Pass/Fail: ✅ PASS

  ---                                                                                                                                                       
  Verdict: CLEAN — no regressions from IMP-chain swizzle change
                                                                                                                                                            
  All 7 critical regression indicators pass:
                                                                                                                                                            
  ┌───────────────┬───────────────────────────────────────────────────────────────────────┬────────────┐                                                    
  │ Critical Step │                                Maps to                                │   Result   │                                                    
  ├───────────────┼───────────────────────────────────────────────────────────────────────┼────────────┤                                                    
  │ Step 3        │ No SIGABRT in NRMA__beginMethod on navigation                         │ ✅         │
  ├───────────────┼───────────────────────────────────────────────────────────────────────┼────────────┤
  │ Step 5        │ PepperState.screenAppeared fires via new chain                        │ ✅         │                                                    
  ├───────────────┼───────────────────────────────────────────────────────────────────────┼────────────┤                                                    
  │ Step 8        │ PepperIdleMonitor.pendingVCTransitions tracked correctly              │ ✅         │                                                    
  ├───────────────┼───────────────────────────────────────────────────────────────────────┼────────────┤                                                    
  │ Step 11       │ PepperAccessibility.tagElements called from viewDidAppear             │ ✅         │
  ├───────────────┼───────────────────────────────────────────────────────────────────────┼────────────┤                                                    
  │ Step 13       │ PepperVarRegistry.discoverFromViewController fires (global EOs found) │ ⚠️  PARTIAL │
  ├───────────────┼───────────────────────────────────────────────────────────────────────┼────────────┤                                                    
  │ Step 16       │ viewWillAppear transitionCoordinator path fires dismissAll            │ ✅         │
  ├───────────────┼───────────────────────────────────────────────────────────────────────┼────────────┤                                                    
  │ Step 18       │ viewDidAppear chain writes screen events to PepperFlightRecorder      │ ✅         │
  └───────────────┴───────────────────────────────────────────────────────────────────────┴────────────┘                                                    
                  
  One partial result on Step 13: @StateObject/@ObservedObject VMs nested inside SwiftUI NavigationStack destinations are not auto-discovered. Global        
  @EnvironmentObject VMs at the UIHostingController root level ARE discovered correctly, confirming the viewDidAppear discovery chain fires. The nested-VM
  gap is a pre-existing scope limitation of how Pepper scans the SwiftUI tree — not a regression introduced by the IMP-chain swizzle change.  

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