IJPL-310 reduce plugin model memory size, simplify read code

GitOrigin-RevId: a89598d83a2a7dd727ed55b59da3905575a002c6
This commit is contained in:
Vladimir Krivosheev
2023-10-26 08:11:14 +02:00
committed by intellij-monorepo-bot
parent a88836f417
commit ef1fc7b5ff
15 changed files with 272 additions and 279 deletions

View File

@@ -1,8 +1,12 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:Suppress("ReplaceGetOrSet")
package com.intellij.ide.plugins
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.util.SystemInfoRt
import kotlinx.collections.immutable.mutate
import kotlinx.collections.immutable.persistentHashMapOf
import org.jetbrains.annotations.ApiStatus
private const val moduleNamePrefix = "com.intellij.platform."
@@ -36,20 +40,22 @@ enum class IdeaPluginPlatform {
};
val moduleId: PluginId = PluginId.getId(moduleNamePrefix + name.lowercase())
abstract fun isHostPlatform(): Boolean
companion object {
private val directory: Map<PluginId, IdeaPluginPlatform> = values().associateBy { it.moduleId }
private val directory = persistentHashMapOf<PluginId, IdeaPluginPlatform>().mutate {
map -> entries.associateByTo(map) { it.moduleId }
}
@JvmStatic
fun getHostPlatformModuleIds(): List<PluginId> =
values().mapNotNull { it.takeIf { it.isHostPlatform() }?.moduleId }
fun getHostPlatformModuleIds(): List<PluginId> = entries.mapNotNull { it.takeIf { it.isHostPlatform() }?.moduleId }
@JvmStatic
fun fromModuleId(moduleId: PluginId): IdeaPluginPlatform? =
directory[moduleId] ?: Unknown.takeIf { looksLikePlatformId(moduleId.idString) }
fun fromModuleId(moduleId: PluginId): IdeaPluginPlatform? {
return directory.get(moduleId) ?: Unknown.takeIf { looksLikePlatformId(moduleId.idString) }
}
private fun looksLikePlatformId(idString: String): Boolean =
idString.startsWith(moduleNamePrefix) && idString != "com.intellij.platform.images"
private fun looksLikePlatformId(idString: String): Boolean {
return idString.startsWith(moduleNamePrefix) && idString != "com.intellij.platform.images"
}
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// 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.plugins
import com.intellij.openapi.components.ComponentConfig
@@ -6,22 +6,25 @@ import com.intellij.openapi.components.ServiceDescriptor
import com.intellij.openapi.extensions.ExtensionDescriptor
import com.intellij.openapi.extensions.ExtensionPointDescriptor
import com.intellij.util.messages.ListenerDescriptor
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.PersistentMap
import kotlinx.collections.immutable.persistentHashMapOf
import kotlinx.collections.immutable.persistentListOf
import org.jetbrains.annotations.ApiStatus
import java.util.*
@ApiStatus.Internal
class ContainerDescriptor {
private var _services: MutableList<ServiceDescriptor>? = null
val services: List<ServiceDescriptor>
get() = _services ?: Collections.emptyList()
get() = _services ?: persistentListOf()
@JvmField var components: MutableList<ComponentConfig>? = null
@JvmField var components: PersistentList<ComponentConfig> = persistentListOf()
@JvmField var listeners: MutableList<ListenerDescriptor>? = null
@JvmField var extensionPoints: MutableList<ExtensionPointDescriptor>? = null
@JvmField var extensionPoints: PersistentList<ExtensionPointDescriptor> = persistentListOf()
@Transient var distinctExtensionPointCount: Int = -1
@Transient @JvmField var extensions: Map<String, MutableList<ExtensionDescriptor>>? = null
@Transient @JvmField var extensions: PersistentMap<String, PersistentList<ExtensionDescriptor>> = persistentHashMapOf()
fun addService(serviceDescriptor: ServiceDescriptor) {
if (_services == null) {
@@ -30,17 +33,8 @@ class ContainerDescriptor {
_services!!.add(serviceDescriptor)
}
internal fun getComponentListToAdd(): MutableList<ComponentConfig> {
var result = components
if (result == null) {
result = ArrayList()
components = result
}
return result
}
override fun toString(): String {
if (_services == null && components == null && extensionPoints == null && extensions == null && listeners == null) {
if (_services == null && components.isEmpty() && extensionPoints.isEmpty() && extensions.isEmpty() && listeners == null) {
return "ContainerDescriptor(empty)"
}
else {

View File

@@ -1,5 +1,5 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:Suppress("ReplaceGetOrSet", "ReplaceNegatedIsEmptyWithIsNotEmpty", "OVERRIDE_DEPRECATION")
@file:Suppress("ReplaceGetOrSet", "ReplaceNegatedIsEmptyWithIsNotEmpty", "OVERRIDE_DEPRECATION", "ReplacePutWithAssignment")
package com.intellij.ide.plugins
import com.intellij.AbstractBundle
@@ -10,6 +10,7 @@ import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.extensions.ExtensionDescriptor
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.extensions.impl.ExtensionPointImpl
import kotlinx.collections.immutable.*
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
@@ -68,7 +69,7 @@ class IdeaPluginDescriptorImpl(raw: RawPluginDescriptor,
private var category: String? = raw.category
@JvmField internal val url: String? = raw.url
@JvmField val pluginDependencies: List<PluginDependency>
@JvmField val incompatibilities: List<PluginId> = raw.incompatibilities ?: Collections.emptyList()
@JvmField val incompatibilities: PersistentList<PluginId> = raw.incompatibilities
init {
// https://youtrack.jetbrains.com/issue/IDEA-206274
@@ -97,15 +98,37 @@ class IdeaPluginDescriptorImpl(raw: RawPluginDescriptor,
@JvmField val actions: List<RawPluginDescriptor.ActionDescriptor> = raw.actions ?: Collections.emptyList()
// extension point name -> list of extension descriptors
val epNameToExtensions: Map<String, MutableList<ExtensionDescriptor>>? = raw.epNameToExtensions
@JvmField val epNameToExtensions: PersistentMap<String, PersistentList<ExtensionDescriptor>> = raw.epNameToExtensions.let { rawMap ->
if (rawMap.size < 2 || !rawMap.containsKey(registryEpName)) {
rawMap.toPersistentHashMap()
}
else {
/**
* What's going on:
* See [com.intellij.ide.plugins.DynamicPluginsTest]#`registry access of key from same plugin`
* This is an ad-hoc solution to the problem, it doesn't fix the root cause. This may also break if this map gets copied
* or transformed into a HashMap somewhere, but it seems it's not the case right now.
* TODO: one way to make a better fix is to introduce loadingOrder on extension points (as it is made for extensions).
*/
persistentMapOf<String, PersistentList<ExtensionDescriptor>>().mutate {
val keys = rawMap.keys.toTypedArray()
keys.sortWith(extensionPointNameComparator)
for (key in keys) {
it.put(key, rawMap.get(key)!!)
}
}
}
}
@JvmField val appContainerDescriptor: ContainerDescriptor = raw.appContainerDescriptor
@JvmField val projectContainerDescriptor: ContainerDescriptor = raw.projectContainerDescriptor
@JvmField val moduleContainerDescriptor: ContainerDescriptor = raw.moduleContainerDescriptor
@JvmField val content: PluginContentDescriptor = raw.contentModules?.let { PluginContentDescriptor(it) } ?: PluginContentDescriptor.EMPTY
@JvmField val content: PluginContentDescriptor = raw.contentModules.takeIf { it.isNotEmpty() }?.let { PluginContentDescriptor(it) }
?: PluginContentDescriptor.EMPTY
@JvmField val dependencies: ModuleDependenciesDescriptor = raw.dependencies
@JvmField var modules: List<PluginId> = raw.modules ?: Collections.emptyList()
@JvmField var modules: PersistentList<PluginId> = raw.modules
private val descriptionChildText = raw.description
@@ -192,7 +215,7 @@ class IdeaPluginDescriptorImpl(raw: RawPluginDescriptor,
if (!isSub) {
if (id == PluginManagerCore.CORE_ID) {
modules = modules + IdeaPluginPlatform.getHostPlatformModuleIds()
modules = modules.addAll(IdeaPluginPlatform.getHostPlatformModuleIds())
}
if (context.isPluginDisabled(id)) {
@@ -346,58 +369,55 @@ class IdeaPluginDescriptorImpl(raw: RawPluginDescriptor,
fun registerExtensions(nameToPoint: Map<String, ExtensionPointImpl<*>>,
containerDescriptor: ContainerDescriptor,
listenerCallbacks: MutableList<in Runnable>?) {
containerDescriptor.extensions?.let {
if (!it.isEmpty()) {
@Suppress("JavaMapForEach")
it.forEach { name, list ->
nameToPoint.get(name)?.registerExtensions(descriptors = list, pluginDescriptor = this, listenerCallbacks = listenerCallbacks)
}
if (!containerDescriptor.extensions.isEmpty()) {
for ((name, list) in containerDescriptor.extensions) {
nameToPoint.get(name)?.registerExtensions(descriptors = list, pluginDescriptor = this, listenerCallbacks = listenerCallbacks)
}
return
}
val unsortedMap = epNameToExtensions ?: return
val map = epNameToExtensions
// app container: in most cases will be only app-level extensions - to reduce map copying, assume that all extensions are app-level and then filter out
// project container: rest of extensions wil be mostly project level
// module container: just use rest, area will not register unrelated extension anyway as no registered point
if (containerDescriptor == appContainerDescriptor) {
val registeredCount = doRegisterExtensions(unsortedMap, nameToPoint, listenerCallbacks)
if (containerDescriptor === appContainerDescriptor) {
val registeredCount = doRegisterExtensions(map = map, nameToPoint = nameToPoint, listenerCallbacks = listenerCallbacks)
containerDescriptor.distinctExtensionPointCount = registeredCount
if (registeredCount == unsortedMap.size) {
projectContainerDescriptor.extensions = Collections.emptyMap()
moduleContainerDescriptor.extensions = Collections.emptyMap()
if (registeredCount == map.size) {
projectContainerDescriptor.extensions = persistentHashMapOf()
moduleContainerDescriptor.extensions = persistentHashMapOf()
}
}
else if (containerDescriptor == projectContainerDescriptor) {
val registeredCount = doRegisterExtensions(unsortedMap, nameToPoint, listenerCallbacks)
else if (containerDescriptor === projectContainerDescriptor) {
val registeredCount = doRegisterExtensions(map = map, nameToPoint = nameToPoint, listenerCallbacks = listenerCallbacks)
containerDescriptor.distinctExtensionPointCount = registeredCount
if (registeredCount == unsortedMap.size) {
containerDescriptor.extensions = unsortedMap
moduleContainerDescriptor.extensions = Collections.emptyMap()
if (registeredCount == map.size) {
containerDescriptor.extensions = map
moduleContainerDescriptor.extensions = persistentHashMapOf()
}
else if (registeredCount == (unsortedMap.size - appContainerDescriptor.distinctExtensionPointCount)) {
moduleContainerDescriptor.extensions = Collections.emptyMap()
else if (registeredCount == (map.size - appContainerDescriptor.distinctExtensionPointCount)) {
moduleContainerDescriptor.extensions = persistentHashMapOf()
}
}
else {
val registeredCount = doRegisterExtensions(unsortedMap, nameToPoint, listenerCallbacks)
val registeredCount = doRegisterExtensions(map = map, nameToPoint = nameToPoint, listenerCallbacks = listenerCallbacks)
if (registeredCount == 0) {
moduleContainerDescriptor.extensions = Collections.emptyMap()
moduleContainerDescriptor.extensions = persistentHashMapOf()
}
}
}
private fun doRegisterExtensions(unsortedMap: Map<String, MutableList<ExtensionDescriptor>>,
private fun doRegisterExtensions(map: PersistentMap<String, PersistentList<ExtensionDescriptor>>,
nameToPoint: Map<String, ExtensionPointImpl<*>>,
listenerCallbacks: MutableList<in Runnable>?): Int {
var registeredCount = 0
for (entry in unsortedMap) {
for (entry in map) {
val point = nameToPoint.get(entry.key) ?: continue
point.registerExtensions(entry.value, this, listenerCallbacks)
point.registerExtensions(descriptors = entry.value, pluginDescriptor = this, listenerCallbacks = listenerCallbacks)
registeredCount++
}
return registeredCount
@@ -479,11 +499,6 @@ class IdeaPluginDescriptorImpl(raw: RawPluginDescriptor,
this.category = category
}
val unsortedEpNameToExtensionElements: Map<String, List<ExtensionDescriptor>>
get() {
return Collections.unmodifiableMap(epNameToExtensions ?: return Collections.emptyMap())
}
override fun getVendorEmail(): String? = vendorEmail
override fun getVendorUrl(): String? = vendorUrl
@@ -547,3 +562,20 @@ private fun checkCycle(descriptor: IdeaPluginDescriptorImpl, configFile: String,
i++
}
}
private const val registryEpName = "com.intellij.registryKey"
private val extensionPointNameComparator = Comparator<String> { o1, o2 ->
if (o1 == registryEpName) {
return@Comparator if (o2 == registryEpName) {
0
}
else {
-1
}
}
if (o2 == registryEpName) {
return@Comparator 1
}
o1.compareTo(o2)
}

View File

@@ -1,7 +1,8 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.plugins
import com.intellij.openapi.extensions.PluginId
import kotlinx.collections.immutable.persistentListOf
import org.jetbrains.annotations.ApiStatus
import java.util.*
@@ -25,7 +26,7 @@ class ModuleDependenciesDescriptor(@JvmField val modules: List<ModuleReference>,
@ApiStatus.Internal
class PluginContentDescriptor(@JvmField val modules: List<ModuleItem>) {
companion object {
@JvmField val EMPTY: PluginContentDescriptor = PluginContentDescriptor(Collections.emptyList())
@JvmField val EMPTY: PluginContentDescriptor = PluginContentDescriptor(persistentListOf())
}
@ApiStatus.Internal

View File

@@ -5,6 +5,10 @@ import com.intellij.openapi.extensions.ExtensionDescriptor
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.util.NlsSafe
import com.intellij.util.xml.dom.XmlElement
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.PersistentMap
import kotlinx.collections.immutable.persistentHashMapOf
import kotlinx.collections.immutable.persistentListOf
import org.jetbrains.annotations.ApiStatus
import java.time.LocalDate
@@ -39,20 +43,20 @@ class RawPluginDescriptor {
@JvmField internal var releaseDate: LocalDate? = null
@JvmField internal var releaseVersion: Int = 0
@JvmField internal var modules: MutableList<PluginId>? = null
@JvmField internal var modules: PersistentList<PluginId> = persistentListOf()
@JvmField internal var depends: MutableList<PluginDependency>? = null
@JvmField internal var actions: MutableList<ActionDescriptor>? = null
@JvmField var incompatibilities: MutableList<PluginId>? = null
@JvmField var incompatibilities: PersistentList<PluginId> = persistentListOf()
@JvmField val appContainerDescriptor: ContainerDescriptor = ContainerDescriptor()
@JvmField val projectContainerDescriptor: ContainerDescriptor = ContainerDescriptor()
@JvmField val moduleContainerDescriptor: ContainerDescriptor = ContainerDescriptor()
@JvmField var epNameToExtensions: MutableMap<String, MutableList<ExtensionDescriptor>>? = null
@JvmField var epNameToExtensions: PersistentMap<String, PersistentList<ExtensionDescriptor>> = persistentHashMapOf()
@JvmField internal var contentModules: MutableList<PluginContentDescriptor.ModuleItem>? = null
@JvmField internal var contentModules: PersistentList<PluginContentDescriptor.ModuleItem> = persistentListOf()
@JvmField internal var dependencies: ModuleDependenciesDescriptor = ModuleDependenciesDescriptor.EMPTY
sealed class ActionDescriptor(

View File

@@ -18,6 +18,10 @@ import com.intellij.util.xml.dom.NoOpXmlInterner
import com.intellij.util.xml.dom.XmlInterner
import com.intellij.util.xml.dom.createNonCoalescingXmlStreamReader
import com.intellij.util.xml.dom.readXmlAsModel
import kotlinx.collections.immutable.mutate
import kotlinx.collections.immutable.persistentHashMapOf
import kotlinx.collections.immutable.persistentHashSetOf
import kotlinx.collections.immutable.persistentListOf
import org.codehaus.stax2.XMLStreamReader2
import org.codehaus.stax2.typed.TypedXMLStreamException
import org.jetbrains.annotations.ApiStatus
@@ -162,20 +166,19 @@ private fun readRootAttributes(reader: XMLStreamReader2, descriptor: RawPluginDe
/**
* Keep in sync with KotlinPluginUtil.KNOWN_KOTLIN_PLUGIN_IDS
*/
private val KNOWN_KOTLIN_PLUGIN_IDS = hashSetOf(
private val KNOWN_KOTLIN_PLUGIN_IDS = persistentHashSetOf(
"org.jetbrains.kotlin",
"com.intellij.appcode.kmm",
"org.jetbrains.kotlin.native.appcode"
)
private val K2_ALLOWED_PLUGIN_IDS = hashSetOf(
*KNOWN_KOTLIN_PLUGIN_IDS.toTypedArray(),
private val K2_ALLOWED_PLUGIN_IDS = KNOWN_KOTLIN_PLUGIN_IDS.addAll(persistentHashSetOf(
"fleet.backend.mercury",
"fleet.backend.mercury.kotlin",
"org.jetbrains.android",
"androidx.compose.plugins.idea",
"org.jetbrains.compose.desktop.ide",
)
))
private fun readRootElementChild(reader: XMLStreamReader2,
descriptor: RawPluginDescriptor,
@@ -215,19 +218,7 @@ private fun readRootElementChild(reader: XMLStreamReader2,
"product-descriptor" -> readProduct(reader, descriptor)
"module" -> {
findAttributeValue(reader, "value")?.let { moduleName ->
var modules = descriptor.modules
if (modules == null) {
descriptor.modules = Collections.singletonList(PluginId.getId(moduleName))
}
else {
if (modules.size == 1) {
val singleton = modules
modules = ArrayList(4)
modules.addAll(singleton)
descriptor.modules = modules
}
modules.add(PluginId.getId(moduleName))
}
descriptor.modules = descriptor.modules.add(PluginId.getId(moduleName))
}
reader.skipElement()
}
@@ -251,12 +242,7 @@ private fun readRootElementChild(reader: XMLStreamReader2,
}
"incompatible-with" -> {
getNullifiedContent(reader)?.let {
var list = descriptor.incompatibilities
if (list == null) {
list = ArrayList()
descriptor.incompatibilities = list
}
list.add(PluginId.getId(it))
descriptor.incompatibilities = descriptor.incompatibilities.add(PluginId.getId(it))
}
}
@@ -314,7 +300,11 @@ private fun readRootElementChild(reader: XMLStreamReader2,
}
}
private val actionNameToEnum = ActionDescriptorName.values().let { it.associateByTo(HashMap(it.size), ActionDescriptorName::name) }
private val actionNameToEnum = run {
persistentHashMapOf<String, ActionDescriptorName>().mutate { map ->
ActionDescriptorName.entries.associateByTo(map, ActionDescriptorName::name)
}
}
private fun readActions(descriptor: RawPluginDescriptor, reader: XMLStreamReader2, readContext: ReadModuleContext) {
var actionElements = descriptor.actions
@@ -459,43 +449,17 @@ private fun readExtensions(reader: XMLStreamReader2, descriptor: RawPluginDescri
}
}
var map: MutableMap<String, MutableList<ExtensionDescriptor>>? = descriptor.epNameToExtensions
if (map == null) {
/**
* What's going on:
* See [com.intellij.ide.plugins.DynamicPluginsTest]#`registry access of key from same plugin`
* This is an ad-hoc solution to the problem, it doesn't fix the root cause. This may also break if this map gets copied
* or transformed into a HashMap somewhere, but it seems it's not the case right now.
* TODO: one way to make a better fix is to introduce loadingOrder on extension points (as it is made for extensions).
*/
map = sortedMapOf(kotlin.Comparator { o1, o2 ->
val registryKey = "com.intellij.registryKey"
if (o1 == registryKey) {
return@Comparator if (o2 == registryKey) { 0 } else { -1 }
}
if (o2 == registryKey) {
return@Comparator 1
}
o1.compareTo(o2)
})
descriptor.epNameToExtensions = map
}
val extensionDescriptor = ExtensionDescriptor(implementation = implementation,
os = os,
orderId = orderId,
order = order,
element = element,
hasExtraAttributes = hasExtraAttributes)
val extensionDescriptor = ExtensionDescriptor(implementation, os, orderId, order, element, hasExtraAttributes)
val list = map.get(qualifiedExtensionPointName)
if (list == null) {
map.put(qualifiedExtensionPointName, Collections.singletonList(extensionDescriptor))
}
else if (list.size == 1) {
val l = ArrayList<ExtensionDescriptor>(4)
l.add(list.get(0))
l.add(extensionDescriptor)
map.put(qualifiedExtensionPointName, l)
}
else {
list.add(extensionDescriptor)
}
val list = descriptor.epNameToExtensions.get(qualifiedExtensionPointName)
@Suppress("IfThenToElvis")
descriptor.epNameToExtensions = descriptor.epNameToExtensions
.put(qualifiedExtensionPointName, if (list == null) persistentListOf(extensionDescriptor) else list.add(extensionDescriptor))
assert(reader.isEndElement)
return@consumeChildElements
@@ -596,13 +560,7 @@ private fun readExtensionPoints(reader: XMLStreamReader2,
}
}
var result = containerDescriptor.extensionPoints
if (result == null) {
result = ArrayList()
containerDescriptor.extensionPoints = result
}
result.add(ExtensionPointDescriptor(
containerDescriptor.extensionPoints = containerDescriptor.extensionPoints.add(ExtensionPointDescriptor(
name = qualifiedName ?: name ?: throw RuntimeException("`name` attribute not specified for extension point at ${reader.location}"),
isNameQualified = qualifiedName != null,
className = `interface` ?: beanClass!!,
@@ -616,14 +574,9 @@ private fun readExtensionPoints(reader: XMLStreamReader2,
private inline fun applyPartialContainer(from: RawPluginDescriptor,
to: RawPluginDescriptor,
crossinline extractor: (RawPluginDescriptor) -> ContainerDescriptor) {
extractor(from).extensionPoints?.let {
extractor(from).extensionPoints.takeIf { it.isNotEmpty() }?.let {
val toContainer = extractor(to)
if (toContainer.extensionPoints == null) {
toContainer.extensionPoints = it
}
else {
toContainer.extensionPoints!!.addAll(it)
}
toContainer.extensionPoints = toContainer.extensionPoints.addAll(it)
}
}
@@ -655,6 +608,7 @@ private fun readServiceDescriptor(reader: XMLStreamReader2, os: ExtensionDescrip
}
}
"client" -> {
@Suppress("DEPRECATION")
when (reader.getAttributeValue(i)) {
"local" -> client = ClientKind.LOCAL
"guest" -> client = ClientKind.GUEST
@@ -692,7 +646,6 @@ private fun readProduct(reader: XMLStreamReader2, descriptor: RawPluginDescripto
}
private fun readComponents(reader: XMLStreamReader2, containerDescriptor: ContainerDescriptor) {
val result = containerDescriptor.getComponentListToAdd()
reader.consumeChildElements("component") {
var isApplicableForDefaultProject = false
var interfaceClass: String? = null
@@ -753,20 +706,19 @@ private fun readComponents(reader: XMLStreamReader2, containerDescriptor: Contai
}
assert(reader.isEndElement)
result.add(ComponentConfig(interfaceClass, implementationClass, headlessImplementationClass, isApplicableForDefaultProject,
os, overrides, options))
containerDescriptor.components = containerDescriptor.components.add(ComponentConfig(interfaceClass,
implementationClass,
headlessImplementationClass,
isApplicableForDefaultProject,
os,
overrides,
options))
}
}
private fun readContent(reader: XMLStreamReader2,
descriptor: RawPluginDescriptor,
readContext: ReadModuleContext) {
var items = descriptor.contentModules
if (items == null) {
items = ArrayList()
descriptor.contentModules = items
}
reader.consumeChildElements { elementName ->
when (elementName) {
"module" -> {
@@ -787,7 +739,7 @@ private fun readContent(reader: XMLStreamReader2,
configFile = "${name.substring(0, index)}.${name.substring(index + 1)}.xml"
}
items.add(PluginContentDescriptor.ModuleItem(name = name, configFile = configFile))
descriptor.contentModules = descriptor.contentModules.add(PluginContentDescriptor.ModuleItem(name = name, configFile = configFile))
}
else -> throw RuntimeException("Unknown content item type: $elementName")
}
@@ -797,8 +749,8 @@ private fun readContent(reader: XMLStreamReader2,
}
private fun readDependencies(reader: XMLStreamReader2, descriptor: RawPluginDescriptor, readContext: ReadModuleContext) {
var modules: MutableList<ModuleDependenciesDescriptor.ModuleReference>? = null
var plugins: MutableList<ModuleDependenciesDescriptor.PluginReference>? = null
var modules = persistentListOf<ModuleDependenciesDescriptor.ModuleReference>()
var plugins = persistentListOf<ModuleDependenciesDescriptor.PluginReference>()
reader.consumeChildElements { elementName ->
when (elementName) {
"module" -> {
@@ -809,10 +761,7 @@ private fun readDependencies(reader: XMLStreamReader2, descriptor: RawPluginDesc
}
}
if (modules == null) {
modules = ArrayList()
}
modules!!.add(ModuleDependenciesDescriptor.ModuleReference(name!!))
modules = modules.add(ModuleDependenciesDescriptor.ModuleReference(name!!))
}
"plugin" -> {
var id: String? = null
@@ -822,16 +771,13 @@ private fun readDependencies(reader: XMLStreamReader2, descriptor: RawPluginDesc
}
}
if (plugins == null) {
plugins = ArrayList()
}
plugins!!.add(ModuleDependenciesDescriptor.PluginReference(PluginId.getId(id!!)))
plugins = plugins.add(ModuleDependenciesDescriptor.PluginReference(PluginId.getId(id!!)))
}
else -> throw RuntimeException("Unknown content item type: $elementName")
}
reader.skipElement()
}
descriptor.dependencies = ModuleDependenciesDescriptor(modules ?: Collections.emptyList(), plugins ?: Collections.emptyList())
descriptor.dependencies = ModuleDependenciesDescriptor(modules, plugins)
assert(reader.isEndElement)
}

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:Suppress("ReplaceGetOrSet", "ReplacePutWithAssignment")
package com.intellij.serviceContainer
@@ -7,42 +7,51 @@ import com.intellij.ide.plugins.IdeaPluginDescriptorImpl
import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.openapi.extensions.ExtensionDescriptor
import com.intellij.openapi.extensions.ExtensionPointDescriptor
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.PersistentMap
import kotlinx.collections.immutable.persistentHashMapOf
import kotlinx.collections.immutable.persistentListOf
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
class PrecomputedExtensionModel(
@JvmField val extensionPoints: List<List<ExtensionPointDescriptor>>,
@JvmField val pluginDescriptors: List<IdeaPluginDescriptor>,
@JvmField val extensionPoints: PersistentList<PersistentList<ExtensionPointDescriptor>>,
@JvmField val pluginDescriptors: PersistentList<IdeaPluginDescriptor>,
@JvmField val extensionPointTotalCount: Int,
@JvmField val nameToExtensions: Map<String, MutableList<Pair<IdeaPluginDescriptor, List<ExtensionDescriptor>>>>
@JvmField val nameToExtensions: PersistentMap<String, PersistentList<Pair<IdeaPluginDescriptor, PersistentList<ExtensionDescriptor>>>>
)
fun precomputeExtensionModel(): PrecomputedExtensionModel {
val extensionPointDescriptors = ArrayList<List<ExtensionPointDescriptor>>()
val pluginDescriptors = ArrayList<IdeaPluginDescriptor>()
var extensionPointDescriptors = persistentListOf<PersistentList<ExtensionPointDescriptor>>()
var pluginDescriptors = persistentListOf<IdeaPluginDescriptor>()
var extensionPointTotalCount = 0
val nameToExtensions = HashMap<String, MutableList<Pair<IdeaPluginDescriptor, List<ExtensionDescriptor>>>>()
val nameToExtensions = persistentHashMapOf<String, PersistentList<Pair<IdeaPluginDescriptor, PersistentList<ExtensionDescriptor>>>>()
// step 1 - collect container level extension points
val modules = PluginManagerCore.getPluginSet().getEnabledModules()
executeRegisterTask(modules) { pluginDescriptor ->
pluginDescriptor.moduleContainerDescriptor.extensionPoints?.let {
extensionPointDescriptors.add(it)
pluginDescriptors.add(pluginDescriptor)
extensionPointTotalCount += it.size
val list = pluginDescriptor.moduleContainerDescriptor.extensionPoints
if (list.isEmpty()) {
return@executeRegisterTask
}
for (descriptor in it) {
nameToExtensions.put(descriptor.getQualifiedName(pluginDescriptor), mutableListOf())
}
extensionPointDescriptors = extensionPointDescriptors.add(list)
pluginDescriptors = pluginDescriptors.add(pluginDescriptor)
extensionPointTotalCount += list.size
for (descriptor in list) {
nameToExtensions.put(descriptor.getQualifiedName(pluginDescriptor), persistentListOf())
}
}
// step 2 - collect container level extensions
executeRegisterTask(modules) { pluginDescriptor ->
val unsortedMap = pluginDescriptor.epNameToExtensions ?: return@executeRegisterTask
for ((name, list) in unsortedMap.entries) {
nameToExtensions.get(name)?.add(pluginDescriptor to list)
val map = pluginDescriptor.epNameToExtensions
for ((name, list) in map.entries) {
nameToExtensions.get(name)?.let {
nameToExtensions.put(name, it.add(pluginDescriptor to list))
}
}
}

View File

@@ -24,19 +24,19 @@ internal class BeanExtensionPoint<T : Any>(
override fun createAdapter(descriptor: ExtensionDescriptor,
pluginDescriptor: PluginDescriptor,
componentManager: ComponentManager): ExtensionComponentAdapter {
return if (componentManager.isInjectionForExtensionSupported) {
SimpleConstructorInjectionAdapter(implementationClassName = className,
pluginDescriptor = pluginDescriptor,
descriptor = descriptor,
implementationClassResolver = this)
if (componentManager.isInjectionForExtensionSupported) {
return SimpleConstructorInjectionAdapter(implementationClassName = className,
pluginDescriptor = pluginDescriptor,
descriptor = descriptor,
implementationClassResolver = this)
}
else {
XmlExtensionAdapter(implementationClassName = className,
pluginDescriptor = pluginDescriptor,
orderId = descriptor.orderId,
order = descriptor.order,
extensionElement = descriptor.element,
implementationClassResolver = this)
return XmlExtensionAdapter(implementationClassName = className,
pluginDescriptor = pluginDescriptor,
orderId = descriptor.orderId,
order = descriptor.order,
extensionElement = descriptor.element,
implementationClassResolver = this)
}
}

View File

@@ -22,7 +22,6 @@ import org.jetbrains.annotations.TestOnly
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
import java.util.concurrent.atomic.AtomicReference
import java.util.function.BiPredicate
import java.util.function.Predicate
import kotlin.concurrent.Volatile
@@ -36,24 +35,22 @@ sealed class ExtensionPointImpl<T : Any>(val name: String,
val componentManager: ComponentManager,
private var extensionClass: Class<T>?,
private val isDynamic: Boolean) : ExtensionPoint<T> {
// immutable list, never modified in-place, only swapped atomically
@Volatile
private var cachedExtensions: PersistentList<T>? = null
@Volatile
private var cachedExtensionsAsArray: Array<T>? = null
// guarded by this
@Volatile
private var adapters = persistentListOf<ExtensionComponentAdapter>()
@Volatile
private var adaptersAreSorted = true
// guarded by this
private var listeners = persistentListOf<ExtensionPointListener<T>>()
private val keyMapperToCacheRef = AtomicReference<ConcurrentMap<*, Map<*, *>>>()
@Volatile
private var keyMapperToCache: ConcurrentMap<*, Map<*, *>>? = null
companion object {
fun setCheckCanceledAction(checkCanceled: Runnable) {
@@ -72,9 +69,15 @@ sealed class ExtensionPointImpl<T : Any>(val name: String,
}
fun <CACHE_KEY : Any?, V : Any?> getCacheMap(): ConcurrentMap<CACHE_KEY, V> {
var keyMapperToCache = keyMapperToCacheRef.get()
var keyMapperToCache = keyMapperToCache
if (keyMapperToCache == null) {
keyMapperToCache = keyMapperToCacheRef.updateAndGet { prev -> prev ?: ConcurrentHashMap<Any?, Map<*, *>>() }
synchronized(this) {
keyMapperToCache = this.keyMapperToCache
if (keyMapperToCache == null) {
keyMapperToCache = ConcurrentHashMap<Any?, Map<*, *>>()
this.keyMapperToCache = keyMapperToCache
}
}
}
@Suppress("UNCHECKED_CAST")
return keyMapperToCache as ConcurrentMap<CACHE_KEY, V>
@@ -216,7 +219,7 @@ sealed class ExtensionPointImpl<T : Any>(val name: String,
}
}
}
return array!!
return array!!.clone()
}
/**
@@ -279,20 +282,11 @@ sealed class ExtensionPointImpl<T : Any>(val name: String,
}
private fun createSequence(): Sequence<T> {
val size: Int
val adapters = sortedAdapters
size = adapters.size
if (size == 0) {
return emptySequence()
}
else {
return adapters.asSequence().mapNotNull(::processAdapter)
}
return if (adapters.isEmpty()) emptySequence() else adapters.asSequence().mapNotNull(::processAdapter)
}
override fun size(): Int {
return cachedExtensions?.size ?: adapters.size
}
override fun size(): Int = adapters.size
val sortedAdapters: List<ExtensionComponentAdapter>
get() {
@@ -454,15 +448,16 @@ sealed class ExtensionPointImpl<T : Any>(val name: String,
cachedExtensions = newList.toPersistentList()
cachedExtensionsAsArray = null
val result = if (newList.isEmpty()) {
persistentListOf<ExtensionComponentAdapter>()
adapters = if (newList.isEmpty()) {
persistentListOf()
}
else {
newList.map {
ObjectComponentAdapter(instance = it, pluginDescriptor = extensionPointPluginDescriptor, loadingOrder = LoadingOrder.ANY)
}.toPersistentList()
adapters.mutate { list ->
newList.mapTo(list) {
ObjectComponentAdapter(instance = it, pluginDescriptor = extensionPointPluginDescriptor, loadingOrder = LoadingOrder.ANY)
}
}
}
adapters = result
adaptersAreSorted = true
POINTS_IN_READONLY_MODE!!.add(this)
@@ -761,8 +756,7 @@ sealed class ExtensionPointImpl<T : Any>(val name: String,
}
fun clearUserCache() {
val map = keyMapperToCacheRef.get()
map?.clear()
keyMapperToCache?.clear()
}
private fun clearCache() {
@@ -790,19 +784,20 @@ sealed class ExtensionPointImpl<T : Any>(val name: String,
* `adapters` is modified directly without copying - method must be called only during start-up.
*/
@Synchronized
fun registerExtensions(descriptors: List<ExtensionDescriptor>,
fun registerExtensions(descriptors: PersistentList<ExtensionDescriptor>,
pluginDescriptor: PluginDescriptor,
listenerCallbacks: MutableList<in Runnable>?) {
adaptersAreSorted = false
val oldSize = adapters.size
for (extensionElement in descriptors) {
if (extensionElement.os == null || componentManager.isSuitableForOs(extensionElement.os)) {
adapters = adapters.add(createAdapter(descriptor = extensionElement,
for (descriptor in descriptors) {
if (descriptor.os == null || componentManager.isSuitableForOs(descriptor.os)) {
adapters = adapters.add(createAdapter(descriptor = descriptor,
pluginDescriptor = pluginDescriptor,
componentManager = componentManager))
}
}
val newSize = adapters.size
clearCache()

View File

@@ -7,6 +7,7 @@ import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.extensions.ExtensionPoint
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.progress.ProcessCanceledException
import kotlinx.collections.immutable.persistentListOf
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.NotNull
import java.util.AbstractMap.SimpleImmutableEntry
@@ -82,7 +83,7 @@ object ExtensionProcessingHelper {
cache = prev
}
}
return cache.get(key) ?: emptyList()
return cache.get(key) ?: persistentListOf()
}
/**

View File

@@ -14,9 +14,9 @@ internal class InterfaceExtensionPoint<T : Any>(
dynamic: Boolean,
private val hasAttributes: Boolean,
) : ExtensionPointImpl<T>(name, className, pluginDescriptor, componentManager, clazz, dynamic) {
public override fun createAdapter(descriptor: ExtensionDescriptor,
pluginDescriptor: PluginDescriptor,
componentManager: ComponentManager): ExtensionComponentAdapter {
override fun createAdapter(descriptor: ExtensionDescriptor,
pluginDescriptor: PluginDescriptor,
componentManager: ComponentManager): ExtensionComponentAdapter {
val implementationClassName = descriptor.implementation
?: throw componentManager.createError(
"Attribute \"implementation\" is not specified for \"$name\" extension",

View File

@@ -249,7 +249,7 @@ object DynamicPlugins {
}
val epNameToExtensions = module.epNameToExtensions
if (epNameToExtensions != null) {
if (!epNameToExtensions.isEmpty()) {
doCheckExtensionsCanUnloadWithoutRestart(
extensions = epNameToExtensions,
descriptor = module,
@@ -352,11 +352,8 @@ object DynamicPlugins {
*/
@JvmStatic
fun allowLoadUnloadSynchronously(module: IdeaPluginDescriptorImpl): Boolean {
val extensions = (module.unsortedEpNameToExtensionElements.takeIf { it.isNotEmpty() } ?: module.appContainerDescriptor.extensions)
if (extensions != null && !extensions.all {
it.key == UIThemeProvider.EP_NAME.name ||
it.key == BundledKeymapBean.EP_NAME.name
}) {
val extensions = (module.epNameToExtensions.takeIf { it.isNotEmpty() } ?: module.appContainerDescriptor.extensions)
if (!extensions.all { it.key == UIThemeProvider.EP_NAME.name || it.key == BundledKeymapBean.EP_NAME.name }) {
return false
}
return checkNoComponentsOrServiceOverrides(module) == null && module.actions.isEmpty()
@@ -709,15 +706,20 @@ object DynamicPlugins {
val appExtensionArea = app.extensionArea
val priorityUnloadListeners = mutableListOf<Runnable>()
val unloadListeners = mutableListOf<Runnable>()
unregisterUnknownLevelExtensions(module.unsortedEpNameToExtensionElements, module, appExtensionArea, openedProjects,
unregisterUnknownLevelExtensions(module.epNameToExtensions, module, appExtensionArea, openedProjects,
priorityUnloadListeners, unloadListeners)
for (epName in (module.appContainerDescriptor.extensions?.keys ?: emptySet())) {
appExtensionArea.unregisterExtensions(epName, module, priorityUnloadListeners, unloadListeners)
for (epName in module.appContainerDescriptor.extensions.keys) {
appExtensionArea.unregisterExtensions(extensionPointName = epName,
pluginDescriptor = module,
priorityListenerCallbacks = priorityUnloadListeners,
listenerCallbacks = unloadListeners)
}
for (epName in (module.projectContainerDescriptor.extensions?.keys ?: emptySet())) {
for (epName in module.projectContainerDescriptor.extensions.keys) {
for (project in openedProjects) {
(project.extensionArea as ExtensionsAreaImpl).unregisterExtensions(epName, module, priorityUnloadListeners,
unloadListeners)
(project.extensionArea as ExtensionsAreaImpl).unregisterExtensions(extensionPointName = epName,
pluginDescriptor = module,
priorityListenerCallbacks = priorityUnloadListeners,
listenerCallbacks = unloadListeners)
}
}
@@ -796,15 +798,15 @@ object DynamicPlugins {
private inline fun processExtensionPoints(pluginDescriptor: IdeaPluginDescriptorImpl,
projects: List<Project>,
processor: (points: List<ExtensionPointDescriptor>, area: ExtensionsAreaImpl) -> Unit) {
pluginDescriptor.appContainerDescriptor.extensionPoints?.let {
pluginDescriptor.appContainerDescriptor.extensionPoints.let {
processor(it, ApplicationManager.getApplication().extensionArea as ExtensionsAreaImpl)
}
pluginDescriptor.projectContainerDescriptor.extensionPoints?.let { extensionPoints ->
pluginDescriptor.projectContainerDescriptor.extensionPoints.let { extensionPoints ->
for (project in projects) {
processor(extensionPoints, project.extensionArea as ExtensionsAreaImpl)
}
}
pluginDescriptor.moduleContainerDescriptor.extensionPoints?.let { extensionPoints ->
pluginDescriptor.moduleContainerDescriptor.extensionPoints.let { extensionPoints ->
for (project in projects) {
for (module in ModuleManager.getInstance(project).modules) {
processor(extensionPoints, module.extensionArea as ExtensionsAreaImpl)
@@ -1300,7 +1302,7 @@ private fun doCheckExtensionsCanUnloadWithoutRestart(
private fun findPluginExtensionPoint(pluginDescriptor: IdeaPluginDescriptorImpl, epName: String): ExtensionPointDescriptor? {
fun findContainerExtensionPoint(containerDescriptor: ContainerDescriptor): ExtensionPointDescriptor? {
return containerDescriptor.extensionPoints?.find { it.nameEquals(epName, pluginDescriptor) }
return containerDescriptor.extensionPoints.find { it.nameEquals(epName, pluginDescriptor) }
}
return findContainerExtensionPoint(pluginDescriptor.appContainerDescriptor)

View File

@@ -1,5 +1,5 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:Suppress("ReplaceNegatedIsEmptyWithIsNotEmpty", "ReplaceGetOrSet", "ReplacePutWithAssignment")
@file:Suppress("ReplaceNegatedIsEmptyWithIsNotEmpty", "ReplaceGetOrSet", "ReplacePutWithAssignment", "LeakingThis")
package com.intellij.serviceContainer
@@ -424,9 +424,10 @@ abstract class ComponentManagerImpl(
}
if (extensionPoints != null) {
containerDescriptor.extensionPoints?.let {
createExtensionPoints(points = it, componentManager = this, result = extensionPoints, pluginDescriptor = module)
}
createExtensionPoints(points = containerDescriptor.extensionPoints,
componentManager = this,
result = extensionPoints,
pluginDescriptor = module)
}
}
}
@@ -499,7 +500,7 @@ abstract class ComponentManagerImpl(
registerComponents2(pluginDescriptor, containerDescriptor, headless)
return
}
for (descriptor in (containerDescriptor.components ?: return)) {
for (descriptor in (containerDescriptor.components)) {
var implementationClassName = descriptor.implementationClass
if (headless && descriptor.headlessImplementationClass != null) {
if (descriptor.headlessImplementationClass.isEmpty()) {
@@ -560,16 +561,15 @@ abstract class ComponentManagerImpl(
private fun registerComponents2Inner(pluginDescriptor: IdeaPluginDescriptor,
containerDescriptor: ContainerDescriptor,
headless: Boolean) {
val configs = containerDescriptor.components ?: return
val components = containerDescriptor.components
if (components.isEmpty()) {
return
}
val pluginClassLoader = pluginDescriptor.pluginClassLoader
val registrationScope = if (pluginClassLoader is PluginAwareClassLoader) {
pluginClassLoader.pluginCoroutineScope
}
else {
null
}
val registrationScope = if (pluginClassLoader is PluginAwareClassLoader) pluginClassLoader.pluginCoroutineScope else null
val registrar = componentContainer.startRegistration(registrationScope)
for (descriptor in configs) {
for (descriptor in components) {
if (descriptor.os != null && !isSuitableForOs(descriptor.os)) {
continue
}
@@ -960,26 +960,28 @@ abstract class ComponentManagerImpl(
LOG.error("$key it is a service, use getService instead of getComponent")
}
if (adapter is BaseComponentAdapter) {
check(!useInstanceContainer)
if (parent != null && adapter.componentManager !== this) {
LOG.error("getComponent must be called on appropriate container (current: $this, expected: ${adapter.componentManager})")
}
when (adapter) {
is BaseComponentAdapter -> {
check(!useInstanceContainer)
if (parent != null && adapter.componentManager !== this) {
LOG.error("getComponent must be called on appropriate container (current: $this, expected: ${adapter.componentManager})")
}
if (containerState.get() == ContainerState.DISPOSE_COMPLETED) {
adapter.throwAlreadyDisposedError(this)
if (containerState.get() == ContainerState.DISPOSE_COMPLETED) {
adapter.throwAlreadyDisposedError(this)
}
return adapter.getInstance(adapter.componentManager, key)
}
is HolderAdapter -> {
check(useInstanceContainer)
// TODO asserts
val holder = adapter.holder
@Suppress("UNCHECKED_CAST")
return holder.getOrCreateInstanceBlocking(key.name, key) as T
}
else -> {
return null
}
return adapter.getInstance(adapter.componentManager, key)
}
else if (adapter is HolderAdapter) {
check(useInstanceContainer)
// TODO asserts
val holder = adapter.holder
@Suppress("UNCHECKED_CAST")
return holder.getOrCreateInstanceBlocking(key.name, key) as T
}
else {
return null
}
}
@@ -1131,7 +1133,7 @@ abstract class ComponentManagerImpl(
return messageBus
}
@Suppress("RetrievingService")
@Suppress("RetrievingService", "SimplifiableServiceRetrieving")
messageBus = getApplication()!!.getService(MessageBusFactory::class.java).createMessageBus(this, parent?.messageBus) as MessageBusImpl
if (StartUpMeasurer.isMeasuringPluginStartupCosts()) {
messageBus.setMessageDeliveryListener { topic, messageName, handler, duration ->

View File

@@ -46,10 +46,9 @@ internal class CollectFUStatisticsAction : GotoActionBase(), DumbAware {
override fun gotoActionPerformed(e: AnActionEvent) {
val project = e.project ?: return
val projectCollectors = UsageCollectors.PROJECT_EP_NAME.extensionList
val applicationCollectors = UsageCollectors.APPLICATION_EP_NAME.extensionList
val collectors = (projectCollectors + applicationCollectors).map(UsageCollectorBean::getCollector)
val collectors = (UsageCollectors.PROJECT_EP_NAME.lazySequence() + UsageCollectors.APPLICATION_EP_NAME.lazySequence())
.map(UsageCollectorBean::getCollector)
.toList()
val ids = collectors.mapTo(HashMultiset.create()) { it.group.id }
val items = collectors

View File

@@ -41,9 +41,11 @@ object UsageCollectors {
}
internal fun getProjectCollectors(invoker: UsagesCollectorConsumer): Collection<ProjectUsagesCollector> {
if (isCalledFromPlugin(invoker)) return emptyList()
if (isCalledFromPlugin(invoker)) {
return emptyList()
}
return PROJECT_EP_NAME.extensions.asSequence()
return PROJECT_EP_NAME.extensionList.asSequence()
.map { it.collector as ProjectUsagesCollector }
.filter { isValidCollector(it) }
.toList()