IDEA-304699 Implement CoroutineScopeModel

The first step towards fixing slow ops related to scope comboboxes
and action groups. At this point most slow ops caused by
scope providers should be fixed, but there may be some unsafe UI
access now. This should be double-checked and tested properly.

There's a bunch of slow ops left in getPredefinedScopesAsync,
specifically in collectContext. These should be fixed separately,
probably a suspending equivalent of this API is required.

Because the code is now a mix of EDT and BGT code,
wrap the data context in an async data context.
It obviously still won't solve the issue of mixing UI
code with BGT code inside those scope providers,
if any, which should be fixed separately if anything pops up.

Add a registry key to turn on the new model, to be able to easily
turn it off in case anything goes wrong. This is still an experimental
implementation.

GitOrigin-RevId: 48c23e8423ee9b20ca57dcad5ea65571fbbe7d64
This commit is contained in:
Sergei Tachenov
2023-12-07 16:22:17 +02:00
committed by intellij-monorepo-bot
parent 90cf6dc0cc
commit 13841d4f9b
3 changed files with 178 additions and 1 deletions

View File

@@ -0,0 +1,169 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.util.scopeChooser
import com.intellij.ide.DataManager
import com.intellij.ide.util.treeView.WeighedItem
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.impl.Utils
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.readAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.ListSeparator
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.search.PredefinedSearchScopeProvider
import com.intellij.psi.search.SearchScope
import com.intellij.psi.search.SearchScopeProvider
import com.intellij.util.OverflowSemaphore
import com.intellij.util.containers.ContainerUtil
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BufferOverflow
import org.jetbrains.concurrency.await
import java.util.concurrent.CopyOnWriteArrayList
import java.util.function.Predicate
internal class CoroutineScopeModel internal constructor(
private val project: Project,
private val coroutineScope: CoroutineScope,
options: Set<ScopeOption>,
) : AbstractScopeModel {
private val semaphore = OverflowSemaphore(permits = 1, overflow = BufferOverflow.DROP_OLDEST)
private val listeners = CopyOnWriteArrayList<ScopeModelListener>()
private val options = mutableSetOf<ScopeOption>().apply { addAll(options) }
private var filter: (ScopeDescriptor) -> Boolean = { true }
override fun setOption(option: ScopeOption, value: Boolean) {
if (value) {
options.add(option)
}
else {
options.remove(option)
}
}
override fun addScopeModelListener(listener: ScopeModelListener) {
listeners += listener
}
override fun removeScopeModelListener(listener: ScopeModelListener) {
listeners -= listener
}
override fun setFilter(filter: (ScopeDescriptor) -> Boolean) {
this.filter = filter
}
override fun refreshScopes(dataContext: DataContext?) {
val asyncDataContext = dataContext?.let { Utils.createAsyncDataContext(it) }
coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
semaphore.withPermit {
yield() // dispatch
val scopes = if (asyncDataContext == null) {
getScopeDescriptors(filter)
}
else {
getScopeDescriptors(asyncDataContext, filter)
}
fireScopesUpdated(scopes)
}
}
}
private fun fireScopesUpdated(scopesSnapshot: ScopesSnapshot) {
listeners.forEach { it.scopesUpdated(scopesSnapshot) }
}
override suspend fun getScopes(dataContext: DataContext): ScopesSnapshot = getScopeDescriptors(dataContext, filter)
@Deprecated("Slow and blocking, use getScopes() in a suspending context, or addScopeModelListener() and refreshScopes()")
override fun getScopesImmediately(dataContext: DataContext): ScopesSnapshot =
ScopeModel.getScopeDescriptors(project, dataContext, options, filter)
private suspend fun getScopeDescriptors(filter: Predicate<in ScopeDescriptor>): ScopesSnapshot {
val dataContext = withContext(Dispatchers.EDT) {
DataManager.getInstance().dataContextFromFocusAsync.then { Utils.createAsyncDataContext(it) }
}.await()
return getScopeDescriptors(dataContext, filter)
}
private suspend fun getScopeDescriptors(
dataContext: DataContext,
filter: Predicate<in ScopeDescriptor>,
): ScopesSnapshot {
val predefinedScopes = withContext(Dispatchers.EDT) {
PredefinedSearchScopeProvider.getInstance().getPredefinedScopesAsync(
project, dataContext,
options.contains(ScopeOption.LIBRARIES),
options.contains(ScopeOption.SEARCH_RESULTS),
options.contains(ScopeOption.FROM_SELECTION),
options.contains(ScopeOption.USAGE_VIEW),
options.contains(ScopeOption.EMPTY_SCOPES)
)
}.await()
return doProcessScopes(dataContext, predefinedScopes, filter)
}
private suspend fun doProcessScopes(
dataContext: DataContext,
predefinedScopes: List<SearchScope>,
filter: Predicate<in ScopeDescriptor>,
): ScopesSnapshot {
val resultScopes = mutableListOf<ScopeDescriptor>()
val resultSeparators: HashMap<String, ListSeparator> = HashMap()
for (searchScope in predefinedScopes) {
val scopeDescriptor = ScopeDescriptor(searchScope)
if (filter.test(scopeDescriptor)) {
resultScopes.add(scopeDescriptor)
}
}
for (provider in ScopeDescriptorProvider.EP_NAME.extensionList) {
val scopes = readAction {
provider.getScopeDescriptors(project, dataContext)
}
for (descriptor in scopes) {
if (filter.test(descriptor)) {
resultScopes.add(descriptor)
}
}
}
for (provider in SearchScopeProvider.EP_NAME.extensionList) {
val separatorName = provider.displayName
if (separatorName.isNullOrEmpty()) continue
val scopes = readAction {
provider.getSearchScopes(project, dataContext)
}
if (scopes.isEmpty()) continue
val scopeSeparator = ScopeSeparator(separatorName)
if (filter.test(scopeSeparator)) {
resultScopes.add(scopeSeparator)
}
var isFirstScope = false
for (scope in scopes.sortedWith(comparator)) {
val scopeDescriptor = ScopeDescriptor(scope)
if (filter.test(scopeDescriptor)) {
if (!isFirstScope) {
isFirstScope = true
resultSeparators[scope.displayName] = ListSeparator(separatorName)
}
resultScopes.add(scopeDescriptor)
}
}
}
return ScopesSnapshotImpl(resultScopes, resultSeparators)
}
}
private val comparator = Comparator { o1: SearchScope, o2: SearchScope ->
val w1 = o1.weight
val w2 = o2.weight
if (w1 == w2) return@Comparator StringUtil.naturalCompare(o1.displayName, o2.displayName)
w1.compareTo(w2)
}
private val SearchScope.weight: Int get() = if (this is WeighedItem) weight else Int.MAX_VALUE

View File

@@ -4,6 +4,7 @@ package com.intellij.ide.util.scopeChooser
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.components.Service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.registry.Registry
import kotlinx.coroutines.CoroutineScope
import org.jetbrains.concurrency.await
import java.util.concurrent.CopyOnWriteArrayList
@@ -14,7 +15,11 @@ class ScopeService(
private val scope: CoroutineScope,
) {
fun createModel(options: Set<ScopeOption>): AbstractScopeModel = LegacyScopeModel(project, HashSet(options))
fun createModel(options: Set<ScopeOption>): AbstractScopeModel =
if (Registry.`is`("coroutine.scope.model", true))
CoroutineScopeModel(project, scope, options)
else
LegacyScopeModel(project, options)
}

View File

@@ -2407,5 +2407,8 @@ ide.services.run.configuration.switch.limit=3
ide.services.run.configuration.switch.limit.description=Show run configurations of supported framework types in Services\
\ if its number reaches the limit.
coroutine.scope.model=true
coroutine.scope.model.description=Use the new experimental coroutine-based model for scope selection comboboxes
# please leave this note as last line
# TODO please use EP com.intellij.registryKey for plugin/product specific keys