[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
This commit is contained in:
Sergei Tachenov
2025-04-15 07:32:52 +00:00
committed by intellij-monorepo-bot
parent 2681b905e6
commit c082a207f0
4 changed files with 68 additions and 2 deletions
@@ -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
@@ -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)
}
@@ -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<PtyProcess
@Override
public @NotNull TtyConnector createTtyConnector(@NotNull PtyProcess process) {
return new PtyProcessTtyConnector(process, myDefaultCharset) {
@Override
public void write(byte[] bytes) throws IOException {
var fusActivity = ReworkedTerminalUsageCollector.getBackendTypingActivityOrNull(bytes);
try {
super.write(bytes);
if (fusActivity != null) {
fusActivity.reportDuration();
}
}
finally {
if (fusActivity != null) {
fusActivity.finishBytesProcessing();
}
}
}
@Override
public void close() {
@@ -59,6 +59,12 @@ object ReworkedTerminalUsageCollector : CounterUsagesCollector() {
DURATION_FIELD,
)
private val backendTypingLatencyEvent = GROUP.registerVarargEvent(
"terminal.backend.typing.latency",
INPUT_EVENT_ID_FIELD,
DURATION_FIELD,
)
@JvmStatic
fun logLocalShellStarted(project: Project, shellCommand: Array<String>) {
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<InputEventIdentityWrapper, FrontendTypingActivityImpl>()
private val backendTypingActivityByByteArray = ConcurrentHashMap<ByteArray, BackendTypingActivityImpl>()
// 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<FrontendTerminalSession>()