diff --git a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/browser/ContinueBrowser.kt b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/browser/ContinueBrowser.kt index 5b4f862c014..a438f8d0209 100644 --- a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/browser/ContinueBrowser.kt +++ b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/browser/ContinueBrowser.kt @@ -13,13 +13,22 @@ import com.intellij.ui.jcef.* import org.cef.CefApp import org.cef.browser.CefBrowser import org.cef.handler.CefLoadHandlerAdapter +import kotlinx.coroutines.* +import java.lang.management.ManagementFactory +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit import javax.swing.JComponent class ContinueBrowser(private val project: Project): Disposable { private val log = Logger.getInstance(ContinueBrowser::class.java.simpleName) - private val browser: JBCefBrowser = JBCefBrowser.createBuilder().setOffScreenRendering(true).build() + private val browser: JBCefBrowser = JBCefBrowser.createBuilder().setOffScreenRendering(false).build() private val myJSQueryOpenInBrowser = JBCefJSQuery.create(browser as JBCefBrowserBase) + private val maintenanceExecutor: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor() + private val memoryMonitorExecutor: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor() + private var lastInteractionTime = System.currentTimeMillis() + private var browserMemoryStats: BrowserMemoryStats? = null init { CefApp.getInstance().registerSchemeHandlerFactory("http", "continue", CustomSchemeHandlerFactory()) @@ -29,6 +38,12 @@ class ContinueBrowser(private val project: Project): Disposable { val messageType = json.messageType val data = json.data val messageId = json.messageId + + // Handle memory usage reports from browser + if (messageType == "memoryUsage" && messageId == "memory-monitor") { + handleBrowserMemoryReport(data) + return@addHandler null + } if (MessageTypes.PASS_THROUGH_TO_CORE.contains(messageType)) { project.service().coreMessenger?.request(messageType, data, messageId) { data -> @@ -68,6 +83,12 @@ class ContinueBrowser(private val project: Project): Disposable { } browser.createImmediately() + + // Schedule periodic maintenance to prevent freezing during idle periods + startMaintenanceScheduler() + + // Start memory monitoring + startMemoryMonitoring() } fun getComponent(): JComponent = @@ -82,6 +103,7 @@ class ContinueBrowser(private val project: Project): Disposable { } fun sendToWebview(messageType: String, data: Any? = null, messageId: String = uuid()) { + updateLastInteractionTime() val json = Gson().toJson(BrowserMessage(messageType, messageId, data)) val jsCode = """window.postMessage($json, "*");""" try { @@ -101,7 +123,164 @@ class ContinueBrowser(private val project: Project): Disposable { browser.cefBrowser.executeJavaScript(script, getGuiUrl(), 0) } + private fun updateLastInteractionTime() { + lastInteractionTime = System.currentTimeMillis() + } + + private fun startMaintenanceScheduler() { + // Run maintenance every 30 minutes + maintenanceExecutor.scheduleAtFixedRate({ + try { + performMaintenance() + } catch (e: Exception) { + log.warn("Error during browser maintenance", e) + } + }, 30, 30, TimeUnit.MINUTES) + } + + private fun startMemoryMonitoring() { + // Log memory usage every 5 minutes + memoryMonitorExecutor.scheduleAtFixedRate({ + try { + logMemoryUsage() + } catch (e: Exception) { + log.warn("Error during memory monitoring", e) + } + }, 5, 5, TimeUnit.MINUTES) + } + + private fun performMaintenance() { + val idleTime = System.currentTimeMillis() - lastInteractionTime + val oneHour = 60 * 60 * 1000L + + // If idle for more than 1 hour, perform maintenance + if (idleTime > oneHour) { + log.info("Performing browser maintenance after ${idleTime / 1000 / 60} minutes of idle time") + + // Log memory before maintenance + logMemoryUsage("Before maintenance") + + // Force garbage collection in the browser + try { + browser.executeJavaScriptAsync( + """if (window.gc) { window.gc(); } else if (window.webkitGC) { window.webkitGC(); }""" + ) + } catch (e: Exception) { + log.warn("Could not trigger browser GC", e) + } + + // Clear any accumulated message queues + browser.jbCefClient.setProperty(JBCefClient.Properties.JS_QUERY_POOL_SIZE, 200) + + // Log memory after maintenance (with delay to allow GC) + maintenanceExecutor.schedule({ + logMemoryUsage("After maintenance") + }, 5, TimeUnit.SECONDS) + } + } + + private fun logMemoryUsage(context: String = "Periodic check") { + // JVM Memory Usage + val runtime = Runtime.getRuntime() + val maxMemory = runtime.maxMemory() + val totalMemory = runtime.totalMemory() + val freeMemory = runtime.freeMemory() + val usedMemory = totalMemory - freeMemory + + val memoryBean = ManagementFactory.getMemoryMXBean() + val heapUsage = memoryBean.heapMemoryUsage + val nonHeapUsage = memoryBean.nonHeapMemoryUsage + + val idleTime = (System.currentTimeMillis() - lastInteractionTime) / 1000 / 60 // minutes + + log.info("Memory Usage [$context] - Idle: ${idleTime}m | " + + "JVM: ${formatBytes(usedMemory)}/${formatBytes(maxMemory)} | " + + "Heap: ${formatBytes(heapUsage.used)}/${formatBytes(heapUsage.max)} | " + + "NonHeap: ${formatBytes(nonHeapUsage.used)}/${formatBytes(nonHeapUsage.max)}") + + // Get browser memory usage via JavaScript + getBrowserMemoryUsage() + } + + private fun getBrowserMemoryUsage() { + try { + browser.executeJavaScriptAsync(""" + if (window.performance && window.performance.memory) { + const mem = window.performance.memory; + window.postIntellijMessage('memoryUsage', { + used: mem.usedJSHeapSize, + total: mem.totalJSHeapSize, + limit: mem.jsHeapSizeLimit + }, 'memory-monitor'); + } else { + window.postIntellijMessage('memoryUsage', { + error: 'Performance memory API not available' + }, 'memory-monitor'); + } + """) + } catch (e: Exception) { + log.warn("Could not query browser memory usage", e) + } + } + + private fun formatBytes(bytes: Long): String { + if (bytes < 1024) return "${bytes}B" + val kb = bytes / 1024.0 + if (kb < 1024) return "%.1fKB".format(kb) + val mb = kb / 1024.0 + if (mb < 1024) return "%.1fMB".format(mb) + val gb = mb / 1024.0 + return "%.1fGB".format(gb) + } + + data class BrowserMemoryStats( + val used: Long, + val total: Long, + val limit: Long + ) + + private fun handleBrowserMemoryReport(data: Any?) { + try { + @Suppress("UNCHECKED_CAST") + val memData = data as? Map + if (memData != null && memData.containsKey("used")) { + val used = (memData["used"] as? Number)?.toLong() ?: 0L + val total = (memData["total"] as? Number)?.toLong() ?: 0L + val limit = (memData["limit"] as? Number)?.toLong() ?: 0L + + browserMemoryStats = BrowserMemoryStats(used, total, limit) + + log.info("Browser Memory: ${formatBytes(used)}/${formatBytes(total)} (limit: ${formatBytes(limit)})") + + // Warn if browser memory usage is high + val usagePercent = if (limit > 0) (used.toDouble() / limit * 100) else 0.0 + if (usagePercent > 80) { + log.warn("High browser memory usage: %.1f%% of limit".format(usagePercent)) + } + } else if (memData?.containsKey("error") == true) { + log.debug("Browser memory monitoring: ${memData["error"]}") + } + } catch (e: Exception) { + log.warn("Error processing browser memory report", e) + } + } + override fun dispose() { + maintenanceExecutor.shutdown() + memoryMonitorExecutor.shutdown() + + try { + if (!maintenanceExecutor.awaitTermination(5, TimeUnit.SECONDS)) { + maintenanceExecutor.shutdownNow() + } + if (!memoryMonitorExecutor.awaitTermination(5, TimeUnit.SECONDS)) { + memoryMonitorExecutor.shutdownNow() + } + } catch (e: InterruptedException) { + maintenanceExecutor.shutdownNow() + memoryMonitorExecutor.shutdownNow() + } + Disposer.dispose(myJSQueryOpenInBrowser) Disposer.dispose(browser) }