Skip to content

Commit f8db362

Browse files
authored
Fix UI freeze when trying to refresh tokens offline (#4880)
`CodeWhispererService#showRecommendationsInPopup` was previously running on EDT, which caused issues when we need to wait for the fairly expensive call chain for `promptReauth` if the connection is expired. This is very visible in the offline case. Fix by moving all the operations onto a background worker and limiting invocations to one at a time. This caused some issues with the test suite since they assumed that `showRecommendationsInPopup` would block until background operations have initialized, which was worked around by passing along job tracking
1 parent ed57e09 commit f8db362

File tree

4 files changed

+63
-9
lines changed

4 files changed

+63
-9
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type" : "bugfix",
3+
"description" : "Fix UI slowdown when Amazon Q Inline Suggestions are enabled, but token cannot be refreshed (#4868)"
4+
}

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/actions/CodeWhispererRecommendationAction.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ import com.intellij.openapi.actionSystem.AnAction
88
import com.intellij.openapi.actionSystem.AnActionEvent
99
import com.intellij.openapi.actionSystem.CommonDataKeys
1010
import com.intellij.openapi.project.DumbAware
11+
import com.intellij.openapi.util.Key
12+
import kotlinx.coroutines.Job
1113
import software.aws.toolkits.jetbrains.services.codewhisperer.model.LatencyContext
1214
import software.aws.toolkits.jetbrains.services.codewhisperer.model.TriggerTypeInfo
1315
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutomatedTriggerType
1416
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService
1517
import software.aws.toolkits.resources.message
1618
import software.aws.toolkits.telemetry.CodewhispererTriggerType
19+
import java.util.concurrent.atomic.AtomicReference
1720

1821
class CodeWhispererRecommendationAction : AnAction(message("codewhisperer.trigger.service")), DumbAware {
1922
override fun getActionUpdateThread() = ActionUpdateThread.BGT
@@ -32,6 +35,12 @@ class CodeWhispererRecommendationAction : AnAction(message("codewhisperer.trigge
3235
}
3336

3437
val triggerType = TriggerTypeInfo(CodewhispererTriggerType.OnDemand, CodeWhispererAutomatedTriggerType.Unknown())
35-
CodeWhispererService.getInstance().showRecommendationsInPopup(editor, triggerType, latencyContext)
38+
val job = CodeWhispererService.getInstance().showRecommendationsInPopup(editor, triggerType, latencyContext)
39+
40+
e.getData(CommonDataKeys.EDITOR)?.getUserData(ACTION_JOB_KEY)?.set(job)
41+
}
42+
43+
companion object {
44+
val ACTION_JOB_KEY = Key.create<AtomicReference<Job?>>("amazonq.codewhisperer.job")
3645
}
3746
}

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererService.kt

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import kotlinx.coroutines.async
2828
import kotlinx.coroutines.delay
2929
import kotlinx.coroutines.isActive
3030
import kotlinx.coroutines.launch
31+
import kotlinx.coroutines.withContext
3132
import software.amazon.awssdk.core.exception.SdkServiceException
3233
import software.amazon.awssdk.core.util.DefaultSdkAutoConstructList
3334
import software.amazon.awssdk.services.codewhisperer.model.CodeWhispererException
@@ -46,6 +47,7 @@ import software.aws.toolkits.core.utils.getLogger
4647
import software.aws.toolkits.core.utils.info
4748
import software.aws.toolkits.core.utils.warn
4849
import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
50+
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
4951
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
5052
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
5153
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
@@ -82,7 +84,6 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.util.FileContextPr
8284
import software.aws.toolkits.jetbrains.utils.isInjectedText
8385
import software.aws.toolkits.jetbrains.utils.isQExpired
8486
import software.aws.toolkits.jetbrains.utils.notifyWarn
85-
import software.aws.toolkits.jetbrains.utils.pluginAwareExecuteOnPooledThread
8687
import software.aws.toolkits.resources.message
8788
import software.aws.toolkits.telemetry.CodewhispererCompletionType
8889
import software.aws.toolkits.telemetry.CodewhispererSuggestionState
@@ -98,26 +99,46 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
9899
Disposer.register(this, codeInsightSettingsFacade)
99100
}
100101

