From 33c7fd4484c54e1c963dce0d5fd79b05d9da4198 Mon Sep 17 00:00:00 2001 From: riii111 Date: Fri, 28 Nov 2025 20:23:31 +0900 Subject: [PATCH 1/3] fix: support go-to-definition for top-level val/var in generated code - Add val/var to definitionPattern regex for definition lookup - Allow build/generated-src directory by excluding specific build outputs instead of entire build directory This fixes go-to-definition failing for generated code like jOOQ Tables.kt --- .../src/main/kotlin/org/javacs/kt/definition/GoToDefinition.kt | 2 +- shared/src/main/kotlin/org/javacs/kt/SourceExclusions.kt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/server/src/main/kotlin/org/javacs/kt/definition/GoToDefinition.kt b/server/src/main/kotlin/org/javacs/kt/definition/GoToDefinition.kt index 77c8a05de..909a2b973 100644 --- a/server/src/main/kotlin/org/javacs/kt/definition/GoToDefinition.kt +++ b/server/src/main/kotlin/org/javacs/kt/definition/GoToDefinition.kt @@ -23,7 +23,7 @@ import java.io.File import java.nio.file.Paths private val cachedTempFiles = mutableMapOf() -private val definitionPattern = Regex("(?:class|interface|object|fun)\\s+(\\w+)") +private val definitionPattern = Regex("(?:class|interface|object|fun|val|var)\\s+(\\w+)") fun goToDefinition( file: CompiledFile, diff --git a/shared/src/main/kotlin/org/javacs/kt/SourceExclusions.kt b/shared/src/main/kotlin/org/javacs/kt/SourceExclusions.kt index de4a512c2..db738f2fd 100644 --- a/shared/src/main/kotlin/org/javacs/kt/SourceExclusions.kt +++ b/shared/src/main/kotlin/org/javacs/kt/SourceExclusions.kt @@ -16,7 +16,8 @@ class SourceExclusions( val excludedPatterns = (listOf( ".git", ".hg", ".svn", // Version control systems ".idea", ".idea_modules", ".vs", ".vscode", ".code-workspace", ".settings", // IDEs - "bazel-*", "bin", "build", "node_modules", "target", // Build systems + "bazel-*", "bin", "node_modules", "target", // Build systems + "build/classes", "build/libs", "build/tmp", "build/reports", "build/kotlin", "build/resources", // Gradle build outputs (but allow build/generated-src) ) + when { !scriptsConfig.enabled -> listOf("*.kts") !scriptsConfig.buildScriptsEnabled -> listOf("*.gradle.kts") From b314665544038641c1d9fa997b842c796af8aa0a Mon Sep 17 00:00:00 2001 From: riii111 Date: Fri, 28 Nov 2025 22:40:44 +0900 Subject: [PATCH 2/3] perf: add JVM memory limits to prevent excessive memory usage - Set initial heap to 256MB, max heap to 2GB - Use G1GC for better memory management - Enable string deduplication to reduce memory footprint This prevents the language server from consuming 3GB+ of memory --- server/build.gradle.kts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 915dcc767..bd6fd8979 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -18,7 +18,13 @@ val applicationName = "kotlin-language-server" application { mainClass.set(serverMainClassName) description = "Code completions, diagnostics and more for Kotlin" - applicationDefaultJvmArgs = listOf("-DkotlinLanguageServer.version=$version") + applicationDefaultJvmArgs = listOf( + "-DkotlinLanguageServer.version=$version", + "-Xms256m", + "-Xmx2048m", // Limit heap to 2GB to prevent memory bloat (supports large projects) + "-XX:+UseG1GC", + "-XX:+UseStringDeduplication" + ) applicationDistribution.into("bin") { filePermissions { unix("755".toInt(radix = 8)) } } } From a06521da8f9c1ef64380301bf0142c62ecb977cd Mon Sep 17 00:00:00 2001 From: riii111 Date: Sat, 29 Nov 2025 09:18:21 +0900 Subject: [PATCH 3/3] perf: reduce memory usage with caching and heap limit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reduce default heap from 2GB to 1GB - Expand decompiled class LRU cache (5→50) to reduce repeated decompilation - Add in-memory cache layer to CachedClassPathResolver to avoid repeated DB transactions - Enable SQLite WAL mode and NORMAL synchronous for better I/O performance --- server/build.gradle.kts | 2 +- .../externalsources/ClassContentProvider.kt | 3 +- .../kt/classpath/CachedClassPathResolver.kt | 58 ++++++++++++------- .../org/javacs/kt/database/DatabaseService.kt | 9 ++- 4 files changed, 48 insertions(+), 24 deletions(-) diff --git a/server/build.gradle.kts b/server/build.gradle.kts index bd6fd8979..972fb1508 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -21,7 +21,7 @@ application { applicationDefaultJvmArgs = listOf( "-DkotlinLanguageServer.version=$version", "-Xms256m", - "-Xmx2048m", // Limit heap to 2GB to prevent memory bloat (supports large projects) + "-Xmx1024m", "-XX:+UseG1GC", "-XX:+UseStringDeduplication" ) diff --git a/server/src/main/kotlin/org/javacs/kt/externalsources/ClassContentProvider.kt b/server/src/main/kotlin/org/javacs/kt/externalsources/ClassContentProvider.kt index 9b3d6618b..ba814cb1a 100644 --- a/server/src/main/kotlin/org/javacs/kt/externalsources/ClassContentProvider.kt +++ b/server/src/main/kotlin/org/javacs/kt/externalsources/ClassContentProvider.kt @@ -25,7 +25,8 @@ class ClassContentProvider( ) { /** Maps recently used (source-)KLS-URIs to their source contents (e.g. decompiled code) and the file extension. */ private val cachedContents = object : LinkedHashMap>() { - override fun removeEldestEntry(eldest: MutableMap.MutableEntry>) = size > 5 + // Decompilation is expensive; larger cache reduces repeated decompilation overhead + override fun removeEldestEntry(eldest: MutableMap.MutableEntry>) = size > 50 } /** diff --git a/shared/src/main/kotlin/org/javacs/kt/classpath/CachedClassPathResolver.kt b/shared/src/main/kotlin/org/javacs/kt/classpath/CachedClassPathResolver.kt index d81667deb..868987be6 100644 --- a/shared/src/main/kotlin/org/javacs/kt/classpath/CachedClassPathResolver.kt +++ b/shared/src/main/kotlin/org/javacs/kt/classpath/CachedClassPathResolver.kt @@ -55,47 +55,63 @@ internal class CachedClassPathResolver( ) : ClassPathResolver { override val resolverType: String get() = "Cached + ${wrapped.resolverType}" + // In-memory cache layer to avoid repeated DB transactions + private var inMemoryClassPathEntries: Set? = null + private var inMemoryBuildScriptEntries: Set? = null + private var inMemoryMetadata: ClasspathMetadata? = null + private var cachedClassPathEntries: Set - get() = transaction(db) { + get() = inMemoryClassPathEntries ?: transaction(db) { ClassPathCacheEntryEntity.all().map { ClassPathEntry( compiledJar = Paths.get(it.compiledJar), sourceJar = it.sourceJar?.let(Paths::get) ) }.toSet() - } - set(newEntries) = transaction(db) { - ClassPathCacheEntry.deleteAll() - newEntries.map { - ClassPathCacheEntryEntity.new { - compiledJar = it.compiledJar.toString() - sourceJar = it.sourceJar?.toString() + }.also { inMemoryClassPathEntries = it } + set(newEntries) { + inMemoryClassPathEntries = newEntries + transaction(db) { + ClassPathCacheEntry.deleteAll() + newEntries.map { + ClassPathCacheEntryEntity.new { + compiledJar = it.compiledJar.toString() + sourceJar = it.sourceJar?.toString() + } } } } private var cachedBuildScriptClassPathEntries: Set - get() = transaction(db) { BuildScriptClassPathCacheEntryEntity.all().map { Paths.get(it.jar) }.toSet() } - set(newEntries) = transaction(db) { - BuildScriptClassPathCacheEntry.deleteAll() - newEntries.map { BuildScriptClassPathCacheEntryEntity.new { jar = it.toString() } } + get() = inMemoryBuildScriptEntries ?: transaction(db) { + BuildScriptClassPathCacheEntryEntity.all().map { Paths.get(it.jar) }.toSet() + }.also { inMemoryBuildScriptEntries = it } + set(newEntries) { + inMemoryBuildScriptEntries = newEntries + transaction(db) { + BuildScriptClassPathCacheEntry.deleteAll() + newEntries.map { BuildScriptClassPathCacheEntryEntity.new { jar = it.toString() } } + } } - private var cachedClassPathMetadata - get() = transaction(db) { + private var cachedClassPathMetadata: ClasspathMetadata? + get() = inMemoryMetadata ?: transaction(db) { ClassPathMetadataCacheEntity.all().map { ClasspathMetadata( includesSources = it.includesSources, buildFileVersion = it.buildFileVersion ) }.firstOrNull() - } - set(newClassPathMetadata) = transaction(db) { - ClassPathMetadataCache.deleteAll() - val newClassPathMetadataRow = newClassPathMetadata ?: ClasspathMetadata() - ClassPathMetadataCacheEntity.new { - includesSources = newClassPathMetadataRow.includesSources - buildFileVersion = newClassPathMetadataRow.buildFileVersion + }.also { inMemoryMetadata = it } + set(newClassPathMetadata) { + inMemoryMetadata = newClassPathMetadata + transaction(db) { + ClassPathMetadataCache.deleteAll() + val newClassPathMetadataRow = newClassPathMetadata ?: ClasspathMetadata() + ClassPathMetadataCacheEntity.new { + includesSources = newClassPathMetadataRow.includesSources + buildFileVersion = newClassPathMetadataRow.buildFileVersion + } } } diff --git a/shared/src/main/kotlin/org/javacs/kt/database/DatabaseService.kt b/shared/src/main/kotlin/org/javacs/kt/database/DatabaseService.kt index 0f80f73b4..620fd17c8 100644 --- a/shared/src/main/kotlin/org/javacs/kt/database/DatabaseService.kt +++ b/shared/src/main/kotlin/org/javacs/kt/database/DatabaseService.kt @@ -62,7 +62,14 @@ class DatabaseService { private fun getDbFromFile(storagePath: Path?): Database? { return storagePath?.let { if (Files.isDirectory(it)) { - Database.connect("jdbc:sqlite:${getDbFilePath(it)}") + Database.connect("jdbc:sqlite:${getDbFilePath(it)}").also { db -> + transaction(db) { + // WAL mode: better concurrent read performance + exec("PRAGMA journal_mode = WAL") + // Reduce fsync calls (safe with WAL mode) + exec("PRAGMA synchronous = NORMAL") + } + } } else { null }