mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[threading] RUST-20908: Compensate parallelism for invokeAndWait
IJ-CR-221203 GitOrigin-RevId: 7f4172a0e7ad0180abf1845ef1142067b1eb3082
This commit is contained in:
committed by
intellij-monorepo-bot
parent
4dbc16b8c4
commit
03d73bbc24
@@ -18,7 +18,8 @@ import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
internal fun waitWithParallelismCompensation(runnable: Runnable) {
|
||||
@ApiStatus.Internal
|
||||
fun waitWithParallelismCompensation(runnable: Runnable) {
|
||||
IntelliJCoroutinesFacade.runAndCompensateParallelism(500.milliseconds, runnable::run)
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -55,6 +55,7 @@ import com.intellij.openapi.progress.impl.ProgressRunner;
|
||||
import com.intellij.openapi.progress.util.PotemkinProgress;
|
||||
import com.intellij.openapi.progress.util.ProgressWindow;
|
||||
import com.intellij.openapi.progress.util.SuvorovProgress;
|
||||
import com.intellij.openapi.progress.util.UtilKt;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.project.ProjectManager;
|
||||
import com.intellij.openapi.project.ex.ProjectManagerEx;
|
||||
@@ -627,7 +628,9 @@ public final class ApplicationImpl extends ClientAwareComponentManager implement
|
||||
// Outer layer context capture & reset
|
||||
final var finalRunnable = AppImplKt.rethrowExceptions(AppScheduledExecutorService::captureContextCancellationForRunnableThatDoesNotOutliveContextScope, locked);
|
||||
|
||||
LaterInvocator.invokeAndWait(state, wrapWithLocks, finalRunnable);
|
||||
UtilKt.waitWithParallelismCompensation(() -> {
|
||||
LaterInvocator.invokeAndWait(state, wrapWithLocks, finalRunnable);
|
||||
});
|
||||
}
|
||||
|
||||
private @NotNull Runnable wrapWithRunIntendedWriteActionAndModality(@NotNull Runnable runnable,
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.intellij.openapi.application.impl.CancellableReadActionWithIndicatorT
|
||||
import com.intellij.openapi.application.impl.CancellableReadActionWithJobTest
|
||||
import com.intellij.openapi.application.impl.EdtCoroutineDispatcherTest
|
||||
import com.intellij.openapi.application.impl.ImplicitReadTest
|
||||
import com.intellij.openapi.application.impl.InvokeAndWaitStarvationTest
|
||||
import com.intellij.openapi.application.impl.LaterInvocatorTest
|
||||
import com.intellij.openapi.application.impl.LockDowngradingTest
|
||||
import com.intellij.openapi.application.impl.ModalCoroutineTest
|
||||
@@ -51,6 +52,7 @@ import org.junit.platform.suite.api.Suite
|
||||
ProgressRunnerTest::class,
|
||||
EdtCoroutineDispatcherTest::class,
|
||||
ImplicitReadTest::class,
|
||||
InvokeAndWaitStarvationTest::class,
|
||||
LaterInvocatorTest::class,
|
||||
ModalCoroutineTest::class,
|
||||
ReadWritePropagationTest::class,
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.openapi.application.impl
|
||||
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.testFramework.common.timeoutRunBlocking
|
||||
import com.intellij.testFramework.junit5.TestApplication
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.future.asCompletableFuture
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.Timeout
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@TestApplication
|
||||
class InvokeAndWaitStarvationTest {
|
||||
|
||||
/**
|
||||
* [com.intellij.openapi.application.Application.invokeAndWait] parks the calling thread until the EDT picks the runnable up.
|
||||
* When the EDT is busy, and every [Dispatchers.Default] thread is parked this way, the dispatcher has nothing left to run
|
||||
* the coroutine that would eventually free the EDT -- neither the queued `invokeAndWait` callers nor the coroutine releasing
|
||||
* the EDT can make progress, and the IDE deadlocks.
|
||||
*
|
||||
* [ApplicationImpl.doInvokeAndWait] avoids this by compensating parallelism while it waits, which spawns extra pool workers.
|
||||
*
|
||||
* `BlockingSuspendingReadActionTest.pending read action do not cause thread starvation for default dispatcher`
|
||||
* covers the same scenario for a read action pending behind a write action.
|
||||
*
|
||||
* @see com.intellij.openapi.progress.util.waitWithParallelismCompensation
|
||||
*/
|
||||
@Test
|
||||
@Timeout(30)
|
||||
fun `invokeAndWait does not cause thread starvation for default dispatcher`(): Unit = timeoutRunBlocking(context = Dispatchers.Default) {
|
||||
val operationsCount = Runtime.getRuntime().availableProcessors().coerceAtLeast(2) * 2
|
||||
val startedInvocations = AtomicInteger()
|
||||
val completedInvocations = AtomicInteger()
|
||||
val edtIsOccupied = Job(coroutineContext.job)
|
||||
val edtMayProceed = Job(coroutineContext.job)
|
||||
val edtMayProceedFuture = edtMayProceed.asCompletableFuture()
|
||||
try {
|
||||
// occupy the EDT, so that every `invokeAndWait` below has to park
|
||||
SwingUtilities.invokeLater {
|
||||
edtIsOccupied.complete()
|
||||
try {
|
||||
edtMayProceedFuture.join()
|
||||
}
|
||||
catch (_: Throwable) {
|
||||
// the surrounding job was cancelled because the test has failed; the EDT must not stay blocked anyway
|
||||
}
|
||||
}
|
||||
coroutineScope {
|
||||
launch(Dispatchers.IO) {
|
||||
// `operationsCount` exceeds the parallelism of `Dispatchers.Default`, so the later callers get a thread
|
||||
// only after the earlier ones have compensated the parallelism they took away
|
||||
while (startedInvocations.get() < operationsCount) {
|
||||
delay(50.milliseconds)
|
||||
}
|
||||
// give the last callers a chance to actually park inside `invokeAndWait`
|
||||
delay(1.seconds)
|
||||
// the EDT is released from `Dispatchers.Default`: without parallelism compensation there is no thread left
|
||||
// to run this continuation
|
||||
withContext(Dispatchers.Default) {
|
||||
edtMayProceed.complete()
|
||||
}
|
||||
}
|
||||
repeat(operationsCount) {
|
||||
launch {
|
||||
edtIsOccupied.join()
|
||||
startedInvocations.incrementAndGet()
|
||||
@Suppress("ForbiddenInSuspectContextMethod") // blocking the coroutine thread is exactly what is under test
|
||||
ApplicationManager.getApplication().invokeAndWait {
|
||||
completedInvocations.incrementAndGet()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
edtMayProceed.complete()
|
||||
}
|
||||
assertEquals(operationsCount, completedInvocations.get())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user