Skip to content

Commit 7a39ac8

Browse files
#247 Implement automatic refresh + fix UI freeze on initial plugin load (reloadSummariesPanelInBackground)
1 parent 21246fb commit 7a39ac8

File tree

7 files changed

+120
-61
lines changed

7 files changed

+120
-61
lines changed

ide-common/src/main/java/org/digma/intellij/plugin/refreshInsightsTask/RefreshService.kt

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import com.intellij.openapi.diagnostic.Logger
44
import com.intellij.openapi.editor.Editor
55
import com.intellij.openapi.fileEditor.FileEditorManager
66
import com.intellij.openapi.project.Project
7+
import com.intellij.openapi.rd.util.withUiContext
8+
import com.intellij.openapi.vfs.VirtualFile
79
import org.digma.intellij.plugin.common.Backgroundable
810
import org.digma.intellij.plugin.document.DocumentInfoContainer
911
import org.digma.intellij.plugin.document.DocumentInfoService
@@ -28,22 +30,37 @@ class RefreshService(private val project: Project) {
2830
}
2931
}
3032

31-
fun refreshAll() {
33+
suspend fun refreshAllForCurrentFile(file: VirtualFile) {
34+
Log.log(logger::debug, "Automatic refreshAllForCurrentFile started for file = {}", file.name)
35+
val scope = insightsViewService.model.scope
36+
val selectedTextEditor = withUiContext {
37+
// this code is on the UI thread
38+
FileEditorManager.getInstance(project).selectedTextEditor
39+
}
40+
if (scope is MethodScope) {
41+
val documentInfoContainer = DocumentInfoService.getInstance(project).getDocumentInfoByMethodInfo(scope.getMethodInfo())
42+
43+
Log.log(logger::debug, "updateInsightsCacheForActiveDocument starts for file = {}", file.name)
44+
updateInsightsCacheForActiveDocument(selectedTextEditor, documentInfoContainer, scope)
45+
Log.log(logger::debug, "updateInsightsCacheForActiveDocument finished for file = {}", file.name)
46+
}
47+
}
48+
49+
fun refreshAllInBackground() {
3250
if (isGeneralRefreshButtonEnabled.getAndSet(false)) {
3351
val scope = insightsViewService.model.scope
3452
val selectedTextEditor = FileEditorManager.getInstance(project).selectedTextEditor
3553
if (scope is MethodScope) {
3654
val documentInfoContainer = DocumentInfoService.getInstance(project).getDocumentInfoByMethodInfo(scope.getMethodInfo())
3755

38-
3956
//updateInsightsCache must run in the background, the use of refreshInsightsTaskScheduledLock makes sure no two threads
4057
//update InsightsCache at the same time.
4158
val task = Runnable {
4259
refreshInsightsTaskScheduledLock.lock()
4360
Log.log(logger::debug, "Lock acquired for refreshAll to {}. ", documentInfoContainer?.documentInfo?.fileUri)
4461
try {
4562
notifyRefreshInsightsTaskStarted(documentInfoContainer?.documentInfo?.fileUri)
46-
updateInsightsCache(selectedTextEditor, documentInfoContainer, scope)
63+
updateInsightsCacheForActiveDocument(selectedTextEditor, documentInfoContainer, scope)
4764
} finally {
4865
refreshInsightsTaskScheduledLock.unlock()
4966
Log.log(logger::debug, "Lock released for refreshAll to {}. ", documentInfoContainer?.documentInfo?.fileUri)
@@ -56,22 +73,13 @@ class RefreshService(private val project: Project) {
5673
}
5774
}
5875

59-
private fun updateInsightsCache(selectedTextEditor: Editor?, documentInfoContainer: DocumentInfoContainer?, scope: MethodScope) {
76+
private fun updateInsightsCacheForActiveDocument(selectedTextEditor: Editor?, documentInfoContainer: DocumentInfoContainer?, scope: MethodScope) {
6077
val selectedDocument = selectedTextEditor?.document
61-
62-
// firstly update actual document that is opened
6378
if (selectedDocument != null) {
6479
documentInfoContainer?.updateCache()
6580
}
6681
insightsViewService.updateInsightsModel(scope.getMethodInfo())
6782
errorsViewService.updateErrorsModel(scope.getMethodInfo())
68-
69-
// update all our local cache in the background
70-
if (selectedDocument != null) {
71-
DocumentInfoService.getInstance(project).updateCacheForOtherOpenedDocuments(documentInfoContainer?.documentInfo?.fileUri)
72-
} else {
73-
DocumentInfoService.getInstance(project).updateCacheForAllOpenedDocuments()
74-
}
7583
}
7684

7785
private fun notifyRefreshInsightsTaskStarted(fileUri: String?) {
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package org.digma.intellij.plugin.editor
2+
3+
import com.intellij.openapi.diagnostic.Logger
4+
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
5+
import com.intellij.openapi.fileEditor.FileEditorManagerListener
6+
import com.intellij.openapi.project.Project
7+
import com.intellij.openapi.rd.util.launchBackground
8+
import com.intellij.openapi.vfs.VirtualFile
9+
import com.jetbrains.rd.util.lifetime.LifetimeDefinition
10+
import kotlinx.coroutines.delay
11+
import org.digma.intellij.plugin.log.Log
12+
import org.digma.intellij.plugin.refreshInsightsTask.RefreshService
13+
14+
class GeneralFileEditorListener(val project: Project) : FileEditorManagerListener {
15+
private val logger: Logger = Logger.getInstance(GeneralFileEditorListener::class.java)
16+
17+
private val refreshService: RefreshService
18+
19+
// lifetimeDefinitionMap keeps info about the lifetime coroutine tasks of each opened file
20+
private val lifetimeDefinitionMap: MutableMap<VirtualFile, LifetimeDefinition> = HashMap()
21+
private val lifetimeDefinitionMapLock = Object()
22+
23+
init {
24+
refreshService = project.getService(RefreshService::class.java)
25+
}
26+
27+
override fun selectionChanged(event: FileEditorManagerEvent) {
28+
super.selectionChanged(event)
29+
refreshAllInsightsForActiveFile(event.oldFile, event.newFile)
30+
}
31+
32+
33+
private fun refreshAllInsightsForActiveFile(oldFile: VirtualFile?, newFile: VirtualFile?) {
34+
if (newFile != null) {
35+
Log.log(logger::debug, "Starting refreshAllInsightsForActiveFile = {}", newFile)
36+
// lock is required here to avoid access and modification of the same map from multiple threads
37+
synchronized(lifetimeDefinitionMapLock) {
38+
createCoroutineTaskForActualFocusedFile(newFile)
39+
terminateCoroutineTaskForPreviouslyFocusedOpenedFile(oldFile)
40+
}
41+
Log.log(logger::debug, "Finished refreshAllInsightsForActiveFile = {}", newFile)
42+
}
43+
}
44+
45+
private fun createCoroutineTaskForActualFocusedFile(newFile: VirtualFile) {
46+
// create a lifetime for actual focused file
47+
// Lifetime by itself is a lightweight and heavily optimized object. It is okay to create new lifetime on each selection change without any performance impact whatsoever.
48+
val newLifetimeDefinition = LifetimeDefinition()
49+
newLifetimeDefinition.lifetime.launchBackground {
50+
// next logic is the Coroutine's body
51+
while (true) {
52+
delay(10000)// 10 seconds
53+
refreshService.refreshAllForCurrentFile(newFile)
54+
}
55+
}
56+
lifetimeDefinitionMap[newFile] = newLifetimeDefinition
57+
}
58+
59+
private fun terminateCoroutineTaskForPreviouslyFocusedOpenedFile(oldFile: VirtualFile?) {
60+
if (lifetimeDefinitionMap[oldFile] != null) {
61+
// terminate the corresponding lifetime, first of all. this will also cancel current task execution
62+
lifetimeDefinitionMap[oldFile]?.terminate(true)
63+
// remove the lifetime for previously active file from the map
64+
lifetimeDefinitionMap.remove(oldFile)
65+
}
66+
}
67+
}

ide-common/src/main/kotlin/org/digma/intellij/plugin/ui/service/SummaryViewService.kt

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import org.digma.intellij.plugin.summary.SummariesProvider
1616
import org.digma.intellij.plugin.ui.model.PanelModel
1717
import org.digma.intellij.plugin.ui.model.listview.ListViewItem
1818
import java.util.*
19+
import java.util.concurrent.locks.ReentrantLock
1920

2021

2122
class SummaryViewService(project: Project) : AbstractViewService(project) {
@@ -25,6 +26,7 @@ class SummaryViewService(project: Project) : AbstractViewService(project) {
2526

2627
val model = Model()
2728

29+
private val rebuildPanelLock = ReentrantLock()
2830
private val environmentChangeConnection: MessageBusConnection = project.messageBus.connect()
2931

3032
companion object {
@@ -37,7 +39,7 @@ class SummaryViewService(project: Project) : AbstractViewService(project) {
3739

3840
//this is for startup
3941
DumbAwareNotifier.getInstance(project).whenSmart {
40-
reload()
42+
reloadSummariesPanelInBackground(project)
4143
}
4244

4345
//this is for when environment changes or connection lost and regained
@@ -47,16 +49,12 @@ class SummaryViewService(project: Project) : AbstractViewService(project) {
4749

4850
override fun environmentChanged(newEnv: String?) {
4951
Log.log(logger::debug, "environmentChanged called")
50-
Backgroundable.ensureBackground(project, "Summary view Reload") {
51-
reload()
52-
}
52+
reloadSummariesPanelInBackground(project)
5353
}
5454

5555
override fun environmentsListChanged(newEnvironments: MutableList<String>?) {
5656
Log.log(logger::debug, "environmentsListChanged called")
57-
Backgroundable.ensureBackground(project, "Summary view Reload") {
58-
reload()
59-
}
57+
reloadSummariesPanelInBackground(project)
6058
}
6159
})
6260
}
@@ -73,6 +71,19 @@ class SummaryViewService(project: Project) : AbstractViewService(project) {
7371
return true
7472
}
7573

74+
private fun reloadSummariesPanelInBackground(project: Project) {
75+
val task = Runnable {
76+
rebuildPanelLock.lock()
77+
Log.log(logger::debug, "Lock acquired for reload Summaries panel process.")
78+
try {
79+
reload()
80+
} finally {
81+
rebuildPanelLock.unlock()
82+
Log.log(logger::debug, "Lock released for reload Summaries panel process.")
83+
}
84+
}
85+
Backgroundable.ensureBackground(project, "Summary view Reload", task)
86+
}
7687

7788
private fun reload() {
7889
Log.log(logger::debug, "reload called")

src/main/kotlin/org/digma/intellij/plugin/ui/common/EnvironmentsPanel.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.digma.intellij.plugin.ui.common
22

3+
import com.intellij.openapi.application.ApplicationManager
34
import com.intellij.openapi.diagnostic.Logger
45
import com.intellij.openapi.project.Project
56
import com.intellij.ui.components.ActionLink
@@ -188,10 +189,9 @@ class EnvironmentsPanel(
188189
val toolTip = buildToolTip(usageStatusResult, currEnv)
189190
val linkText = buildLinkText(currEnv, isSelectedEnv)
190191

191-
if (SwingUtilities.isEventDispatchThread()) {
192-
buildEnvironmentsPanelButtons(currEnv, linkText, isSelectedEnv, toolTip, hasUsageFunction)
193-
} else {
194-
SwingUtilities.invokeLater {
192+
// Please use this method instead of javax.swing.SwingUtilities.invokeLater(Runnable) or com.intellij.util.ui.UIUtil methods for the reasons described in ModalityState documentation.
193+
ApplicationManager.getApplication().invokeLater {
194+
Runnable {
195195
buildEnvironmentsPanelButtons(currEnv, linkText, isSelectedEnv, toolTip, hasUsageFunction)
196196
}
197197
}

src/main/kotlin/org/digma/intellij/plugin/ui/common/Panels.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ private fun getGeneralRefreshButton(project: Project): JButton {
113113
generalRefreshIconButton.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
114114

115115
generalRefreshIconButton.addActionListener {
116-
refreshService.refreshAll()
116+
refreshService.refreshAllInBackground()
117117
}
118118
return generalRefreshIconButton
119119
}

src/main/kotlin/org/digma/intellij/plugin/ui/list/insights/InsightsCommon.kt

Lines changed: 3 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@ private fun rebuildPanel(
146146

147147
if(insight.customStartTime != null || isRecalculateButtonPressed)
148148
bodyWrapper.add(getTimeInfoMessagePanel(
149-
insightPanel = insightPanel,
150149
customStartTime = insight.customStartTime,
151150
actualStartTime = insight.actualStartTime,
152151
isRecalculateButtonPressed = isRecalculateButtonPressed,
@@ -175,7 +174,6 @@ private fun rebuildPanel(
175174
}
176175

177176
private fun getTimeInfoMessagePanel(
178-
insightPanel: DigmaResettablePanel,
179177
customStartTime: Date?,
180178
actualStartTime: Date?,
181179
isRecalculateButtonPressed: Boolean,
@@ -205,7 +203,7 @@ private fun getTimeInfoMessagePanel(
205203
timeInfoMessageLabelPanel.isOpaque = false
206204
timeInfoMessageLabelPanel.add(timeInfoMessageLabel)
207205
if (shouldShowApplyNewTimeFilterLabel(isRecalculateButtonPressed, identicalStartTimes)) {
208-
timeInfoMessageLabelPanel.add(getRefreshInsightButton(insightPanel, project))
206+
timeInfoMessageLabelPanel.add(getRefreshInsightButton(project))
209207
}
210208
return timeInfoMessageLabelPanel
211209
}
@@ -324,12 +322,11 @@ private fun showHintMessage(
324322
HintManager.getInstance().showHint(recalculateAction, RelativePoint.getSouthWestOf(threeDotsIcon), HintManager.HIDE_BY_ESCAPE, 2000)
325323
}
326324

327-
private fun getRefreshInsightButton(insightPanel: DigmaResettablePanel, project: Project): ActionLink {
325+
private fun getRefreshInsightButton(project: Project): ActionLink {
328326
val refreshAction = ActionLink(REFRESH)
329327
refreshAction.addActionListener {
330328
val refreshService: RefreshService = project.getService(RefreshService::class.java)
331-
refreshService.refreshAll()
332-
rebuildInsightPanel(insightPanel)
329+
refreshService.refreshAllInBackground()
333330
}
334331
refreshAction.border = empty()
335332
refreshAction.isOpaque = false
@@ -356,34 +353,6 @@ fun genericPanelForSingleInsight(project: Project, modelObject: Any?): JPanel {
356353
}
357354

358355

359-
360-
361-
362-
internal fun insightsIconPanelBorder(icon: Icon, text: String, panelsLayoutHelper: PanelsLayoutHelper): JPanel {
363-
364-
val panel = InsightAlignedPanel(panelsLayoutHelper)
365-
panel.layout = BorderLayout()
366-
panel.isOpaque = false
367-
panel.border = empty(2,0,0, getInsightIconPanelRightBorderSize())
368-
369-
val iconLabel = JLabel(icon)
370-
iconLabel.horizontalAlignment = SwingConstants.CENTER
371-
panel.add(iconLabel, BorderLayout.CENTER)
372-
373-
if (text.isNotBlank()) {
374-
val textLabel = JLabel(text)
375-
textLabel.horizontalAlignment = SwingConstants.CENTER
376-
panel.add(textLabel, BorderLayout.SOUTH)
377-
}
378-
379-
addCurrentLargestWidthIconPanel(panelsLayoutHelper,panel.preferredSize.width)
380-
381-
return panel
382-
}
383-
384-
385-
386-
387356
internal fun getInsightIconPanelRightBorderSize():Int{
388357
return 5
389358
}

src/main/resources/META-INF/plugin.xml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,12 @@
9595
<listener
9696
class="org.digma.intellij.plugin.emvironment.EnvironmentChangeHandler"
9797
topic="org.digma.intellij.plugin.analytics.EnvironmentChanged"/>
98-
<listener class="org.digma.intellij.plugin.debugger.DebuggerListener"
99-
topic="com.intellij.xdebugger.XDebuggerManagerListener"/>
98+
<listener
99+
class="org.digma.intellij.plugin.debugger.DebuggerListener"
100+
topic="com.intellij.xdebugger.XDebuggerManagerListener"/>
101+
<listener
102+
class="org.digma.intellij.plugin.editor.GeneralFileEditorListener"
103+
topic="com.intellij.openapi.fileEditor.FileEditorManagerListener"/>
100104
</projectListeners>
101105

102106

0 commit comments

Comments
 (0)