[terminal] IJPL-182482 Reimplement the backend output latency measurement

This implementation is more simple:
1. We track the last time of reading from the TTY.
2. We remember the time from step 1 when we text buffer is changed the first time. Here output accumulation session starts.
3. Once output is collected, we put the time from step 2 to the created `TerminalOutputUpdatedEvent` and reset it. Here output accumulation session ends.
4. Once we update the backend models with this event, we report the backend output latency and forward the event to the frontend.

GitOrigin-RevId: ed4e4920b46e5bdd7feb27fa7200e4858b629632
This commit is contained in:
Konstantin Hudyakov
2025-04-15 07:32:53 +00:00
committed by intellij-monorepo-bot
parent 1426c4858b
commit 3868836dd4
13 changed files with 154 additions and 135 deletions
@@ -7,6 +7,7 @@ import com.intellij.terminal.session.dto.TerminalOutputModelStateDto
import com.intellij.terminal.session.dto.TerminalStateDto
import kotlinx.serialization.Serializable
import org.jetbrains.annotations.ApiStatus
import kotlin.time.TimeMark
@ApiStatus.Internal
@Serializable
@@ -15,11 +16,11 @@ sealed interface TerminalOutputEvent
@ApiStatus.Internal
@Serializable
data class TerminalContentUpdatedEvent(
val id: Int,
val text: String,
val styles: List<StyleRangeDto>,
val startLineLogicalIndex: Long,
val firstCharIndex: Long,
val lastCharIndex: Long,
val readTime: TimeMark?,
) : TerminalOutputEvent
@ApiStatus.Internal
@@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.onEach
import org.jetbrains.plugins.terminal.block.reworked.*
import org.jetbrains.plugins.terminal.block.ui.TerminalUiUtils
import org.jetbrains.plugins.terminal.fus.ReworkedTerminalUsageCollector
import kotlin.coroutines.cancellation.CancellationException
/**
@@ -83,6 +84,11 @@ internal class StateAwareTerminalSession(private val delegate: TerminalSession)
val styles = event.styles.map { it.toStyleRange() }
val model = getCurrentOutputModel()
model.updateContent(event.startLineLogicalIndex, event.text, styles)
val latency = event.readTime?.elapsedNow()
if (latency != null) {
ReworkedTerminalUsageCollector.logBackendOutputLatency(event.id, latency)
}
}
is TerminalCursorPositionChangedEvent -> {
val model = getCurrentOutputModel()
@@ -2,7 +2,7 @@
package com.intellij.terminal.backend
import com.intellij.terminal.session.StyleRange
import com.intellij.terminal.session.TerminalContentUpdatedEvent
import com.intellij.terminal.session.dto.StyleRangeDto
import com.intellij.terminal.session.dto.toDto
import com.jediterm.terminal.model.TerminalLine
import com.jediterm.terminal.model.TerminalTextBuffer
@@ -21,7 +21,7 @@ internal class TerminalContentChangesTracker(
private var lastChangedVisualLine: Int = 0
private var anyLineChanged: Boolean = false
private val listeners: MutableList<(TerminalContentUpdatedEvent) -> Unit> = CopyOnWriteArrayList()
private val listeners: MutableList<(TerminalContentUpdate) -> Unit> = CopyOnWriteArrayList()
init {
textBuffer.addChangesListener(object : TextBufferChangesListener {
@@ -58,11 +58,11 @@ internal class TerminalContentChangesTracker(
})
}
fun addHistoryOverflowListener(listener: (TerminalContentUpdatedEvent) -> Unit) {
fun addHistoryOverflowListener(listener: (TerminalContentUpdate) -> Unit) {
listeners.add(listener)
}
fun getContentUpdate(): TerminalContentUpdatedEvent? {
fun getContentUpdate(): TerminalContentUpdate? {
return getContentUpdate(emptyList())
}
@@ -73,14 +73,14 @@ internal class TerminalContentChangesTracker(
}
}
private fun getContentUpdate(additionalLines: List<TerminalLine>): TerminalContentUpdatedEvent? {
private fun getContentUpdate(additionalLines: List<TerminalLine>): TerminalContentUpdate? {
return if (anyLineChanged) {
collectOutput(additionalLines)
}
else null
}
private fun collectOutput(additionalLines: List<TerminalLine>): TerminalContentUpdatedEvent {
private fun collectOutput(additionalLines: List<TerminalLine>): TerminalContentUpdate {
check(anyLineChanged) { "It is expected that this method is called only if something is changed" }
// Transform to the TextBuffer coordinates: negative indexes for history, positive for the screen.
@@ -98,12 +98,10 @@ internal class TerminalContentChangesTracker(
lastChangedVisualLine = textBuffer.effectiveHistoryLinesCount + textBuffer.screenLinesCount
anyLineChanged = false
return TerminalContentUpdatedEvent(
return TerminalContentUpdate(
text = output.text,
styles = output.styleRanges.map { it.toDto() },
startLineLogicalIndex = logicalLineIndex,
firstCharIndex = -1,
lastCharIndex = -1,
)
}
@@ -121,6 +119,12 @@ internal class TerminalContentChangesTracker(
}
}
internal data class TerminalContentUpdate(
val text: String,
val styles: List<StyleRangeDto>,
val startLineLogicalIndex: Long,
)
/**
* Consider the sequence of wrapped lines in the Text Buffer as a single logical line.
*/
@@ -3,26 +3,24 @@ package com.intellij.terminal.backend
import com.intellij.terminal.session.*
import com.intellij.terminal.session.dto.toDto
import com.intellij.util.asDisposable
import com.jediterm.terminal.CursorShape
import com.jediterm.terminal.emulator.mouse.MouseFormat
import com.jediterm.terminal.emulator.mouse.MouseMode
import com.jediterm.terminal.model.TerminalTextBuffer
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import org.jetbrains.plugins.terminal.block.reworked.TerminalShellIntegrationEventsListener
import org.jetbrains.plugins.terminal.block.ui.withLock
import java.util.concurrent.atomic.AtomicInteger
private val outputUpdateEventIdCounter = AtomicInteger(0)
@OptIn(ExperimentalCoroutinesApi::class)
internal fun createTerminalOutputFlow(
textBuffer: TerminalTextBuffer,
fusActivity: BackendOutputActivity,
terminalDisplay: TerminalDisplayImpl,
controller: ObservableJediTerminal,
services: JediTermServices,
shellIntegrationController: TerminalShellIntegrationController,
coroutineScope: CoroutineScope,
ensureEmulationActive: () -> Unit,
): MutableSharedFlow<List<TerminalOutputEvent>> {
val outputFlow = MutableSharedFlow<List<TerminalOutputEvent>>(
// Do not buffer a lot of events here.
@@ -36,23 +34,39 @@ internal fun createTerminalOutputFlow(
onBufferOverflow = BufferOverflow.SUSPEND
)
val textBuffer = services.textBuffer
val controller = services.controller
val terminalDisplay = services.terminalDisplay
val discardedHistoryTracker = TerminalDiscardedHistoryTracker(textBuffer)
val contentChangesTracker = TerminalContentChangesTracker(textBuffer, discardedHistoryTracker)
val cursorPositionTracker = TerminalCursorPositionTracker(textBuffer, discardedHistoryTracker, terminalDisplay)
val outputLatencyTracker = TerminalOutputLatencyTracker(services.ttyConnector, textBuffer, coroutineScope.asDisposable())
/**
* Events should be sent in the following order: content update, cursor position update, other events.
* This function allows providing content update if it is precalculated, and the other optional event to be sent last.
*/
fun collectAndSendEvents(
contentUpdateEvent: TerminalContentUpdatedEvent?,
contentUpdate: TerminalContentUpdate?,
otherEvent: TerminalOutputEvent?,
ensureActive: () -> Unit = { ensureEmulationActive() },
ensureActive: () -> Unit = { ensureEmulationActive(services.terminalStarter) },
) {
textBuffer.withLock {
val contentUpdate = contentUpdateEvent ?: contentChangesTracker.getContentUpdate()
val actualContentUpdate = contentUpdate ?: contentChangesTracker.getContentUpdate()
val contentUpdateEvent = if (actualContentUpdate != null) {
TerminalContentUpdatedEvent(
id = outputUpdateEventIdCounter.getAndIncrement(),
text = actualContentUpdate.text,
styles = actualContentUpdate.styles,
startLineLogicalIndex = actualContentUpdate.startLineLogicalIndex,
readTime = outputLatencyTracker.getCurUpdateTtyReadTimeAndReset(),
)
}
else null
val cursorPositionUpdate = cursorPositionTracker.getCursorPositionUpdate()
val updates = listOfNotNull(contentUpdate, cursorPositionUpdate, otherEvent)
val updates = listOfNotNull(contentUpdateEvent, cursorPositionUpdate, otherEvent)
if (updates.isNotEmpty()) {
// Block the shell output reading if any of the following:
// 1. There are no active collectors: then there is no need to read the shell output.
@@ -67,14 +81,18 @@ internal fun createTerminalOutputFlow(
coroutineScope.launch(Dispatchers.IO) {
while (true) {
collectAndSendEvents(contentUpdateEvent = null, otherEvent = null, ensureActive = { ensureActive(); ensureEmulationActive() })
collectAndSendEvents(
contentUpdate = null,
otherEvent = null,
ensureActive = { ensureActive(); ensureEmulationActive(services.terminalStarter) }
)
delay(10)
}
}
contentChangesTracker.addHistoryOverflowListener { contentUpdate ->
collectAndSendEvents(contentUpdateEvent = contentUpdate, otherEvent = null)
collectAndSendEvents(contentUpdate = contentUpdate, otherEvent = null)
}
var curState = TerminalState(
@@ -96,28 +114,28 @@ internal fun createTerminalOutputFlow(
override fun arrowKeysModeChanged(isApplication: Boolean) {
textBuffer.withLock {
curState = curState.copy(isApplicationArrowKeys = isApplication)
collectAndSendEvents(contentUpdateEvent = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
collectAndSendEvents(contentUpdate = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
}
}
override fun keypadModeChanged(isApplication: Boolean) {
textBuffer.withLock {
curState = curState.copy(isApplicationKeypad = isApplication)
collectAndSendEvents(contentUpdateEvent = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
collectAndSendEvents(contentUpdate = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
}
}
override fun autoNewLineChanged(isEnabled: Boolean) {
textBuffer.withLock {
curState = curState.copy(isAutoNewLine = isEnabled)
collectAndSendEvents(contentUpdateEvent = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
collectAndSendEvents(contentUpdate = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
}
}
override fun altSendsEscapeChanged(isEnabled: Boolean) {
textBuffer.withLock {
curState = curState.copy(isAltSendsEscape = isEnabled)
collectAndSendEvents(contentUpdateEvent = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
collectAndSendEvents(contentUpdate = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
}
}
@@ -129,7 +147,7 @@ internal fun createTerminalOutputFlow(
override fun beforeAlternateScreenBufferChanged(isEnabled: Boolean) {
textBuffer.withLock {
curState = curState.copy(isAlternateScreenBuffer = isEnabled)
collectAndSendEvents(contentUpdateEvent = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
collectAndSendEvents(contentUpdate = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
}
}
})
@@ -138,48 +156,48 @@ internal fun createTerminalOutputFlow(
override fun cursorVisibilityChanged(isVisible: Boolean) {
textBuffer.withLock {
curState = curState.copy(isCursorVisible = isVisible)
collectAndSendEvents(contentUpdateEvent = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
collectAndSendEvents(contentUpdate = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
}
}
override fun cursorShapeChanged(cursorShape: CursorShape?) {
textBuffer.withLock {
curState = curState.copy(cursorShape = cursorShape)
collectAndSendEvents(contentUpdateEvent = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
collectAndSendEvents(contentUpdate = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
}
}
override fun mouseModeChanged(mode: MouseMode) {
textBuffer.withLock {
curState = curState.copy(mouseMode = mode)
collectAndSendEvents(contentUpdateEvent = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
collectAndSendEvents(contentUpdate = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
}
}
override fun mouseFormatChanged(format: MouseFormat) {
textBuffer.withLock {
curState = curState.copy(mouseFormat = format)
collectAndSendEvents(contentUpdateEvent = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
collectAndSendEvents(contentUpdate = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
}
}
override fun bracketedPasteModeChanged(isEnabled: Boolean) {
textBuffer.withLock {
curState = curState.copy(isBracketedPasteMode = isEnabled)
collectAndSendEvents(contentUpdateEvent = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
collectAndSendEvents(contentUpdate = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
}
}
override fun windowTitleChanged(title: String) {
textBuffer.withLock {
curState = curState.copy(windowTitle = title)
collectAndSendEvents(contentUpdateEvent = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
collectAndSendEvents(contentUpdate = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
}
}
override fun beep() {
textBuffer.withLock {
collectAndSendEvents(contentUpdateEvent = null, otherEvent = TerminalBeepEvent)
collectAndSendEvents(contentUpdate = null, otherEvent = TerminalBeepEvent)
}
}
})
@@ -188,26 +206,32 @@ internal fun createTerminalOutputFlow(
override fun initialized() {
textBuffer.withLock {
curState = curState.copy(isShellIntegrationEnabled = true)
collectAndSendEvents(contentUpdateEvent = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
collectAndSendEvents(contentUpdate = null, otherEvent = TerminalStateChangedEvent(curState.toDto()))
}
}
override fun commandStarted(command: String) {
collectAndSendEvents(contentUpdateEvent = null, otherEvent = TerminalCommandStartedEvent(command))
collectAndSendEvents(contentUpdate = null, otherEvent = TerminalCommandStartedEvent(command))
}
override fun commandFinished(command: String, exitCode: Int) {
collectAndSendEvents(contentUpdateEvent = null, otherEvent = TerminalCommandFinishedEvent(command, exitCode))
collectAndSendEvents(contentUpdate = null, otherEvent = TerminalCommandFinishedEvent(command, exitCode))
}
override fun promptStarted() {
collectAndSendEvents(contentUpdateEvent = null, otherEvent = TerminalPromptStartedEvent)
collectAndSendEvents(contentUpdate = null, otherEvent = TerminalPromptStartedEvent)
}
override fun promptFinished() {
collectAndSendEvents(contentUpdateEvent = null, otherEvent = TerminalPromptFinishedEvent)
collectAndSendEvents(contentUpdate = null, otherEvent = TerminalPromptFinishedEvent)
}
})
return outputFlow
}
private fun ensureEmulationActive(starter: StopAwareTerminalStarter) {
if (Thread.interrupted() || starter.isStopped) {
throw CancellationException("Terminal emulation was stopped")
}
}
@@ -0,0 +1,54 @@
package com.intellij.terminal.backend
import com.intellij.openapi.Disposable
import com.jediterm.terminal.model.TerminalTextBuffer
import com.jediterm.terminal.model.TextBufferChangesListener
import kotlin.time.TimeMark
import kotlin.time.TimeSource
internal class TerminalOutputLatencyTracker(
ttyConnector: ObservableTtyConnector,
textBuffer: TerminalTextBuffer,
parentDisposable: Disposable,
) {
// Variables are guarded by TerminalTextBuffer lock
/**
* Last moment when we read some bytes from the TTY.
* Guarded by TerminalTextBuffer lock.
*/
private var lastTtyReadTime: TimeMark? = null
/**
* First moment of reading the bytes from the TTY, that caused changing the text buffer.
* Guarded by TerminalTextBuffer lock.
*/
private var firstChangeTtyReadTime: TimeMark? = null
init {
ttyConnector.addListener(parentDisposable, object : TtyConnectorListener {
override fun charsRead(buf: CharArray, offset: Int, length: Int) {
lastTtyReadTime = TimeSource.Monotonic.markNow()
}
})
textBuffer.addChangesListener(object : TextBufferChangesListener {
override fun linesChanged(fromIndex: Int) {
if (firstChangeTtyReadTime == null) {
firstChangeTtyReadTime = lastTtyReadTime
}
}
})
}
/**
* Returns the first time of reading bytes from the TTY that corresponds
* to the currently uncollected changes in the text buffer.
* And then resets the timer, so the current text buffer changes are considered as collected.
*/
fun getCurUpdateTtyReadTimeAndReset(): TimeMark? {
val result = firstChangeTtyReadTime
firstChangeTtyReadTime = null
return result
}
}
@@ -29,7 +29,6 @@ import org.jetbrains.plugins.terminal.LocalBlockTerminalRunner
import org.jetbrains.plugins.terminal.ShellStartupOptions
import org.jetbrains.plugins.terminal.util.STOP_EMULATOR_TIMEOUT
import org.jetbrains.plugins.terminal.util.waitFor
import java.util.concurrent.CancellationException
internal fun startTerminalProcess(
project: Project,
@@ -64,13 +63,9 @@ internal fun createTerminalSession(
val shellIntegrationController = TerminalShellIntegrationController(services.controller)
shellIntegrationController.addListener(TerminalShellIntegrationStatisticsListener(project))
val outputFlow = createTerminalOutputFlow(
services.textBuffer,
fusActivity,
services.terminalDisplay,
services.controller,
services,
shellIntegrationController,
outputScope,
ensureEmulationActive = { ensureEmulationActive(services.terminalStarter) }
)
val inputScope = coroutineScope.childScope("Terminal input handling")
@@ -101,8 +96,7 @@ internal fun createTerminalSession(
}
private fun createJediTermServices(
connector: TtyConnector,
fusActivity: BackendOutputActivity,
connector: ObservableTtyConnector,
termSize: TermSize,
maxHistoryLinesCount: Int,
settings: JBTerminalSystemSettingsProviderBase,
@@ -118,11 +112,10 @@ private fun createJediTermServices(
connector,
TtyBasedArrayDataStream(connector),
typeAheadManager,
executorService,
fusActivity
executorService
)
return JediTermServices(textBuffer, terminalDisplay, controller, executorService, terminalStarter)
return JediTermServices(textBuffer, terminalDisplay, controller, executorService, terminalStarter, connector)
}
private fun startTerminalEmulation(terminalStarter: TerminalStarter) {
@@ -142,16 +135,11 @@ private fun startTerminalEmulation(terminalStarter: TerminalStarter) {
}
}
private fun ensureEmulationActive(starter: StopAwareTerminalStarter) {
if (Thread.interrupted() || starter.isStopped) {
throw CancellationException("Terminal emulation was stopped")
}
}
internal class JediTermServices(
val textBuffer: TerminalTextBuffer,
val terminalDisplay: TerminalDisplayImpl,
val controller: ObservableJediTerminal,
val executorService: TerminalExecutorServiceManager,
val terminalStarter: StopAwareTerminalStarter,
val ttyConnector: ObservableTtyConnector,
)
@@ -3,7 +3,6 @@ package com.intellij.terminal.backend
import com.intellij.terminal.backend.util.scrollDown
import com.intellij.terminal.backend.util.write
import com.intellij.terminal.session.TerminalContentUpdatedEvent
import com.jediterm.terminal.model.StyleState
import com.jediterm.terminal.model.TerminalTextBuffer
import junit.framework.TestCase.assertEquals
@@ -49,7 +48,7 @@ internal class TerminalContentChangesTrackerTest {
textBuffer.write("sixth", 3, 0)
// Update will be saved there on the next scroll
var update: TerminalContentUpdatedEvent? = null
var update: TerminalContentUpdate? = null
contentChangesTracker.addHistoryOverflowListener {
update = it
}
@@ -91,7 +90,7 @@ internal class TerminalContentChangesTrackerTest {
textBuffer.write("sixth", 3, 0)
// Update will be saved there on the next scroll
var update: TerminalContentUpdatedEvent? = null
var update: TerminalContentUpdate? = null
contentChangesTracker.addHistoryOverflowListener {
update = it
}
@@ -2,7 +2,6 @@
package com.intellij.terminal.backend
import com.intellij.terminal.backend.util.write
import com.intellij.terminal.session.TerminalContentUpdatedEvent
import com.intellij.terminal.session.TerminalCursorPositionChangedEvent
import com.jediterm.terminal.model.StyleState
import com.jediterm.terminal.model.TerminalTextBuffer
@@ -31,10 +30,7 @@ internal class TerminalCursorPositionTrackerTest {
val contentUpdate = contentChangesTracker.getContentUpdate() ?: error("Content update is null")
val cursorUpdate = cursorPositionTracker.getCursorPositionUpdate() ?: error("Cursor update is null")
// We expect that moving cursor position to the next line creates this line in the TextBuffer,
// and it is being caught by the TerminalContentChangesTracker.
// The character indices correspond to LongRange.EMPTY used in the test FUS activity stub.
assertThat(contentUpdate).isEqualTo(TerminalContentUpdatedEvent("", emptyList(), 1, 1L, 0L))
assertThat(contentUpdate).isEqualTo(TerminalContentUpdate("", emptyList(), 1))
assertThat(cursorUpdate).isEqualTo(TerminalCursorPositionChangedEvent(1, 0))
}
}
@@ -133,7 +133,6 @@ internal class ReworkedTerminalView(
outputEditor.putUserData(TerminalBlocksModel.KEY, blocksModel)
val fusActivity = FrontendLatencyService.getInstance().startFrontendOutputActivity(
sessionFuture,
outputEditor = outputEditor as EditorImpl,
alternateBufferEditor = alternateBufferEditor as EditorImpl,
)
@@ -2,23 +2,18 @@ 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
@@ -41,11 +36,10 @@ internal class FrontendLatencyServiceImpl : FrontendLatencyService {
}
override fun startFrontendOutputActivity(
sessionFuture: CompletableFuture<TerminalSession>,
outputEditor: EditorImpl,
alternateBufferEditor: EditorImpl,
): FrontendOutputActivity {
return FrontendOutputActivityImpl(sessionFuture, outputEditor, alternateBufferEditor)
return FrontendOutputActivityImpl(outputEditor, alternateBufferEditor)
}
}
@@ -90,21 +84,15 @@ private class FrontendTypingActivityImpl(override val id: Int) : FrontendTypingA
}
private class FrontendOutputActivityImpl(
sessionFuture: CompletableFuture<TerminalSession>,
private val outputEditor: EditorImpl,
private val alternateBufferEditor: EditorImpl,
) : FrontendOutputActivity {
private val sessionId = AtomicReference<UID?>()
private val pendingEvents = ArrayBlockingQueue<ReceivedEvent>(100)
private val pendingPaints = ArrayBlockingQueue<ReceivedEvent>(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() }
@@ -150,15 +138,8 @@ private class FrontendOutputActivityImpl(
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,
eventId = receivedEvent.event.id,
duration = latency,
repainted = painted,
)
@@ -10,7 +10,6 @@ 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
@@ -20,8 +19,7 @@ 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.
*/
@ApiStatus.Internal
class FrontendTerminalSession(val id: TerminalSessionId) : TerminalSession {
internal class FrontendTerminalSession(private val id: TerminalSessionId) : TerminalSession {
@Volatile
override var isClosed: Boolean = false
private set
@@ -5,11 +5,9 @@ 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 {
@@ -26,11 +24,9 @@ interface FrontendLatencyService {
fun getFrontendTypingActivityOrNull(event: TerminalInputEvent): FrontendTypingActivity?
fun startFrontendOutputActivity(
sessionFuture: CompletableFuture<TerminalSession>,
outputEditor: EditorImpl,
alternateBufferEditor: EditorImpl,
): FrontendOutputActivity
}
@ApiStatus.Internal
@@ -6,7 +6,6 @@ import com.intellij.internal.statistic.eventLog.events.EventFields
import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Version
import com.intellij.platform.rpc.UID
import com.intellij.util.system.OS
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.plugins.terminal.fus.TerminalShellInfoStatistics.KNOWN_SHELLS
@@ -26,11 +25,7 @@ object ReworkedTerminalUsageCollector : CounterUsagesCollector() {
private val SHELL_STR_FIELD = EventFields.String("shell", KNOWN_SHELLS.toList())
private val EXIT_CODE_FIELD = EventFields.Int("exit_code")
private val EXECUTION_TIME_FIELD = EventFields.Long("execution_time", "Time in milliseconds")
private val INPUT_EVENT_ID_FIELD = EventFields.Int("input_event_id")
private val SESSION_ID = EventFields.Int("session_id")
private val CHAR_INDEX = EventFields.Long("char_index")
private val FIRST_CHAR_INDEX = EventFields.Long("first_char_index")
private val LAST_CHAR_INDEX = EventFields.Long("last_char_index")
private val EVENT_ID_FIELD = EventFields.Int("event_id")
private val DURATION_FIELD = EventFields.createDurationField(DurationUnit.MILLISECONDS, "duration_millis")
private val REPAINTED_FIELD = EventFields.Boolean("editor_repainted")
@@ -52,35 +47,25 @@ object ReworkedTerminalUsageCollector : CounterUsagesCollector() {
private val frontendTypingLatencyEvent = GROUP.registerVarargEvent(
"terminal.frontend.typing.latency",
INPUT_EVENT_ID_FIELD,
EVENT_ID_FIELD,
DURATION_FIELD,
)
private val backendTypingLatencyEvent = GROUP.registerVarargEvent(
"terminal.backend.typing.latency",
INPUT_EVENT_ID_FIELD,
EVENT_ID_FIELD,
DURATION_FIELD,
)
private val backendMinOutputLatencyEvent = GROUP.registerVarargEvent(
"terminal.backend.min.output.latency",
SESSION_ID,
CHAR_INDEX,
DURATION_FIELD,
)
private val backendMaxOutputLatencyEvent = GROUP.registerVarargEvent(
"terminal.backend.max.output.latency",
SESSION_ID,
CHAR_INDEX,
private val backendOutputLatencyEvent = GROUP.registerVarargEvent(
"terminal.backend.output.latency",
EVENT_ID_FIELD,
DURATION_FIELD,
)
private val frontendOutputLatencyEvent = GROUP.registerVarargEvent(
"terminal.frontend.output.latency",
SESSION_ID,
FIRST_CHAR_INDEX,
LAST_CHAR_INDEX,
EVENT_ID_FIELD,
DURATION_FIELD,
REPAINTED_FIELD,
)
@@ -110,7 +95,7 @@ object ReworkedTerminalUsageCollector : CounterUsagesCollector() {
@ApiStatus.Internal
fun logFrontendLatency(inputEventId: Int, duration: Duration) {
frontendTypingLatencyEvent.log(
INPUT_EVENT_ID_FIELD with inputEventId,
EVENT_ID_FIELD with inputEventId,
DURATION_FIELD with duration
)
}
@@ -118,35 +103,23 @@ object ReworkedTerminalUsageCollector : CounterUsagesCollector() {
@ApiStatus.Internal
fun logBackendLatency(inputEventId: Int, duration: Duration) {
backendTypingLatencyEvent.log(
INPUT_EVENT_ID_FIELD with inputEventId,
EVENT_ID_FIELD with inputEventId,
DURATION_FIELD with duration
)
}
@ApiStatus.Internal
fun logBackendMinOutputLatency(sessionId: UID, charIndex: Long, duration: Duration) {
backendMinOutputLatencyEvent.log(
SESSION_ID with sessionId,
CHAR_INDEX with charIndex,
fun logBackendOutputLatency(eventId: Int, duration: Duration) {
backendOutputLatencyEvent.log(
EVENT_ID_FIELD with eventId,
DURATION_FIELD with duration
)
}
@ApiStatus.Internal
fun logBackendMaxOutputLatency(sessionId: UID, charIndex: Long, duration: Duration) {
backendMaxOutputLatencyEvent.log(
SESSION_ID with sessionId,
CHAR_INDEX with charIndex,
DURATION_FIELD with duration
)
}
@ApiStatus.Internal
fun logFrontendOutputLatency(sessionId: UID, firstCharIndex: Long, lastCharIndex: Long, duration: Duration, repainted: Boolean) {
fun logFrontendOutputLatency(eventId: Int, duration: Duration, repainted: Boolean) {
frontendOutputLatencyEvent.log(
SESSION_ID with sessionId,
FIRST_CHAR_INDEX with firstCharIndex,
LAST_CHAR_INDEX with lastCharIndex,
EVENT_ID_FIELD with eventId,
DURATION_FIELD with duration,
REPAINTED_FIELD with repainted,
)