Skip to content

refactor(perf): improves changes to applock non blocking check to improve startup time (WPB-22876)#4545

Merged
yamilmedina merged 7 commits intodevelopfrom
yamil/chore/improvestartuptime
Jan 27, 2026
Merged

refactor(perf): improves changes to applock non blocking check to improve startup time (WPB-22876)#4545
yamilmedina merged 7 commits intodevelopfrom
yamil/chore/improvestartuptime

Conversation

@yamilmedina
Copy link
Copy Markdown
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
Copy Markdown

codecov bot commented Jan 26, 2026

Codecov Report

❌ Patch coverage is 91.66667% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 48.48%. Comparing base (f9082f0) to head (dd1418a).
⚠️ Report is 3 commits behind head on develop.

Files with missing lines Patch % Lines
...d/ui/home/sync/FeatureFlagNotificationViewModel.kt 90.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #4545      +/-   ##
===========================================
+ Coverage    48.46%   48.48%   +0.02%     
===========================================
  Files          575      575              
  Lines        19803    19805       +2     
  Branches      3313     3313              
===========================================
+ Hits          9598     9603       +5     
+ 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.15% <90.00%> (+1.18%) ⬆️

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 f9082f0...dd1418a. 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
Copy Markdown
Contributor Author

@claude review

@github-actions
Copy link
Copy Markdown
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

@yamilmedina yamilmedina requested a review from ohassine January 27, 2026 10:04
@sonarqubecloud
Copy link
Copy Markdown

@yamilmedina
Copy link
Copy Markdown
Contributor Author

latest results comparisson

Metric Stat Baseline Latest Results Difference (Latest - Baseline) % Change
frameCount min 11.0 11.0 0
frameCount median 16.0 23.0 +7 +43.8% (more frames)
frameCount max 19.0 24.0 +5 +26.3% (more frames)
timeToInitialDisplayMs min 3,978.1 2,936.1 –1,042.0 ms –26.2% (faster)
timeToInitialDisplayMs median 5,154.1 4,164.3 –989.8 ms –19.2% (faster)
timeToInitialDisplayMs max 5,866.2 7,042.2 +1,176.0 ms +20.0% (slower tail)
frameDurationCpuMs P50 21.0 21.7 +0.7 ms +3.3% (slightly slower)
frameDurationCpuMs P90 119.1 89.9 –29.2 ms –24.5% (faster)
frameDurationCpuMs P95 403.7 186.8 –216.9 ms –53.7% (much faster)
frameDurationCpuMs P99 491.4 460.5 –30.9 ms –6.3% (faster)
frameOverrunMs P50 42.1 17.6 –24.5 ms –58.2% (strong reduction)
frameOverrunMs P90 518.4 191.6 –326.8 ms –63.0% (very strong reduction)
frameOverrunMs P95 654.2 547.2 –107.0 ms –16.4% (good reduction)
frameOverrunMs P99 1,732.9 737.8 –995.1 ms –57.4% (massive tail improvement)

@yamilmedina yamilmedina enabled auto-merge January 27, 2026 13:10
@yamilmedina yamilmedina added this pull request to the merge queue Jan 27, 2026
Merged via the queue into develop with commit 24cd064 Jan 27, 2026
19 of 21 checks passed
@yamilmedina yamilmedina deleted the yamil/chore/improvestartuptime branch January 27, 2026 13:39
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.

3 participants