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)