From 9cca64f6ac2aa153e7dd339d5308e79504cd535d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Apr 2026 19:34:12 +0000 Subject: [PATCH] perf: tune image/video OkHttp dispatcher and connection pool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OkHttp's default dispatcher caps inflight requests per host at 5 and total inflight at 64. Amethyst feeds typically pull most media from a single host (e.g. a primary Blossom/imgproxy server), so that per-host cap serialized feed loading — the browser-feels-faster effect. - Give the images/videos factory a dedicated Dispatcher (16 per host, 128 total on device; 5/64 on emulator). - Give it a larger ConnectionPool (32 idle, 5 min keep-alive) so HTTP/2 connections to the common media host stay warm across scrolls and avoid repeated TLS handshakes. - Extract isEmulator() to a shared helper used by both the relay and image/video factories. The relay factory is untouched (already tuned for websockets). https://claude.ai/code/session_01PUbqGyUc6oq6V1MdmLi8sw --- .../amethyst/service/okhttp/IsEmulator.kt | 39 +++++++++++++++++++ .../service/okhttp/OkHttpClientFactory.kt | 24 ++++++++++++ .../okhttp/OkHttpClientFactoryForRelays.kt | 17 -------- 3 files changed, 63 insertions(+), 17 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/IsEmulator.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/IsEmulator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/IsEmulator.kt new file mode 100644 index 0000000000..48764445e2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/IsEmulator.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.okhttp + +import android.os.Build + +internal fun isEmulator(): Boolean = + Build.FINGERPRINT.startsWith("generic") || + Build.FINGERPRINT.lowercase().contains("emulator") || + Build.MODEL.contains("google_sdk") || + Build.MODEL.lowercase().contains("droid4x") || + Build.MODEL.contains("Emulator") || + Build.MODEL.contains("Android SDK built for x86") || + Build.MANUFACTURER.contains("Genymotion") || + (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) || + "google_sdk" == Build.PRODUCT || + Build.HARDWARE.contains("goldfish") || + Build.HARDWARE.contains("ranchu") || + Build.HARDWARE.contains("vbox86") || + Build.HARDWARE.contains("nox") || + Build.HARDWARE.contains("cuttlefish") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt index 0dcdbb60b6..e409c6c5e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt @@ -24,10 +24,13 @@ import com.vitorpamplona.amethyst.service.okhttp.OkHttpClientFactoryForRelays.Co import com.vitorpamplona.amethyst.service.okhttp.OkHttpClientFactoryForRelays.Companion.DEFAULT_SOCKS_PORT import com.vitorpamplona.amethyst.service.okhttp.OkHttpClientFactoryForRelays.Companion.DEFAULT_TIMEOUT_ON_MOBILE_SECS import com.vitorpamplona.amethyst.service.okhttp.OkHttpClientFactoryForRelays.Companion.DEFAULT_TIMEOUT_ON_WIFI_SECS +import okhttp3.ConnectionPool +import okhttp3.Dispatcher import okhttp3.OkHttpClient import java.net.InetSocketAddress import java.net.Proxy import java.time.Duration +import java.util.concurrent.TimeUnit class OkHttpClientFactory( keyCache: EncryptionKeyCache, @@ -36,9 +39,30 @@ class OkHttpClientFactory( // val logging = LoggingInterceptor() val keyDecryptor = EncryptedBlobInterceptor(keyCache) + // Most images/videos in a feed come from a small set of hosts (e.g. a single + // Blossom/imgproxy server). OkHttp's default dispatcher caps inflight requests + // per host at 5, which serializes feed loading. Raise the limits so the feed + // can parallelize downloads the way a browser does. + private val dispatcher = + Dispatcher().apply { + if (!isEmulator()) { + maxRequestsPerHost = 16 + maxRequests = 128 + } else { + maxRequestsPerHost = 5 + maxRequests = 64 + } + } + + // Keep more HTTP/2 connections warm so scrolling doesn't repeatedly re-TLS + // to the same media host. + private val connectionPool = ConnectionPool(32, 5, TimeUnit.MINUTES) + private val rootClient = OkHttpClient .Builder() + .dispatcher(dispatcher) + .connectionPool(connectionPool) .followRedirects(true) .followSslRedirects(true) .addInterceptor(DefaultContentTypeInterceptor(userAgent)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactoryForRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactoryForRelays.kt index 556c91bf07..e4b81b31fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactoryForRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactoryForRelays.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.service.okhttp -import android.os.Build import com.vitorpamplona.quartz.utils.Log import okhttp3.Dispatcher import okhttp3.OkHttpClient @@ -38,22 +37,6 @@ class OkHttpClientFactoryForRelays( const val DEFAULT_TIMEOUT_ON_WIFI_SECS: Int = 10 const val DEFAULT_TIMEOUT_ON_MOBILE_SECS: Int = 30 const val WEBSOCKET_PING_INTERVAL_SECS: Long = 120 - - private fun isEmulator(): Boolean = - Build.FINGERPRINT.startsWith("generic") || - Build.FINGERPRINT.lowercase().contains("emulator") || - Build.MODEL.contains("google_sdk") || - Build.MODEL.lowercase().contains("droid4x") || - Build.MODEL.contains("Emulator") || - Build.MODEL.contains("Android SDK built for x86") || - Build.MANUFACTURER.contains("Genymotion") || - (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) || - "google_sdk" == Build.PRODUCT || - Build.HARDWARE.contains("goldfish") || - Build.HARDWARE.contains("ranchu") || - Build.HARDWARE.contains("vbox86") || - Build.HARDWARE.contains("nox") || - Build.HARDWARE.contains("cuttlefish") } val myDispatcher =