mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[terminal] IJPL-179213 delay hyperlink highlightings a bit to analyze fewer lines overall when output is fast
This makes behavior similar to `ConsoleViewImpl` and results in performance improvements: 1. It fixes excessive memory consumption by `AsyncFilterRunner`. Previously, the `AsyncFilterRunner.myResults` field was ever-growing because `runTasks` never completes because new lines were arriving fast enough. 2. It reduces the overall CPU utilization when computing hyperlinks. Before: ~60% of all lines were analyzed After: ~40% of all lines are analyzed GitOrigin-RevId: eb036306c07f6e193c5b1b4c9d1b57de28e433e1
This commit is contained in:
committed by
intellij-monorepo-bot
parent
9da3ec13c5
commit
47fd9ac5e6
+58
-3
@@ -3,29 +3,44 @@ package org.jetbrains.plugins.terminal.block.reworked.hyperlinks
|
||||
|
||||
import com.intellij.execution.impl.EditorHyperlinkSupport
|
||||
import com.intellij.execution.impl.ExpirableTokenProvider
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.application.asContextElement
|
||||
import com.intellij.openapi.editor.Document
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.event.DocumentEvent
|
||||
import com.intellij.openapi.editor.event.DocumentListener
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.util.SystemProperties
|
||||
import com.intellij.util.asDisposable
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.*
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.annotations.TestOnly
|
||||
import org.jetbrains.plugins.terminal.block.hyperlinks.CompositeFilterWrapper
|
||||
import org.jetbrains.plugins.terminal.block.reworked.TerminalOutputModel
|
||||
import org.jetbrains.plugins.terminal.block.reworked.TerminalOutputModelListener
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@ApiStatus.Internal
|
||||
class TerminalHyperlinkHighlighter private constructor(
|
||||
project: Project,
|
||||
private val editor: Editor,
|
||||
coroutineScope: CoroutineScope,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
) {
|
||||
|
||||
private val filterWrapper: CompositeFilterWrapper = CompositeFilterWrapper(project, coroutineScope)
|
||||
private val tokenProvider: ExpirableTokenProvider = ExpirableTokenProvider()
|
||||
|
||||
// should be accessed in EDT
|
||||
private var actualStartOffset: Int = INFINITE
|
||||
|
||||
// should be accessed in EDT
|
||||
private var delayedHighlightingJob: Job? = null
|
||||
|
||||
private val document: Document
|
||||
get() = editor.document
|
||||
|
||||
@@ -36,6 +51,17 @@ class TerminalHyperlinkHighlighter private constructor(
|
||||
filterWrapper.addFiltersUpdatedListener {
|
||||
rehighlightAll()
|
||||
}
|
||||
document.addDocumentListener(object : DocumentListener {
|
||||
override fun documentChanged(event: DocumentEvent) {
|
||||
// There are only two possible types of document changes:
|
||||
// 1. Deletion from the document top (trimming).
|
||||
// 2. Replacement of the document bottom.
|
||||
if (event.getOffset() == 0 && event.getNewLength() == 0) {
|
||||
// This is a deletion from the document top.
|
||||
actualStartOffset = max(0, actualStartOffset - event.getOldLength())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun rehighlightAll() {
|
||||
@@ -46,9 +72,21 @@ class TerminalHyperlinkHighlighter private constructor(
|
||||
|
||||
@RequiresEdt(generateAssertion = false)
|
||||
fun highlightHyperlinks(startOffset: Int) {
|
||||
actualStartOffset = min(actualStartOffset, startOffset)
|
||||
if (delayedHighlightingJob.isNotActive()) {
|
||||
delayedHighlightingJob = coroutineScope.launch(Dispatchers.EDT + ModalityState.any().asContextElement()) {
|
||||
delay(DELAY)
|
||||
doHyperlinkHighlighting()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun doHyperlinkHighlighting() {
|
||||
val filter = filterWrapper.getFilter() ?: return // if null, `rehighlightAll` will follow
|
||||
if (filter.isEmpty) return
|
||||
if (document.textLength == 0) return
|
||||
val startOffset = actualStartOffset
|
||||
actualStartOffset = INFINITE
|
||||
val startLine = document.getLineNumber(startOffset)
|
||||
val endLine = max(0, document.lineCount - 1)
|
||||
|
||||
@@ -71,6 +109,11 @@ class TerminalHyperlinkHighlighter private constructor(
|
||||
filterWrapper.awaitFiltersComputed()
|
||||
}
|
||||
|
||||
@TestOnly
|
||||
internal suspend fun awaitDelayedHighlightings() {
|
||||
delayedHighlightingJob?.join()
|
||||
}
|
||||
|
||||
@TestOnly
|
||||
internal fun getHyperlinkSupport(): EditorHyperlinkSupport = hyperlinkSupport
|
||||
|
||||
@@ -84,6 +127,18 @@ class TerminalHyperlinkHighlighter private constructor(
|
||||
})
|
||||
return hyperlinkHighlighter
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see com.intellij.execution.impl.ConsoleViewImpl.DEFAULT_FLUSH_DELAY
|
||||
*/
|
||||
private val DELAY: Duration = SystemProperties.getIntProperty("console.flush.delay.ms", 200).milliseconds
|
||||
|
||||
/**
|
||||
* A value greater than possible document length.
|
||||
* A document length cannot be greater than 2048 MB, because [Document.getTextLength] return type is `Int`.
|
||||
*/
|
||||
private const val INFINITE: Int = Integer.MAX_VALUE
|
||||
|
||||
private fun Job?.isNotActive(): Boolean = this == null || !this.isActive
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -254,7 +254,7 @@ internal class TerminalHyperlinkHighlighterTest : BasePlatformTestCase() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertHyperlinks(linkText: String, expectedCount: Int): List<RangeHighlighter> {
|
||||
private suspend fun assertHyperlinks(linkText: String, expectedCount: Int): List<RangeHighlighter> {
|
||||
val text = editor.document.text
|
||||
val expectedRanges = text.allOccurrencesOf(linkText).map {
|
||||
TextRange(it, it + linkText.length)
|
||||
@@ -268,13 +268,14 @@ internal class TerminalHyperlinkHighlighterTest : BasePlatformTestCase() {
|
||||
return rangeHighlighters
|
||||
}
|
||||
|
||||
private fun awaitHyperlinks(timeout: Duration): List<RangeHighlighter> {
|
||||
private suspend fun awaitHyperlinks(timeout: Duration): List<RangeHighlighter> {
|
||||
hyperlinkHighlighter.awaitDelayedHighlightings()
|
||||
val hyperlinkSupport = hyperlinkHighlighter.getHyperlinkSupport()
|
||||
hyperlinkSupport.waitForPendingFilters(timeout.inWholeMilliseconds)
|
||||
return hyperlinkSupport.getAllHyperlinks(0, editor.document.textLength)
|
||||
}
|
||||
|
||||
private fun assertInlays(inlayText: String, expectedCount: Int): List<Inlay<*>> {
|
||||
private suspend fun assertInlays(inlayText: String, expectedCount: Int): List<Inlay<*>> {
|
||||
val text = editor.document.text
|
||||
val expectedOffsets = text.allOccurrencesOf(inlayText).map { it + inlayText.length }.toList()
|
||||
val inlays = awaitInlays(AWAIT_FILTER_TIMEOUT)
|
||||
@@ -287,7 +288,8 @@ internal class TerminalHyperlinkHighlighterTest : BasePlatformTestCase() {
|
||||
return inlays
|
||||
}
|
||||
|
||||
private fun awaitInlays(timeout: Duration): List<Inlay<*>> {
|
||||
private suspend fun awaitInlays(timeout: Duration): List<Inlay<*>> {
|
||||
hyperlinkHighlighter.awaitDelayedHighlightings()
|
||||
val hyperlinkSupport = hyperlinkHighlighter.getHyperlinkSupport()
|
||||
hyperlinkSupport.waitForPendingFilters(timeout.inWholeMilliseconds)
|
||||
return hyperlinkSupport.collectAllInlays()
|
||||
|
||||
Reference in New Issue
Block a user