mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[terminal] IJ-MR-158015 Refactor: extract FrontendLatencyService
Now most of this complicated logic is in the frontend module. GitOrigin-RevId: 26456964aa897bfed073addb6064b973e75f07f6
This commit is contained in:
committed by
intellij-monorepo-bot
parent
0fe5626210
commit
9f6e1bd228
@@ -19,6 +19,10 @@
|
||||
id="terminalFindPrevious" order="first"/>
|
||||
<editorActionHandler action="Replace" implementationClass="com.intellij.terminal.frontend.action.TerminalReplaceHandler"
|
||||
id="terminalReplace" order="first"/>
|
||||
|
||||
<applicationService
|
||||
serviceInterface="org.jetbrains.plugins.terminal.fus.FrontendLatencyService"
|
||||
serviceImplementation="com.intellij.terminal.frontend.fus.FrontendLatencyServiceImpl"/>
|
||||
</extensions>
|
||||
|
||||
<extensions defaultExtensionNs="org.jetbrains.plugins.terminal">
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
+189
@@ -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<TerminalSession>,
|
||||
outputEditor: EditorImpl,
|
||||
alternateBufferEditor: EditorImpl,
|
||||
): FrontendOutputActivity {
|
||||
return FrontendOutputActivityImpl(sessionFuture, outputEditor, alternateBufferEditor)
|
||||
}
|
||||
}
|
||||
|
||||
private val frontendTypingActivityId = AtomicInteger()
|
||||
private var currentKeyEventTypingActivity: FrontendTypingActivityImpl? = null
|
||||
private val frontendTypingActivityByInputEvent = ConcurrentHashMap<IdentityWrapper<TerminalInputEvent>, 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<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() }
|
||||
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 <T> ArrayBlockingQueue<T>.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<T : Any>(private val instance: T) {
|
||||
override fun equals(other: Any?): Boolean = instance === (other as? IdentityWrapper<T>)?.instance
|
||||
override fun hashCode(): Int = System.identityHashCode(instance)
|
||||
}
|
||||
|
||||
private fun <T : Any> T.toIdentity(): IdentityWrapper<T> = IdentityWrapper(this)
|
||||
|
||||
private val LOG = logger<FrontendLatencyService>()
|
||||
+3
-1
@@ -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
|
||||
|
||||
@@ -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<TerminalSession>,
|
||||
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()
|
||||
}
|
||||
+4
-199
@@ -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<TerminalSession>,
|
||||
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<IdentityWrapper<TerminalInputEvent>, FrontendTypingActivityImpl>()
|
||||
|
||||
// used to track individual instances of data classes
|
||||
private class IdentityWrapper<T : Any>(private val instance: T) {
|
||||
override fun equals(other: Any?): Boolean = instance === (other as? IdentityWrapper<T>)?.instance
|
||||
override fun hashCode(): Int = System.identityHashCode(instance)
|
||||
}
|
||||
|
||||
private fun <T : Any> T.toIdentity(): IdentityWrapper<T> = 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<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?.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 <T> ArrayBlockingQueue<T>.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<FrontendTerminalSession>()
|
||||
|
||||
Reference in New Issue
Block a user