IJPL-165058 Further improvements to AsyncRendering - make sure non-suspending renderers do not spin hundreds of threads

(cherry picked from commit 4dd4fa0920ad178d5ae6a0809b27d034160b4a94)

IJ-CR-151474

GitOrigin-RevId: 674f829893359bb8f4e6f6e4bac59abf47075f89
This commit is contained in:
Piotr Tomiak
2024-12-12 14:03:33 +01:00
committed by intellij-monorepo-bot
parent 050782b5ea
commit ebac977c88
2 changed files with 29 additions and 5 deletions

View File

@@ -7,6 +7,11 @@ abstract class SuspendingLookupElementRenderer<T : LookupElement> : LookupElemen
/**
* Render LookupElement in a coroutine. There is no guarantee that the element is valid.
* The method will usually be called without a read lock.
*
* Avoid changing the dispatcher on which the coroutine runs, as this may cause
* spawning of hundreds of worker threads on unlimited dispatchers.
* E.g. `withContext`, `readAction`, `runBlocking`, `runBlockingCancellable` may move
* the coroutine to a different dispatcher.
*/
abstract suspend fun renderElementSuspending(element: T, presentation: LookupElementPresentation)

View File

@@ -11,6 +11,10 @@ import com.intellij.openapi.application.readAction
import com.intellij.openapi.util.Key
import com.intellij.util.indexing.DumbModeAccessType
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.sync.withPermit
internal class AsyncRendering(private val lookup: LookupImpl) {
companion object {
@@ -31,6 +35,9 @@ internal class AsyncRendering(private val lookup: LookupImpl) {
}
}
private val nonSuspendingRenderersMutex = Mutex(false)
private val suspendingRenderersSemaphore = Semaphore(10)
fun getLastComputed(element: LookupElement): LookupElementPresentation = element.getUserData(LAST_COMPUTED_PRESENTATION)!!
fun scheduleRendering(element: LookupElement, renderer: LookupElementRenderer<LookupElement>) {
@@ -44,13 +51,25 @@ internal class AsyncRendering(private val lookup: LookupImpl) {
val job = lookup.coroutineScope.launch(limitedDispatcher) {
val job = coroutineContext.job
if (renderer is SuspendingLookupElementRenderer<LookupElement>) {
renderInBackgroundSuspending(element, renderer)
} else
readAction {
if (element.isValid) {
renderInBackground(element, renderer)
// Suspending renderers work on the `limitedDispatcher`, so there is no need to limit the throughput.
// However, the user code may still move the coroutine to a different dispatcher, so just in case
// limit the throughput.
suspendingRenderersSemaphore.withPermit {
renderInBackgroundSuspending(element, renderer)
}
} else {
// `readAction` redirects the coroutine to the `Dispatcher.default` leaving `limitedDispatcher` free.
// The next coroutine is then processed on the `limitedDispatcher` and so on. Ultimately, this can spin
// a new dispatcher worker thread for almost each item on the list. We should only allow a single
// non-suspending renderer to run within the read action.
nonSuspendingRenderersMutex.withLock {
readAction {
if (element.isValid) {
renderInBackground(element, renderer)
}
}
}
}
synchronized(LAST_COMPUTATION) {
element.replace(LAST_COMPUTATION, job, null)
}