Skip to content

Conversation

@yamilmedina
Copy link
Contributor

@yamilmedina yamilmedina commented Jan 26, 2026

https://wearezeta.atlassian.net/browse/WPB-22876


PR Submission Checklist for internal contributors

  • The PR Title

    • conforms to the style of semantic commits messages¹ supported in Wire's Github Workflow²
    • contains a reference JIRA issue number like SQPIT-764
    • answers the question: If merged, this PR will: ... ³
  • The PR Description

    • is free of optional paragraphs and you have filled the relevant parts to the best of your ability

What's new in this PR?

Change/remove the usage of isUserAppLockSet() checks from blocking to using the flow alternative and use as a state property.

Solutions

Use the flow version isAppLockPasscodeSetFlow() and collect/update the state property.

Needs releases with:

  • GitHub link to other pull request

Testing

Test Coverage (Optional)

  • I have added automated test to this contribution

Attachments (Optional)

With app lock changes to flow:

StartupBenchmarkWithLogin_startUpWithoutBaselineProfiler
frameCount               min    15.0,   median    23.0,   max    27.0
timeToInitialDisplayMs   min 3,500.3,   median 3,835.8,   max 4,134.4
frameDurationCpuMs       P50     14.2,   P90     82.1,   P95    142.1,   P99    301.7
frameOverrunMs           P50      1.9,   P90    141.5,   P95    352.0,   P99  1,937.5

Without (current dev):

StartupBenchmarkWithLogin_startUpWithoutBaselineProfiler
frameCount               min    11.0,   median    16.0,   max    19.0
timeToInitialDisplayMs   min 3,978.1,   median 5,154.1,   max 5,866.2
frameDurationCpuMs       P50     21.0,   P90    119.1,   P95    403.7,   P99    491.4
frameOverrunMs           P50     42.1,   P90    518.4,   P95    654.2,   P99  1,732.9 

Summary (by AI on the results)

Performance Improvements
Startup Speed (TTID)
├─ Median: 25.6% faster (1,318 ms saved!)
├─ Min:    12.0% faster (478 ms saved)
└─ Max:    29.5% faster (1,732 ms saved)
Frame Rendering Quality (CPU time per frame)
├─ P50: 32.4% faster (6.8 ms saved per frame)
├─ P90: 31.1% faster (37.0 ms saved)
├─ P95: 64.8% faster (262 ms saved!)
└─ P99: 38.6% faster (190 ms saved)
Frame Overrun (Dropped Frames / Overrun ms)
├─ P50: 95.5% reduction (40.2 ms saved – almost eliminated!)
├─ P90: 72.7% reduction (377 ms saved)
├─ P95: 46.2% reduction (302 ms saved)
└─ P99: 11.8% worse (+205 ms regression – tail became heavier)

PR Post Submission Checklist for internal contributors (Optional)

  • Wire's Github Workflow has automatically linked the PR to a JIRA issue

PR Post Merge Checklist for internal contributors

  • If any soft of configuration variable was introduced by this PR, it has been added to the relevant documents and the CI jobs have been updated.

References
  1. https://sparkbox.com/foundry/semantic_commit_messages
  2. https://github.com/wireapp/.github#usage
  3. E.g. feat(conversation-list): Sort conversations by most emojis in the title #SQPIT-764.

@yamilmedina yamilmedina changed the title refactor(perf): improves changes to applock non blocking to improve startup time (WPB-22876) refactor(perf): improves changes to applock non blocking check to improve startup time (WPB-22876) Jan 26, 2026
@codecov
Copy link

codecov bot commented Jan 26, 2026

Codecov Report

❌ Patch coverage is 90.90909% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 48.49%. Comparing base (7131494) to head (de74b56).

Files with missing lines Patch % Lines
...d/ui/home/sync/FeatureFlagNotificationViewModel.kt 88.88% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #4545      +/-   ##
===========================================
+ Coverage    48.46%   48.49%   +0.02%     
===========================================
  Files          575      575              
  Lines        19803    19807       +4     
  Branches      3313     3313              
===========================================
+ Hits          9598     9605       +7     
+ Misses        9190     9187       -3     
  Partials      1015     1015              
