perf: reduce webView startup time - #21220
neronguyen wants to merge 1 commit into
Conversation
|
Thanks! CI/tests are failing As a general note: aim to scope this change just to the WebView, not a full performance improvement for the general startup process. This PR looks like it potentially removes |
| private suspend fun setupWebView(): Boolean = | ||
| setupSuspend("setupWebView") { | ||
| suspendCancellableCoroutine { continuation -> | ||
| val executor = Executors.newSingleThreadExecutor() |
There was a problem hiding this comment.
Any source for using this pattern, or could you comment on why it's used here?
There was a problem hiding this comment.
suspendCancellableCoroutine to convert callback pattern to coroutines so I can wait for the startUp to complete before other setups
setupSuspend just follow your setup but can apply to suspend functions. Because the setup finishes with callback, so have to convert it to suspend to measure its exec time. Or should implement time measure inside the back, no need to implement a dedicate setupSuspend ?
but as your above review, I should scope down the changes to make it not affect the general startup process
There was a problem hiding this comment.
I see the coMeasureTime function to measure suspend function. I also flat the code with function startUpWebView inside function setupWebView for readable.
8251897 to
c3ad8a9
Compare
c3ad8a9 to
13927bf
Compare
|
hi @david-allison , I just resolved conflicts and updated it with cleaner approach - don't block the main setup flow anymore, could you check it please |
13927bf to
9ffaed2
Compare
|
Ok, I'm back, sorry this sat.
I did a verification run and there's no significant change: We block synchronously on the WebView via I've now done some WebView startup work. The shape of this PR should:
Would you be open to a more in-depth refactoring:
|
|
Yes I'm willing to make a small refactoring here. I come with some ideas to summary before getting into the implementation:
|
9ffaed2 to
1f4590e
Compare
There was a problem hiding this comment.
Thanks so much for the follow-through!
The suspendCancellableCoroutine pattern doesn't sit right with me, given the API added to androidx.webkit was designed around callbacks.
As-of now, we're still blocking on the completion of a WebView, so we're pulling in a negligible performance benefit here
This was meant to be a good first issue (scoping whether the API was useable for us), and I feel we're a little too far into the weeds. Let's re-scope this to a more targeted change: getting setWebContentsDebuggingEnabled executing asynchronously, and deferring the 'hard' startup work for a later PR
Apply this patch: https://github.com/ankidroid/Anki-Android/wiki/Development-guide#applying-a-patch
Make any changes you feel would improve it, then let's get it in
Note
Assisted-by: Claude Fable 5.1
Subject: [PATCH] improvement(startup): WebView startup helper
perf(startup): WebView startup via plain callback
---
Index: AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt
--- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt (revision 1f4590eb619a39c86366645323aa647e84615638)
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt (revision af4c56116bcc785b03b10d42f63ba39f556a48cc)
@@ -22,11 +22,7 @@
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ProcessLifecycleOwner
import anki.collection.OpChanges
-import com.ichi2.anki.AnkiDroidApp.Companion.instance
-import com.ichi2.anki.AnkiDroidApp.Companion.isInitialized
-import com.ichi2.anki.AnkiDroidApp.Companion.makeBackendUsable
import com.ichi2.anki.AnkiDroidApp.Companion.sharedPreferencesTestingOverride
-import com.ichi2.anki.AnkiDroidApp.Companion.sharedPrefs
import com.ichi2.anki.analytics.initializeAnalytics
import com.ichi2.anki.browser.SharedPreferencesLastDeckIdRepository
import com.ichi2.anki.common.android.AdaptionUtil
@@ -58,7 +54,6 @@
import com.ichi2.anki.preferences.SharedPreferencesProvider
import com.ichi2.anki.reviewreminders.ReminderLogTree
import com.ichi2.anki.servicelayer.DebugInfoService
-import com.ichi2.anki.servicelayer.StartupWebViewService
import com.ichi2.anki.servicelayer.ThrowableFilterService
import com.ichi2.anki.services.NotificationService
import com.ichi2.anki.settings.Prefs
@@ -72,6 +67,7 @@
import com.ichi2.utils.LanguageUtil
import com.ichi2.utils.measureTime
import com.ichi2.utils.setWebContentsDebuggingEnabled
+import com.ichi2.utils.startUpWebView
import com.ichi2.widget.DayRolloverAlarm
import com.ichi2.widget.WidgetNotificationScheduler
import com.ichi2.widget.cardanalysis.CardAnalysisWidget
@@ -177,7 +173,6 @@
showThemedToast(this.applicationContext, getString(R.string.user_is_a_robot), false)
}
- setupWebView()
setupContextMenus()
setup("makeBackendUsable") { makeBackendUsable(this) }
@@ -189,6 +184,8 @@
if (!checkWebViewAvailable()) {
return
}
+ // after the probe: startUpWebView throws on its executor if the WebView provider is missing (5794)
+ setupWebView()
// Forget the last deck that was used in the CardBrowser
CardBrowser.clearLastDeckId()
@@ -452,24 +449,19 @@
}
/**
- * Start up WebView to speed up its later usages.
- * Set up WebView functionalities.
+ * Starts asynchronously loading the WebView on a background thread.
+ *
+ * TODO: This only handles a subset of WebView init, and will not produce a performance
+ * improvement until this pattern is used for all WebView init.
*/
- private fun setupWebView() {
+ private fun setupWebView() =
setup("setupWebView") {
- applicationScope.launch {
- val result = StartupWebViewService.startUpWebView(this@AnkiDroidApp)
- if (result.isSuccess) {
- setWebContentsDebuggingEnabled(Prefs.isWebDebugEnabled)
- } else {
- val e = result.exceptionOrNull() ?: return@launch
- fatalInitializationError = FatalInitializationError.WebViewError(e)
- sendExceptionReport(e, "setupWebView")
- Timber.e(e, "setupWebView")
- }
- }
+ startUpWebView(
+ context = this,
+ onSuccess = { setWebContentsDebuggingEnabled(Prefs.isWebDebugEnabled) },
+ onFailure = { e -> Timber.w(e, "startUpWebView") },
+ )
}
- }
/**
* @return the app version, OS version and device model, provided when syncing.
Index: AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt b/AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt
--- a/AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt (revision 1f4590eb619a39c86366645323aa647e84615638)
+++ b/AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt (revision af4c56116bcc785b03b10d42f63ba39f556a48cc)
@@ -27,13 +27,17 @@
import androidx.appcompat.app.AlertDialog
import androidx.core.content.pm.PackageInfoCompat
import androidx.webkit.WebViewCompat
+import androidx.webkit.WebViewOutcomeReceiver
+import androidx.webkit.WebViewStartUpConfig
+import androidx.webkit.WebViewStartUpResult
+import androidx.webkit.WebViewStartupException
import com.ichi2.anki.R
import com.ichi2.anki.common.crashreporting.CrashReportService
-import com.ichi2.anki.servicelayer.StartupWebViewService
import com.ichi2.anki.utils.openUrl
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
+import java.util.concurrent.Executors
@JvmInline
value class WebViewVersion(
@@ -172,18 +176,15 @@
*
* @return A [WebViewInfo] object with WebView package details.
*/
-suspend fun getWebViewInfo(context: Context): WebViewInfo {
- StartupWebViewService.startUpWebView(context)
- return withContext(Dispatchers.Main) {
- val packageInfo =
- runCatching { WebViewCompat.getCurrentWebViewPackage(context) }.getOrNull()
+suspend fun getWebViewInfo(context: Context): WebViewInfo =
+ withContext(Dispatchers.Main) {
+ val packageInfo = runCatching { WebViewCompat.getCurrentWebViewPackage(context) }.getOrNull()
WebViewInfo(
userAgent = getWebviewUserAgent(context),
packageName = packageInfo?.packageName,
versionCode = runCatching { packageInfo?.let { PackageInfoCompat.getLongVersionCode(it) } }.getOrNull(),
)
}
-}
private fun showOutdatedWebViewDialog(
context: Context,
@@ -235,3 +236,34 @@
// android.util.AndroidRuntimeException: android.webkit.WebViewFactory$MissingWebViewPackageException: Failed to load WebView provider: No WebView installed
Timber.w(e, "setWebContentsDebuggingEnabled")
}
+
+/**
+ * Starts the WebView on a background thread, so later WebView usage is faster.
+ *
+ * Exactly one of [onSuccess] or [onFailure] is called, on the main thread, once startup completes.
+ *
+ * Background work runs on a dedicated thread, released once startup completes.
+ *
+ * @see WebViewCompat.startUpWebView
+ */
+fun startUpWebView(
+ context: Context,
+ onSuccess: () -> Unit,
+ onFailure: (Throwable) -> Unit,
+) {
+ val executor = Executors.newSingleThreadExecutor()
+ val config = WebViewStartUpConfig.Builder(executor).build()
+ val callback =
+ object : WebViewOutcomeReceiver<WebViewStartUpResult, WebViewStartupException> {
+ override fun onResult(result: WebViewStartUpResult?) {
+ executor.shutdown()
+ onSuccess()
+ }
+
+ override fun onError(e: WebViewStartupException) {
+ executor.shutdown()
+ onFailure(e)
+ }
+ }
+ WebViewCompat.startUpWebView(context, config, callback)
+}
Index: AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/StartupWebViewService.kt
===================================================================
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/StartupWebViewService.kt b/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/StartupWebViewService.kt
deleted file mode 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/StartupWebViewService.kt (revision 1f4590eb619a39c86366645323aa647e84615638)
+++ /dev/null (revision 1f4590eb619a39c86366645323aa647e84615638)
@@ -1,64 +0,0 @@
-package com.ichi2.anki.servicelayer
-
-import android.content.Context
-import androidx.webkit.WebViewCompat
-import androidx.webkit.WebViewOutcomeReceiver
-import androidx.webkit.WebViewStartUpConfig
-import androidx.webkit.WebViewStartUpResult
-import androidx.webkit.WebViewStartupException
-import com.ichi2.anki.common.coroutines.applicationScope
-import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.async
-import kotlinx.coroutines.suspendCancellableCoroutine
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import java.util.concurrent.Executors
-import kotlin.coroutines.resume
-
-object StartupWebViewService {
- private var startUpWebViewDeferred: Deferred<Result<Unit>>? = null
- private val mutex = Mutex()
-
- /**
- * Ensure only one startup at the same time.
- * Fatal error if fails.
- * So no need for another wait for completion function or reset the deferred.
- */
- suspend fun startUpWebView(context: Context): Result<Unit> {
- val deferred =
- mutex.withLock {
- startUpWebViewDeferred?.let { return@withLock it }
-
- val newJob =
- applicationScope.async {
- startUpWebViewPrivate(context)
- }
-
- startUpWebViewDeferred = newJob
- newJob
- }
-
- return deferred.await()
- }
-
- private suspend fun startUpWebViewPrivate(context: Context): Result<Unit> =
- suspendCancellableCoroutine { continuation ->
- val executor = Executors.newSingleThreadExecutor()
- val config = WebViewStartUpConfig.Builder(executor).build()
-
- val callback =
- object : WebViewOutcomeReceiver<WebViewStartUpResult, WebViewStartupException> {
- override fun onResult(result: WebViewStartUpResult?) {
- executor.shutdown()
- continuation.resume(Result.success(Unit))
- }
-
- override fun onError(e: WebViewStartupException) {
- executor.shutdown()
- continuation.resume(Result.failure(e))
- }
- }
-
- WebViewCompat.startUpWebView(context, config, callback)
- }
-}
|
I see, the hard work like make startUpWebView synchronized from all threads and make use of this approach for all WebView init will be on later PR. I will apply the patch above and commit it with you as co-author. I feel the patch fit with what we have discussed here. So sorry for my bad habit avoiding using callbacks outside Compose codes. Thank you much for supporting me so far. I'm willing to work on this next WebView PR or the one you suggest. |
Co-authored-by: David Allison <62114487+david-allison@users.noreply.github.com>
1f4590e to
7c7a305
Compare
This reduces webView startup time by setup webView when app launch
Purpose / Description
Reduces webView startup time by integrating
androidx:startUpWebViewand setup webView when app launchFixes
Approach
I setup webView with new API and make other setups wait until this setup is complete, which already warms up the webView framework for other later usages
How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration (SDK version(s), emulator or physical, etc)
Learning (optional, can help others)
Checklist