From 451b06bb3ec960698586248b6c54434c59bd3f56 Mon Sep 17 00:00:00 2001 From: Vladimir Dolzhenko Date: Thu, 12 Dec 2019 11:45:48 +0100 Subject: [PATCH] IDEA-CR-56509 CancellationCheck is added Relates to #KT-35135 GitOrigin-RevId: c74d2ca3f6a6c2d1cebc922ee9d903fb5c631d3d --- .../progress/impl/CancellationCheck.kt | 83 +++++++++++++++++++ .../progress/impl/CancellationCheckTest.java | 55 ++++++++++++ .../util/resources/misc/registry.properties | 5 ++ 3 files changed, 143 insertions(+) create mode 100644 platform/platform-impl/src/com/intellij/openapi/progress/impl/CancellationCheck.kt create mode 100644 platform/platform-tests/testSrc/com/intellij/openapi/progress/impl/CancellationCheckTest.java diff --git a/platform/platform-impl/src/com/intellij/openapi/progress/impl/CancellationCheck.kt b/platform/platform-impl/src/com/intellij/openapi/progress/impl/CancellationCheck.kt new file mode 100644 index 000000000000..b6b7679533d8 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/progress/impl/CancellationCheck.kt @@ -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 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 runWithCancellationCheck(block: () -> T): T = + INSTANCE.withCancellationCheck(block) + } +} + + diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/progress/impl/CancellationCheckTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/progress/impl/CancellationCheckTest.java new file mode 100644 index 000000000000..061f5572e00a --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/progress/impl/CancellationCheckTest.java @@ -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; + }); + } +} diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index 05062a366e94..8f964b07ea7e 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -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