Files with missing lines Coverage Δ
...tlin/com/wire/android/datastore/GlobalDataStore.kt 19.29% <ø> (+0.96%) ⬆️
...rc/main/kotlin/com/wire/android/ui/WireActivity.kt 0.00% <ø> (ø)
...otlin/com/wire/android/ui/home/FeatureFlagState.kt 81.57% <100.00%> (+0.49%) ⬆️
...d/ui/home/sync/FeatureFlagNotificationViewModel.kt 59.72% <88.88%> (+1.75%) ⬆️

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 7131494...de74b56. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@yamilmedina
Copy link
Contributor Author

@claude review

@github-actions
Copy link
Contributor

github-actions bot commented Jan 26, 2026

PR Review: Refactor App Lock Check to Non-Blocking Flow

Summary

This PR successfully removes the blocking runBlocking call from isAppLockPasscodeSet() and refactors it to use a reactive Flow-based approach. The change shows significant performance improvements in startup benchmarks (~26% faster median TTID).


✅ Positive Aspects

Performance Improvements

  • Excellent benchmark data: The PR includes comprehensive before/after metrics showing substantial improvements:
    • Median TTID: 26.4% faster (1.36s saved)
    • Frame rendering quality: up to 63.7% smoother at P95
    • Significantly reduced frame overruns
  • Eliminates blocking I/O on main thread: Removes runBlocking which was likely causing the startup delays

Code Quality

  • Proper reactive pattern: Uses Flow collection with state updates instead of blocking calls
  • Good test coverage: Tests were updated to reflect the new Flow-based API
  • Minimal scope: Focused change that only touches what's necessary

⚠️ Issues & Concerns

1. CRITICAL: Potential Memory Leak in ViewModel

Location: FeatureFlagNotificationViewModel.kt:279-281

private suspend fun isAppLockSet() = globalDataStore.get().isAppLockPasscodeSetFlow().collect { isSet ->
    featureFlagState = featureFlagState.copy(isUserAppLockSet = isSet)
}

Problem: The collect call is a suspending infinite loop that never completes. This function is launched in init block and will continue collecting forever, even if the ViewModel is cleared.

Impact:

  • Memory leak: The coroutine continues running after ViewModel destruction
  • Resource waste: Unnecessary Flow collection after component is disposed

Recommended Fix:

private fun observeAppLockSet() {
    viewModelScope.launch {
        globalDataStore.get().isAppLockPasscodeSetFlow().collect { isSet ->
            featureFlagState = featureFlagState.copy(isUserAppLockSet = isSet)
        }
    }
}

init {
    viewModelScope.launch { initialSync() }
    observeAppLockSet()  // Launch as separate coroutine
}

Or better yet, integrate it into observeStatesAfterInitialSync:

private suspend fun observeStatesAfterInitialSync(userId: UserId) {
    coroutineScope {
        launch { setFileSharingState(userId) }
        launch { observeTeamSettingsSelfDeletionStatus(userId) }
        launch { setGuestRoomLinkFeatureFlag(userId) }
        launch { setE2EIRequiredState(userId) }
        launch { setTeamAppLockFeatureFlag(userId) }
        launch { observeCallEndedBecauseOfConversationDegraded(userId) }
        launch { observeShouldNotifyForRevokedCertificate(userId) }
        launch { observeAppLockSet() }  // Add here
    }
}

2. Race Condition Risk

Location: FeatureFlagNotificationViewModel.kt:161 and WireActivity.kt:461

The app lock status is now checked from state (featureFlagState.isUserAppLockSet) but the Flow collection is launched independently in init. There's a timing issue:

  1. init block launches initialSync()
  2. init block calls isAppLockSet() which starts collecting
  3. setTeamAppLockFeatureFlag() checks featureFlagState.isUserAppLockSet at line 161

Problem: The state might not be initialized yet when setTeamAppLockFeatureFlag() first executes, leading to incorrect shouldBlockApp calculation.

Impact: User might see the app lock dialog unnecessarily on first app launch.

Recommended Fix: Ensure app lock state is initialized before calling observeStatesAfterInitialSync, or add initial value:

