From 7d897c3c439f554b84116b6dc041779ba557b114 Mon Sep 17 00:00:00 2001 From: arun28082007 Date: Sat, 8 Aug 2026 13:15:37 +0000 Subject: [PATCH 1/9] feat-) memory aware thread pool and heap pressure detection --- .../common/MemoryAwareDispatchers.kt | 30 ++++++++++ .../org/cosmicide/common/MemoryMonitor.kt | 48 +++++++++++++++ .../java/org/cosmicide/common/MemoryUtils.kt | 60 +++++++++++++++++++ .../org/cosmicide/common/ProcessConfig.kt | 26 ++++++++ .../main/java/org/cosmicide/gradle/Main.java | 12 +++- 5 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 common/src/main/java/org/cosmicide/common/MemoryAwareDispatchers.kt create mode 100644 common/src/main/java/org/cosmicide/common/MemoryMonitor.kt create mode 100644 common/src/main/java/org/cosmicide/common/MemoryUtils.kt create mode 100644 common/src/main/java/org/cosmicide/common/ProcessConfig.kt diff --git a/common/src/main/java/org/cosmicide/common/MemoryAwareDispatchers.kt b/common/src/main/java/org/cosmicide/common/MemoryAwareDispatchers.kt new file mode 100644 index 000000000..0d43947cd --- /dev/null +++ b/common/src/main/java/org/cosmicide/common/MemoryAwareDispatchers.kt @@ -0,0 +1,30 @@ +package org.cosmicide.common + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asCoroutineDispatcher +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicInteger + +object MemoryAwareDispatchers { + + private val ioDispatcher: CoroutineDispatcher by lazy { + val threadCount = MemoryUtils.recommendedIOThreads() + Executors.newFixedThreadPool(threadCount) { runnable -> + Thread(runnable, "cosmic-io-${threadCount}").apply { + isDaemon = true + priority = Thread.NORM_PRIORITY - 1 + } + }.asCoroutineDispatcher() + } + + val IO: CoroutineDispatcher + get() = ioDispatcher + + val Main: CoroutineDispatcher + get() = Dispatchers.Main + + fun shutdown() { + (ioDispatcher as? java.io.Closeable)?.close() + } +} diff --git a/common/src/main/java/org/cosmicide/common/MemoryMonitor.kt b/common/src/main/java/org/cosmicide/common/MemoryMonitor.kt new file mode 100644 index 000000000..e5d291a16 --- /dev/null +++ b/common/src/main/java/org/cosmicide/common/MemoryMonitor.kt @@ -0,0 +1,48 @@ +package org.cosmicide.common + +import android.os.Handler +import android.os.HandlerThread +import java.lang.Runtime.getRuntime + +class MemoryMonitor( + private val intervalMs: Long = 5000L, + private val onMemoryPressureChanged: (MemoryUtils.MemoryPressure) -> Unit +) { + private var monitorThread: HandlerThread? = null + private var handler: Handler? = null + private var lastPressure = MemoryUtils.MemoryPressure.LOW + private var running = false + + fun start() { + if (running) return + running = true + + monitorThread = HandlerThread("cosmic-memory-monitor").apply { start() } + handler = Handler(monitorThread!!.looper) + + handler?.post(object : Runnable { + override fun run() { + if (!running) return + + val currentPressure = MemoryUtils.memoryPressureLevel + if (currentPressure != lastPressure) { + lastPressure = currentPressure + onMemoryPressureChanged(currentPressure) + } + + handler?.postDelayed(this, intervalMs) + } + }) + } + + fun stop() { + running = false + handler?.removeCallbacksAndMessages(null) + monitorThread?.quitSafely() + monitorThread = null + handler = null + } + + val currentPressure: MemoryUtils.MemoryPressure + get() = MemoryUtils.memoryPressureLevel +} diff --git a/common/src/main/java/org/cosmicide/common/MemoryUtils.kt b/common/src/main/java/org/cosmicide/common/MemoryUtils.kt new file mode 100644 index 000000000..469a16747 --- /dev/null +++ b/common/src/main/java/org/cosmicide/common/MemoryUtils.kt @@ -0,0 +1,60 @@ +package org.cosmicide.common + +import android.os.Build +import android.os.Debug +import java.lang.Runtime.getRuntime + +object MemoryUtils { + + val maxMemory: Long + get() = getRuntime().maxMemory() + + val totalMemory: Long + get() = getRuntime().totalMemory() + + val freeMemory: Long + get() = getRuntime().freeMemory() + + val usedMemory: Long + get() = totalMemory - freeMemory + + val availableMemory: Long + get() = maxMemory - usedMemory + + val memoryPressureLevel: MemoryPressure + get() { + val usageRatio = usedMemory.toFloat() / maxMemory + return when { + usageRatio > 0.9f -> MemoryPressure.CRITICAL + usageRatio > 0.75f -> MemoryPressure.HIGH + usageRatio > 0.5f -> MemoryPressure.MODERATE + else -> MemoryPressure.LOW + } + } + + fun recommendedWorkerCount(): Int { + val cores = Runtime.getRuntime().availableProcessors() + val memMb = availableMemory / (1024 * 1024) + + return when { + memMb < 128 -> 1 + memMb < 256 -> minOf(2, cores) + memMb < 512 -> minOf(4, cores) + else -> minOf(cores, 8) + } + } + + fun recommendedIOThreads(): Int { + val workers = recommendedWorkerCount() + return maxOf(workers, 2) + } + + fun isLowMemory(): Boolean = memoryPressureLevel.ordinal >= MemoryPressure.HIGH.ordinal + + enum class MemoryPressure { + LOW, + MODERATE, + HIGH, + CRITICAL + } +} diff --git a/common/src/main/java/org/cosmicide/common/ProcessConfig.kt b/common/src/main/java/org/cosmicide/common/ProcessConfig.kt new file mode 100644 index 000000000..0f471c309 --- /dev/null +++ b/common/src/main/java/org/cosmicide/common/ProcessConfig.kt @@ -0,0 +1,26 @@ +package org.cosmicide.common + +import java.lang.Runtime.getRuntime + +object ProcessConfig { + + fun maxParallelProcesses(): Int { + val workers = MemoryUtils.recommendedWorkerCount() + return when (MemoryUtils.memoryPressureLevel) { + MemoryUtils.MemoryPressure.CRITICAL -> 1 + MemoryUtils.MemoryPressure.HIGH -> 1 + MemoryUtils.MemoryPressure.MODERATE -> 2 + MemoryUtils.MemoryPressure.LOW -> workers + } + } + + fun shouldThrottle(): Boolean { + return MemoryUtils.isLowMemory() + } + + fun subprocessMemoryLimitMb(): Long { + val availableMb = MemoryUtils.availableMemory / (1024 * 1024) + val maxProcesses = maxParallelProcesses() + return if (maxProcesses > 0) availableMb / maxProcesses else availableMb / 2 + } +} diff --git a/feature/tooling/src/main/java/org/cosmicide/gradle/Main.java b/feature/tooling/src/main/java/org/cosmicide/gradle/Main.java index c83ae0fdc..abf8176d6 100644 --- a/feature/tooling/src/main/java/org/cosmicide/gradle/Main.java +++ b/feature/tooling/src/main/java/org/cosmicide/gradle/Main.java @@ -278,6 +278,16 @@ private static String stackTrace(Throwable throwable) { return stringWriter.toString(); } + private static int recommendedThreadCount() { + int cores = Runtime.getRuntime().availableProcessors(); + long maxMb = Runtime.getRuntime().maxMemory() / (1024 * 1024); + + if (maxMb < 128) return 1; + if (maxMb < 256) return Math.min(2, cores); + if (maxMb < 512) return Math.min(4, cores); + return Math.min(cores, 8); + } + private static final class GradleToolingServer implements Closeable { private final ProtocolWriter writer; private final ExecutorService executor; @@ -292,7 +302,7 @@ private static final class GradleToolingServer implements Closeable { this.writer = writer; this.project = project; this.connector = connectorFor(project); - this.executor = Executors.newCachedThreadPool(runnable -> { + this.executor = Executors.newFixedThreadPool(recommendedThreadCount(), runnable -> { Thread thread = new Thread(runnable, "cosmic-gradle-provider-worker"); thread.setDaemon(false); return thread; From 25207fef17b4e554ee1a0b40c5f02aa439dcddbe Mon Sep 17 00:00:00 2001 From: arun28082007 Date: Sat, 8 Aug 2026 13:36:42 +0000 Subject: [PATCH 2/9] utilized new changes --- app/src/main/kotlin/org/cosmicide/App.kt | 18 ++++ .../org/cosmicide/common/AppDispatchers.kt | 9 ++ .../common/MemoryAwareDispatchers.kt | 2 + .../cosmicide/editor/lsp/LspEditorAdapter.kt | 61 +++++------- .../plugin/AndroidCommandExecutionService.kt | 5 +- .../org/cosmicide/plugin/CosmicPluginHost.kt | 23 ++++- .../org/cosmicide/plugin/PluginMarketplace.kt | 7 +- .../java/org/cosmicide/common/BlockCache.kt | 39 ++++++++ .../org/cosmicide/common/DiskIndexStore.kt | 98 +++++++++++++++++++ .../java/org/cosmicide/common/IndexManager.kt | 60 ++++++++++++ .../common/MemoryAwareDispatchers.kt | 18 ++-- .../java/org/cosmicide/common/MemoryUtils.kt | 2 - .../loading/PluginClassLoaderFactory.kt | 22 +++++ 13 files changed, 301 insertions(+), 63 deletions(-) create mode 100644 app/src/main/kotlin/org/cosmicide/common/AppDispatchers.kt create mode 100644 app/src/main/kotlin/org/cosmicide/common/MemoryAwareDispatchers.kt create mode 100644 common/src/main/java/org/cosmicide/common/BlockCache.kt create mode 100644 common/src/main/java/org/cosmicide/common/DiskIndexStore.kt create mode 100644 common/src/main/java/org/cosmicide/common/IndexManager.kt diff --git a/app/src/main/kotlin/org/cosmicide/App.kt b/app/src/main/kotlin/org/cosmicide/App.kt index 213200f20..1e4750055 100644 --- a/app/src/main/kotlin/org/cosmicide/App.kt +++ b/app/src/main/kotlin/org/cosmicide/App.kt @@ -26,6 +26,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.cosmicide.common.Analytics +import org.cosmicide.common.IndexManager +import org.cosmicide.common.MemoryMonitor +import org.cosmicide.common.MemoryUtils import org.cosmicide.common.Prefs import org.cosmicide.editor.lsp.handleLspShowDocument import org.cosmicide.plugin.CosmicPluginHost @@ -81,6 +84,8 @@ import java.util.TimeZone class App : Application() { + private lateinit var memoryMonitor: MemoryMonitor + companion object { /** @@ -117,6 +122,8 @@ class App : Application() { instance = WeakReference(this) HookManager.context = WeakReference(this) + IndexManager.init(this) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { HiddenApiBypass.addHiddenApiExemptions() } @@ -125,6 +132,13 @@ class App : Application() { CosmicPluginHost.init(this) + memoryMonitor = MemoryMonitor(intervalMs = 5000L) { pressure -> + if (pressure == MemoryUtils.MemoryPressure.CRITICAL) { + android.util.Log.w("App", "Critical memory pressure: throttling LSP servers") + } + } + memoryMonitor.start() + Analytics.setAnalyticsCollectionEnabled(Prefs.analyticsEnabled) applyLSP4JHooks() @@ -132,6 +146,10 @@ class App : Application() { override fun onTerminate() { super.onTerminate() + if (::memoryMonitor.isInitialized) { + memoryMonitor.stop() + } + IndexManager.shutdown() } fun loadTextmateTheme() { diff --git a/app/src/main/kotlin/org/cosmicide/common/AppDispatchers.kt b/app/src/main/kotlin/org/cosmicide/common/AppDispatchers.kt new file mode 100644 index 000000000..bed1dc700 --- /dev/null +++ b/app/src/main/kotlin/org/cosmicide/common/AppDispatchers.kt @@ -0,0 +1,9 @@ +package org.cosmicide.common + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asCoroutineDispatcher + +object AppDispatchers { + val IO = MemoryAwareDispatchers.IO.asCoroutineDispatcher() + val Main = Dispatchers.Main +} diff --git a/app/src/main/kotlin/org/cosmicide/common/MemoryAwareDispatchers.kt b/app/src/main/kotlin/org/cosmicide/common/MemoryAwareDispatchers.kt new file mode 100644 index 000000000..9ada37d4b --- /dev/null +++ b/app/src/main/kotlin/org/cosmicide/common/MemoryAwareDispatchers.kt @@ -0,0 +1,2 @@ +// MemoryAwareDispatchers lives in common module. +// Use org.cosmicide.common.MemoryAwareDispatchers.IO (ExecutorService). diff --git a/app/src/main/kotlin/org/cosmicide/editor/lsp/LspEditorAdapter.kt b/app/src/main/kotlin/org/cosmicide/editor/lsp/LspEditorAdapter.kt index 88e084db0..bf7ca3dcf 100644 --- a/app/src/main/kotlin/org/cosmicide/editor/lsp/LspEditorAdapter.kt +++ b/app/src/main/kotlin/org/cosmicide/editor/lsp/LspEditorAdapter.kt @@ -31,6 +31,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.cosmicide.common.AppDispatchers +import org.cosmicide.common.IndexManager import org.cosmicide.editor.LspServerConnection import org.cosmicide.editor.LspServerDefinition import org.cosmicide.editor.LspServerRequest @@ -45,9 +47,6 @@ import java.io.InputStream import java.io.OutputStream import java.net.URI import java.net.URL -import java.nio.file.AtomicMoveNotSupportedException -import java.nio.file.Files -import java.nio.file.StandardCopyOption import java.security.MessageDigest import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap @@ -65,7 +64,7 @@ fun CodeEditor.configureLspLanguage( val lspProject = LspProjects.forRoot(request.project.root.absolutePath) ensureRequestTimeouts() - CoroutineScope(Dispatchers.IO).launch { + CoroutineScope(AppDispatchers.IO).launch { ensureInitializationTimeout(definition.initializationTimeoutMillis) val fileExtensions = definition.fileExtensions.toList() val requestedFileExtension = fileExtensions.firstOrNull { @@ -176,7 +175,7 @@ fun CodeEditor.configureLspLanguage( fun CodeEditor.disposeLspLanguage() { val lspEditors = LspProjects.editorsFor(this) - CoroutineScope(Dispatchers.IO).launch { + CoroutineScope(AppDispatchers.IO).launch { lspEditors.forEach(LspEditor::dispose) } } @@ -448,23 +447,14 @@ private fun createTextMateLanguage( return createTextMateLanguage(definition, grammarText) } - val cacheFile = grammarCacheFile(context, grammarLink) - var cachedGrammarText: String? = null - if (cacheFile.isFile) { - cachedGrammarText = runCatching { cacheFile.inputStream().readGrammarText() } - .onFailure { - Log.w(TAG, "Discarding unreadable grammar cache ${cacheFile.name}", it) - cacheFile.delete() - } - .getOrNull() - } + var cachedGrammarText: String? = readGrammarViaIndexManager(grammarLink) - if (cachedGrammarText != null && isTextMateGrammarCacheFresh(cacheFile)) { + if (cachedGrammarText != null) { try { return createTextMateLanguage(definition, cachedGrammarText) } catch (e: Exception) { - Log.w(TAG, "Discarding invalid grammar cache ${cacheFile.name}", e) - cacheFile.delete() + Log.w(TAG, "Discarding invalid grammar cache for $grammarLink", e) + IndexManager.invalidateProject("grammar:$grammarLink") cachedGrammarText = null } } @@ -479,7 +469,7 @@ private fun createTextMateLanguage( return try { createTextMateLanguage(definition, refreshedGrammarText).also { - runCatching { cacheGrammar(cacheFile, refreshedGrammarText) } + runCatching { cacheGrammarViaIndexManager(grammarLink, refreshedGrammarText) } .onFailure { error -> Log.w(TAG, "Unable to cache grammar from $grammarLink", error) } @@ -552,27 +542,20 @@ private fun grammarCacheFile(context: Context, grammarLink: String): File { .resolve("$cacheKey.grammar") } -private fun cacheGrammar(cacheFile: File, grammarText: String) { - val temporaryFile = File.createTempFile(cacheFile.name, ".tmp", cacheFile.parentFile) - try { - temporaryFile.writeText(grammarText, Charsets.UTF_8) - try { - Files.move( - temporaryFile.toPath(), - cacheFile.toPath(), - StandardCopyOption.ATOMIC_MOVE, - StandardCopyOption.REPLACE_EXISTING - ) - } catch (_: AtomicMoveNotSupportedException) { - Files.move( - temporaryFile.toPath(), - cacheFile.toPath(), - StandardCopyOption.REPLACE_EXISTING - ) +private fun cacheGrammarViaIndexManager(grammarLink: String, grammarText: String) { + val key = "grammar:${grammarLink}" + IndexManager.getOrBuildIndex(key, "text") { grammarText.toByteArray(Charsets.UTF_8) } +} + +private fun readGrammarViaIndexManager(grammarLink: String): String? { + val key = "grammar:${grammarLink}" + return try { + val segment = IndexManager.getOrBuildIndex(key, "text") { + throw IllegalStateException("Grammar not cached: $grammarLink") } - cacheFile.setLastModified(System.currentTimeMillis()) - } finally { - temporaryFile.delete() + String(segment.readBlock(0, segment.size.toInt()), Charsets.UTF_8) + } catch (e: Exception) { + null } } diff --git a/app/src/main/kotlin/org/cosmicide/plugin/AndroidCommandExecutionService.kt b/app/src/main/kotlin/org/cosmicide/plugin/AndroidCommandExecutionService.kt index a48c78469..034d13651 100644 --- a/app/src/main/kotlin/org/cosmicide/plugin/AndroidCommandExecutionService.kt +++ b/app/src/main/kotlin/org/cosmicide/plugin/AndroidCommandExecutionService.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.cosmicide.common.AppDispatchers import org.cosmicide.common.Prefs import org.cosmicide.exec.ProcessExecutor import org.cosmicide.exec.linux.LinuxProcessRunner @@ -44,7 +45,7 @@ internal class AndroidCommandExecutionService(context: Context) : override suspend fun execute( request: CommandRequest, onOutput: (String) -> Unit - ): CommandResult = withContext(Dispatchers.IO) { + ): CommandResult = withContext(AppDispatchers.IO) { require(request.workingDirectory.isDirectory) { "Working directory does not exist: ${request.workingDirectory.absolutePath}" } @@ -52,7 +53,7 @@ internal class AndroidCommandExecutionService(context: Context) : val process = start(request, redirectErrorStream = true) val captured = StringBuilder() val callerJob = currentCoroutineContext()[Job] - val cancellationWatcher = CoroutineScope(Dispatchers.IO).launch { + val cancellationWatcher = CoroutineScope(AppDispatchers.IO).launch { while (callerJob?.isActive == true) delay(CANCELLATION_POLL_MILLIS.milliseconds) if (callerJob?.isCancelled == true && process.isAlive) process.destroy() } diff --git a/app/src/main/kotlin/org/cosmicide/plugin/CosmicPluginHost.kt b/app/src/main/kotlin/org/cosmicide/plugin/CosmicPluginHost.kt index d78e7f27b..f59432e1f 100644 --- a/app/src/main/kotlin/org/cosmicide/plugin/CosmicPluginHost.kt +++ b/app/src/main/kotlin/org/cosmicide/plugin/CosmicPluginHost.kt @@ -35,6 +35,9 @@ object CosmicPluginHost { @Volatile private var initialized = false + @Volatile + private var pluginsLoaded = false + var pluginManager: AndroidPluginManager? = null private set @@ -76,23 +79,33 @@ object CosmicPluginHost { throwable ) } - manager.loadInstalledPlugins().forEach { result -> - result.onFailure { descriptorId, reason, throwable -> - Log.w(TAG, "Failed to load plugin $descriptorId: $reason", throwable) - } - } } initialized = true } } + private fun ensurePluginsLoaded() { + if (pluginsLoaded) return + synchronized(this) { + if (pluginsLoaded) return + pluginManager?.loadInstalledPlugins()?.forEach { result -> + result.onFailure { descriptorId, reason, throwable -> + Log.w(TAG, "Failed to load plugin $descriptorId: $reason", throwable) + } + } + pluginsLoaded = true + } + } + fun enabledExtensions(point: org.cosmicide.plugin.api.ExtensionPoint): List where T : Any, T : ConfigurableExtension { + ensurePluginsLoaded() return extensionRegistry.extensions(point).filter(extensionSettings::isEnabled) } fun configurableExtensions(): List { + ensurePluginsLoaded() return buildList { addRegistrations(EditorExtensionPoints.LANGUAGE_PROVIDER, "Editor languages") addRegistrations(EditorExtensionPoints.LSP_SERVER_PROVIDER, "Language servers") diff --git a/app/src/main/kotlin/org/cosmicide/plugin/PluginMarketplace.kt b/app/src/main/kotlin/org/cosmicide/plugin/PluginMarketplace.kt index a3ad3e64a..20c7d3d84 100644 --- a/app/src/main/kotlin/org/cosmicide/plugin/PluginMarketplace.kt +++ b/app/src/main/kotlin/org/cosmicide/plugin/PluginMarketplace.kt @@ -10,6 +10,7 @@ import com.google.gson.JsonObject import com.google.gson.JsonParser import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.cosmicide.common.AppDispatchers import org.cosmicide.plugin.api.PluginDescriptor import org.cosmicide.plugin.api.PluginHandle import org.cosmicide.plugin.api.PluginLoadResult @@ -51,13 +52,13 @@ class PluginMarketplace( private val appContext = context.applicationContext suspend fun fetch(repositoryUrl: String): List = - withContext(Dispatchers.IO) { + withContext(AppDispatchers.IO) { val json = downloadText(repositoryUrl, MAX_INDEX_BYTES) parsePluginRepository(json) } suspend fun install(entry: PluginRepositoryEntry): PluginInstallResult = - withContext(Dispatchers.IO) { + withContext(AppDispatchers.IO) { val pluginManager = checkNotNull(manager()) { "Plugin runtime is not initialized" } val pluginRoot = FileUtil.pluginDir.apply { mkdirs() } val token = UUID.randomUUID().toString() @@ -120,7 +121,7 @@ class PluginMarketplace( } } - suspend fun uninstall(pluginId: String) = withContext(Dispatchers.IO) { + suspend fun uninstall(pluginId: String) = withContext(AppDispatchers.IO) { val pluginManager = checkNotNull(manager()) { "Plugin runtime is not initialized" } val target = FileUtil.pluginDir.resolve(pluginId) require(target.isDirectory) { diff --git a/common/src/main/java/org/cosmicide/common/BlockCache.kt b/common/src/main/java/org/cosmicide/common/BlockCache.kt new file mode 100644 index 000000000..18140ef87 --- /dev/null +++ b/common/src/main/java/org/cosmicide/common/BlockCache.kt @@ -0,0 +1,39 @@ +package org.cosmicide.common + +import java.util.LinkedHashMap + +class BlockCache(private val maxBlocks: Int = 256) { + + private val cache = object : LinkedHashMap(64, 0.75f, true) { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean { + return size > maxBlocks + } + } + + fun getOrLoad(key: String, loader: () -> ByteArray): ByteArray { + synchronized(cache) { + cache[key]?.let { return it } + } + + val data = loader() + synchronized(cache) { + cache[key] = data + } + return data + } + + fun invalidate(key: String) { + synchronized(cache) { + cache.remove(key) + } + } + + fun clear() { + synchronized(cache) { + cache.clear() + } + } + + val size: Int + get() = synchronized(cache) { cache.size } +} diff --git a/common/src/main/java/org/cosmicide/common/DiskIndexStore.kt b/common/src/main/java/org/cosmicide/common/DiskIndexStore.kt new file mode 100644 index 000000000..b40378132 --- /dev/null +++ b/common/src/main/java/org/cosmicide/common/DiskIndexStore.kt @@ -0,0 +1,98 @@ +package org.cosmicide.common + +import java.io.File +import java.io.RandomAccessFile +import java.nio.ByteBuffer +import java.nio.MappedByteBuffer +import java.nio.channels.FileChannel +import java.security.MessageDigest +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong + +class DiskIndexStore(private val storeDir: File) { + + init { + storeDir.mkdirs() + } + + fun getOrCreateSegment(key: String, builder: () -> ByteArray): DiskIndexSegment { + val hash = sha256(key) + val segmentFile = storeDir.resolve("$hash.idx") + val metaFile = storeDir.resolve("$hash.meta") + + if (segmentFile.exists() && metaFile.exists()) { + val size = metaFile.readText().trim().toLongOrNull() ?: 0L + return DiskIndexSegment(segmentFile, size) + } + + val data = builder() + writeAtomic(segmentFile, data) + metaFile.writeText(data.size.toString()) + + return DiskIndexSegment(segmentFile, data.size.toLong()) + } + + fun invalidate(key: String) { + val hash = sha256(key) + storeDir.resolve("$hash.idx").delete() + storeDir.resolve("$hash.meta").delete() + } + + fun clear() { + storeDir.listFiles()?.forEach { it.delete() } + } + + private fun writeAtomic(file: File, data: ByteArray) { + val temp = File.createTempFile(file.name, ".tmp", file.parentFile) + try { + temp.writeBytes(data) + temp.renameTo(file) + } finally { + if (temp.exists()) temp.delete() + } + } + + private fun sha256(input: String): String { + val digest = MessageDigest.getInstance("SHA-256") + return digest.digest(input.toByteArray(Charsets.UTF_8)) + .joinToString("") { "%02x".format(it) } + } +} + +class DiskIndexSegment( + private val file: File, + val size: Long +) { + private val raf: RandomAccessFile by lazy { RandomAccessFile(file, "r") } + private val channel: FileChannel by lazy { raf.channel } + + @Volatile + private var mappedBuffer: MappedByteBuffer? = null + + fun readBlock(offset: Long, length: Int): ByteArray { + val buf = ByteArray(length) + synchronized(this) { + channel.read(ByteBuffer.wrap(buf), offset) + } + return buf + } + + fun map(): MappedByteBuffer { + return mappedBuffer ?: synchronized(this) { + mappedBuffer ?: channel.map( + FileChannel.MapMode.READ_ONLY, + 0, + size + ).also { mappedBuffer = it } + } + } + + fun close() { + synchronized(this) { + mappedBuffer?.force() + mappedBuffer = null + channel.close() + raf.close() + } + } +} diff --git a/common/src/main/java/org/cosmicide/common/IndexManager.kt b/common/src/main/java/org/cosmicide/common/IndexManager.kt new file mode 100644 index 000000000..20f8d53a1 --- /dev/null +++ b/common/src/main/java/org/cosmicide/common/IndexManager.kt @@ -0,0 +1,60 @@ +package org.cosmicide.common + +import android.content.Context +import java.io.File + +object IndexManager { + + private lateinit var store: DiskIndexStore + private val cache = BlockCache(maxBlocks = 512) + private val activeSegments = ConcurrentHashMap() + + fun init(context: Context) { + val cacheDir = context.cacheDir.resolve("index-cache").apply { mkdirs() } + store = DiskIndexStore(cacheDir) + } + + fun getOrBuildIndex( + projectKey: String, + indexType: String, + builder: () -> ByteArray + ): DiskIndexSegment { + val key = "$projectKey:$indexType" + + activeSegments[key]?.let { return it } + + val segment = store.getOrCreateSegment(key, builder) + activeSegments[key] = segment + return segment + } + + fun readCached(key: String, blockOffset: Long, blockLength: Int): ByteArray { + val cacheKey = "$key:$blockOffset:$blockLength" + return cache.getOrLoad(cacheKey) { + activeSegments[key]?.readBlock(blockOffset, blockLength) + ?: throw IllegalStateException("Index segment not loaded: $key") + } + } + + fun invalidateProject(projectKey: String) { + val prefix = "$projectKey:" + activeSegments.keys.filter { it.startsWith(prefix) }.forEach { key -> + activeSegments[key]?.close() + activeSegments.remove(key) + store.invalidate(key) + } + } + + fun invalidateAll() { + activeSegments.values.forEach { it.close() } + activeSegments.clear() + cache.clear() + store.clear() + } + + fun shutdown() { + activeSegments.values.forEach { it.close() } + activeSegments.clear() + cache.clear() + } +} diff --git a/common/src/main/java/org/cosmicide/common/MemoryAwareDispatchers.kt b/common/src/main/java/org/cosmicide/common/MemoryAwareDispatchers.kt index 0d43947cd..0cdb593a5 100644 --- a/common/src/main/java/org/cosmicide/common/MemoryAwareDispatchers.kt +++ b/common/src/main/java/org/cosmicide/common/MemoryAwareDispatchers.kt @@ -1,30 +1,24 @@ package org.cosmicide.common -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.asCoroutineDispatcher import java.util.concurrent.Executors -import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.ExecutorService object MemoryAwareDispatchers { - private val ioDispatcher: CoroutineDispatcher by lazy { + private val ioExecutor: ExecutorService by lazy { val threadCount = MemoryUtils.recommendedIOThreads() Executors.newFixedThreadPool(threadCount) { runnable -> Thread(runnable, "cosmic-io-${threadCount}").apply { isDaemon = true priority = Thread.NORM_PRIORITY - 1 } - }.asCoroutineDispatcher() + } } - val IO: CoroutineDispatcher - get() = ioDispatcher - - val Main: CoroutineDispatcher - get() = Dispatchers.Main + val IO: ExecutorService + get() = ioExecutor fun shutdown() { - (ioDispatcher as? java.io.Closeable)?.close() + ioExecutor.shutdownNow() } } diff --git a/common/src/main/java/org/cosmicide/common/MemoryUtils.kt b/common/src/main/java/org/cosmicide/common/MemoryUtils.kt index 469a16747..26ba21c1a 100644 --- a/common/src/main/java/org/cosmicide/common/MemoryUtils.kt +++ b/common/src/main/java/org/cosmicide/common/MemoryUtils.kt @@ -1,7 +1,5 @@ package org.cosmicide.common -import android.os.Build -import android.os.Debug import java.lang.Runtime.getRuntime object MemoryUtils { diff --git a/plugin-runtime/src/main/java/org/cosmicide/plugin/runtime/loading/PluginClassLoaderFactory.kt b/plugin-runtime/src/main/java/org/cosmicide/plugin/runtime/loading/PluginClassLoaderFactory.kt index 6613c8e07..8b716fb0e 100644 --- a/plugin-runtime/src/main/java/org/cosmicide/plugin/runtime/loading/PluginClassLoaderFactory.kt +++ b/plugin-runtime/src/main/java/org/cosmicide/plugin/runtime/loading/PluginClassLoaderFactory.kt @@ -16,9 +16,11 @@ package org.cosmicide.plugin.runtime.loading import android.content.Context import android.os.Build +import android.util.Log import dalvik.system.DexClassLoader import org.cosmicide.plugin.api.PluginDescriptor import java.io.File +import java.security.MessageDigest /** * Creates an isolated class loader for each installed plugin. @@ -64,6 +66,16 @@ class PluginClassLoaderFactory( } val optimizedDirectory = createOptimizedDirectory(descriptor) + val contentHash = computeContentHash(artifacts) + val hashFile = File(optimizedDirectory, ".content-hash") + + if (hashFile.exists() && hashFile.readText() == contentHash) { + Log.d(TAG, "Plugin ${descriptor.id} unchanged, reusing cached DEX") + } else { + optimizedDirectory.listFiles()?.filter { it.extension == "dex" }?.forEach { it.delete() } + hashFile.writeText(contentHash) + } + val dexPath = artifacts.joinToString(File.pathSeparator) { it.absolutePath } @@ -76,6 +88,15 @@ class PluginClassLoaderFactory( ) } + private fun computeContentHash(artifacts: List): String { + val digest = MessageDigest.getInstance("SHA-256") + artifacts.sortedBy { it.absolutePath }.forEach { file -> + digest.update(file.name.toByteArray(Charsets.UTF_8)) + digest.update(file.readBytes()) + } + return digest.digest().joinToString("") { "%02x".format(it) } + } + private fun createOptimizedDirectory( descriptor: PluginDescriptor ): File { @@ -119,6 +140,7 @@ class PluginClassLoaderFactory( } private companion object { + private const val TAG = "PluginClassLoader" val INVALID_CACHE_NAME_CHARACTERS = Regex("[^A-Za-z0-9._-]") } } From ab6f23df291bb7357509d80c6b969ab846a6bad5 Mon Sep 17 00:00:00 2001 From: arun28082007 Date: Sat, 8 Aug 2026 13:43:18 +0000 Subject: [PATCH 3/9] Update --- .../main/kotlin/org/cosmicide/model/ProjectRepository.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/org/cosmicide/model/ProjectRepository.kt b/app/src/main/kotlin/org/cosmicide/model/ProjectRepository.kt index 71ae0d355..c84b268f6 100644 --- a/app/src/main/kotlin/org/cosmicide/model/ProjectRepository.kt +++ b/app/src/main/kotlin/org/cosmicide/model/ProjectRepository.kt @@ -5,6 +5,7 @@ import org.cosmicide.project.Project import org.cosmicide.project.ProjectTypeProvider import org.cosmicide.util.FileUtil import java.io.File +import java.util.concurrent.ConcurrentHashMap interface ProjectRepository { fun projects(): List @@ -17,13 +18,16 @@ internal class FileSystemProjectRepository( private val projectTypeProviders: () -> List = { emptyList() } ) : ProjectRepository { private val root = projectsDirectory.canonicalFile + private val languageCache = ConcurrentHashMap() override fun projects(): List = root .listFiles { file -> file.isDirectory } ?.sortedByDescending(File::lastModified) .orEmpty() .map { projectRoot -> - Project(projectRoot, detectLanguage(projectRoot)) + Project(projectRoot, languageCache.getOrPut(projectRoot.absolutePath) { + detectLanguage(projectRoot) + }) } override fun delete(project: Project) { @@ -32,6 +36,7 @@ internal class FileSystemProjectRepository( "Projects can only be deleted from the configured projects directory" } require(target.isDirectory) { "Project no longer exists: ${project.name}" } + languageCache.remove(project.root.absolutePath) check(target.deleteRecursively()) { "Could not delete ${project.name}" } } From 70876a11d356422f4e8adc96455a0c2698bbc0cc Mon Sep 17 00:00:00 2001 From: arun28082007 Date: Sat, 8 Aug 2026 13:46:39 +0000 Subject: [PATCH 4/9] ci: add fork-only build workflow --- .github/workflows/build.yml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..a9c89e13f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,36 @@ +name: Build Debug APK + +on: + push: + branches: [ "memory" ] + workflow_dispatch: + +jobs: + build: + if: github.repository == 'arun28082007/Cosmic-IDE' + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: ./gradlew assembleProdDebug + + - name: Upload debug APK + uses: actions/upload-artifact@v4 + with: + name: app-debug + path: app/build/outputs/apk/prod/debug/*.apk From 4f31dfcaf7dc81003c61fd9f0cb9200953c010c5 Mon Sep 17 00:00:00 2001 From: arun28082007 Date: Sat, 8 Aug 2026 13:57:26 +0000 Subject: [PATCH 5/9] fix: add missing ConcurrentHashMap import --- common/src/main/java/org/cosmicide/common/DiskIndexStore.kt | 1 - common/src/main/java/org/cosmicide/common/IndexManager.kt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/org/cosmicide/common/DiskIndexStore.kt b/common/src/main/java/org/cosmicide/common/DiskIndexStore.kt index b40378132..a6dfa259b 100644 --- a/common/src/main/java/org/cosmicide/common/DiskIndexStore.kt +++ b/common/src/main/java/org/cosmicide/common/DiskIndexStore.kt @@ -7,7 +7,6 @@ import java.nio.MappedByteBuffer import java.nio.channels.FileChannel import java.security.MessageDigest import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicLong class DiskIndexStore(private val storeDir: File) { diff --git a/common/src/main/java/org/cosmicide/common/IndexManager.kt b/common/src/main/java/org/cosmicide/common/IndexManager.kt index 20f8d53a1..b3b187e7a 100644 --- a/common/src/main/java/org/cosmicide/common/IndexManager.kt +++ b/common/src/main/java/org/cosmicide/common/IndexManager.kt @@ -2,6 +2,7 @@ package org.cosmicide.common import android.content.Context import java.io.File +import java.util.concurrent.ConcurrentHashMap object IndexManager { From 08f73f6102478690c2f0da833fd9c81027e36e65 Mon Sep 17 00:00:00 2001 From: arun28082007 Date: Sat, 8 Aug 2026 14:08:50 +0000 Subject: [PATCH 6/9] ci update only for fork --- .github/workflows/build.yml | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a9c89e13f..8d665f77c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,32 +5,46 @@ on: branches: [ "memory" ] workflow_dispatch: + + jobs: build: if: github.repository == 'arun28082007/Cosmic-IDE' - runs-on: ubuntu-latest + runs-on: macos-latest steps: - name: Checkout repository uses: actions/checkout@v4 + - name: Setup Zig + uses: mlugg/setup-zig@v2 + - name: Set up JDK 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@v4.2.1 with: distribution: 'temurin' java-version: '17' - - name: Setup Android SDK - uses: android-actions/setup-android@v3 + - name: Install patch tools + run: brew install binutils jq ncurses zstd gnu-tar python + + - name: Generate glibc.tar.zst + run: bash ./scripts/build-glibc.sh - - name: Grant execute permission for gradlew + - name: Give permission to executable run: chmod +x gradlew - - name: Build with Gradle - run: ./gradlew assembleProdDebug + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Build with gradle + uses: gradle/gradle-build-action@v3.1.0 + with: + gradle-version: nightly + arguments: assembleProdDebug - - name: Upload debug APK + - name: Upload debug apks uses: actions/upload-artifact@v4 with: - name: app-debug - path: app/build/outputs/apk/prod/debug/*.apk + name: app-arm64-v8a + path: app/build/outputs/apk/prod/debug/app-prod-arm64-v8a-debug.apk From ac9de57e29bb4d8e047e1797190777bbf4df0cf3 Mon Sep 17 00:00:00 2001 From: arun28082007 Date: Mon, 10 Aug 2026 06:25:42 +0000 Subject: [PATCH 7/9] some chnages after dev reviee --- .../src/main/java/org/cosmicide/gradle/Main.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/feature/tooling/src/main/java/org/cosmicide/gradle/Main.java b/feature/tooling/src/main/java/org/cosmicide/gradle/Main.java index abf8176d6..c83ae0fdc 100644 --- a/feature/tooling/src/main/java/org/cosmicide/gradle/Main.java +++ b/feature/tooling/src/main/java/org/cosmicide/gradle/Main.java @@ -278,16 +278,6 @@ private static String stackTrace(Throwable throwable) { return stringWriter.toString(); } - private static int recommendedThreadCount() { - int cores = Runtime.getRuntime().availableProcessors(); - long maxMb = Runtime.getRuntime().maxMemory() / (1024 * 1024); - - if (maxMb < 128) return 1; - if (maxMb < 256) return Math.min(2, cores); - if (maxMb < 512) return Math.min(4, cores); - return Math.min(cores, 8); - } - private static final class GradleToolingServer implements Closeable { private final ProtocolWriter writer; private final ExecutorService executor; @@ -302,7 +292,7 @@ private static final class GradleToolingServer implements Closeable { this.writer = writer; this.project = project; this.connector = connectorFor(project); - this.executor = Executors.newFixedThreadPool(recommendedThreadCount(), runnable -> { + this.executor = Executors.newCachedThreadPool(runnable -> { Thread thread = new Thread(runnable, "cosmic-gradle-provider-worker"); thread.setDaemon(false); return thread; From 2488b6f12fa53f98083cbdd2a01eef58c0995eb7 Mon Sep 17 00:00:00 2001 From: Arun Anand <68988353+arun28082007@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:29:13 +0530 Subject: [PATCH 8/9] Delete app/src/main/kotlin/org/cosmicide/common/MemoryAwareDispatchers.kt --- .../main/kotlin/org/cosmicide/common/MemoryAwareDispatchers.kt | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 app/src/main/kotlin/org/cosmicide/common/MemoryAwareDispatchers.kt diff --git a/app/src/main/kotlin/org/cosmicide/common/MemoryAwareDispatchers.kt b/app/src/main/kotlin/org/cosmicide/common/MemoryAwareDispatchers.kt deleted file mode 100644 index 9ada37d4b..000000000 --- a/app/src/main/kotlin/org/cosmicide/common/MemoryAwareDispatchers.kt +++ /dev/null @@ -1,2 +0,0 @@ -// MemoryAwareDispatchers lives in common module. -// Use org.cosmicide.common.MemoryAwareDispatchers.IO (ExecutorService). From 7480f15b8496c9e5ca71224dbc10fb5d1b5fb2ee Mon Sep 17 00:00:00 2001 From: Arun Anand <68988353+arun28082007@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:30:13 +0530 Subject: [PATCH 9/9] Delete .github/workflows/build.yml --- .github/workflows/build.yml | 50 ------------------------------------- 1 file changed, 50 deletions(-) delete mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 8d665f77c..000000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Build Debug APK - -on: - push: - branches: [ "memory" ] - workflow_dispatch: - - - -jobs: - build: - if: github.repository == 'arun28082007/Cosmic-IDE' - runs-on: macos-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Zig - uses: mlugg/setup-zig@v2 - - - name: Set up JDK 17 - uses: actions/setup-java@v4.2.1 - with: - distribution: 'temurin' - java-version: '17' - - - name: Install patch tools - run: brew install binutils jq ncurses zstd gnu-tar python - - - name: Generate glibc.tar.zst - run: bash ./scripts/build-glibc.sh - - - name: Give permission to executable - run: chmod +x gradlew - - - name: Setup Android SDK - uses: android-actions/setup-android@v3 - - - name: Build with gradle - uses: gradle/gradle-build-action@v3.1.0 - with: - gradle-version: nightly - arguments: assembleProdDebug - - - name: Upload debug apks - uses: actions/upload-artifact@v4 - with: - name: app-arm64-v8a - path: app/build/outputs/apk/prod/debug/app-prod-arm64-v8a-debug.apk