[debugger] IDEA-362335 Hot swap button: limit local history search to prevent UI slowness

(cherry picked from commit 7e959747892b039ba66d98a6090c2b1212376a76)

IJ-CR-148479

GitOrigin-RevId: 7f75e41e28915ee846cddbecb0ae1109d268294b
This commit is contained in:
Maksim Zuev
2024-11-04 16:21:03 +01:00
committed by intellij-monorepo-bot
parent 9421bfc3bb
commit 5d907d803d

View File

@@ -23,6 +23,7 @@ import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
import kotlinx.coroutines.*
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.TestOnly
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
@Service(Service.Level.APP)
@@ -33,13 +34,23 @@ private class ChangesProcessingService(private val coroutineScope: CoroutineScop
@OptIn(ExperimentalCoroutinesApi::class)
private val documentChangeDispatcher = Dispatchers.Default.limitedParallelism(1)
private val allCache = Long2ObjectOpenHashMap<Object2IntOpenHashMap<VirtualFile>>()
private val queueSizeEstimate = AtomicInteger()
private var lastSearchTimeNs = -1L
private val documentListener = object : DocumentListener {
override fun documentChanged(event: DocumentEvent) {
if (logger.isDebugEnabled) {
logger.debug("Document changed: ${event.document}")
}
onDocumentChange(event.document)
queueSizeEstimate.incrementAndGet()
coroutineScope.launch(documentChangeDispatcher) {
try {
onDocumentChange(event.document)
}
finally {
queueSizeEstimate.decrementAndGet()
}
}
}
}
@@ -61,8 +72,8 @@ private class ChangesProcessingService(private val coroutineScope: CoroutineScop
collectors.add(collector, collector)
}
private fun onDocumentChange(document: Document) = coroutineScope.launch(documentChangeDispatcher) {
val virtualFile = FileDocumentManager.getInstance().getFile(document) ?: return@launch
private suspend fun onDocumentChange(document: Document) = coroutineScope {
val virtualFile = FileDocumentManager.getInstance().getFile(document) ?: return@coroutineScope
val filteredCollectors = collectors
.map { collector -> collector to async(Dispatchers.Default) { collector.filters.all { it.isApplicable(virtualFile) } } }
.filter { it.second.await() }
@@ -71,7 +82,7 @@ private class ChangesProcessingService(private val coroutineScope: CoroutineScop
if (logger.isDebugEnabled) {
logger.debug("Document change skipped as filtered: $document")
}
return@launch
return@coroutineScope
}
if (logger.isDebugEnabled) {
logger.debug("Document change processing: $document")
@@ -82,16 +93,31 @@ private class ChangesProcessingService(private val coroutineScope: CoroutineScop
for ((timestamp, collectors) in groupedByTimeStamp) {
val cache = allCache.computeIfAbsent(timestamp) { Object2IntOpenHashMap() }
val doLocalHistorySearch = collectors.none { it.hasMassiveChanges() }
val doLocalHistorySearch = canDoLocalHistorySearch()
val timeStartNs = System.nanoTime()
val hasChanges = hasChangesSinceLastReset(virtualFile, timestamp, contentHash, doLocalHistorySearch, cache)
if (doLocalHistorySearch) {
lastSearchTimeNs = System.nanoTime() - timeStartNs
}
for (collector in collectors) {
launch(Dispatchers.Default) {
coroutineScope.launch(Dispatchers.Default) {
collector.processDocumentChange(hasChanges, virtualFile, document)
}
}
}
}
/**
* This check is introduced for optimization.
* When there are a large number of changed files, each new file has a minimal effect on the overall status,
* whether there are changes for HotSwap.
* For the sake of performance, some checks (like local history) can be skipped.
*
* With this check applied, the search time for a set of changes should be no more than 1 second.
*/
private fun canDoLocalHistorySearch(): Boolean =
queueSizeEstimate.get() < 5 && lastSearchTimeNs < TimeUnit.MILLISECONDS.toNanos(200)
private fun dropUnusedTimestamps(active: Set<Long>) {
allCache.keys.minus(active).forEach { allCache.remove(it) }
}
@@ -135,14 +161,6 @@ class SourceFileChangesCollectorImpl(
currentChanges = hashSetOf()
}
/**
* This check is introduced for optimization.
* When there are a large number of changed files, each new file has a minimal effect on the overall status,
* whether there are changes for HotSwap.
* For the sake of performance, some checks (like local history) can be skipped.
*/
internal fun hasMassiveChanges(): Boolean = currentChanges.size > 100
internal fun processDocumentChange(hasChangesSinceLastReset: Boolean, file: VirtualFile, document: Document) {
val currentChanges = currentChanges
if (hasChangesSinceLastReset) {