diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt index 8a9e9824914..ddc9c440cfb 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt @@ -111,6 +111,14 @@ class CodeWhispererConfigurable(private val project: Project) : .resizableColumn() .align(Align.FILL) } + row { + checkBox("Enable CPU profiling") + .bindSelected( + { LspSettings.getInstance().isCpuProfilingEnabled() }, + { LspSettings.getInstance().setCpuProfilingEnabled(it) } + ) + .comment("Enable CPU profiling for the LSP server to help diagnose performance issues") + } } group(message("aws.settings.codewhisperer.group.general")) { diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt index 5bc04abbb85..700b86ddb06 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt @@ -28,6 +28,7 @@ import com.intellij.util.io.DigestUtil import com.intellij.util.net.HttpConfigurable import com.intellij.util.net.JdkProxyProvider import com.intellij.util.net.ssl.CertificateManager +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Job @@ -125,6 +126,19 @@ internal class LSPProcessListener : ProcessListener { override fun processTerminated(event: ProcessEvent) { LOG.info { "LSP process terminated with exit code ${event.exitCode}" } + + // Check for CPU profile file after process termination only if profiling was enabled + if (LspSettings.getInstance().isCpuProfilingEnabled()) { + val processId = ProcessHandle.current().pid() + val profileDir = System.getProperty("java.io.tmpdir").trimEnd('/') + val profilePath = "$profileDir/node-profile-$processId.cpuprofile" + if (Files.exists(Path.of(profilePath))) { + LOG.info { "CPU profile file created: $profilePath" } + } else { + LOG.warn { "CPU profile file not found at: $profilePath" } + } + } + try { this.outputStreamWriter.close() this.outputStream.close() @@ -465,9 +479,20 @@ private class AmazonQServerInstance(private val project: Project, private val cs val cmd = NodeExePatcher.patch(nodePath) .withParameters( - LspSettings.getInstance().getArtifactPath() ?: artifact.resolve("aws-lsp-codewhisperer.js").toString(), - "--stdio", - "--set-credentials-encryption-key", + buildList { + if (LspSettings.getInstance().isCpuProfilingEnabled()) { + val processId = ProcessHandle.current().pid() + val profileDir = System.getProperty("java.io.tmpdir").trimEnd('/') + val profilePath = "node-profile-$processId.cpuprofile" + LOG.info { "Node.js CPU profile will be saved to: $profileDir $profilePath" } + add("--cpu-prof") + add("--cpu-prof-dir=$profileDir") + add("--cpu-prof-name=$profilePath") + } + add(LspSettings.getInstance().getArtifactPath() ?: artifact.resolve("aws-lsp-codewhisperer.js").toString()) + add("--stdio") + add("--set-credentials-encryption-key") + } ).withEnvironment( buildMap { extraCaCerts?.let { @@ -721,18 +746,28 @@ private class AmazonQServerInstance(private val project: Project, private val cs // otherwise can deadlock waiting for frozen flare process cs.launch { languageServer.apply { - withTimeout(2000) { + withTimeout(5000) { shutdown().await() exit() } } }.asCompletableFuture() - .get(3000, TimeUnit.MILLISECONDS) + .get(6000, TimeUnit.MILLISECONDS) + } catch (e: CancellationException) { + // Expected during shutdown, proceed with cleanup + launcherHandler.process.destroy() + Thread.sleep(500) } catch (e: Exception) { LOG.warn(e) { "LSP shutdown failed" } - launcherHandler.killProcess() + launcherHandler.process.destroy() + Thread.sleep(500) + } finally { + if (!launcherHandler.isProcessTerminated) { + launcherHandler.killProcess() + } } } else if (!launcherHandler.isProcessTerminated) { + Thread.sleep(500) launcherHandler.killProcess() } } diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/LspSettings.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/LspSettings.kt index bc199de4917..28b1a66b9f6 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/LspSettings.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/LspSettings.kt @@ -27,6 +27,8 @@ class LspSettings : PersistentStateComponent { fun getNodeRuntimePath() = state.nodeRuntimePath + fun isCpuProfilingEnabled() = state.cpuProfilingEnabled + fun setArtifactPath(artifactPath: String?) { state.artifactPath = artifactPath.nullize(nullizeSpaces = true) } @@ -35,6 +37,10 @@ class LspSettings : PersistentStateComponent { state.nodeRuntimePath = nodeRuntimePath.nullize(nullizeSpaces = true) } + fun setCpuProfilingEnabled(enabled: Boolean) { + state.cpuProfilingEnabled = enabled + } + companion object { fun getInstance(): LspSettings = service() } @@ -43,4 +49,5 @@ class LspSettings : PersistentStateComponent { class LspConfiguration : BaseState() { var artifactPath by string() var nodeRuntimePath by string() + var cpuProfilingEnabled by property(false) }