From c082a207f04f60c51ec84f72031a784dbdf158ed Mon Sep 17 00:00:00 2001 From: Sergei Tachenov Date: Mon, 17 Mar 2025 16:21:31 +0200 Subject: [PATCH] [terminal] IJPL-182482 Implement terminal backend typing latency This process is not single-threaded, but it involves only one thread transfer: from the receiving coroutine to the terminal thread, which uses an executor that is never shut down, so it's pretty much a guarantee that for every byte array there will be at least an attempt to write it. Using this fact, we can store a byte array to activity map and remove activities when the byte arrays are written. Using a try-finally block there ensures that the activity is eventually finished. The activity ID is transferred along with the event, to ensure that the same ID is used on the frontend and on the backend. GitOrigin-RevId: 66587a5cca59053dab1e4bf2bee9bd098fe10637 --- .../terminal/session/TerminalInputEvent.kt | 2 +- .../terminal/frontend/TerminalInput.kt | 2 +- .../terminal/LocalTerminalDirectRunner.java | 16 ++++++ .../fus/ReworkedTerminalUsageCollector.kt | 50 +++++++++++++++++++ 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/platform/execution-impl/src/com/intellij/terminal/session/TerminalInputEvent.kt b/platform/execution-impl/src/com/intellij/terminal/session/TerminalInputEvent.kt index 1d94eadf9cbc..fd89d963f07b 100644 --- a/platform/execution-impl/src/com/intellij/terminal/session/TerminalInputEvent.kt +++ b/platform/execution-impl/src/com/intellij/terminal/session/TerminalInputEvent.kt @@ -15,7 +15,7 @@ data class TerminalResizeEvent(val newSize: TerminalSizeDto) : TerminalInputEven @ApiStatus.Internal @Serializable -data class TerminalWriteBytesEvent(val bytes: ByteArray) : TerminalInputEvent { +data class TerminalWriteBytesEvent(val bytes: ByteArray, val id: Int? = null) : TerminalInputEvent { override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false 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 e66b78b9f503..c1ed3ca9009b 100644 --- a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TerminalInput.kt +++ b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TerminalInput.kt @@ -96,8 +96,8 @@ class TerminalInput( } fun sendBytes(data: ByteArray) { - val writeBytesEvent = TerminalWriteBytesEvent(data) val fusActivity = ReworkedTerminalUsageCollector.getCurrentKeyEventTypingActivityOrNull() + val writeBytesEvent = TerminalWriteBytesEvent(bytes = data, id = fusActivity?.id) fusActivity?.startTerminalInputEventProcessing(writeBytesEvent) sendEvent(writeBytesEvent) } diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/LocalTerminalDirectRunner.java b/plugins/terminal/src/org/jetbrains/plugins/terminal/LocalTerminalDirectRunner.java index 0a12b9a9a391..c95f519d8f7a 100644 --- a/plugins/terminal/src/org/jetbrains/plugins/terminal/LocalTerminalDirectRunner.java +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/LocalTerminalDirectRunner.java @@ -25,6 +25,7 @@ import org.jetbrains.plugins.terminal.runner.LocalOptionsConfigurer; import org.jetbrains.plugins.terminal.runner.LocalShellIntegrationInjector; import org.jetbrains.plugins.terminal.runner.LocalTerminalStartCommandBuilder; +import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -188,6 +189,21 @@ public class LocalTerminalDirectRunner extends AbstractTerminalRunner) { localShellStartedEvent.log(project, @@ -88,6 +94,13 @@ object ReworkedTerminalUsageCollector : CounterUsagesCollector() { ) } + internal fun logBackendLatency(inputEventId: Int, duration: Duration) { + backendTypingLatencyEvent.log( + INPUT_EVENT_ID_FIELD with inputEventId, + DURATION_FIELD with duration + ) + } + fun startFrontendTypingActivity(e: KeyEvent): FrontendTypingActivity? { ThreadingAssertions.softAssertEventDispatchThread() if (e.id != KeyEvent.KEY_TYPED) return null @@ -104,6 +117,18 @@ object ReworkedTerminalUsageCollector : CounterUsagesCollector() { fun getFrontendTypingActivityOrNull(event: TerminalInputEvent): FrontendTypingActivity? { return frontendTypingActivityByInputEvent[InputEventIdentityWrapper(event)] } + + fun tryStartBackendTypingActivity(event: TerminalWriteBytesEvent) { + val id = event.id ?: return + val bytes = event.bytes + val activity = BackendTypingActivityImpl(id, bytes) + backendTypingActivityByByteArray[bytes] = activity + } + + @JvmStatic + fun getBackendTypingActivityOrNull(bytes: ByteArray): BackendTypingActivity? { + return backendTypingActivityByByteArray[bytes] + } } @ApiStatus.Internal @@ -115,10 +140,19 @@ interface FrontendTypingActivity { fun finishTerminalInputEventProcessing() } +@ApiStatus.Internal +interface BackendTypingActivity { + val id: Int + fun reportDuration() + fun finishBytesProcessing() +} + private val frontendTypingActivityId = AtomicInteger() private var currentKeyEventTypingActivity: FrontendTypingActivityImpl? = null private val frontendTypingActivityByInputEvent = ConcurrentHashMap() +private val backendTypingActivityByByteArray = ConcurrentHashMap() + // TerminalWriteBytesEvent is a data class, but we need to track individual events, not their content private class InputEventIdentityWrapper(private val event: TerminalInputEvent) { override fun equals(other: Any?): Boolean = event === (other as? InputEventIdentityWrapper)?.event @@ -161,4 +195,20 @@ private class FrontendTypingActivityImpl(override val id: Int) : FrontendTypingA } } +private class BackendTypingActivityImpl(override val id: Int, private val bytes: ByteArray) : BackendTypingActivity { + private val start = TimeSource.Monotonic.markNow() + + override fun reportDuration() { + val duration = start.elapsedNow() + ReworkedTerminalUsageCollector.logBackendLatency( + inputEventId = id, + duration = duration, + ) + } + + override fun finishBytesProcessing() { + backendTypingActivityByByteArray.remove(bytes) + } +} + private val LOG = logger()