Skip to content

Commit 0c8cf35

Browse files
authored
Merge pull request #2504 from digma-ai/improve-navigation-change-service
Improve navigation change service
2 parents 0ed3428 + 0f34c88 commit 0c8cf35

File tree

2 files changed

+85
-64
lines changed

2 files changed

+85
-64
lines changed

jvm-common/src/main/kotlin/org/digma/intellij/plugin/idea/navigation/NavigationDiscoveryChangeService.kt

Lines changed: 80 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.digma.intellij.plugin.idea.navigation
22

33
import com.intellij.openapi.Disposable
4+
import com.intellij.openapi.application.readAction
45
import com.intellij.openapi.components.Service
56
import com.intellij.openapi.diagnostic.Logger
67
import com.intellij.openapi.project.Project
@@ -23,9 +24,8 @@ import kotlinx.coroutines.isActive
2324
import kotlinx.coroutines.launch
2425
import kotlinx.datetime.Clock
2526
import kotlinx.datetime.Instant
27+
import org.apache.commons.codec.digest.DigestUtils
2628
import org.digma.intellij.plugin.common.isProjectValid
27-
import org.digma.intellij.plugin.common.isValidVirtualFile
28-
import org.digma.intellij.plugin.common.runInReadAccessWithResult
2929
import org.digma.intellij.plugin.errorreporting.ErrorReporter
3030
import org.digma.intellij.plugin.idea.psi.isJvmSupportedFile
3131
import org.digma.intellij.plugin.log.Log
@@ -56,7 +56,7 @@ class NavigationDiscoveryChangeService(private val project: Project, private val
5656

5757
private val logger = Logger.getInstance(this::class.java)
5858

59-
private val quitePeriod = 5.seconds
59+
private val quitePeriod = 30.seconds
6060

6161
//using LinkedHashSet to keep insertion order so we process files in the order they were changed.
6262
//there is only write operation to this set, never read or remove. before processing, the set is replaced with a new instance
@@ -77,9 +77,9 @@ class NavigationDiscoveryChangeService(private val project: Project, private val
7777
private var lastChangedFileEventTime: Instant? = null
7878
private var lastBulkFileChangeEventTime: Instant? = null
7979

80-
private val changeFilesProcessingJob: Job = launchBulkProcessingLoop(changedFilesBulks, ChangedFileProcessor())
81-
private val bulkEventsProcessingJob: Job = launchBulkProcessingLoop(bulkEventsBulks, FileEventsProcessor())
82-
private val quitePeriodManagerJob: Job = launchQuitePeriodManager()
80+
private var changeFilesProcessingJob: Job? = null
81+
private var bulkEventsProcessingJob: Job? = null
82+
private var quitePeriodManagerJob: Job? = null
8383
private var launchFullUpdateJob: Job? = null
8484

8585
private val paused = AtomicBoolean(false)
@@ -88,6 +88,11 @@ class NavigationDiscoveryChangeService(private val project: Project, private val
8888

8989

9090
companion object {
91+
92+
fun isEnabled(): Boolean {
93+
return java.lang.Boolean.parseBoolean(System.getProperty("org.digma.intellij.plugin.enableNavigationDiscoveryChangedFilesUpdate", "true"))
94+
}
95+
9196
fun createChangedFilesSet(): MutableSet<String> {
9297
return LinkedHashSet()
9398
}
@@ -99,15 +104,24 @@ class NavigationDiscoveryChangeService(private val project: Project, private val
99104

100105

101106
init {
102-
PsiManager.getInstance(project).addPsiTreeChangeListener(NavigationDiscoveryPsiTreeChangeListener(), this)
103-
VirtualFileManager.getInstance().addAsyncFileListener(NavigationDiscoveryAsyncFileChangeListener(), this)
107+
if (isEnabled()) {
108+
109+
changeFilesProcessingJob = launchBulkProcessingLoop(changedFilesBulks, ChangedFileProcessor())
110+
bulkEventsProcessingJob = launchBulkProcessingLoop(bulkEventsBulks, FileEventsProcessor())
111+
quitePeriodManagerJob = launchQuitePeriodManager()
112+
113+
PsiManager.getInstance(project).addPsiTreeChangeListener(NavigationDiscoveryPsiTreeChangeListener(), this)
114+
VirtualFileManager.getInstance().addAsyncFileListener(NavigationDiscoveryAsyncFileChangeListener(), this)
115+
} else {
116+
Log.log(logger::info, project, "I am disabled, not starting listeners")
117+
}
104118
}
105119

106120

107121
override fun dispose() {
108-
changeFilesProcessingJob.cancel()
109-
bulkEventsProcessingJob.cancel()
110-
quitePeriodManagerJob.cancel()
122+
changeFilesProcessingJob?.cancel()
123+
bulkEventsProcessingJob?.cancel()
124+
quitePeriodManagerJob?.cancel()
111125
}
112126

113127

@@ -173,6 +187,7 @@ class NavigationDiscoveryChangeService(private val project: Project, private val
173187

174188

175189
private fun <T> launchBulkProcessingLoop(queue: Queue<Iterable<T>>, itemProcessor: ItemProcessor<T>): Job {
190+
176191
return cs.launch {
177192
Log.log(logger::trace, project, "Starting bulk processing loop for {}", itemProcessor::class.java)
178193
while (isActive && isProjectValid(project)) {
@@ -183,9 +198,9 @@ class NavigationDiscoveryChangeService(private val project: Project, private val
183198
if (bulk == null) {
184199
delay(5.seconds)
185200
} else {
201+
Log.log(logger::trace, project, "processing bulk of {}", itemProcessor::class.java)
186202
bulk.forEach {
187-
if (!isActive) {
188-
// Cancelled, stop processing
203+
if (!isActive || isPaused()) {
189204
return@forEach
190205
}
191206
itemProcessor.process(it)
@@ -230,6 +245,8 @@ class NavigationDiscoveryChangeService(private val project: Project, private val
230245
return
231246
}
232247

248+
pause()
249+
233250
/*
234251
when the currently running coroutine completes another one can start.
235252
if another one wants to start while calling the buildNavigationDiscoveryFullUpdate it will be
@@ -239,8 +256,7 @@ class NavigationDiscoveryChangeService(private val project: Project, private val
239256
but as said there is a very small chance for that to happen.
240257
*/
241258

242-
243-
//make sure not to launch twice because events keep coming until pause is called
259+
//another safety lock to make sure not to launch twice because events keep coming.
244260
launchFullUpdateLock.withLock {
245261

246262
if (launchFullUpdateJob?.isActive == true) {
@@ -257,7 +273,7 @@ class NavigationDiscoveryChangeService(private val project: Project, private val
257273

258274
//pause and clear all collected events so no more update tasks will run until resume
259275
Log.log(logger::trace, project, "pausing and clearing all collected events in launchFullUpdateJob")
260-
pause()
276+
261277
//replace the lists , never call clear or remove to avoid concurrent modifications
262278
changedFiles = createChangedFilesSet()
263279
bulkEvents = createBulkEventsList()
@@ -269,7 +285,7 @@ class NavigationDiscoveryChangeService(private val project: Project, private val
269285
//during this time the update tasks that already started will probably finish
270286
Log.log(logger::trace, project, "waiting for quite period in launchFullUpdateJob")
271287
var quitePeriod = ZERO
272-
while (isActive && quitePeriod < 10.seconds) {
288+
while (isActive && quitePeriod < 30.seconds) {
273289

274290
Log.log(logger::trace, project, "checking quite period in launchFullUpdateJob, current quite period {}", quitePeriod)
275291

@@ -312,6 +328,8 @@ class NavigationDiscoveryChangeService(private val project: Project, private val
312328
Log.log(logger::trace, project, "quite period completed in launchFullUpdateJob, launching full update")
313329
//what ever happens resume must be called before this coroutine completes
314330
try {
331+
//resume before launching full update to make sure we don't miss updates
332+
resume()
315333
//if the coroutine was canceled don't call the update, probably the project was closed
316334
if (isActive) {
317335
Log.log(logger::info, project, "launching full update span discovery in launchFullUpdateJob")
@@ -343,20 +361,29 @@ class NavigationDiscoveryChangeService(private val project: Project, private val
343361
return
344362
}
345363

346-
//protect against high memory consumption, if so many files are changed at once pause collecting changed files and launch full update
347-
//200 is just a number that seems reasonable to do a full update instead of updating separate files
348-
if (changedFiles.size > 200) {
364+
//protect against high memory consumption, if so many files are changed at once, pause collecting changed files and launch full update.
365+
//100 is just a number that seems reasonable to do a full update instead of updating separate files.
366+
if (changedFiles.size > 100) {
349367

350368
Log.log(logger::trace, project, "discovered too many changed files {}", changedFiles.size)
351369

352-
pauseAndLaunchFullUpdate()
370+
launchFullUpdateLock.withLock {
371+
372+
if (isPaused()) {
373+
return@withLock
374+
}
375+
376+
pauseAndLaunchFullUpdate()
353377

354-
ActivityMonitor.getInstance(project).registerCustomEvent(
355-
"LargeBulkUpdate", mapOf(
356-
"eventName" to "changeFiles",
357-
"changedFiles.size" to changedFiles.size
378+
ActivityMonitor.getInstance(project).registerCustomEvent(
379+
"LargeBulkUpdate", mapOf(
380+
"eventName" to "changeFiles",
381+
"changedFiles.size" to changedFiles.size,
382+
"project.hash" to DigestUtils.md2Hex(project.name)
383+
)
358384
)
359-
)
385+
}
386+
360387
return
361388
}
362389
Log.log(logger::trace, project, "adding changed file {}", virtualFile.url)
@@ -397,20 +424,30 @@ class NavigationDiscoveryChangeService(private val project: Project, private val
397424
return
398425
}
399426

400-
//protect against high memory consumption, if so many files are changed at once pause collecting events and launch a full update
401-
//200 is just a number that seems reasonable to do a full update instead of updating separate files
402-
if (bulkEvents.size > 200) {
427+
//protect against high memory consumption, if so many files are changed at once, pause collecting events and launch a full update.
428+
//100 is just a number that seems reasonable to do a full update instead of updating separate files.
429+
if (bulkEvents.size > 100 || events.size > 100) {
403430

404431
Log.log(logger::trace, project, "discovered too many bulk change events {}", bulkEvents.size)
405432

406-
pauseAndLaunchFullUpdate()
433+
launchFullUpdateLock.withLock {
434+
435+
if (isPaused()) {
436+
return@withLock
437+
}
407438

408-
ActivityMonitor.getInstance(project).registerCustomEvent(
409-
"LargeBulkUpdate", mapOf(
410-
"eventName" to "bulkEvents",
411-
"bulkEvents.size" to bulkEvents.size
439+
pauseAndLaunchFullUpdate()
440+
441+
val size = if (bulkEvents.size > 100) bulkEvents.size else events.size
442+
ActivityMonitor.getInstance(project).registerCustomEvent(
443+
"LargeBulkUpdate", mapOf(
444+
"eventName" to "bulkEvents",
445+
"bulkEvents.size" to size,
446+
"project.hash" to DigestUtils.md2Hex(project.name)
447+
)
412448
)
413-
)
449+
}
450+
414451
return
415452
}
416453
bulkEvents.addAll(events)
@@ -449,25 +486,21 @@ class NavigationDiscoveryChangeService(private val project: Project, private val
449486

450487

451488
private interface ItemProcessor<T> {
452-
fun process(item: T)
489+
suspend fun process(item: T)
453490
}
454491

455492

456493
private inner class FileEventsProcessor : ItemProcessor<VFileEvent> {
457-
override fun process(item: VFileEvent) {
494+
override suspend fun process(item: VFileEvent) {
458495

459496
try {
460497

461498
if (isRelevantFile(project, item.file)) {
462499

463500
//run a quick check if the event is for a java or kotlin file and abort processing if not.
464-
//if we can't find PsiFile continue processing, maybe it's a copy or delete or other event that we want to process
465501
item.file?.let { vf ->
466-
val psiFile = findPsiFileForVirtualFile(vf)
467-
psiFile?.let { psf ->
468-
if (!isJavaOrKotlinFile(psf)) {
469-
return
470-
}
502+
if (!isJavaOrKotlinFile(vf)) {
503+
return
471504
}
472505
}
473506

@@ -524,7 +557,7 @@ class NavigationDiscoveryChangeService(private val project: Project, private val
524557

525558

526559
private inner class ChangedFileProcessor : ItemProcessor<String> {
527-
override fun process(item: String) {
560+
override suspend fun process(item: String) {
528561

529562
try {
530563

@@ -542,9 +575,10 @@ class NavigationDiscoveryChangeService(private val project: Project, private val
542575
}
543576

544577

545-
private fun updateNavigation(project: Project, file: VirtualFile) {
578+
private suspend fun updateNavigation(project: Project, file: VirtualFile) {
546579
if (isValidRelevantFile(project, file)) {
547-
val psiFile = runInReadAccessWithResult {
580+
581+
val psiFile = readAction {
548582
PsiManager.getInstance(project).findFile(file)
549583
}
550584
psiFile?.let {
@@ -583,18 +617,6 @@ class NavigationDiscoveryChangeService(private val project: Project, private val
583617
}
584618

585619

586-
private fun findPsiFileForVirtualFile(virtualFile: VirtualFile): PsiFile? {
587-
return try {
588-
runInReadAccessWithResult {
589-
virtualFile.takeIf { isValidVirtualFile(virtualFile) }?.let {
590-
PsiManager.getInstance(project).findFile(it)
591-
}
592-
}
593-
} catch (e: Throwable) {
594-
null
595-
}
596-
}
597-
598620
private fun isJavaOrKotlinFile(psiFile: PsiFile): Boolean {
599621
return psiFile.language.displayName.equals("Java", true) ||
600622
psiFile.language.displayName.equals("Kotlin", true)

jvm-common/src/main/kotlin/org/digma/intellij/plugin/idea/navigation/navigationUtils.kt

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
package org.digma.intellij.plugin.idea.navigation
22

3+
import com.intellij.openapi.application.readAction
34
import com.intellij.openapi.project.Project
45
import com.intellij.openapi.roots.ProjectFileIndex
56
import com.intellij.openapi.util.Computable
67
import com.intellij.openapi.vfs.VirtualFile
7-
import org.digma.intellij.plugin.common.ReadActions
88
import org.digma.intellij.plugin.common.executeCatchingIgnorePCE
99
import org.digma.intellij.plugin.common.executeCatchingWithResultIgnorePCE
1010
import org.digma.intellij.plugin.common.isProjectValid
1111
import org.digma.intellij.plugin.common.isValidVirtualFile
1212
import org.digma.intellij.plugin.common.runWIthRetryIgnorePCE
1313
import org.digma.intellij.plugin.common.runWIthRetryWithResultIgnorePCE
1414
import org.digma.intellij.plugin.idea.navigation.model.NavigationProcessContext
15-
import java.util.function.Supplier
1615

1716

1817
fun executeCatchingWithRetry(
@@ -53,7 +52,7 @@ fun isRelevantFile(project: Project, file: VirtualFile?): Boolean {
5352

5453
//this method checks isInContent which needs read access. don't call it from EDT events like
5554
// onChange in psi tree listeners because it will cause short freezes, only call it on background processing
56-
fun isValidRelevantFile(project: Project, file: VirtualFile?): Boolean {
55+
suspend fun isValidRelevantFile(project: Project, file: VirtualFile?): Boolean {
5756
return isRelevantFile(project, file) &&
5857
isValidVirtualFile(file) &&
5958
file != null &&
@@ -63,8 +62,8 @@ fun isValidRelevantFile(project: Project, file: VirtualFile?): Boolean {
6362

6463
//don't call this method on EDT events like onChange in psi tree listeners because it will
6564
// cause short freezes, only call it on background processing
66-
fun isInContent(project: Project, file: VirtualFile): Boolean {
67-
return ReadActions.ensureReadAction(Supplier {
65+
suspend fun isInContent(project: Project, file: VirtualFile): Boolean {
66+
return readAction {
6867
ProjectFileIndex.getInstance(project).isInContent(file)
69-
})
68+
}
7069
}

0 commit comments

Comments
 (0)