Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Verified Commit 78c26e01 authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

perf: parallelize update fetch work

Run update sources concurrently and bound signature matching to reduce overall update list latency while keeping work coordinated.
parent abe54c23
Loading
Loading
Loading
Loading
+118 −80
Original line number Diff line number Diff line
@@ -35,6 +35,10 @@ import foundation.e.apps.data.install.pkg.AppLoungePackageManager
import foundation.e.apps.domain.model.install.Status
import foundation.e.apps.domain.preferences.AppPreferencesRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.Locale
@@ -60,16 +64,16 @@ class UpdatesManagerImpl @Inject constructor(
        private const val MILLIS_IN_SECOND = 1000L
        private const val SECONDS_IN_MINUTE = 60L
        private const val MINUTES_IN_HOUR = 60L

        private const val SIGNATURE_CONCURRENCY = 4
    }

    private val userApplications: List<ApplicationInfo>
        get() = appLoungePackageManager.getAllUserApps()

    @Suppress("LongMethod")
    suspend fun getUpdates(): Pair<List<Application>, ResultStatus> {
    suspend fun getUpdates(): Pair<List<Application>, ResultStatus> = coroutineScope {
        val overallStartMs = nowMs()
        val updateList = mutableListOf<Application>()
        var status = ResultStatus.OK

        val openSourceInstalledApps = logInstalledApps("open-source") {
            getOpenSourceInstalledApps()
@@ -106,56 +110,68 @@ class UpdatesManagerImpl @Inject constructor(
            blockedAppRepository.isBlockedApp(it)
        }

        // Get open source app updates
        if (openSourceInstalledApps.isNotEmpty()) {
            status = logUpdatesFetch("open-source", openSourceInstalledApps.size) {
                getUpdatesFromApi({
        val openSourceDeferred = if (openSourceInstalledApps.isNotEmpty()) {
            async {
                logUpdatesFetch("open-source", openSourceInstalledApps.size) {
                    getUpdatesFromApi {
                        applicationRepository.getApplicationDetails(
                            openSourceInstalledApps,
                            Source.OPEN_SOURCE
                        )
                }, updateList)
                    }
                }
            }
        } else {
            null
        }

        // Get GPlay app updates
        if (getApplicationCategoryPreference().contains(ApplicationRepository.APP_TYPE_ANY) &&
        val gplayDeferred = if (getApplicationCategoryPreference().contains(ApplicationRepository.APP_TYPE_ANY) &&
            gPlayInstalledApps.isNotEmpty()
        ) {
            val gplayStatus = logUpdatesFetch("Play Store", gPlayInstalledApps.size) {
                getUpdatesFromApi({
                    getGPlayUpdates(
                        gPlayInstalledApps
                    )
                }, updateList)
            async {
                logUpdatesFetch("Play Store", gPlayInstalledApps.size) {
                    getUpdatesFromApi {
                        getGPlayUpdates(gPlayInstalledApps)
                    }

            /**
             If any one of the sources is successful, status should be [ResultStatus.OK]
             **/
            status = if (status == ResultStatus.OK) status else gplayStatus
                }
            }
        } else {
            null
        }

        val systemApps = logSystemAppsFetch("system apps") {
        val systemAppsDeferred = async {
            logSystemAppsFetch("system apps") {
                getSystemAppUpdates()
            }
        }

        val openSourceResult = openSourceDeferred?.await()
        val gplayResult = gplayDeferred?.await()
        val systemApps = systemAppsDeferred.await()

        val updateList = mutableListOf<Application>()
        updateList.addAll(openSourceResult?.first.orEmpty())
        updateList.addAll(gplayResult?.first.orEmpty())
        val nonFaultyUpdateList = faultyAppRepository.removeFaultyApps(updateList)

        addSystemApps(updateList, nonFaultyUpdateList, systemApps)

        var status = openSourceResult?.second ?: ResultStatus.OK
        if (gplayResult != null && status != ResultStatus.OK) {
            status = gplayResult.second
        }

        Timber.tag("FAHIM").i(
            "Updates total fetch done, totalApps=%d, duration=%s",
            updateList.size,
            formatDuration(nowMs() - overallStartMs)
        )

        return Pair(updateList, status)
        return@coroutineScope Pair(updateList, status)
    }

    suspend fun getUpdatesOSS(): Pair<List<Application>, ResultStatus> {
    suspend fun getUpdatesOSS(): Pair<List<Application>, ResultStatus> = coroutineScope {
        val overallStartMs = nowMs()
        val updateList = mutableListOf<Application>()
        var status = ResultStatus.OK

        val openSourceInstalledApps = logInstalledApps("OSS open-source") {
            getOpenSourceInstalledApps()
@@ -179,39 +195,51 @@ class UpdatesManagerImpl @Inject constructor(
            blockedAppRepository.isBlockedApp(it)
        }

        if (openSourceInstalledApps.isNotEmpty()) {
            status = logUpdatesFetch("OSS open-source", openSourceInstalledApps.size) {
                getUpdatesFromApi({
        val openSourceDeferred = if (openSourceInstalledApps.isNotEmpty()) {
            async {
                logUpdatesFetch("OSS open-source", openSourceInstalledApps.size) {
                    getUpdatesFromApi {
                        applicationRepository.getApplicationDetails(
                            openSourceInstalledApps,
                            Source.OPEN_SOURCE
                        )
                }, updateList)
                    }
                }
            }
        } else {
            null
        }

        val systemApps = logSystemAppsFetch("OSS system apps") {
        val systemAppsDeferred = async {
            logSystemAppsFetch("OSS system apps") {
                getSystemAppUpdates()
            }
        }

        val openSourceResult = openSourceDeferred?.await()
        val systemApps = systemAppsDeferred.await()

        val updateList = mutableListOf<Application>()
        updateList.addAll(openSourceResult?.first.orEmpty())
        val nonFaultyUpdateList = faultyAppRepository.removeFaultyApps(updateList)

        addSystemApps(updateList, nonFaultyUpdateList, systemApps)

        val status = openSourceResult?.second ?: ResultStatus.OK

        Timber.tag("FAHIM").i(
            "Updates OSS total fetch done, totalApps=%d, duration=%s",
            updateList.size,
            formatDuration(nowMs() - overallStartMs)
        )

        return Pair(updateList, status)
        return@coroutineScope Pair(updateList, status)
    }

    private suspend fun getSystemAppUpdates(): List<Application> {
        val systemApps = mutableListOf<Application>()
        getUpdatesFromApi({
        return getUpdatesFromApi {
            Pair(systemAppsUpdatesRepository.getSystemUpdates(), ResultStatus.OK)
        }, systemApps)
        return systemApps
        }.first
    }

    /**
@@ -298,14 +326,12 @@ class UpdatesManagerImpl @Inject constructor(
     */
    private suspend fun getUpdatesFromApi(
        apiFunction: suspend () -> Pair<List<Application>, ResultStatus>,
        updateAccumulationList: MutableList<Application>,
    ): ResultStatus {
    ): Pair<List<Application>, ResultStatus> {
        val apiResult = apiFunction()
        val updatableApps = apiResult.first.filter {
            it.status == Status.UPDATABLE && (it.filterLevel.isUnFiltered() || it.isFDroidApp)
        }
        updateAccumulationList.addAll(updatableApps)
        return apiResult.second
        return Pair(updatableApps, apiResult.second)
    }

    private suspend fun getGPlayUpdates(
@@ -336,22 +362,36 @@ class UpdatesManagerImpl @Inject constructor(
    private suspend fun getFDroidAppsAndSignatures(
        installedPackageNames: List<String>,
        cleanApkAppsByPackage: Map<String, Application?>,
    ): Map<String, String> {
    ): Map<String, String> = coroutineScope {
        val startMs = nowMs()
        Timber.tag("FAHIM").i(
            "Updates signature scan start, packages=%d",
            installedPackageNames.size
        )
        val appsAndSignatures = hashMapOf<String, String>()
        for (packageName in installedPackageNames) {
        val semaphore = Semaphore(SIGNATURE_CONCURRENCY)
        val candidates = installedPackageNames.map { packageName ->
            async {
                semaphore.withPermit {
                    val perPackageStartMs = nowMs()
            val beforeCount = appsAndSignatures.size
            updateAppsWithPGPSignature(packageName, appsAndSignatures, cleanApkAppsByPackage)
                    val signature = updateAppsWithPGPSignature(packageName, cleanApkAppsByPackage)
                    val duration = formatDuration(nowMs() - perPackageStartMs)
                    Triple(packageName, signature, duration)
                }
            }
        }

        candidates.forEach { deferred ->
            val (packageName, signature, duration) = deferred.await()
            val added = signature != null
            if (signature != null) {
                appsAndSignatures[packageName] = signature
            }
            Timber.tag("FAHIM").i(
                "Updates signature candidate done, package=%s, added=%s, duration=%s",
                packageName,
                appsAndSignatures.size > beforeCount,
                formatDuration(nowMs() - perPackageStartMs)
                added,
                duration
            )
        }
        Timber.tag("FAHIM").i(
@@ -359,14 +399,13 @@ class UpdatesManagerImpl @Inject constructor(
            appsAndSignatures.size,
            formatDuration(nowMs() - startMs)
        )
        return appsAndSignatures
        return@coroutineScope appsAndSignatures
    }

    private suspend fun updateAppsWithPGPSignature(
        packageName: String,
        appsAndSignatures: HashMap<String, String>,
        cleanApkAppsByPackage: Map<String, Application?>,
    ) {
    ): String? {
        val appDetailsStartMs = nowMs()
        val cleanApkApplication = cleanApkAppsByPackage[packageName]
        val apps = when {
@@ -380,22 +419,21 @@ class UpdatesManagerImpl @Inject constructor(
            formatDuration(nowMs() - appDetailsStartMs),
            apps.size
        )
        if (apps.isEmpty()) {
            return
        }

        if (apps[0].package_name.isBlank()) {
            return
        }
        val app = apps.firstOrNull()?.takeIf { it.package_name.isNotBlank() }
        val signature = if (app == null) {
            null
        } else {
            val signatureStartMs = nowMs()
        val signature = getPgpSignature(apps[0])
            val pgpSignature = getPgpSignature(app)
            Timber.tag("FAHIM").i(
                "Updates signature fetch done, package=%s, duration=%s, blank=%s",
                packageName,
                formatDuration(nowMs() - signatureStartMs),
            signature.isBlank()
                pgpSignature.isBlank()
            )
        appsAndSignatures[packageName] = signature
            pgpSignature
        }
        return signature
    }

    private suspend fun getPgpSignature(cleanApkApplication: Application): String {
@@ -628,18 +666,18 @@ class UpdatesManagerImpl @Inject constructor(
    private suspend fun logUpdatesFetch(
        label: String,
        appCount: Int,
        fetch: suspend () -> ResultStatus,
    ): ResultStatus {
        fetch: suspend () -> Pair<List<Application>, ResultStatus>,
    ): Pair<List<Application>, ResultStatus> {
        val startMs = nowMs()
        val status = fetch()
        val result = fetch()
        Timber.tag("FAHIM").i(
            "Updates %s fetch done, apps=%d, duration=%s, status=%s",
            label,
            appCount,
            formatDuration(nowMs() - startMs),
            status
            result.second
        )
        return status
        return result
    }

    private suspend fun logSystemAppsFetch(