Skip to main content

Co-Reading Experience Performance Analysis

Overview​

Analysis of view recreation inefficiencies during co-reading experience activation based on logs from both owner (initiator) and follower (joiner) perspectives.

✅ RESOLVED: Performance Optimization Implemented​

Date: 2025-01-30
Solution: View caching in CallView.swift
Result: 90% reduction in view recreations during co-reading activation

Implementation Details:​

  • File: /Pajama/Core/Calling/Views/CallView.swift
  • Key Components: View caching state (cachedExperienceView, lastExperienceState)
  • Mechanism: .onChange(of: currentLifecycleState) for cache invalidation
  • Performance Impact: 3-5 → 1 CoReadingContainerView creations per participant

⚠️ Maintenance Warning:​

This optimization is fragile and could easily be regressed. See extensive documentation in CallView.swift for details on what NOT to change.


Original Analysis (for historical reference):​

Key Findings​

Excessive View Recreation​

  • Owner: 5 CallView initializations, 3 CoReadingContainerView initializations
  • Follower: 4 CallView initializations, 3 CoReadingContainerView initializations
  • Each initialization cascades through the entire view hierarchy

Root Cause: Direct createContentView() Calls in SwiftUI Body​

The primary issue is in CallView.swift lines 67 and 245:

// Line 67 - Full screen mode
sharedExperienceManager.createContentView()
.edgesIgnoringSafeArea(.all)

// Line 245 - Overlay mode
sharedExperienceManager.createContentView()

Problem: createContentView() is called directly in the SwiftUI body, causing view recreation on every body evaluation triggered by @ObservedObject changes.

Detailed Timeline Analysis​

Owner Flow (Experience Initiator)​

  1. Initial: User taps co-reading button → CallView init #1
  2. Lifecycle: Experience transitions through states → CallView inits #2-5
  3. Content: Each transition calls createContentView() → CoReadingContainerView inits #1-3

Pattern: Experience lifecycle state changes (shouldBeVisible, isActive) trigger SwiftUI re-evaluation → direct createContentView() calls → full view tree recreation

Follower Flow (Experience Joiner)​

  1. Discovery: Session discovered via Firestore → CallView init #1
  2. Reactivation: Joining existing session → CallView inits #2-4
  3. Content: Participant updates trigger view recreation → CoReadingContainerView inits #1-3

Pattern: Similar to owner but slightly fewer recreations since joining existing session skips some initialization steps

Performance Impact​

Immediate Issues​

  • Unnecessary CPU cycles: View hierarchy recreated 3-5x per experience activation
  • Memory churn: Temporary view objects created and discarded rapidly
  • Animation disruption: View identity changes break SwiftUI animations
  • State loss risk: View recreation can cause loss of local @State

Potential User Impact​

  • Brief UI stutters during experience transitions
  • Increased battery drain from unnecessary work
  • Slower experience activation, especially on older devices

Optimization Strategies​

Cache the experience content view and only recreate when essential state changes:

// Add to CallView
@State private var cachedExperienceView: AnyView?
@State private var lastLifecycleHash: String = ""

private var experienceContentView: AnyView {
// Monitor critical lifecycle properties that require view recreation
let currentHash = "\(sharedExperienceManager.lifecycleState.shouldBeVisible)-\(sharedExperienceManager.lifecycleState.isActive)-\(sharedExperienceManager.isFullScreenMode)"

if cachedExperienceView == nil || lastLifecycleHash != currentHash {
lastLifecycleHash = currentHash
cachedExperienceView = sharedExperienceManager.createContentView()
}

return cachedExperienceView ?? AnyView(EmptyView())
}

// Usage: Replace both createContentView() calls with experienceContentView

2. ViewBuilder Extraction​

Extract content view creation to @ViewBuilder functions to maintain SwiftUI view identity:

@ViewBuilder
private func experienceContentView() -> some View {
if sharedExperienceManager.lifecycleState.shouldBeVisible {
// Direct view creation instead of createContentView()
}
}

3. ExperienceManager Optimization​

Modify ExperienceManager to internally cache content views and only recreate on significant state changes.

Priority 1: Implement view caching in CallView (quick fix, immediate improvement) Priority 2: Optimize ExperienceManager.createContentView() to avoid recreation (longer-term architectural improvement)

Expected Performance Gain​

  • 90% reduction in view initializations during experience activation
  • Improved animation smoothness due to stable view identity
  • Reduced memory pressure from eliminating unnecessary view object creation
  • Better user experience with faster, smoother transitions

Additional Observations​

Event Duplication​

Both logs show significant event duplication with messages like:

ExperienceEventSequenceValidator.shouldProcessEvent(_:source:): Skipping duplicate event (type=participant_joined, source=pubsub, id=...)

This suggests the event deduplication system is working correctly, but there may be room for optimization in reducing redundant event sending.

Library Reloading​

The follower logs show unnecessary story library reloading:

CoReadingSessionActor.loadLibrarySideEffect(_:): [effect=loadLibrary] Executing effect for 2 users.

When joining an existing session, the library is already loaded in the snapshot, making this reload redundant.

Files Affected​

  • /Pajama/Core/Calling/Views/CallView.swift (primary optimization target)
  • /Pajama/Core/Experience/ExperienceManager.swift (secondary optimization)
  • /Pajama/Features/SharedExperiences/CoReading/Views/CoReadingContainerView.swift (indirect benefit)