11package org.digma.intellij.plugin.idea.navigation
22
33import com.intellij.openapi.Disposable
4+ import com.intellij.openapi.application.readAction
45import com.intellij.openapi.components.Service
56import com.intellij.openapi.diagnostic.Logger
67import com.intellij.openapi.project.Project
@@ -23,9 +24,8 @@ import kotlinx.coroutines.isActive
2324import kotlinx.coroutines.launch
2425import kotlinx.datetime.Clock
2526import kotlinx.datetime.Instant
27+ import org.apache.commons.codec.digest.DigestUtils
2628import org.digma.intellij.plugin.common.isProjectValid
27- import org.digma.intellij.plugin.common.isValidVirtualFile
28- import org.digma.intellij.plugin.common.runInReadAccessWithResult
2929import org.digma.intellij.plugin.errorreporting.ErrorReporter
3030import org.digma.intellij.plugin.idea.psi.isJvmSupportedFile
3131import 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 )
0 commit comments