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()