mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-06 03:21:12 +07:00
[performance] IJPL-161370 Detect UI freezes caused by third-party plugins and disable them
(cherry picked from commit 57013d91d5767602b1cb6db72d644e83bed2339b) IJ-CR-146685 GitOrigin-RevId: e1a46a784e5c742410087590b1013a6d553f415b
This commit is contained in:
committed by
intellij-monorepo-bot
parent
d47bffbfe0
commit
22b4b596ea
@@ -17,5 +17,6 @@
|
||||
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
|
||||
<orderEntry type="library" name="kotlinx-serialization-json" level="project" />
|
||||
<orderEntry type="library" name="kotlinx-serialization-core" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.statistics" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -6,4 +6,9 @@
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<editorNotificationProvider implementation="com.intellij.platform.diagnostic.plugin.freeze.PluginFreezeNotifier"/>
|
||||
</extensions>
|
||||
|
||||
<actions>
|
||||
<action id="ResetFreezeNotificationState" class="com.intellij.platform.diagnostic.plugin.freeze.ResetFreezeNotificationStateAction"
|
||||
internal="true" text="Reset Freezes Notification State"/>
|
||||
</actions>
|
||||
</idea-plugin>
|
||||
@@ -1,4 +1,4 @@
|
||||
action.disable.plugin.text=Disable plugin
|
||||
action.close.panel.text=Dismiss
|
||||
action.ignore.plugin.text=Ignore freezes in plugin
|
||||
notification.content.plugin.caused.freeze.detected=Freeze caused by the {0} plugin
|
||||
notification.content.plugin.caused.freeze.detected=Plugin ''{0}'' might be slowing things down
|
||||
@@ -1,12 +1,12 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.platform.diagnostic.plugin.freeze
|
||||
|
||||
import com.intellij.featureStatistics.fusCollectors.LifecycleUsageTriggerCollector
|
||||
import com.intellij.ide.plugins.PluginManagerCore
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.impl.ApplicationInfoImpl
|
||||
import com.intellij.openapi.extensions.PluginId
|
||||
import com.intellij.openapi.fileEditor.FileEditor
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.ui.EditorNotificationPanel
|
||||
@@ -21,23 +21,25 @@ internal class PluginFreezeNotifier : EditorNotificationProvider {
|
||||
private val freezeStorageService = PluginsFreezesService.getInstance()
|
||||
|
||||
override fun collectNotificationData(project: Project, file: VirtualFile): Function<in FileEditor, out JComponent?>? {
|
||||
val frozenPlugin = freezeWatcher.latestFrozenPlugin ?: return null
|
||||
val frozenPlugin = freezeWatcher.getFreezeReason() ?: return null
|
||||
val pluginDescriptor = PluginManagerCore.getPlugin(frozenPlugin) ?: return null
|
||||
val application = ApplicationManager.getApplication()
|
||||
if (pluginDescriptor.isImplementationDetail || ApplicationInfoImpl.getShadowInstance().isEssentialPlugin(frozenPlugin)) return null
|
||||
if (pluginDescriptor.isBundled && !application.isEAP && !application.isInternal) return null
|
||||
if (freezeStorageService.shouldBeIgnored(frozenPlugin)) return null
|
||||
|
||||
freezeStorageService.setLatestFreezeDate(frozenPlugin)
|
||||
|
||||
return Function {
|
||||
EditorNotificationPanel(EditorNotificationPanel.Status.Warning).apply {
|
||||
text = PluginFreezeBundle.message("notification.content.plugin.caused.freeze.detected", pluginDescriptor.name)
|
||||
createActionLabel(PluginFreezeBundle.message("action.disable.plugin.text")) {
|
||||
disablePlugin(frozenPlugin)
|
||||
|
||||
LifecycleUsageTriggerCollector.pluginDisabledOnFreeze(frozenPlugin)
|
||||
}
|
||||
createActionLabel(PluginFreezeBundle.message("action.ignore.plugin.text")) {
|
||||
freezeStorageService.mutePlugin(frozenPlugin)
|
||||
|
||||
LifecycleUsageTriggerCollector.pluginFreezeIgnored(frozenPlugin)
|
||||
|
||||
closePanel(project)
|
||||
}
|
||||
createActionLabel(PluginFreezeBundle.message("action.close.panel.text")) {
|
||||
@@ -47,7 +49,7 @@ internal class PluginFreezeNotifier : EditorNotificationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
//TODO support dynamic plugins
|
||||
// TODO support dynamic plugins
|
||||
private fun disablePlugin(frozenPlugin: PluginId) {
|
||||
val pluginDisabled = PluginManagerCore.disablePlugin(frozenPlugin)
|
||||
if (pluginDisabled) {
|
||||
@@ -56,8 +58,7 @@ internal class PluginFreezeNotifier : EditorNotificationProvider {
|
||||
}
|
||||
|
||||
private fun closePanel(project: Project) {
|
||||
freezeWatcher.latestFrozenPlugin = null
|
||||
FileEditorManager.getInstance(project).openFiles.forEach { it -> EditorNotifications.getInstance(project).updateNotifications(it) }
|
||||
freezeWatcher.reset()
|
||||
EditorNotifications.getInstance(project).updateAllNotifications()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,24 +2,30 @@ package com.intellij.platform.diagnostic.plugin.freeze
|
||||
|
||||
import com.intellij.diagnostic.IdePerformanceListener
|
||||
import com.intellij.diagnostic.ThreadDump
|
||||
import com.intellij.featureStatistics.fusCollectors.LifecycleUsageTriggerCollector
|
||||
import com.intellij.ide.plugins.PluginUtilImpl
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.ReadAction
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.extensions.PluginId
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.intellij.platform.diagnostic.freezeAnalyzer.FreezeAnalyzer
|
||||
import com.intellij.threadDumpParser.ThreadState
|
||||
import com.intellij.ui.EditorNotifications
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.absolutePathString
|
||||
|
||||
@Service(Service.Level.APP)
|
||||
internal class PluginFreezeWatcher : IdePerformanceListener, Disposable {
|
||||
@Volatile
|
||||
var latestFrozenPlugin: PluginId? = null
|
||||
private val stackTracePattern = """at (\S+)\.(\S+)\(([^:]+):(\d+)\)""".toRegex()
|
||||
private var latestFrozenPlugin: PluginId? = null
|
||||
@Volatile
|
||||
private var lastFreezeDuration: Long = -1
|
||||
|
||||
private val stackTracePattern: Regex = """at (\S+)\.(\S+)\(([^:]+):(\d+)\)""".toRegex()
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@@ -27,19 +33,63 @@ internal class PluginFreezeWatcher : IdePerformanceListener, Disposable {
|
||||
}
|
||||
|
||||
init {
|
||||
ApplicationManager.getApplication().messageBus.connect(this).subscribe(IdePerformanceListener.TOPIC, this)
|
||||
ApplicationManager.getApplication().messageBus.connect(this)
|
||||
.subscribe(IdePerformanceListener.TOPIC, this)
|
||||
}
|
||||
|
||||
fun getFreezeReason(): PluginId? = latestFrozenPlugin
|
||||
|
||||
fun reset() {
|
||||
latestFrozenPlugin = null
|
||||
lastFreezeDuration = -1
|
||||
}
|
||||
|
||||
override fun dispose() {}
|
||||
|
||||
override fun uiFreezeStarted(reportDir: Path) {
|
||||
if (latestFrozenPlugin != null) return
|
||||
|
||||
lastFreezeDuration = -1
|
||||
}
|
||||
|
||||
override fun uiFreezeFinished(durationMs: Long, reportDir: Path?) {
|
||||
if (lastFreezeDuration < 0) {
|
||||
lastFreezeDuration = durationMs
|
||||
|
||||
reportCounters()
|
||||
}
|
||||
}
|
||||
|
||||
override fun dumpedThreads(toFile: Path, dump: ThreadDump) {
|
||||
if (latestFrozenPlugin != null) return // user have not yet handled previous failure
|
||||
|
||||
val freezeCausingThreads = FreezeAnalyzer.analyzeFreeze(dump.rawDump, null)?.threads.orEmpty()
|
||||
val pluginIds = freezeCausingThreads.mapNotNull { analyzeFreezeCausingPlugin(it) }
|
||||
latestFrozenPlugin = pluginIds.groupingBy { it }.eachCount().maxByOrNull { it.value }?.key
|
||||
val frozenPlugin = pluginIds.groupingBy { it }.eachCount().maxByOrNull { it.value }?.key ?: return
|
||||
|
||||
ProjectManager.getInstance().openProjects.firstOrNull()?.let { project ->
|
||||
FileEditorManager.getInstance(project).focusedEditor?.file?.let { file ->
|
||||
EditorNotifications.getInstance(project).updateNotifications(file)
|
||||
val freezeStorageService = PluginsFreezesService.getInstance()
|
||||
if (freezeStorageService.shouldBeIgnored(frozenPlugin)) return
|
||||
freezeStorageService.setLatestFreezeDate(frozenPlugin)
|
||||
|
||||
latestFrozenPlugin = frozenPlugin
|
||||
|
||||
Logger.getInstance(PluginFreezeWatcher::class.java)
|
||||
.warn("Plugin '$frozenPlugin' caused IDE freeze." +
|
||||
"Find thread dumps at ${toFile.absolutePathString()}")
|
||||
|
||||
reportCounters()
|
||||
|
||||
ReadAction.compute<Unit, Throwable> {
|
||||
for (project in ProjectManager.getInstance().openProjects) {
|
||||
EditorNotifications.getInstance(project).updateAllNotifications()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun reportCounters() {
|
||||
latestFrozenPlugin?.let {
|
||||
if (lastFreezeDuration > 0) {
|
||||
LifecycleUsageTriggerCollector.pluginFreezeReported(it, lastFreezeDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,21 +3,20 @@ package com.intellij.platform.diagnostic.plugin.freeze
|
||||
|
||||
import com.intellij.openapi.components.*
|
||||
import com.intellij.openapi.extensions.PluginId
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
private const val NOTIFICATION_COOLDOWN_DAYS: Long = 1L
|
||||
|
||||
@Service(Service.Level.APP)
|
||||
@State(
|
||||
name = "PluginFreezes",
|
||||
storages = [Storage(value = "pluginFreezes.xml")]
|
||||
storages = [Storage(value = "pluginFreezes.xml", roamingType = RoamingType.DISABLED)]
|
||||
)
|
||||
internal class PluginsFreezesService : PersistentStateComponent<PluginsFreezesServiceState> {
|
||||
private var state: PluginsFreezesServiceState = PluginsFreezesServiceState()
|
||||
|
||||
companion object {
|
||||
private const val NOTIFICATION_COOLDOWN_DAYS = 1L
|
||||
|
||||
@JvmStatic
|
||||
fun getInstance(): PluginsFreezesService = service()
|
||||
}
|
||||
@@ -27,14 +26,19 @@ internal class PluginsFreezesService : PersistentStateComponent<PluginsFreezesSe
|
||||
}
|
||||
|
||||
fun setLatestFreezeDate(pluginId: PluginId) {
|
||||
state.latestNotificationData[pluginId.idString] = Instant.now().toString()
|
||||
state.latestNotificationTime[pluginId.idString] = Instant.now().toString()
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
state.latestNotificationTime.clear()
|
||||
state.mutedPlugins.clear()
|
||||
}
|
||||
|
||||
fun shouldBeIgnored(pluginId: PluginId): Boolean {
|
||||
val pluginIdString = pluginId.idString
|
||||
if (state.mutedPlugins[pluginIdString] == true) return true
|
||||
|
||||
val lastNotification = state.latestNotificationData[pluginIdString]?.let { Instant.parse(it) } ?: return false
|
||||
val lastNotification = state.latestNotificationTime[pluginIdString]?.let { Instant.parse(it) } ?: return false
|
||||
return Instant.now().isBefore(lastNotification.plus(NOTIFICATION_COOLDOWN_DAYS, ChronoUnit.DAYS))
|
||||
}
|
||||
|
||||
@@ -45,8 +49,7 @@ internal class PluginsFreezesService : PersistentStateComponent<PluginsFreezesSe
|
||||
}
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
data class PluginsFreezesServiceState(
|
||||
var latestNotificationData: MutableMap<String, String> = mutableMapOf(),
|
||||
internal data class PluginsFreezesServiceState(
|
||||
var latestNotificationTime: MutableMap<String, String> = mutableMapOf(),
|
||||
var mutedPlugins: MutableMap<String, Boolean> = mutableMapOf(),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.platform.diagnostic.plugin.freeze
|
||||
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.application.WriteAction
|
||||
import com.intellij.openapi.project.DumbAwareAction
|
||||
|
||||
internal class ResetFreezeNotificationStateAction: DumbAwareAction() {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
WriteAction.run<RuntimeException> {
|
||||
PluginsFreezesService.getInstance().reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,7 @@ import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.diagnostic.Attachment
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.project.DumbAwareAction
|
||||
import com.intellij.openapi.ui.InputValidator
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import com.intellij.util.TimeoutUtil
|
||||
import java.awt.event.ActionEvent.CTRL_MASK
|
||||
import java.awt.event.ActionEvent.SHIFT_MASK
|
||||
@@ -100,27 +97,3 @@ internal class DropAnOutOfMemoryErrorAction : DumbAwareAction() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class SimulateFreeze : DumbAwareAction() {
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val durationString = Messages.showInputDialog(
|
||||
e.project,
|
||||
"Enter freeze duration in ms",
|
||||
"Freeze Simulator",
|
||||
null,
|
||||
"",
|
||||
object : InputValidator {
|
||||
override fun checkInput(inputString: String?): Boolean = StringUtil.parseInt(inputString, -1) > 0
|
||||
override fun canClose(inputString: String?): Boolean = StringUtil.parseInt(inputString, -1) > 0
|
||||
}) ?: return
|
||||
simulatedFreeze(durationString.toLong())
|
||||
}
|
||||
|
||||
// Keep it a function to detect it in EA
|
||||
private fun simulatedFreeze(ms: Long) {
|
||||
Thread.sleep(ms)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.intellij.internal.statistic.collectors.fus.MethodNameRuleValidator;
|
||||
import com.intellij.internal.statistic.eventLog.EventLogGroup;
|
||||
import com.intellij.internal.statistic.eventLog.events.*;
|
||||
import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector;
|
||||
import com.intellij.internal.statistic.utils.PluginInfo;
|
||||
import com.intellij.internal.statistic.utils.StatisticsUploadAssistant;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
@@ -27,7 +28,7 @@ import static com.intellij.internal.statistic.utils.PluginInfoDetectorKt.getPlug
|
||||
public final class LifecycleUsageTriggerCollector extends CounterUsagesCollector {
|
||||
private static final Logger LOG = Logger.getInstance(LifecycleUsageTriggerCollector.class);
|
||||
|
||||
private static final EventLogGroup LIFECYCLE = new EventLogGroup("lifecycle", 70);
|
||||
private static final EventLogGroup LIFECYCLE = new EventLogGroup("lifecycle", 71);
|
||||
|
||||
private static final EventField<Boolean> eapField = EventFields.Boolean("eap");
|
||||
private static final EventField<Boolean> testField = EventFields.Boolean("test");
|
||||
@@ -63,7 +64,14 @@ public final class LifecycleUsageTriggerCollector extends CounterUsagesCollector
|
||||
|
||||
private static final EventId FRAME_DEACTIVATED = LIFECYCLE.registerEvent("frame.deactivated");
|
||||
|
||||
private static final EventId1<Long> IDE_FREEZE = LIFECYCLE.registerEvent("ide.freeze", EventFields.Long("duration_ms"));
|
||||
private static final EventId1<Long> IDE_FREEZE = LIFECYCLE.registerEvent("ide.freeze", EventFields.DurationMs);
|
||||
|
||||
private static final EventId2<PluginInfo, Long> IDE_FREEZE_REPORTED_PLUGIN =
|
||||
LIFECYCLE.registerEvent("ide.freeze.plugin", EventFields.PluginInfo, EventFields.DurationMs);
|
||||
private static final EventId1<PluginInfo> IDE_FREEZE_PLUGIN_DISABLED =
|
||||
LIFECYCLE.registerEvent("ide.freeze.disabled.plugin", EventFields.PluginInfo);
|
||||
private static final EventId1<PluginInfo> IDE_FREEZE_PLUGIN_IGNORED =
|
||||
LIFECYCLE.registerEvent("ide.freeze.ignored.plugin", EventFields.PluginInfo);
|
||||
|
||||
private static final ClassEventField errorField = EventFields.Class("error");
|
||||
private static final EventField<VMOptions.MemoryKind> memoryErrorKindField =
|
||||
@@ -80,6 +88,7 @@ public final class LifecycleUsageTriggerCollector extends CounterUsagesCollector
|
||||
|
||||
private static final EventId IDE_DEADLOCK_DETECTED = LIFECYCLE.registerEvent("ide.deadlock.detected");
|
||||
|
||||
|
||||
private enum ProjectOpenMode {New, Same, Attach}
|
||||
private static final EventField<ProjectOpenMode> projectOpenModeField = EventFields.Enum("mode", ProjectOpenMode.class, mode -> Strings.toLowerCase(mode.name()));
|
||||
private static final EventId1<ProjectOpenMode> PROJECT_FRAME_SELECTED = LIFECYCLE.registerEvent("project.frame.selected", projectOpenModeField);
|
||||
@@ -219,4 +228,16 @@ public final class LifecycleUsageTriggerCollector extends CounterUsagesCollector
|
||||
public static void onEarlyErrorsIgnored(int numErrors) {
|
||||
EARLY_ERRORS.log(numErrors);
|
||||
}
|
||||
|
||||
public static void pluginFreezeReported(@NotNull PluginId pluginId, long durationMs) {
|
||||
IDE_FREEZE_REPORTED_PLUGIN.log(getPluginInfoById(pluginId), durationMs);
|
||||
}
|
||||
|
||||
public static void pluginDisabledOnFreeze(@NotNull PluginId id) {
|
||||
IDE_FREEZE_PLUGIN_DISABLED.log(getPluginInfoById(id));
|
||||
}
|
||||
|
||||
public static void pluginFreezeIgnored(@NotNull PluginId id) {
|
||||
IDE_FREEZE_PLUGIN_IGNORED.log(getPluginInfoById(id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1267,7 +1267,6 @@
|
||||
<action id="DropAnOutOfMemoryError" internal="true" class="com.intellij.diagnostic.DropAnOutOfMemoryErrorAction"
|
||||
text="Drop an OutOfMemoryError" description="Hold down SHIFT for OOME in Metaspace"/>
|
||||
</group>
|
||||
<action id="SimulateFreeze" internal="true" class="com.intellij.diagnostic.SimulateFreeze" text="Simulate a Freeze"/>
|
||||
<separator/>
|
||||
<action internal="true" id="ReloadProjectAction" class="com.intellij.internal.ReloadProjectAction"/>
|
||||
<group id="Internal.Trust" popup="true">
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<resource-bundle>messages.PerformanceTestingBundle</resource-bundle>
|
||||
|
||||
<description><![CDATA[
|
||||
Plugin for automated execution of test scripts, capturing performance snapshots
|
||||
and gathering performance statistics.
|
||||
]]></description>
|
||||
Plugin for automated execution of test scripts, capturing performance snapshots
|
||||
and gathering performance statistics.
|
||||
]]></description>
|
||||
|
||||
<content>
|
||||
<module name="intellij.performanceTesting.remoteDriver"/>
|
||||
@@ -33,6 +33,9 @@
|
||||
<separator/>
|
||||
<add-to-group group-id="HelpDiagnosticTools" anchor="last"/>
|
||||
</group>
|
||||
|
||||
<action id="SimulateFreeze" internal="true" class="com.jetbrains.performancePlugin.actions.SimulateFreeze"
|
||||
text="Simulate a Freeze"/>
|
||||
</actions>
|
||||
|
||||
<extensionPoints>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.jetbrains.performancePlugin.actions
|
||||
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.project.DumbAwareAction
|
||||
import com.intellij.openapi.ui.InputValidator
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
|
||||
internal class SimulateFreeze : DumbAwareAction() {
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val durationString = Messages.showInputDialog(
|
||||
e.project,
|
||||
"Enter freeze duration in ms",
|
||||
"Freeze Simulator",
|
||||
null,
|
||||
"",
|
||||
object : InputValidator {
|
||||
override fun checkInput(inputString: String?): Boolean = StringUtil.parseInt(inputString, -1) > 0
|
||||
override fun canClose(inputString: String?): Boolean = StringUtil.parseInt(inputString, -1) > 0
|
||||
}) ?: return
|
||||
simulatedFreeze(durationString.toLong())
|
||||
}
|
||||
|
||||
// Keep it a function to detect it in EA
|
||||
private fun simulatedFreeze(ms: Long) {
|
||||
Thread.sleep(ms)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user