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

Commit e49c690e authored by Kshitij Gupta's avatar Kshitij Gupta
Browse files

SystemUI: Implement IntrinsicLockDispatcher for background tasks

- This commit refactors the background coroutine dispatcher in SystemUI
  to utilize a new, custom IntrinsicLockDispatcher.
- Previously, newFixedThreadPoolContext was used which internally uses
  ScheduledThreadPoolExecutor, that has been found to cause unintended
  waiting on the main thread, during background work and poor scheduling
  decisions. This was due to its reliance on a single internal
  ReentrantLock for all queue operations, leading to lock contention.
- This new dispatcher uses a ThreadPoolExecutor paired with a custom,
  ReentrantLock-free SynchronizedLinkedBlockingQueue. This queue uses
  intrinsic monitor locks (synchronized) to mitigate contention between
  worker threads and task-submitting threads.
- Improvements: A shell script was written to unlock, drag the shade,
  dismiss the shade and relock the device. During this, a perfetto trace
  was captured with the flag on and off, both across multiple
  iterations. The following improvements were seen in regards to stall
  durations (decrease is good):
  - Mean duration decreased by 5.97%
  - Peak duration decreased by 25.96%
  - Sum duration decreased by 24.77%
  - Trough duration slightly increased by 0.97%

Flag: com.android.systemui.sysui_intrinsic_lock_dispatcher
Test: Manual
Bug: 419472502
Change-Id: I0585c354311b1e7abec908de85265dd0a62e2764
parent a6f51b89
Loading
Loading
Loading
Loading
+13 −4
Original line number Diff line number Diff line
@@ -17,12 +17,14 @@
package com.android.systemui.util.kotlin

import android.os.Handler
import com.android.systemui.Flags
import com.android.systemui.coroutines.newTracingContext
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.NotifInflation
import com.android.systemui.dagger.qualifiers.UiBackground
import com.android.systemui.util.kotlin.dispatchers.newIntrinsicLockFixedThreadPoolContext
import com.android.systemui.util.settings.SettingsSingleThreadBackground
import dagger.Module
import dagger.Provides
@@ -72,10 +74,17 @@ class SysUICoroutinesModule {
            // would share those threads with other dependencies using Dispatchers.IO.
            // Using a dedicated thread pool we have guarantees only SystemUI is able to schedule
            // code on those.
            if (Flags.sysuiIntrinsicLockDispatcher()) {
                newIntrinsicLockFixedThreadPoolContext(
                    nThreads = Runtime.getRuntime().availableProcessors(),
                    name ="SystemUIBg",
                )
            } else {
                newFixedThreadPoolContext(
                    nThreads = Runtime.getRuntime().availableProcessors(),
                    name = "SystemUIBg",
                )
            }
        } else {
            Dispatchers.IO
        }
+97 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.util.kotlin.dispatchers

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.DelicateCoroutinesApi
import java.io.Closeable
import java.util.concurrent.ExecutorService
import java.util.concurrent.ThreadFactory
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import kotlin.coroutines.CoroutineContext

/**
 * A coroutine dispatcher backed by a ThreadPoolExecutor that uses a custom,
 * ReentrantLock-free BlockingQueue.
 *
 * This serves as a drop-in replacement for `newFixedThreadPoolContext` to avoid
 * the main-thread stalls caused by lock contention in the standard implementation.
 *
 * @param corePoolSize The number of threads to keep in the pool, even if they are idle.
 * @param maxPoolSize The maximum number of threads to allow in the pool.
 * @param keepAliveTime When the number of threads is greater than the core, this is the maximum
 * time that excess idle threads will wait for new tasks before terminating.
 * @param unit The time unit for the keepAliveTime argument.
 * @param dispatcherName A base name for the dispatcher's threads, useful for debugging.
 */