102+
private var job: Job? = null
101103
fun showRecommendationsInPopup(
102104
editor: Editor,
103105
triggerTypeInfo: TriggerTypeInfo,
104106
latencyContext: LatencyContext
107+
): Job? {
108+
if (job == null || job?.isCompleted == true) {
109+
job = cs.launch(getCoroutineBgContext()) {
110+
doShowRecommendationsInPopup(editor, triggerTypeInfo, latencyContext)
111+
}
112+
}
113+
114+
// did some wrangling, but compiler didn't believe this can't be null
115+
return job
116+
}
117+
118+
private suspend fun doShowRecommendationsInPopup(
119+
editor: Editor,
120+
triggerTypeInfo: TriggerTypeInfo,
121+
latencyContext: LatencyContext
105122
) {
106123
val project = editor.project ?: return
107124
if (!isCodeWhispererEnabled(project)) return
108125

109126
latencyContext.credentialFetchingStart = System.nanoTime()
110127

128+
// try to refresh automatically if possible, otherwise ask user to login again
111129
if (isQExpired(project)) {
130+
// consider changing to only running once a ~minute since this is relatively expensive
112131
// say the connection is un-refreshable if refresh fails for 3 times
113132
val shouldReauth = if (refreshFailure < MAX_REFRESH_ATTEMPT) {
114-
pluginAwareExecuteOnPooledThread {
133+
val attempt = withContext(getCoroutineBgContext()) {
115134
promptReAuth(project)
116-
}.get().also { success ->
117-
if (!success) {
118-
refreshFailure++
119-
}
120135
}
136+
137+
if (!attempt) {
138+
refreshFailure++
139+
}
140+
141+
attempt
121142
} else {
122143
true
123144
}

plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestBase.kt

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import com.intellij.testFramework.DisposableRule
88
import com.intellij.testFramework.RuleChain
99
import com.intellij.testFramework.replaceService
1010
import com.intellij.testFramework.runInEdtAndWait
11+
import kotlinx.coroutines.Job
12+
import kotlinx.coroutines.delay
13+
import kotlinx.coroutines.test.runTest
14+
import kotlinx.coroutines.yield
1115
import org.assertj.core.api.Assertions.assertThat
1216
import org.junit.After
1317
import org.junit.Before
@@ -32,6 +36,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestU
3236
import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonResponse
3337
import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonTestLeftContext
3438
import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.testValidAccessToken
39+
import software.aws.toolkits.jetbrains.services.codewhisperer.actions.CodeWhispererRecommendationAction
3540
import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor
3641
import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererLoginType
3742
import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorManager
@@ -51,6 +56,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhisp
5156
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_DIM_HEX
5257
import software.aws.toolkits.jetbrains.utils.rules.PythonCodeInsightTestFixtureRule
5358
import software.aws.toolkits.resources.message
59+
import java.util.concurrent.atomic.AtomicReference
5460

5561
// TODO: restructure testbase, too bulky and hard to debug
5662
open class CodeWhispererTestBase {
@@ -180,12 +186,26 @@ open class CodeWhispererTestBase {
180186
runInEdtAndWait {}
181187
}
182188

189+
/**
190+
* Block until manual action has either failed or completed
191+
*/
183192
fun invokeCodeWhispererService() {
193+
val jobRef = AtomicReference<Job?>()
184194
runInEdtAndWait {
195+
projectRule.fixture.editor.putUserData(CodeWhispererRecommendationAction.ACTION_JOB_KEY, jobRef)
196+
// does not block, so we need to extract something to track the async task
185197
projectRule.fixture.performEditorAction(codeWhispererRecommendationActionId)
186198
}
187-
while (CodeWhispererInvocationStatus.getInstance().hasExistingInvocation()) {
188-
Thread.sleep(10)
199+
200+
runTest {
201+
// wait for CodeWhispererService#showRecommendationsInPopup to complete, if started
202+
jobRef.get()?.join()
203+
204+
// wait for subsequent background operations to be complete
205+
while (CodeWhispererInvocationStatus.getInstance().hasExistingInvocation()) {
206+
yield()
207+
delay(10)
208+
}
189209
}
190210
}
191211

0 commit comments

Comments
 (0)