diff --git a/plugins/terminal/frontend/resources/intellij.terminal.frontend.xml b/plugins/terminal/frontend/resources/intellij.terminal.frontend.xml
index 3c32896f4b8e..f9d1cea1836a 100644
--- a/plugins/terminal/frontend/resources/intellij.terminal.frontend.xml
+++ b/plugins/terminal/frontend/resources/intellij.terminal.frontend.xml
@@ -19,6 +19,10 @@
id="terminalFindPrevious" order="first"/>
+
+
diff --git a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/ReworkedTerminalView.kt b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/ReworkedTerminalView.kt
index 9608f21b1256..efdd9839f699 100644
--- a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/ReworkedTerminalView.kt
+++ b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/ReworkedTerminalView.kt
@@ -43,7 +43,7 @@ import org.jetbrains.plugins.terminal.block.reworked.hyperlinks.TerminalHyperlin
import org.jetbrains.plugins.terminal.block.ui.*
import org.jetbrains.plugins.terminal.block.ui.TerminalUi.useTerminalDefaultBackground
import org.jetbrains.plugins.terminal.block.util.TerminalDataContextUtils
-import org.jetbrains.plugins.terminal.fus.ReworkedTerminalUsageCollector
+import org.jetbrains.plugins.terminal.fus.FrontendLatencyService
import org.jetbrains.plugins.terminal.util.terminalProjectScope
import java.awt.Component
import java.awt.Dimension
@@ -132,7 +132,7 @@ internal class ReworkedTerminalView(
TerminalBlocksDecorator(outputEditor, blocksModel, scrollingModel, coroutineScope.childScope("TerminalBlocksDecorator"))
outputEditor.putUserData(TerminalBlocksModel.KEY, blocksModel)
- val fusActivity = ReworkedTerminalUsageCollector.startFrontendOutputActivity(
+ val fusActivity = FrontendLatencyService.getInstance().startFrontendOutputActivity(
sessionFuture,
outputEditor = outputEditor as EditorImpl,
alternateBufferEditor = alternateBufferEditor as EditorImpl,
diff --git a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TerminalEventDispatcher.kt b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TerminalEventDispatcher.kt
index af1dbe3513ce..56a7f629790b 100644
--- a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TerminalEventDispatcher.kt
+++ b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TerminalEventDispatcher.kt
@@ -23,7 +23,7 @@ import org.intellij.lang.annotations.Language
import org.jetbrains.annotations.NonNls
import org.jetbrains.plugins.terminal.block.output.TerminalEventsHandler
import org.jetbrains.plugins.terminal.block.reworked.TerminalSessionModel
-import org.jetbrains.plugins.terminal.fus.ReworkedTerminalUsageCollector
+import org.jetbrains.plugins.terminal.fus.FrontendLatencyService
import java.awt.AWTEvent
import java.awt.event.InputEvent
import java.awt.event.KeyEvent
@@ -61,7 +61,7 @@ internal abstract class TerminalEventDispatcher(
override fun dispatch(e: AWTEvent): Boolean {
if (e is KeyEvent) {
- val fusActivity = ReworkedTerminalUsageCollector.startFrontendTypingActivity(e)
+ val fusActivity = FrontendLatencyService.getInstance().startFrontendTypingActivity(e)
try {
dispatchKeyEvent(e)
}
diff --git a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TerminalInput.kt b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TerminalInput.kt
index c1ed3ca9009b..fad0087bf5e9 100644
--- a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TerminalInput.kt
+++ b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TerminalInput.kt
@@ -13,7 +13,7 @@ import kotlinx.coroutines.channels.ClosedSendChannelException
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.future.await
import org.jetbrains.plugins.terminal.block.reworked.TerminalSessionModel
-import org.jetbrains.plugins.terminal.fus.ReworkedTerminalUsageCollector
+import org.jetbrains.plugins.terminal.fus.FrontendLatencyService
import java.nio.charset.StandardCharsets
import java.util.concurrent.CompletableFuture
@@ -37,7 +37,7 @@ class TerminalInput(
capacity = 10000,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
onUndeliveredElement = { event ->
- ReworkedTerminalUsageCollector.getFrontendTypingActivityOrNull(event)?.finishTerminalInputEventProcessing()
+ FrontendLatencyService.getInstance().getFrontendTypingActivityOrNull(event)?.finishTerminalInputEventProcessing()
}
)
@@ -55,7 +55,7 @@ class TerminalInput(
for (event in bufferChannel) {
currentEvent = event
targetChannel.send(event)
- val fusActivity = ReworkedTerminalUsageCollector.getFrontendTypingActivityOrNull(event)
+ val fusActivity = FrontendLatencyService.getInstance().getFrontendTypingActivityOrNull(event)
fusActivity?.reportDuration()
fusActivity?.finishTerminalInputEventProcessing()
currentEvent = null
@@ -72,7 +72,7 @@ class TerminalInput(
}
finally {
if (currentEvent != null) {
- ReworkedTerminalUsageCollector.getFrontendTypingActivityOrNull(currentEvent)?.finishTerminalInputEventProcessing()
+ FrontendLatencyService.getInstance().getFrontendTypingActivityOrNull(currentEvent)?.finishTerminalInputEventProcessing()
}
}
}
@@ -96,7 +96,7 @@ class TerminalInput(
}
fun sendBytes(data: ByteArray) {
- val fusActivity = ReworkedTerminalUsageCollector.getCurrentKeyEventTypingActivityOrNull()
+ val fusActivity = FrontendLatencyService.getInstance().getCurrentKeyEventTypingActivityOrNull()
val writeBytesEvent = TerminalWriteBytesEvent(bytes = data, id = fusActivity?.id)
fusActivity?.startTerminalInputEventProcessing(writeBytesEvent)
sendEvent(writeBytesEvent)
@@ -115,7 +115,7 @@ class TerminalInput(
}
private fun sendEvent(event: TerminalInputEvent) {
- val fusActivity = ReworkedTerminalUsageCollector.getFrontendTypingActivityOrNull(event)
+ val fusActivity = FrontendLatencyService.getInstance().getFrontendTypingActivityOrNull(event)
val result = bufferChannel.trySend(event)
if (fusActivity != null && result.isFailure) {
fusActivity.finishTerminalInputEventProcessing()
diff --git a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/fus/FrontendLatencyServiceImpl.kt b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/fus/FrontendLatencyServiceImpl.kt
new file mode 100644
index 000000000000..f1402243d0c8
--- /dev/null
+++ b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/fus/FrontendLatencyServiceImpl.kt
@@ -0,0 +1,189 @@
+package com.intellij.terminal.frontend.fus
+
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.editor.impl.EditorImpl
+import com.intellij.platform.rpc.UID
+import com.intellij.terminal.session.TerminalContentUpdatedEvent
+import com.intellij.terminal.session.TerminalInputEvent
+import com.intellij.terminal.session.TerminalSession
+import com.intellij.terminal.session.TerminalWriteBytesEvent
+import com.intellij.util.concurrency.ThreadingAssertions
+import fleet.multiplatform.shims.ConcurrentHashMap
+import org.jetbrains.plugins.terminal.block.reworked.session.FrontendTerminalSession
+import org.jetbrains.plugins.terminal.fus.FrontendLatencyService
+import org.jetbrains.plugins.terminal.fus.FrontendOutputActivity
+import org.jetbrains.plugins.terminal.fus.FrontendTypingActivity
+import org.jetbrains.plugins.terminal.fus.ReworkedTerminalUsageCollector
+import java.awt.event.KeyEvent
+import java.util.concurrent.ArrayBlockingQueue
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.atomic.AtomicInteger
+import java.util.concurrent.atomic.AtomicReference
+import kotlin.time.TimeMark
+import kotlin.time.TimeSource
+
+internal class FrontendLatencyServiceImpl : FrontendLatencyService {
+ override fun startFrontendTypingActivity(e: KeyEvent): FrontendTypingActivity? {
+ ThreadingAssertions.softAssertEventDispatchThread()
+ if (e.id != KeyEvent.KEY_TYPED) return null
+ val activity = FrontendTypingActivityImpl(frontendTypingActivityId.incrementAndGet())
+ currentKeyEventTypingActivity = activity
+ return activity
+ }
+
+ override fun getCurrentKeyEventTypingActivityOrNull(): FrontendTypingActivity? {
+ ThreadingAssertions.softAssertEventDispatchThread()
+ return currentKeyEventTypingActivity
+ }
+
+ override fun getFrontendTypingActivityOrNull(event: TerminalInputEvent): FrontendTypingActivity? {
+ return frontendTypingActivityByInputEvent[event.toIdentity()]
+ }
+
+ override fun startFrontendOutputActivity(
+ sessionFuture: CompletableFuture,
+ outputEditor: EditorImpl,
+ alternateBufferEditor: EditorImpl,
+ ): FrontendOutputActivity {
+ return FrontendOutputActivityImpl(sessionFuture, outputEditor, alternateBufferEditor)
+ }
+}
+
+private val frontendTypingActivityId = AtomicInteger()
+private var currentKeyEventTypingActivity: FrontendTypingActivityImpl? = null
+private val frontendTypingActivityByInputEvent = ConcurrentHashMap, FrontendTypingActivityImpl>()
+
+private class FrontendTypingActivityImpl(override val id: Int) : FrontendTypingActivity {
+ private val start = TimeSource.Monotonic.markNow()
+ private var writeBytesEvent: TerminalWriteBytesEvent? = null
+
+ override fun startTerminalInputEventProcessing(writeBytesEvent: TerminalWriteBytesEvent) {
+ this.writeBytesEvent = writeBytesEvent
+ frontendTypingActivityByInputEvent[writeBytesEvent.toIdentity()] = this
+ if (frontendTypingActivityByInputEvent.size > 10000) {
+ LOG.error(Throwable(
+ "Too many simultaneous frontend typing activities, likely a leak!" +
+ " Ensure that startTerminalInputEventProcessing() calls are paired with finishTerminalInputEventProcessing()"
+ ))
+ }
+ }
+
+ override fun finishKeyEventProcessing() {
+ ThreadingAssertions.softAssertEventDispatchThread()
+ currentKeyEventTypingActivity = null
+ }
+
+ override fun reportDuration() {
+ val duration = start.elapsedNow()
+ ReworkedTerminalUsageCollector.logFrontendLatency(
+ inputEventId = id,
+ duration,
+ )
+ }
+
+ override fun finishTerminalInputEventProcessing() {
+ val inputEvent = writeBytesEvent
+ if (inputEvent != null) {
+ frontendTypingActivityByInputEvent.remove(inputEvent.toIdentity())
+ }
+ }
+}
+
+private class FrontendOutputActivityImpl(
+ sessionFuture: CompletableFuture,
+ private val outputEditor: EditorImpl,
+ private val alternateBufferEditor: EditorImpl,
+) : FrontendOutputActivity {
+
+ private val sessionId = AtomicReference()
+ private val pendingEvents = ArrayBlockingQueue(100)
+ private val pendingPaints = ArrayBlockingQueue(100)
+ private var editorRepaintRequests = 0L
+ private var editorRepaintRequestsBeforeModelUpdate = 0L
+
+ init {
+ sessionFuture.whenComplete { session, _ ->
+ sessionId.set((session as? FrontendTerminalSession?)?.id?.eid)
+ }
+ outputEditor.setRepaintCallback { editorRepaintRequested() }
+ alternateBufferEditor.setRepaintCallback { editorRepaintRequested() }
+ outputEditor.setPaintCallback { editorPainted() }
+ alternateBufferEditor.setPaintCallback { editorPainted() }
+ }
+
+ override fun eventReceived(event: TerminalContentUpdatedEvent) {
+ pendingEvents.addDroppingOldest(ReceivedEvent(TimeSource.Monotonic.markNow(), event))
+ }
+
+ override fun beforeModelUpdate() {
+ editorRepaintRequestsBeforeModelUpdate = editorRepaintRequests
+ }
+
+ private fun editorRepaintRequested() {
+ ++editorRepaintRequests
+ }
+
+ override fun afterModelUpdate() {
+ val repaintRequested = editorRepaintRequests > editorRepaintRequestsBeforeModelUpdate
+ val editorShowing = outputEditor.component.isShowing || alternateBufferEditor.component.isShowing
+ if (!editorShowing) {
+ pendingPaints.clear() // editor no longer showing, so if there were unprocessed requests, they won't complete
+ }
+ val repaintExpected = repaintRequested && editorShowing
+ while (true) {
+ val pendingEvent = pendingEvents.poll() ?: break
+ if (repaintExpected) {
+ pendingPaints.addDroppingOldest(pendingEvent)
+ }
+ else {
+ reportLatency(pendingEvent, false)
+ }
+ }
+ }
+
+ private fun editorPainted() {
+ while (true) {
+ val pendingPaint = pendingPaints.poll() ?: break
+ reportLatency(pendingPaint, true)
+ }
+ }
+
+ private fun reportLatency(receivedEvent: ReceivedEvent, painted: Boolean) {
+ val latency = receivedEvent.time.elapsedNow()
+ val sessionId = this.sessionId.get()
+ if (sessionId == null) {
+ LOG.error("For some reason sessionId was not initialized, likely a bug")
+ return
+ }
+ ReworkedTerminalUsageCollector.logFrontendOutputLatency(
+ sessionId = sessionId,
+ firstCharIndex = receivedEvent.event.firstCharIndex,
+ lastCharIndex = receivedEvent.event.lastCharIndex,
+ duration = latency,
+ repainted = painted,
+ )
+ }
+
+ private data class ReceivedEvent(val time: TimeMark, val event: TerminalContentUpdatedEvent)
+}
+
+private fun ArrayBlockingQueue.addDroppingOldest(element: T) {
+ var overflow = false
+ while (!offer(element)) {
+ overflow = true
+ poll()
+ }
+ if (overflow) {
+ LOG.warn("Overflow in the frontend output activity queue, too many requests, maybe the queue is too small?")
+ }
+}
+
+// used to track individual instances of data classes
+private class IdentityWrapper(private val instance: T) {
+ override fun equals(other: Any?): Boolean = instance === (other as? IdentityWrapper)?.instance
+ override fun hashCode(): Int = System.identityHashCode(instance)
+}
+
+private fun T.toIdentity(): IdentityWrapper = IdentityWrapper(this)
+
+private val LOG = logger()
diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/block/reworked/session/FrontendTerminalSession.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/block/reworked/session/FrontendTerminalSession.kt
index 07a86021b55d..e31b49e85faf 100644
--- a/plugins/terminal/src/org/jetbrains/plugins/terminal/block/reworked/session/FrontendTerminalSession.kt
+++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/block/reworked/session/FrontendTerminalSession.kt
@@ -10,6 +10,7 @@ import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.onEach
+import org.jetbrains.annotations.ApiStatus
import org.jetbrains.plugins.terminal.block.reworked.session.rpc.TerminalSessionApi
import org.jetbrains.plugins.terminal.block.reworked.session.rpc.TerminalSessionId
@@ -19,7 +20,8 @@ import org.jetbrains.plugins.terminal.block.reworked.session.rpc.TerminalSession
* Normally, it should be located in the frontend module, but it can't be moved there
* because it should be accessible from the shared terminal widget creating API with a lot of external usages.
*/
-internal class FrontendTerminalSession(internal val id: TerminalSessionId) : TerminalSession {
+@ApiStatus.Internal
+class FrontendTerminalSession(val id: TerminalSessionId) : TerminalSession {
@Volatile
override var isClosed: Boolean = false
private set
diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/fus/FrontendLatencyService.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/fus/FrontendLatencyService.kt
new file mode 100644
index 000000000000..8abd77809bb2
--- /dev/null
+++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/fus/FrontendLatencyService.kt
@@ -0,0 +1,50 @@
+// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.plugins.terminal.fus
+
+import com.intellij.openapi.components.service
+import com.intellij.openapi.editor.impl.EditorImpl
+import com.intellij.terminal.session.TerminalContentUpdatedEvent
+import com.intellij.terminal.session.TerminalInputEvent
+import com.intellij.terminal.session.TerminalSession
+import com.intellij.terminal.session.TerminalWriteBytesEvent
+import org.jetbrains.annotations.ApiStatus
+import java.awt.event.KeyEvent
+import java.util.concurrent.CompletableFuture
+
+@ApiStatus.Internal
+interface FrontendLatencyService {
+
+ companion object {
+ @JvmStatic
+ fun getInstance(): FrontendLatencyService = service()
+ }
+
+ fun startFrontendTypingActivity(e: KeyEvent): FrontendTypingActivity?
+
+ fun getCurrentKeyEventTypingActivityOrNull(): FrontendTypingActivity?
+
+ fun getFrontendTypingActivityOrNull(event: TerminalInputEvent): FrontendTypingActivity?
+
+ fun startFrontendOutputActivity(
+ sessionFuture: CompletableFuture,
+ outputEditor: EditorImpl,
+ alternateBufferEditor: EditorImpl,
+ ): FrontendOutputActivity
+
+}
+
+@ApiStatus.Internal
+interface FrontendTypingActivity {
+ val id: Int
+ fun startTerminalInputEventProcessing(writeBytesEvent: TerminalWriteBytesEvent)
+ fun finishKeyEventProcessing()
+ fun reportDuration()
+ fun finishTerminalInputEventProcessing()
+}
+
+@ApiStatus.Internal
+interface FrontendOutputActivity {
+ fun eventReceived(event: TerminalContentUpdatedEvent)
+ fun beforeModelUpdate()
+ fun afterModelUpdate()
+}
diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/fus/ReworkedTerminalUsageCollector.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/fus/ReworkedTerminalUsageCollector.kt
index 64a0e60537ce..495a3798990e 100644
--- a/plugins/terminal/src/org/jetbrains/plugins/terminal/fus/ReworkedTerminalUsageCollector.kt
+++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/fus/ReworkedTerminalUsageCollector.kt
@@ -4,31 +4,15 @@ package org.jetbrains.plugins.terminal.fus
import com.intellij.internal.statistic.eventLog.EventLogGroup
import com.intellij.internal.statistic.eventLog.events.EventFields
import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector
-import com.intellij.openapi.diagnostic.logger
-import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Version
import com.intellij.platform.rpc.UID
-import com.intellij.terminal.session.TerminalContentUpdatedEvent
-import com.intellij.terminal.session.TerminalInputEvent
-import com.intellij.terminal.session.TerminalSession
-import com.intellij.terminal.session.TerminalWriteBytesEvent
-import com.intellij.util.concurrency.ThreadingAssertions
import com.intellij.util.system.OS
-import fleet.multiplatform.shims.ConcurrentHashMap
import org.jetbrains.annotations.ApiStatus
-import org.jetbrains.plugins.terminal.block.reworked.session.FrontendTerminalSession
import org.jetbrains.plugins.terminal.fus.TerminalShellInfoStatistics.KNOWN_SHELLS
import org.jetbrains.plugins.terminal.fus.TerminalShellInfoStatistics.getShellNameForStat
-import java.awt.event.KeyEvent
-import java.util.concurrent.ArrayBlockingQueue
-import java.util.concurrent.CompletableFuture
-import java.util.concurrent.atomic.AtomicInteger
-import java.util.concurrent.atomic.AtomicReference
import kotlin.time.Duration
import kotlin.time.DurationUnit
-import kotlin.time.TimeMark
-import kotlin.time.TimeSource
private const val GROUP_ID = "terminal"
@@ -123,7 +107,8 @@ object ReworkedTerminalUsageCollector : CounterUsagesCollector() {
EXECUTION_TIME_FIELD with executionTime.inWholeMilliseconds)
}
- internal fun logFrontendLatency(inputEventId: Int, duration: Duration) {
+ @ApiStatus.Internal
+ fun logFrontendLatency(inputEventId: Int, duration: Duration) {
frontendTypingLatencyEvent.log(
INPUT_EVENT_ID_FIELD with inputEventId,
DURATION_FIELD with duration
@@ -156,7 +141,8 @@ object ReworkedTerminalUsageCollector : CounterUsagesCollector() {
)
}
- internal fun logFrontendOutputLatency(sessionId: UID, firstCharIndex: Long, lastCharIndex: Long, duration: Duration, repainted: Boolean) {
+ @ApiStatus.Internal
+ fun logFrontendOutputLatency(sessionId: UID, firstCharIndex: Long, lastCharIndex: Long, duration: Duration, repainted: Boolean) {
frontendOutputLatencyEvent.log(
SESSION_ID with sessionId,
FIRST_CHAR_INDEX with firstCharIndex,
@@ -165,185 +151,4 @@ object ReworkedTerminalUsageCollector : CounterUsagesCollector() {
REPAINTED_FIELD with repainted,
)
}
-
- fun startFrontendTypingActivity(e: KeyEvent): FrontendTypingActivity? {
- ThreadingAssertions.softAssertEventDispatchThread()
- if (e.id != KeyEvent.KEY_TYPED) return null
- val activity = FrontendTypingActivityImpl(frontendTypingActivityId.incrementAndGet())
- currentKeyEventTypingActivity = activity
- return activity
- }
-
- fun getCurrentKeyEventTypingActivityOrNull(): FrontendTypingActivity? {
- ThreadingAssertions.softAssertEventDispatchThread()
- return currentKeyEventTypingActivity
- }
-
- fun getFrontendTypingActivityOrNull(event: TerminalInputEvent): FrontendTypingActivity? {
- return frontendTypingActivityByInputEvent[event.toIdentity()]
- }
-
- @JvmStatic
- fun startFrontendOutputActivity(
- sessionFuture: CompletableFuture,
- outputEditor: EditorImpl,
- alternateBufferEditor: EditorImpl,
- ): FrontendOutputActivity {
- return FrontendOutputActivityImpl(sessionFuture, outputEditor, alternateBufferEditor)
- }
}
-
-@ApiStatus.Internal
-interface FrontendTypingActivity {
- val id: Int
- fun startTerminalInputEventProcessing(writeBytesEvent: TerminalWriteBytesEvent)
- fun finishKeyEventProcessing()
- fun reportDuration()
- fun finishTerminalInputEventProcessing()
-}
-
-@ApiStatus.Internal
-interface FrontendOutputActivity {
- fun eventReceived(event: TerminalContentUpdatedEvent)
- fun beforeModelUpdate()
- fun afterModelUpdate()
-}
-
-private val frontendTypingActivityId = AtomicInteger()
-private var currentKeyEventTypingActivity: FrontendTypingActivityImpl? = null
-private val frontendTypingActivityByInputEvent = ConcurrentHashMap, FrontendTypingActivityImpl>()
-
-// used to track individual instances of data classes
-private class IdentityWrapper(private val instance: T) {
- override fun equals(other: Any?): Boolean = instance === (other as? IdentityWrapper)?.instance
- override fun hashCode(): Int = System.identityHashCode(instance)
-}
-
-private fun T.toIdentity(): IdentityWrapper = IdentityWrapper(this)
-
-private class FrontendTypingActivityImpl(override val id: Int) : FrontendTypingActivity {
- private val start = TimeSource.Monotonic.markNow()
- private var writeBytesEvent: TerminalWriteBytesEvent? = null
-
- override fun startTerminalInputEventProcessing(writeBytesEvent: TerminalWriteBytesEvent) {
- this.writeBytesEvent = writeBytesEvent
- frontendTypingActivityByInputEvent[writeBytesEvent.toIdentity()] = this
- if (frontendTypingActivityByInputEvent.size > 10000) {
- LOG.error(Throwable(
- "Too many simultaneous frontend typing activities, likely a leak!" +
- " Ensure that startTerminalInputEventProcessing() calls are paired with finishTerminalInputEventProcessing()"
- ))
- }
- }
-
- override fun finishKeyEventProcessing() {
- ThreadingAssertions.softAssertEventDispatchThread()
- currentKeyEventTypingActivity = null
- }
-
- override fun reportDuration() {
- val duration = start.elapsedNow()
- ReworkedTerminalUsageCollector.logFrontendLatency(
- inputEventId = id,
- duration,
- )
- }
-
- override fun finishTerminalInputEventProcessing() {
- val inputEvent = writeBytesEvent
- if (inputEvent != null) {
- frontendTypingActivityByInputEvent.remove(inputEvent.toIdentity())
- }
- }
-}
-
-private class FrontendOutputActivityImpl(
- sessionFuture: CompletableFuture,
- private val outputEditor: EditorImpl,
- private val alternateBufferEditor: EditorImpl,
-) : FrontendOutputActivity {
-
- private val sessionId = AtomicReference()
- private val pendingEvents = ArrayBlockingQueue(100)
- private val pendingPaints = ArrayBlockingQueue(100)
- private var editorRepaintRequests = 0L
- private var editorRepaintRequestsBeforeModelUpdate = 0L
-
- init {
- sessionFuture.whenComplete { session, _ ->
- sessionId.set((session as? FrontendTerminalSession?)?.id?.uid)
- }
- outputEditor.setRepaintCallback { editorRepaintRequested() }
- alternateBufferEditor.setRepaintCallback { editorRepaintRequested() }
- outputEditor.setPaintCallback { editorPainted() }
- alternateBufferEditor.setPaintCallback { editorPainted() }
- }
-
- override fun eventReceived(event: TerminalContentUpdatedEvent) {
- pendingEvents.addDroppingOldest(ReceivedEvent(TimeSource.Monotonic.markNow(), event))
- }
-
- override fun beforeModelUpdate() {
- editorRepaintRequestsBeforeModelUpdate = editorRepaintRequests
- }
-
- private fun editorRepaintRequested() {
- ++editorRepaintRequests
- }
-
- override fun afterModelUpdate() {
- val repaintRequested = editorRepaintRequests > editorRepaintRequestsBeforeModelUpdate
- val editorShowing = outputEditor.component.isShowing || alternateBufferEditor.component.isShowing
- if (!editorShowing) {
- pendingPaints.clear() // editor no longer showing, so if there were unprocessed requests, they won't complete
- }
- val repaintExpected = repaintRequested && editorShowing
- while (true) {
- val pendingEvent = pendingEvents.poll() ?: break
- if (repaintExpected) {
- pendingPaints.addDroppingOldest(pendingEvent)
- }
- else {
- reportLatency(pendingEvent, false)
- }
- }
- }
-
- private fun editorPainted() {
- while (true) {
- val pendingPaint = pendingPaints.poll() ?: break
- reportLatency(pendingPaint, true)
- }
- }
-
- private fun reportLatency(receivedEvent: ReceivedEvent, painted: Boolean) {
- val latency = receivedEvent.time.elapsedNow()
- val sessionId = this.sessionId.get()
- if (sessionId == null) {
- LOG.error("For some reason sessionId was not initialized, likely a bug")
- return
- }
- ReworkedTerminalUsageCollector.logFrontendOutputLatency(
- sessionId = sessionId,
- firstCharIndex = receivedEvent.event.firstCharIndex,
- lastCharIndex = receivedEvent.event.lastCharIndex,
- duration = latency,
- repainted = painted,
- )
- }
-
- private data class ReceivedEvent(val time: TimeMark, val event: TerminalContentUpdatedEvent)
-}
-
-private fun ArrayBlockingQueue.addDroppingOldest(element: T) {
- var overflow = false
- while (!offer(element)) {
- overflow = true
- poll()
- }
- if (overflow) {
- LOG.warn("Overflow in the frontend output activity queue, too many requests, maybe the queue is too small?")
- }
-}
-
-private val LOG = logger()