class IntrinsicLockDispatcher(
    corePoolSize: Int,
    maxPoolSize: Int,
    keepAliveTime: Long = 10L,
    unit: TimeUnit = TimeUnit.MILLISECONDS,
    dispatcherName: String = "IntrinsicLockDispatcherPool"
) : CoroutineDispatcher(), Closeable {

    private val executor: ExecutorService

    companion object {
        /**
         * Helper function to create a [ThreadFactory] with a custom name prefix for
         * easier debugging.
         */
        private fun threadFactory(namePrefix: String) = object : ThreadFactory {
            private val count = AtomicInteger(0)
            override fun newThread(r: Runnable): Thread {
                return Thread(r, "$namePrefix-${count.incrementAndGet()}").also { it.isDaemon = true }
            }
        }
    }

    init {
        executor = ThreadPoolExecutor(
            corePoolSize,
            maxPoolSize,
            keepAliveTime,
            unit,
            SynchronizedLinkedBlockingQueue(),
            threadFactory(dispatcherName)
        )
    }
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        executor.execute(block)
    }

    override fun close() {
        executor.shutdown()
    }
}

/**
 * Creates a new CoroutineDispatcher with a fixed-size thread pool that is free
 * of `ReentrantLock` to avoid specific main-thread contention issues.
 */
@DelicateCoroutinesApi
fun newIntrinsicLockFixedThreadPoolContext(nThreads: Int, name: String): CoroutineDispatcher {
    require(nThreads >= 1) { "Expected at least one thread, but $nThreads specified" }
    return IntrinsicLockDispatcher(
        corePoolSize = nThreads,
        maxPoolSize = nThreads,
        dispatcherName = name
    )
}
+103 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.util.kotlin.dispatchers

import java.util.AbstractQueue
import java.util.ArrayDeque
import java.util.concurrent.BlockingQueue
import java.util.concurrent.TimeUnit

/**
 * A custom implementation of a blocking queue that uses a simple LinkedList
 * and intrinsic monitor locks (`synchronized`) instead of `ReentrantLock`.
 * This avoids the specific contention issues that can arise from ReentrantLock
 * in scenarios like the one described in the Perfetto case study.
 */
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
internal class SynchronizedLinkedBlockingQueue : AbstractQueue<Runnable>(), BlockingQueue<Runnable> {

    private val queue = ArrayDeque<Runnable>()

    override val size: Int
        @Synchronized get() = queue.size

    @Synchronized
    override fun offer(e: Runnable): Boolean {
        // This queue is unbounded, so it always succeeds.
        queue.addLast(e)
        (this as Object).notifyAll()
        return true
    }

    @Synchronized
    override fun offer(e: Runnable, timeout: Long, unit: TimeUnit): Boolean {
        // Unbounded queue, timeout is irrelevant.
        return offer(e)
    }

    @Synchronized
    override fun put(e: Runnable) {
        queue.addLast(e)
        (this as Object).notifyAll()
    }

    @Synchronized
    override fun take(): Runnable {
        while (queue.isEmpty()) {
            (this as Object).wait()
        }
        return queue.removeFirst()
    }

    @Synchronized
    override fun poll(timeout: Long, unit: TimeUnit): Runnable? {
        if (queue.isNotEmpty()) {
            return queue.removeFirst()
        }
        val millis = unit.toMillis(timeout)
        // Check for timeout > 0 to avoid waiting forever on wait(0).
        if (millis > 0) {
            (this as Object).wait(millis)
        }
        return queue.pollFirst()
    }

    @Synchronized
    override fun poll(): Runnable? = queue.pollFirst()

    @Synchronized
    override fun peek(): Runnable? = queue.peekFirst()

    // Not required by ThreadPoolExecutor
    override fun iterator(): MutableIterator<Runnable> {
        throw UnsupportedOperationException("iterator() not supported by SynchronizedLinkedBlockingQueue")
    }

    // Not required by ThreadPoolExecutor
    override fun drainTo(c: MutableCollection<in Runnable>): Int {
        throw UnsupportedOperationException("drainTo() not supported by SynchronizedLinkedBlockingQueue")
    }

    // Not required by ThreadPoolExecutor
    override fun drainTo(c: MutableCollection<in Runnable>, maxElements: Int): Int {
        throw UnsupportedOperationException("drainTo() not supported by SynchronizedLinkedBlockingQueue")
    }

    override fun remainingCapacity(): Int {
        return Int.MAX_VALUE // Unbounded
    }
}