IDEA-CR-56509 CancellationCheck is added

Relates to #KT-35135

GitOrigin-RevId: c74d2ca3f6a6c2d1cebc922ee9d903fb5c631d3d
This commit is contained in:
Vladimir Dolzhenko
2019-12-13 13:31:47 +00:00
committed by intellij-monorepo-bot
parent 5b0f17f657
commit 451b06bb3e
3 changed files with 143 additions and 0 deletions
@@ -0,0 +1,83 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.openapi.progress.impl
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.util.registry.Registry
import org.jetbrains.annotations.TestOnly
/**
* It is used to check if [ProgressManager.checkCanceled] is invoked often enough - at least once per a threshold ms.
*
* For global usage [CancellationCheck.runWithCancellationCheck] could be used:
* - it has to be enabled with a registry key `ide.cancellation.check.enabled`, it is disabled by default
* - threshold (in ms) is specified with a registry key `ide.cancellation.check.threshold`, default is 500
*/
class CancellationCheck private constructor(val thresholdMs: () -> Long, val checkEnabled: () -> Boolean) {
@TestOnly
internal constructor(thresholdMs: Long): this(thresholdMs = { thresholdMs }, checkEnabled = { true })
private val statusRecord = ThreadLocal.withInitial { CanceledStatusRecord() }
private val hook = CoreProgressManager.CheckCanceledHook { indicator ->
checkCancellationDiff(statusRecord.get())
return@CheckCanceledHook indicator != null
}
private fun checkCancellationDiff(record: CanceledStatusRecord) {
if (record.enabled) {
val now = System.currentTimeMillis()
val diff = now - record.timestamp
if (diff > thresholdMs()) {
LOG.error("${Thread.currentThread().name} last checkCanceled was $diff ms ago")
}
record.timestamp = now
}
}
private fun enableCancellationTimer(record: CanceledStatusRecord, enabled: Boolean) {
val progressManagerImpl = ProgressManager.getInstance() as ProgressManagerImpl
if (enabled) progressManagerImpl.addCheckCanceledHook(hook) else progressManagerImpl.removeCheckCanceledHook(hook)
record.enabled = enabled
record.timestamp = System.currentTimeMillis()
}
fun <T> withCancellationCheck(block: () -> T): T {
if (!checkEnabled()) return block()
val record = statusRecord.get()
if (record.enabled) return block()
enableCancellationTimer(record, true)
try {
return block()
} finally {
try {
checkCancellationDiff(record)
}
finally {
enableCancellationTimer(record,false)
}
}
}
private data class CanceledStatusRecord(var enabled: Boolean = false, var timestamp: Long = System.currentTimeMillis())
companion object {
private val LOG = Logger.getInstance(CancellationCheck::class.java)
@JvmStatic
private val INSTANCE: CancellationCheck =
CancellationCheck(
thresholdMs = { Registry.intValue("ide.cancellation.check.threshold").toLong() },
checkEnabled = { Registry.`is`("ide.cancellation.check.enabled") }
)
@JvmStatic
fun <T> runWithCancellationCheck(block: () -> T): T =
INSTANCE.withCancellationCheck(block)
}
}
@@ -0,0 +1,55 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.openapi.progress.impl;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.util.ProgressIndicatorBase;
import com.intellij.testFramework.LightPlatformTestCase;
public class CancellationCheckTest extends LightPlatformTestCase {
private final ProgressIndicator myProgressIndicator = new ProgressIndicatorBase();
public void testNormal() {
int period = 10;
int times = 10;
CancellationCheck cancellation = new CancellationCheck(times * period);
runWithCheckCancellation(cancellation, period, times, 1);
}
public void testReentrant() {
int period = 10;
int times = 5;
CancellationCheck cancellation = new CancellationCheck(times * period);
runWithCheckCancellation(cancellation, period, times, 3);
}
public void testExceededThreshold() {
CancellationCheck cancellation = new CancellationCheck(1);
assertThrows(Throwable.class,
"AWT-EventQueue-0 last checkCanceled was ",
() -> {
runWithCheckCancellation(cancellation, 10, 1, 1);
});
}
private void runWithCheckCancellation(CancellationCheck cancellation, int period, int times, int depth) {
cancellation.withCancellationCheck(() -> {
for (int attempt = 0; attempt < times * 2; attempt++) {
myProgressIndicator.checkCanceled();
try {
Thread.sleep(period);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (depth - 1 > 0) {
runWithCheckCancellation(cancellation, period, times, depth - 1);
}
}
return null;
});
}
}
@@ -1805,6 +1805,11 @@ ide.io.preemptive.safe.write=false
ide.io.preemptive.safe.write.description=Activates more destructive variant of Settings | Appearance & Behavior | System Settings | Use "safe write" - \
which deletes and recreates files instead of overwriting them in-place.
ide.cancellation.check.enabled=false
ide.cancellation.check.enabled.description=Enable checkCanceled invocation frequency check
ide.cancellation.check.threshold=500
ide.cancellation.check.threshold.description=Report error if checkCanceled invoked more than threshold ms ago
light.edit.file.open.enabled=false
light.edit.file.open.enabled.description=Open files in off-project editor instead of creating a temporary project