Skip to content
This repository has been archived by the owner on Oct 18, 2024. It is now read-only.

Commit

Permalink
fix(editor): Dispatchers.IO thread pool is exhausted if files are con…
Browse files Browse the repository at this point in the history
…tinuously opened and closed (fixes #1551)
  • Loading branch information
itsaky committed Dec 17, 2023
1 parent 29993c5 commit cd55f9d
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 47 deletions.
84 changes: 47 additions & 37 deletions app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,17 @@ import io.github.rosemoe.sora.widget.CodeEditor
import io.github.rosemoe.sora.widget.component.Magnifier
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.io.Closeable
import java.io.File
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext

/**
* A view that handles opened code editor.
Expand All @@ -112,6 +113,15 @@ class CodeEditorView(
private val codeEditorScope = CoroutineScope(
Dispatchers.Default + CoroutineName("CodeEditorView"))

/**
* The [CoroutineContext][kotlin.coroutines.CoroutineContext] used to reading and writing the file
* in this editor. We use a separate, single-threaded context assuming that the file will be either
* read from or written to at a time, but not both. If in future we add support for anything like
* that, the number of thread should probably be increased.
*/
@OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class)
private val readWriteContext = newSingleThreadContext("CodeEditorView")

private val binding: LayoutCodeEditorBinding
get() = checkNotNull(_binding) { "Binding has been destroyed" }

Expand Down Expand Up @@ -222,8 +232,7 @@ class CodeEditorView(
val file = this.file ?: return false

if (!isModified && file.exists()) {
log.info(file.name)
log.info("File was not modified. Skipping save operation.")
log.info("File was not modified. Skipping save operation.", file.name)
return false
}

Expand All @@ -232,16 +241,19 @@ class CodeEditorView(
return false
}

disableEditingAndRun(Dispatchers.IO) {
// Do not call suspend functions in this scope
// the writeTo function acquires lock to the Content object before writing and releases
// the lock after writing
// if there are any suspend function calls in between, the lock and unlock calls might not
// be called on the same thread
text.writeTo(file, this@CodeEditorView::updateReadWriteProgress)
}
withContext(Dispatchers.Main.immediate) {

withEditingDisabled {
withContext(readWriteContext) {
// Do not call suspend functions in this scope
// the writeTo function acquires lock to the Content object before writing and releases
// the lock after writing
// if there are any suspend function calls in between, the lock and unlock calls might not
// be called on the same thread
text.writeTo(file, this@CodeEditorView::updateReadWriteProgress)
}
}

withContext(Dispatchers.Main) {
_binding?.rwProgress?.isVisible = false
}

Expand All @@ -267,44 +279,40 @@ class CodeEditorView(
}
}

private suspend inline fun <R> disableEditingAndRun(
context: CoroutineContext = EmptyCoroutineContext,
crossinline action: CoroutineScope.() -> R
): R {
return withContext(Dispatchers.Main) {
private inline fun <R : Any?> withEditingDisabled(action: () -> R): R {
return try {
_binding?.editor?.isEditable = false

val result = withContext(context) {
action()
}

action()
} finally {
_binding?.editor?.isEditable = true
return@withContext result
}
}

private fun readFileAndApplySelection(file: File, selection: Range) {
updateReadWriteProgress(0)
codeEditorScope.launch {
val content = disableEditingAndRun(Dispatchers.IO) {
selection.validate()
file.readContent(this@CodeEditorView::updateReadWriteProgress)
}
codeEditorScope.launch(Dispatchers.Main.immediate) {
updateReadWriteProgress(0)

withEditingDisabled {

val content = withContext(readWriteContext) {
selection.validate()
file.readContent(this@CodeEditorView::updateReadWriteProgress)
}

withContext(Dispatchers.Main) {
initializeContent(content, file, selection)
_binding?.rwProgress?.isVisible = false
}
}
}

private fun initializeContent(content: Content, file: File, selection: Range) {
binding.editor.apply {
val ideEditor = binding.editor
ideEditor.postInLifecycle {
val args = Bundle().apply {
putString(IEditor.KEY_FILE, file.absolutePath)
}

setText(content, args)
ideEditor.setText(content, args)

// editor.setText(...) sets the modified flag to true
// but in this case, file is read from disk and hence the contents are not modified at all
Expand All @@ -313,11 +321,11 @@ class CodeEditorView(
markUnmodified()
postRead(file)

validateRange(selection)
setSelection(selection)
}
ideEditor.validateRange(selection)
ideEditor.setSelection(selection)

configureEditorIfNeeded()
configureEditorIfNeeded()
}
}

private fun postRead(file: File) {
Expand Down Expand Up @@ -514,5 +522,7 @@ class CodeEditorView(
notifyClose()
release()
}

readWriteContext.use { }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ import io.github.rosemoe.sora.lang.analysis.StyleReceiver
import io.github.rosemoe.sora.lang.styling.CodeBlock
import io.github.rosemoe.sora.lang.styling.Styles
import io.github.rosemoe.sora.text.ContentReference
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.newSingleThreadContext
import java.lang.RuntimeException
import java.util.concurrent.CancellationException
import java.util.concurrent.LinkedBlockingQueue

Expand All @@ -61,11 +62,15 @@ internal class TsAnalyzeWorker(

var stylesReceiver: StyleReceiver? = null

private val analyzerScope = CoroutineScope(Dispatchers.Default + CoroutineName("TsAnalyzeWorker"))
@OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class)
private val analyzerContext = newSingleThreadContext("TsAnalyzeWorkerContext")

private val analyzerScope = CoroutineScope(analyzerContext)
private val messageChannel = LinkedBlockingQueue<Message<*>>()
private var analyzerJob: Job? = null

private var isInitialized = false
private var isDestroyed = false

private var tree: TSTree? = null
private val localText = UTF16StringFactory.newString()
Expand All @@ -74,14 +79,28 @@ internal class TsAnalyzeWorker(
}

fun init(init: Init) {
if (isDestroyed) {
log.warn("Received Init after TsAnalyzeWorker has been destroyed. Ignoring...")
return
}

messageChannel.offer(init)
}

fun onMod(mod: Mod) {
if (isDestroyed) {
log.warn("Received Mod after TsAnalyzeWorker has been destroyed. Ignoring...")
return
}

messageChannel.offer(mod)
}

fun stop() {
log.debug("Stopping TsAnalyzeWorker...")
isDestroyed = true

analyzerContext.close()
messageChannel.clear()
analyzerJob?.cancel(CancellationException("Requested to be stopped"))
localText.close()
Expand All @@ -90,7 +109,8 @@ internal class TsAnalyzeWorker(
}

fun start() {
analyzerJob = analyzerScope.launch(Dispatchers.Default) {
check(!isDestroyed) { "TsAnalyeWorker has already been destroyed" }
analyzerJob = analyzerScope.launch {
while (isActive) {
processNextMessage()
}
Expand All @@ -105,8 +125,8 @@ internal class TsAnalyzeWorker(
}
}

private suspend fun processNextMessage() {
val message = withContext(Dispatchers.IO) { messageChannel.take() }
private fun processNextMessage() {
val message = messageChannel.take()

try {
when (message) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ object IDEColorSchemeProvider {
private const val SCHEME_IS_DARK = "scheme.isDark"
private const val SCHEME_FILE = "scheme.file"

private var isDefaultSchemeLoaded = false
private var isCurrentSchemeLoaded = false

/**
* The default color scheme.
*
Expand All @@ -58,7 +61,10 @@ object IDEColorSchemeProvider {
*/
private var defaultScheme: IDEColorScheme? = null
get() {
return field ?: getColorScheme(DEFAULT_COLOR_SCHEME).also { field = it }
return field ?: getColorScheme(DEFAULT_COLOR_SCHEME).also { scheme ->
field = scheme
isDefaultSchemeLoaded = scheme != null
}
}

/**
Expand All @@ -69,7 +75,10 @@ object IDEColorSchemeProvider {
*/
private var currentScheme: IDEColorScheme? = null
get() {
return field ?: getColorScheme(colorScheme).also { field = it }
return field ?: getColorScheme(colorScheme).also { scheme ->
field = scheme
isCurrentSchemeLoaded = scheme != null
}
}

/**
Expand Down Expand Up @@ -179,9 +188,21 @@ object IDEColorSchemeProvider {
context: Context,
coroutineScope: CoroutineScope,
type: String? = null,
callbackContext: CoroutineContext = Dispatchers.Main,
callbackContext: CoroutineContext = Dispatchers.Main.immediate,
callback: (SchemeAndroidIDE?) -> Unit
) {

// If the scheme has already been loaded, do not bother to dispatch an IO coroutine
// simply invoke the callback on the requested context
if (isCurrentSchemeLoaded && isDefaultSchemeLoaded) {
coroutineScope.launch(callbackContext) {
callback(readScheme(context, type))
}
return
}

// scheme has not been loaded
// load the scheme using the IO dispatcher
coroutineScope.launch(Dispatchers.IO) {
val scheme = readScheme(context, type)
withContext(callbackContext) {
Expand Down Expand Up @@ -252,7 +273,10 @@ object IDEColorSchemeProvider {
fun destroy() {
this.schemes.clear()
this.currentScheme = null
this.isCurrentSchemeLoaded = false

this.defaultScheme = null
this.isDefaultSchemeLoaded = false
}

@WorkerThread
Expand Down

0 comments on commit cd55f9d

Please sign in to comment.