From c402d9a900b5ab1bff99177c04a3091d046a6737 Mon Sep 17 00:00:00 2001 From: Dmitry Bushev Date: Tue, 12 Apr 2022 18:17:47 +0300 Subject: [PATCH] Implement Events Monitor (#3392) PR adds a monitor that handles messages between the language server and the runtime and dumps them as a CSV file `/tmp/enso-api-events-*********.csv` ``` UTC timestamp,Direction,Request Id,Message class ``` # Important Notes :warning: Monitor is enabled when the log level is set to trace. You should pass `-vv` (very verbose) option to the backend when starting IDE ``` enso -- -vv ``` --- CHANGELOG.md | 1 + .../enso/languageserver/boot/MainModule.scala | 12 ++- .../monitoring/EventsMonitor.scala | 20 +++++ .../runtime/ApiEventsMonitor.scala | 77 +++++++++++++++++++ .../runtime/RuntimeConnector.scala | 20 +++-- 5 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 engine/language-server/src/main/scala/org/enso/languageserver/monitoring/EventsMonitor.scala create mode 100644 engine/language-server/src/main/scala/org/enso/languageserver/runtime/ApiEventsMonitor.scala diff --git a/CHANGELOG.md b/CHANGELOG.md index 27dea42380d..821beb28c02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -152,6 +152,7 @@ [3379]: https://github.com/enso-org/enso/pull/3379 [3381]: https://github.com/enso-org/enso/pull/3381 [3383]: https://github.com/enso-org/enso/pull/3383 +[3392]: https://github.com/enso-org/enso/pull/3392 #### Enso Compiler diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala b/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala index cb7d1e662cb..0890cf1b301 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala @@ -20,7 +20,8 @@ import org.enso.languageserver.libraries._ import org.enso.languageserver.monitoring.{ HealthCheckEndpoint, IdlenessEndpoint, - IdlenessMonitor + IdlenessMonitor, + NoopEventsMonitor } import org.enso.languageserver.protocol.binary.{ BinaryConnectionControllerFactory, @@ -145,9 +146,16 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) { "lock-manager-service" ) + val runtimeEventsMonitor = + if (logLevel == LogLevel.Trace) ApiEventsMonitor() + else new NoopEventsMonitor + log.trace( + s"Started runtime events monitor ${runtimeEventsMonitor.getClass.getName}." + ) + lazy val runtimeConnector = system.actorOf( - RuntimeConnector.props(lockManagerService), + RuntimeConnector.props(lockManagerService, runtimeEventsMonitor), "runtime-connector" ) diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/monitoring/EventsMonitor.scala b/engine/language-server/src/main/scala/org/enso/languageserver/monitoring/EventsMonitor.scala new file mode 100644 index 00000000000..85ef05e1803 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/monitoring/EventsMonitor.scala @@ -0,0 +1,20 @@ +package org.enso.languageserver.monitoring + +/** Diagnostic tool that processes event messages. Used for debugging or + * performance review. + */ +trait EventsMonitor { + + /** Process the event message. + * + * @param event the event message + */ + def registerEvent(event: Any): Unit +} + +/** Events monitor that does nothing. */ +final class NoopEventsMonitor extends EventsMonitor { + + /** @inheritdoc */ + override def registerEvent(event: Any): Unit = () +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ApiEventsMonitor.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ApiEventsMonitor.scala new file mode 100644 index 00000000000..84f13b523b9 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ApiEventsMonitor.scala @@ -0,0 +1,77 @@ +package org.enso.languageserver.runtime + +import org.enso.languageserver.monitoring.EventsMonitor +import org.enso.polyglot.runtime.Runtime.ApiEnvelope +import org.enso.polyglot.runtime.Runtime.Api + +import java.nio.charset.StandardCharsets +import java.nio.file.{Files, Path, StandardOpenOption} +import java.time.Clock + +/** Gather messages between the language server and the runtime and write them + * to the provided file in CSV format. + * + * @param path the path where to write the events + * @param clock the system clock + */ +final class ApiEventsMonitor(path: Path, clock: Clock) extends EventsMonitor { + + import ApiEventsMonitor.Direction + + /** @inheritdoc */ + override def registerEvent(event: Any): Unit = + event match { + case envelope: ApiEnvelope => + registerApiEvent(envelope) + case RuntimeConnector.MessageFromRuntime(envelope) => + registerApiEvent(envelope) + case _ => + } + + def registerApiEvent(event: ApiEnvelope): Unit = + event match { + case Api.Request(requestId, payload) => + Files.write( + path, + entry(Direction.Request, requestId, payload.getClass), + StandardOpenOption.APPEND, + StandardOpenOption.SYNC + ) + + case Api.Response(correlationId, payload) => + Files.write( + path, + entry(Direction.Response, correlationId, payload.getClass), + StandardOpenOption.APPEND, + StandardOpenOption.SYNC + ) + } + + private def entry( + direction: Direction, + requestId: Option[Api.RequestId], + payload: Class[_] + ): Array[Byte] = { + val requestIdEntry = requestId.fold("")(_.toString) + val payloadEntry = payload.getSimpleName + val timeEntry = clock.instant() + s"$timeEntry,$direction,$requestIdEntry,$payloadEntry${System.lineSeparator()}" + .getBytes(StandardCharsets.UTF_8) + } +} +object ApiEventsMonitor { + + /** Create default instance of [[ApiEventsMonitor]]. */ + def apply(): ApiEventsMonitor = + new ApiEventsMonitor( + Files.createTempFile("enso-api-events-", ".csv"), + Clock.systemUTC() + ) + + /** Direction of the message. */ + sealed trait Direction + object Direction { + case object Request extends Direction + case object Response extends Direction + } +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/RuntimeConnector.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/RuntimeConnector.scala index a25245ebba7..1eb1943ea12 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/RuntimeConnector.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/RuntimeConnector.scala @@ -2,6 +2,7 @@ package org.enso.languageserver.runtime import akka.actor.{Actor, ActorRef, Props, Stash} import com.typesafe.scalalogging.LazyLogging +import org.enso.languageserver.monitoring.EventsMonitor import org.enso.languageserver.runtime.RuntimeConnector.{ Destroy, MessageFromRuntime @@ -15,8 +16,10 @@ import org.graalvm.polyglot.io.MessageEndpoint import java.nio.ByteBuffer /** An actor managing a connection to Enso's runtime server. */ -class RuntimeConnector(handlers: Map[Class[_], ActorRef]) - extends Actor +class RuntimeConnector( + handlers: Map[Class[_], ActorRef], + eventsMonitor: EventsMonitor +) extends Actor with LazyLogging with UnhandledLogging with Stash { @@ -36,6 +39,11 @@ class RuntimeConnector(handlers: Map[Class[_], ActorRef]) case _ => stash() } + def registerEvent: PartialFunction[Any, Any] = { case event => + eventsMonitor.registerEvent(event) + event + } + /** Performs communication between runtime and language server. * * Requests and responses can be sent in both directions and this Actor's @@ -62,7 +70,7 @@ class RuntimeConnector(handlers: Map[Class[_], ActorRef]) def initialized( engine: MessageEndpoint, senders: Map[Runtime.Api.RequestId, ActorRef] - ): Receive = { + ): Receive = registerEvent.andThen { case Destroy => context.stop(self) case msg: Runtime.ApiEnvelope => @@ -120,13 +128,15 @@ object RuntimeConnector { /** Helper for creating instances of the [[RuntimeConnector]] actor. * * @param lockManagerService a reference to the lock manager service actor + * @param monitor events monitor that handles messages between the language + * server and the runtime * @return a [[Props]] instance for the newly created actor. */ - def props(lockManagerService: ActorRef): Props = { + def props(lockManagerService: ActorRef, monitor: EventsMonitor): Props = { val lockRequests = LockManagerService.handledRequestTypes.map(_ -> lockManagerService) val handlers: Map[Class[_], ActorRef] = Map.from(lockRequests) - Props(new RuntimeConnector(handlers)) + Props(new RuntimeConnector(handlers, monitor)) } /** Endpoint implementation used to handle connections with the runtime.