init {
    viewModelScope.launch {
        // Get initial value first
        val isSet = globalDataStore.get().isAppLockPasscodeSetFlow().first()
        featureFlagState = featureFlagState.copy(isUserAppLockSet = isSet)
        
        // Then start observing
        initialSync()
        launch {
            globalDataStore.get().isAppLockPasscodeSetFlow().collect { isSet ->
                featureFlagState = featureFlagState.copy(isUserAppLockSet = isSet)
            }
        }
    }
}

3. Leftover runBlocking Import

Location: GlobalDataStore.kt:38

import kotlinx.coroutines.runBlocking

This import is now unused after removing the isAppLockPasscodeSet() function and should be removed for cleanliness.

4. Missing Function Removal in GlobalDataStore

Location: GlobalDataStore.kt:164-168

The PR diff shows that isAppLockPasscodeSet() blocking function was removed, but I notice it might still exist in the current file at lines 164-168. Please verify this function is completely removed:

fun isAppLockPasscodeSet(): Boolean = runBlocking {
    context.dataStore.data.map {
        it.contains(APP_LOCK_PASSCODE)
    }.first()
}

This function should not exist in the final version.

5. Test Coverage Gap

Location: FeatureFlagNotificationViewModelTest.kt

The test was updated to mock isAppLockPasscodeSetFlow() as a Flow, but there's no test verifying:

  1. The state is properly initialized at startup
  2. The state updates when the Flow emits new values
  3. The race condition scenario (team app lock check before state initialization)

Recommended: Add a test like:

@Test
fun givenAppLockFlow_whenValueChanges_thenStateIsUpdated() = runTest {
    val appLockFlow = MutableStateFlow(false)
    val (_, viewModel) = Arrangement()
        .withCurrentSessionsFlow(flowOf(CurrentSessionResult.Success(AccountInfo.Valid(TestUser.USER_ID))))
        .withIsAppLockSetup(appLockFlow)
        .arrange()
    
    advanceUntilIdle()
    assertEquals(false, viewModel.featureFlagState.isUserAppLockSet)
    
    appLockFlow.emit(true)
    advanceUntilIdle()
    assertEquals(true, viewModel.featureFlagState.isUserAppLockSet)
}

🔍 Minor Observations

Import Ordering

Location: WireActivity.kt:93-97

The import order was changed (moved LogManagementScreenDestination up). While functionally fine, this seems unintentional. Consider keeping imports alphabetically sorted or IDE-organized.

Documentation

The PR description and benchmark data are excellent. Consider adding a comment in the code explaining why the Flow-based approach was chosen (performance).


🎯 Recommendations

Must Fix (Blocking)

  1. ⚠️ Fix the infinite collect call in isAppLockSet() to prevent memory leak
  2. ⚠️ Ensure app lock state initialization happens before it's first accessed
  3. 🧹 Remove unused runBlocking import

Should Fix

  1. Add test coverage for state initialization and updates
  2. Verify isAppLockPasscodeSet() blocking function is fully removed

Nice to Have

  1. Fix import ordering in WireActivity.kt
  2. Add code comment explaining the performance motivation

📊 Overall Assessment

Code Quality: Good ⭐⭐⭐⭐☆
Test Coverage: Adequate ⭐⭐⭐☆☆
Performance Impact: Excellent ⭐⭐⭐⭐⭐
Security: No concerns ✅

The performance improvements are impressive and well-documented. The refactoring follows Android/Kotlin best practices by eliminating blocking calls. However, the implementation has a critical memory leak issue that must be addressed before merge.


Review completed by Claude Code

Comment on lines 68 to 69
initialSync()
isAppLockSet()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initialSync() collects indefinitely, which prevents isAppLockSet() from ever being called.
We can launch them in separate coroutines

viewModelScope.launch {
    initialSync()
}

viewModelScope.launch {
    isAppLockSet()
}

Copy link
Contributor Author

@yamilmedina yamilmedina Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch! addressed, we also had a race condition there as per Claude comment too, so I'm now launching this separately.

@sonarqubecloud
Copy link


init {
viewModelScope.launch { initialSync() }
viewModelScope.launch {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this cause the home screen to flash for a split second untill this check is done?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants