mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
fix(editor): Dispatchers.IO thread pool is exhausted if files are continuously opened and closed (fixes #1551)
This commit is contained in:
@@ -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.
|
||||
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -267,31 +279,26 @@ 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
|
||||
}
|
||||
@@ -299,12 +306,13 @@ class CodeEditorView(
|
||||
}
|
||||
|
||||
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
|
||||
@@ -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) {
|
||||
@@ -514,5 +522,7 @@ class CodeEditorView(
|
||||
notifyClose()
|
||||
release()
|
||||
}
|
||||
|
||||
readWriteContext.use { }
|
||||
}
|
||||
}
|
||||
+27
-7
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
+27
-3
@@ -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.
|
||||
*
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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) {
|
||||
@@ -252,7 +273,10 @@ object IDEColorSchemeProvider {
|
||||
fun destroy() {
|
||||
this.schemes.clear()
|
||||
this.currentScheme = null
|
||||
this.isCurrentSchemeLoaded = false
|
||||
|
||||
this.defaultScheme = null
|
||||
this.isDefaultSchemeLoaded = false
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
||||
Reference in New Issue
Block a user