diff --git a/docs/language-server/protocol-language-server.md b/docs/language-server/protocol-language-server.md index 2c35ce831d9b..0194470b1147 100644 --- a/docs/language-server/protocol-language-server.md +++ b/docs/language-server/protocol-language-server.md @@ -185,6 +185,9 @@ transport formats, please look [here](./protocol-architecture). - [`library/preinstall`](#librarypreinstall) - [Runtime Operations](#runtime-operations) - [`runtime/getComponentGroups`](#runtimegetcomponentgroups) +- [Profiling Operations](#profiling-operations) + - [`profiling/start`](#profilingstart) + - [`profiling/stop`](#profilingstop) - [Errors](#errors-75) - [`Error`](#error) - [`AccessDeniedError`](#accessdeniederror) @@ -5091,6 +5094,63 @@ interface RuntimeGetComponentGroupsResult { None +## Profiling Operations + +### `profiling/start` + +Sent from the client to the server to initiate gathering the profiling data. +This command should be followed by the [`profiling/stop`](#profilingstop) +request to store the gathered data. After the profiling is started, subsequent +`profiling/start` commands will do nothing. + +- **Type:** Request +- **Direction:** Client -> Server +- **Connection:** Protocol +- **Visibility:** Public + +#### Parameters + +```typescript +interface ProfilingStartParameters {} +``` + +#### Result + +```typescript +interface ProfilingStartResult {} +``` + +#### Errors + +None + +### `profiling/stop` + +Sent from the client to the server to finish gathering the profiling data. The +collected data is stored in the `ENSO_DATA_DIRECTORY/profiling` directory. After +the profiling is stopped, subsequent `profiling/stop` commands will do nothing. + +- **Type:** Request +- **Direction:** Client -> Server +- **Connection:** Protocol +- **Visibility:** Public + +#### Parameters + +```typescript +interface ProfilingStopParameters {} +``` + +#### Result + +```typescript +interface ProfilingStopResult {} +``` + +#### Errors + +None + ## Errors The language server component also has its own set of errors. This section is diff --git a/engine/language-server/src/main/java/org/enso/languageserver/package-info.java b/engine/language-server/src/main/java/org/enso/languageserver/package-info.java deleted file mode 100644 index ad19a1ecfb67..000000000000 --- a/engine/language-server/src/main/java/org/enso/languageserver/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package org.enso.languageserver; diff --git a/engine/language-server/src/main/java/org/enso/languageserver/runtime/events/RuntimeEventsMonitor.java b/engine/language-server/src/main/java/org/enso/languageserver/runtime/events/RuntimeEventsMonitor.java new file mode 100644 index 000000000000..0ec1b9af2830 --- /dev/null +++ b/engine/language-server/src/main/java/org/enso/languageserver/runtime/events/RuntimeEventsMonitor.java @@ -0,0 +1,112 @@ +package org.enso.languageserver.runtime.events; + +import java.io.IOException; +import java.io.PrintStream; +import java.time.Clock; +import java.time.Instant; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.XMLFormatter; + +import org.enso.languageserver.runtime.RuntimeConnector; +import org.enso.polyglot.runtime.Runtime; +import org.enso.polyglot.runtime.Runtime$Api$Request; +import org.enso.polyglot.runtime.Runtime$Api$Response; +import org.enso.profiling.events.EventsMonitor; +import scala.Option; + +/** + * Gather messages between the language server and the runtime and write them to the provided file + * in XML format. + */ +public final class RuntimeEventsMonitor implements EventsMonitor { + + private final PrintStream out; + private final Clock clock; + + private static final XMLFormatter EVENT_FORMAT = new XMLFormatter(); + private static final String XML_TAG = ""; + private static final String RECORDS_TAG_OPEN = ""; + private static final String RECORDS_TAG_CLOSE = ""; + private static final String MESSAGE_SEPARATOR = ","; + private static final String MESSAGE_EMPTY_REQUEST_ID = ""; + + /** + * Create an instance of {@link RuntimeEventsMonitor}. + * + * @param out the output stream. + * @param clock the system clock. + */ + public RuntimeEventsMonitor(PrintStream out, Clock clock) { + this.out = out; + this.clock = clock; + + out.println(XML_TAG); + out.println(RECORDS_TAG_OPEN); + } + + /** + * Create an instance of {@link RuntimeEventsMonitor}. + * + * @param out the output stream. + */ + public RuntimeEventsMonitor(PrintStream out) { + this(out, Clock.systemUTC()); + } + + /** + * Direction of the message. + */ + private enum Direction { + REQUEST, + RESPONSE + } + + @Override + public void registerEvent(Object event) { + if (event instanceof Runtime.ApiEnvelope envelope) { + registerApiEnvelope(envelope); + } else if (event instanceof RuntimeConnector.MessageFromRuntime messageFromRuntime) { + registerApiEnvelope(messageFromRuntime.message()); + } + } + + @Override + public void close() throws IOException { + out.println(RECORDS_TAG_CLOSE); + out.close(); + } + + private void registerApiEnvelope(Runtime.ApiEnvelope event) { + if (event instanceof Runtime$Api$Request request) { + String entry = + buildEntry(Direction.REQUEST, request.requestId(), request.payload().getClass()); + out.print(entry); + } else if (event instanceof Runtime$Api$Response response) { + String entry = + buildEntry(Direction.RESPONSE, response.correlationId(), response.payload().getClass()); + out.print(entry); + } + } + + private String buildEntry(Direction direction, Option requestId, Class payload) { + String requestIdEntry = requestId.fold(() -> MESSAGE_EMPTY_REQUEST_ID, UUID::toString); + String payloadEntry = payload.getSimpleName(); + Instant timeEntry = clock.instant(); + + String message = + new StringBuilder() + .append(direction) + .append(MESSAGE_SEPARATOR) + .append(requestIdEntry) + .append(MESSAGE_SEPARATOR) + .append(payloadEntry) + .toString(); + + LogRecord record = new LogRecord(Level.INFO, message); + record.setInstant(timeEntry); + + return EVENT_FORMAT.format(record); + } +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/boot/LanguageServerComponent.scala b/engine/language-server/src/main/scala/org/enso/languageserver/boot/LanguageServerComponent.scala index facebd1cd148..f10f8cc69360 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/boot/LanguageServerComponent.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/boot/LanguageServerComponent.scala @@ -14,8 +14,13 @@ import org.enso.languageserver.runtime.RuntimeKiller.{ RuntimeShutdownResult, ShutDownRuntime } -import org.enso.profiling.{FileSampler, MethodsSampler, NoopSampler} +import org.enso.profiling.sampler.{ + MethodsSampler, + NoopSampler, + OutputStreamSampler +} import org.slf4j.event.Level + import scala.concurrent.duration._ import scala.concurrent.{Await, ExecutionContextExecutor, Future} @@ -105,12 +110,14 @@ class LanguageServerComponent(config: LanguageServerConfig, logLevel: Level) private def startSampling(config: LanguageServerConfig): MethodsSampler = { val sampler = config.profilingConfig.profilingPath match { case Some(path) => - new FileSampler(path.toFile) + OutputStreamSampler.ofFile(path.toFile) case None => - NoopSampler() + new NoopSampler() } sampler.start() - config.profilingConfig.profilingTime.foreach(sampler.stop(_)) + config.profilingConfig.profilingTime.foreach(timeout => + sampler.scheduleStop(timeout.length, timeout.unit, ec) + ) sampler } 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 5a56469efad7..95e511a2de01 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 @@ -22,9 +22,9 @@ import org.enso.languageserver.libraries._ import org.enso.languageserver.monitoring.{ HealthCheckEndpoint, IdlenessEndpoint, - IdlenessMonitor, - NoopEventsMonitor + IdlenessMonitor } +import org.enso.languageserver.profiling.ProfilingManager import org.enso.languageserver.protocol.binary.{ BinaryConnectionControllerFactory, InboundMessageDecoder @@ -35,6 +35,7 @@ import org.enso.languageserver.protocol.json.{ } import org.enso.languageserver.requesthandler.monitoring.PingHandler import org.enso.languageserver.runtime._ +import org.enso.languageserver.runtime.events.RuntimeEventsMonitor import org.enso.languageserver.search.SuggestionsHandler import org.enso.languageserver.session.SessionRouter import org.enso.languageserver.text.BufferRegistry @@ -45,10 +46,11 @@ import org.enso.librarymanager.local.DefaultLocalLibraryProvider import org.enso.librarymanager.published.PublishedLibraryCache import org.enso.lockmanager.server.LockManagerService import org.enso.logger.Converter -import org.enso.logger.masking.{MaskedPath, Masking} +import org.enso.logger.masking.Masking import org.enso.logger.JulHandler import org.enso.logger.akka.AkkaConverter import org.enso.polyglot.{HostAccessFactory, RuntimeOptions, RuntimeServerInfo} +import org.enso.profiling.events.NoopEventsMonitor import org.enso.searcher.sql.{SqlDatabase, SqlSuggestionsRepo} import org.enso.text.{ContentBasedVersioning, Sha3_224VersionCalculator} import org.graalvm.polyglot.Engine @@ -57,11 +59,12 @@ import org.graalvm.polyglot.io.MessageEndpoint import org.slf4j.event.Level import org.slf4j.LoggerFactory -import java.io.File +import java.io.{File, PrintStream} import java.net.URI +import java.nio.charset.StandardCharsets import java.time.Clock + import scala.concurrent.duration._ -import scala.util.{Failure, Success} /** A main module containing all components of the server. * @@ -170,19 +173,10 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: Level) { val runtimeEventsMonitor = languageServerConfig.profiling.runtimeEventsLogPath match { case Some(path) => - ApiEventsMonitor(path) match { - case Success(monitor) => - monitor - case Failure(exception) => - log.error( - "Failed to create runtime events monitor for [{}].", - MaskedPath(path), - exception - ) - new NoopEventsMonitor - } + val out = new PrintStream(path.toFile, StandardCharsets.UTF_8) + new RuntimeEventsMonitor(out) case None => - new NoopEventsMonitor + new NoopEventsMonitor() } log.trace( "Started runtime events monitor [{}].", @@ -373,6 +367,12 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: Level) { "project-settings-manager" ) + val profilingManager = + system.actorOf( + ProfilingManager.props(runtimeConnector, distributionManager), + "profiling-manager" + ) + val libraryLocations = LibraryLocations.resolve( distributionManager, @@ -448,6 +448,7 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: Level) { runtimeConnector = runtimeConnector, idlenessMonitor = idlenessMonitor, projectSettingsManager = projectSettingsManager, + profilingManager = profilingManager, libraryConfig = libraryConfig, config = languageServerConfig ) 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 deleted file mode 100644 index 85ef05e1803b..000000000000 --- a/engine/language-server/src/main/scala/org/enso/languageserver/monitoring/EventsMonitor.scala +++ /dev/null @@ -1,20 +0,0 @@ -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/profiling/ProfilingApi.scala b/engine/language-server/src/main/scala/org/enso/languageserver/profiling/ProfilingApi.scala new file mode 100644 index 000000000000..ed584b6323ad --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/profiling/ProfilingApi.scala @@ -0,0 +1,31 @@ +package org.enso.languageserver.profiling + +import org.enso.jsonrpc.{HasParams, HasResult, Method, Unused} + +object ProfilingApi { + + case object ProfilingStart extends Method("profiling/start") { + + implicit val hasParams: HasParams.Aux[this.type, Unused.type] = + new HasParams[this.type] { + type Params = Unused.type + } + implicit val hasResult: HasResult.Aux[this.type, Unused.type] = + new HasResult[this.type] { + type Result = Unused.type + } + } + + case object ProfilingStop extends Method("profiling/stop") { + + implicit val hasParams: HasParams.Aux[this.type, Unused.type] = + new HasParams[this.type] { + type Params = Unused.type + } + implicit val hasResult: HasResult.Aux[this.type, Unused.type] = + new HasResult[this.type] { + type Result = Unused.type + } + } + +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/profiling/ProfilingManager.scala b/engine/language-server/src/main/scala/org/enso/languageserver/profiling/ProfilingManager.scala new file mode 100644 index 000000000000..0384643e2de8 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/profiling/ProfilingManager.scala @@ -0,0 +1,163 @@ +package org.enso.languageserver.profiling + +import akka.actor.{Actor, ActorRef, Props} +import com.typesafe.scalalogging.LazyLogging +import org.enso.distribution.DistributionManager +import org.enso.languageserver.runtime.RuntimeConnector +import org.enso.languageserver.runtime.events.RuntimeEventsMonitor +import org.enso.profiling.events.NoopEventsMonitor +import org.enso.profiling.sampler.{MethodsSampler, OutputStreamSampler} + +import java.io.{ByteArrayOutputStream, PrintStream} +import java.nio.charset.StandardCharsets +import java.nio.file.{Files, Path} +import java.time.{Clock, Instant, ZoneOffset} +import java.time.format.{DateTimeFormatter, DateTimeFormatterBuilder} +import java.time.temporal.ChronoField + +import scala.util.{Failure, Success, Try} + +/** Handles the profiling commands. + * + * @param runtimeConnector the connection to runtime + * @param distributionManager the distribution manager + * @param clock the system clock + */ +final class ProfilingManager( + runtimeConnector: ActorRef, + distributionManager: DistributionManager, + clock: Clock +) extends Actor + with LazyLogging { + + import ProfilingManager._ + + override def preStart(): Unit = { + Files.createDirectories(distributionManager.paths.profiling) + } + + override def receive: Receive = + initialized(None) + + private def initialized(sampler: Option[RunningSampler]): Receive = { + case ProfilingProtocol.ProfilingStartRequest => + sampler match { + case Some(_) => + sender() ! ProfilingProtocol.ProfilingStartResponse + case None => + val instant = clock.instant() + val result = new ByteArrayOutputStream() + val sampler = new OutputStreamSampler(result) + + sampler.start() + + val eventsMonitor = createEventsMonitor(instant) + runtimeConnector ! RuntimeConnector.RegisterEventsMonitor( + eventsMonitor + ) + + sender() ! ProfilingProtocol.ProfilingStartResponse + context.become( + initialized(Some(RunningSampler(instant, sampler, result))) + ) + } + + case ProfilingProtocol.ProfilingStopRequest => + sampler match { + case Some(RunningSampler(instant, sampler, result)) => + sampler.stop() + + Try(saveSamplerResult(result.toByteArray, instant)) match { + case Failure(exception) => + logger.error("Failed to save the sampler's result.", exception) + case Success(samplesPath) => + logger.trace("Saved the sampler's result to {}", samplesPath) + } + + runtimeConnector ! RuntimeConnector.RegisterEventsMonitor( + new NoopEventsMonitor + ) + + sender() ! ProfilingProtocol.ProfilingStopResponse + context.become(initialized(None)) + case None => + sender() ! ProfilingProtocol.ProfilingStopResponse + } + } + + private def saveSamplerResult( + result: Array[Byte], + instant: Instant + ): Path = { + val samplesFileName = createSamplesFileName(instant) + val samplesPath = + distributionManager.paths.profiling.resolve(samplesFileName) + + Files.write(samplesPath, result) + + samplesPath + } + + private def createEventsMonitor(instant: Instant): RuntimeEventsMonitor = { + val eventsLogFileName = createEventsFileName(instant) + val eventsLogPath = + distributionManager.paths.profiling.resolve(eventsLogFileName) + val out = new PrintStream(eventsLogPath.toFile, StandardCharsets.UTF_8) + new RuntimeEventsMonitor(out) + } +} + +object ProfilingManager { + + private val PROFILING_FILE_PREFIX = "enso-language-server" + private val SAMPLES_FILE_EXT = ".npss" + private val EVENTS_FILE_EXT = ".log" + + private val PROFILING_FILE_DATE_PART_FORMATTER = + new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .append(DateTimeFormatter.ISO_LOCAL_DATE) + .appendLiteral('T') + .appendValue(ChronoField.HOUR_OF_DAY, 2) + .appendLiteral('-') + .appendValue(ChronoField.MINUTE_OF_HOUR, 2) + .optionalStart() + .appendLiteral('-') + .appendValue(ChronoField.SECOND_OF_MINUTE, 2) + .toFormatter() + .withZone(ZoneOffset.UTC) + + private case class RunningSampler( + instant: Instant, + sampler: MethodsSampler, + result: ByteArrayOutputStream + ) + + private def createProfilingFileName(instant: Instant): String = { + val datePart = PROFILING_FILE_DATE_PART_FORMATTER.format(instant) + s"$PROFILING_FILE_PREFIX-$datePart" + } + + def createSamplesFileName(instant: Instant): String = { + val baseName = createProfilingFileName(instant) + s"$baseName$SAMPLES_FILE_EXT" + } + + def createEventsFileName(instant: Instant): String = { + val baseName = createProfilingFileName(instant) + s"$baseName$EVENTS_FILE_EXT" + } + + /** Creates the configuration object used to create a [[ProfilingManager]]. + * + * @param runtimeConnector the connection to runtime + * @param distributionManager the distribution manager + * @param clock the system clock + */ + def props( + runtimeConnector: ActorRef, + distributionManager: DistributionManager, + clock: Clock = Clock.systemUTC() + ): Props = + Props(new ProfilingManager(runtimeConnector, distributionManager, clock)) +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/profiling/ProfilingProtocol.scala b/engine/language-server/src/main/scala/org/enso/languageserver/profiling/ProfilingProtocol.scala new file mode 100644 index 000000000000..040f7deee326 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/profiling/ProfilingProtocol.scala @@ -0,0 +1,17 @@ +package org.enso.languageserver.profiling + +object ProfilingProtocol { + + /** A request to start the profiling. */ + case object ProfilingStartRequest + + /** A response to request to start the profiling. */ + case object ProfilingStartResponse + + /** A request to stop the profiling. */ + case object ProfilingStopRequest + + /** A response to request to stop the profiling. */ + case object ProfilingStopResponse + +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala index bd18305bdc6c..9b8a4ae916fc 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala @@ -35,6 +35,10 @@ import org.enso.languageserver.libraries.LibraryConfig import org.enso.languageserver.libraries.handler._ import org.enso.languageserver.monitoring.MonitoringApi.{InitialPing, Ping} import org.enso.languageserver.monitoring.MonitoringProtocol +import org.enso.languageserver.profiling.ProfilingApi.{ + ProfilingStart, + ProfilingStop +} import org.enso.languageserver.refactoring.RefactoringApi.{ RenameProject, RenameSymbol @@ -47,6 +51,10 @@ import org.enso.languageserver.requesthandler.monitoring.{ InitialPingHandler, PingHandler } +import org.enso.languageserver.requesthandler.profiling.{ + ProfilingStartHandler, + ProfilingStopHandler +} import org.enso.languageserver.requesthandler.refactoring.{ RenameProjectHandler, RenameSymbolHandler @@ -102,8 +110,10 @@ import scala.concurrent.duration._ * @param contentRootManager manages the available content roots * @param contextRegistry a router that dispatches execution context requests * @param suggestionsHandler a reference to the suggestions requests handler + * @param runtimeConnector a reference to the runtime connector * @param idlenessMonitor a reference to the idleness monitor actor * @param projectSettingsManager a reference to the project settings manager + * @param profilingManager a reference to the profiling manager * @param libraryConfig configuration of the library ecosystem * @param requestTimeout a request timeout */ @@ -123,6 +133,7 @@ class JsonConnectionController( val runtimeConnector: ActorRef, val idlenessMonitor: ActorRef, val projectSettingsManager: ActorRef, + val profilingManager: ActorRef, val libraryConfig: LibraryConfig, val languageServerConfig: Config, requestTimeout: FiniteDuration = 10.seconds @@ -630,6 +641,12 @@ class JsonConnectionController( RuntimeGetComponentGroups -> runtime.GetComponentGroupsHandler.props( requestTimeout, runtimeConnector + ), + ProfilingStart -> ProfilingStartHandler + .props(requestTimeout, profilingManager), + ProfilingStop -> ProfilingStopHandler.props( + requestTimeout, + profilingManager ) ) } @@ -675,6 +692,10 @@ object JsonConnectionController { * @param contentRootManager manages the available content roots * @param contextRegistry a router that dispatches execution context requests * @param suggestionsHandler a reference to the suggestions requests handler + * @param runtimeConnector a reference to the runtime connector + * @param idlenessMonitor a reference to the idleness monitor actor + * @param projectSettingsManager a reference to the project settings manager + * @param profilingManager a reference to the profiling manager * @param libraryConfig configuration of the library ecosystem * @param requestTimeout a request timeout * @return a configuration object @@ -695,6 +716,7 @@ object JsonConnectionController { runtimeConnector: ActorRef, idlenessMonitor: ActorRef, projectSettingsManager: ActorRef, + profilingManager: ActorRef, libraryConfig: LibraryConfig, languageServerConfig: Config, requestTimeout: FiniteDuration = 10.seconds @@ -716,6 +738,7 @@ object JsonConnectionController { runtimeConnector = runtimeConnector, idlenessMonitor = idlenessMonitor, projectSettingsManager = projectSettingsManager, + profilingManager = profilingManager, libraryConfig = libraryConfig, languageServerConfig = languageServerConfig, requestTimeout = requestTimeout diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionControllerFactory.scala b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionControllerFactory.scala index aed904fe5656..c1888e941962 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionControllerFactory.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionControllerFactory.scala @@ -13,8 +13,17 @@ import java.util.UUID * @param mainComponent the main initialization logic * @param bufferRegistry the buffer registry actor ref * @param capabilityRouter the capability router actor ref - * @param system the actor system + * @param fileManager performs operations with file system + * @param vcsManager performs operations with VCS + * @param contentRootManager manages the available content roots + * @param contextRegistry a router that dispatches execution context requests + * @param suggestionsHandler a reference to the suggestions requests handler + * @param runtimeConnector a reference to the runtime connector + * @param idlenessMonitor a reference to the idleness monitor actor + * @param projectSettingsManager a reference to the project settings manager + * @param profilingManager a reference to the profiling manager * @param libraryConfig configuration of the library ecosystem + * @param system the actor system */ class JsonConnectionControllerFactory( mainComponent: InitializationComponent, @@ -31,6 +40,7 @@ class JsonConnectionControllerFactory( runtimeConnector: ActorRef, idlenessMonitor: ActorRef, projectSettingsManager: ActorRef, + profilingManager: ActorRef, libraryConfig: LibraryConfig, config: Config )(implicit system: ActorSystem) @@ -59,6 +69,7 @@ class JsonConnectionControllerFactory( runtimeConnector = runtimeConnector, idlenessMonitor = idlenessMonitor, projectSettingsManager = projectSettingsManager, + profilingManager = profilingManager, libraryConfig = libraryConfig, languageServerConfig = config ), diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala index b935a862e422..ff62b66d390d 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala @@ -24,6 +24,10 @@ import org.enso.languageserver.runtime.VisualizationApi._ import org.enso.languageserver.session.SessionApi.InitProtocolConnection import org.enso.languageserver.text.TextApi._ import org.enso.languageserver.libraries.LibraryApi._ +import org.enso.languageserver.profiling.ProfilingApi.{ + ProfilingStart, + ProfilingStop +} import org.enso.languageserver.runtime.RuntimeApi.RuntimeGetComponentGroups import org.enso.languageserver.vcsmanager.VcsManagerApi._ import org.enso.languageserver.workspace.WorkspaceApi.ProjectInfo @@ -102,6 +106,8 @@ object JsonRpc { .registerRequest(LibraryPublish) .registerRequest(LibraryPreinstall) .registerRequest(RuntimeGetComponentGroups) + .registerRequest(ProfilingStart) + .registerRequest(ProfilingStop) .registerNotification(TaskStarted) .registerNotification(TaskProgressUpdate) .registerNotification(TaskFinished) diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/profiling/ProfilingStartHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/profiling/ProfilingStartHandler.scala new file mode 100644 index 000000000000..570a6d43b502 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/profiling/ProfilingStartHandler.scala @@ -0,0 +1,68 @@ +package org.enso.languageserver.requesthandler.profiling + +import akka.actor.{Actor, ActorRef, Cancellable, Props} +import com.typesafe.scalalogging.LazyLogging +import org.enso.jsonrpc._ +import org.enso.languageserver.profiling.{ProfilingApi, ProfilingProtocol} +import org.enso.languageserver.requesthandler.RequestTimeout +import org.enso.languageserver.util.UnhandledLogging + +import scala.concurrent.duration.FiniteDuration + +/** A request handler for `profiling/start` commands. + * + * @param timeout a request timeout + * @param profilingManager a reference to the profiling manager + */ +class ProfilingStartHandler(timeout: FiniteDuration, profilingManager: ActorRef) + extends Actor + with LazyLogging + with UnhandledLogging { + + import context.dispatcher + + override def receive: Receive = requestStage + + private def requestStage: Receive = { + case Request(ProfilingApi.ProfilingStart, id, _) => + profilingManager ! ProfilingProtocol.ProfilingStartRequest + val cancellable = + context.system.scheduler.scheduleOnce(timeout, self, RequestTimeout) + context.become( + responseStage( + id, + sender(), + cancellable + ) + ) + } + + private def responseStage( + id: Id, + replyTo: ActorRef, + cancellable: Cancellable + ): Receive = { + case RequestTimeout => + logger.error("Request [{}] timed out.", id) + replyTo ! ResponseError(Some(id), Errors.RequestTimeout) + context.stop(self) + + case ProfilingProtocol.ProfilingStartResponse => + replyTo ! ResponseResult(ProfilingApi.ProfilingStart, id, Unused) + cancellable.cancel() + context.stop(self) + } + +} + +object ProfilingStartHandler { + + /** Creates configuration object used to create a [[ProfilingStartHandler]]. + * + * @param timeout request timeout + * @param profilingManager reference to the profiling manager + */ + def props(timeout: FiniteDuration, profilingManager: ActorRef): Props = + Props(new ProfilingStartHandler(timeout, profilingManager)) + +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/profiling/ProfilingStopHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/profiling/ProfilingStopHandler.scala new file mode 100644 index 000000000000..4a815b51148a --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/profiling/ProfilingStopHandler.scala @@ -0,0 +1,68 @@ +package org.enso.languageserver.requesthandler.profiling + +import akka.actor.{Actor, ActorRef, Cancellable, Props} +import com.typesafe.scalalogging.LazyLogging +import org.enso.jsonrpc._ +import org.enso.languageserver.profiling.{ProfilingApi, ProfilingProtocol} +import org.enso.languageserver.requesthandler.RequestTimeout +import org.enso.languageserver.util.UnhandledLogging + +import scala.concurrent.duration.FiniteDuration + +/** A request handler for `profiling/stop` commands. + * + * @param timeout a request timeout + * @param profilingManager a reference to the profiling manager + */ +class ProfilingStopHandler(timeout: FiniteDuration, profilingManager: ActorRef) + extends Actor + with LazyLogging + with UnhandledLogging { + + import context.dispatcher + + override def receive: Receive = requestStage + + private def requestStage: Receive = { + case Request(ProfilingApi.ProfilingStop, id, _) => + profilingManager ! ProfilingProtocol.ProfilingStopRequest + val cancellable = + context.system.scheduler.scheduleOnce(timeout, self, RequestTimeout) + context.become( + responseStage( + id, + sender(), + cancellable + ) + ) + } + + private def responseStage( + id: Id, + replyTo: ActorRef, + cancellable: Cancellable + ): Receive = { + case RequestTimeout => + logger.error("Request [{}] timed out.", id) + replyTo ! ResponseError(Some(id), Errors.RequestTimeout) + context.stop(self) + + case ProfilingProtocol.ProfilingStopResponse => + replyTo ! ResponseResult(ProfilingApi.ProfilingStop, id, Unused) + cancellable.cancel() + context.stop(self) + } + +} + +object ProfilingStopHandler { + + /** Creates configuration object used to create a [[ProfilingStopHandler]]. + * + * @param timeout request timeout + * @param profilingManager reference to the profiling manager + */ + def props(timeout: FiniteDuration, profilingManager: ActorRef): Props = + Props(new ProfilingStopHandler(timeout, profilingManager)) + +} 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 deleted file mode 100644 index f7b25c3dee45..000000000000 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ApiEventsMonitor.scala +++ /dev/null @@ -1,97 +0,0 @@ -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 -import java.util.logging.XMLFormatter - -import scala.util.Try -import java.util.logging.LogRecord -import java.util.logging.Level - -/** Gather messages between the language server and the runtime and write them - * to the provided file in XML format. - * - * @param path the path where to write the events - * @param clock the system clock - */ -final class ApiEventsMonitor(path: Path, clock: Clock) extends EventsMonitor { - private lazy val fmt = { - Files.write( - path, - "\n".getBytes(StandardCharsets.UTF_8), - StandardOpenOption.APPEND, - StandardOpenOption.SYNC - ) - new XMLFormatter() - } - - import ApiEventsMonitor.Direction - - /** @inheritdoc */ - override def registerEvent(event: Any): Unit = - event match { - case envelope: ApiEnvelope => - registerApiEvent(envelope) - case RuntimeConnector.MessageFromRuntime(envelope) => - registerApiEvent(envelope) - case _ => - } - - private 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() - val msg = s"$direction,$requestIdEntry,$payloadEntry" - val record = new LogRecord(Level.INFO, msg) - record.setInstant(timeEntry) - fmt.format(record).getBytes(StandardCharsets.UTF_8) - } -} -object ApiEventsMonitor { - - /** Create default instance of [[ApiEventsMonitor]]. - * - * @param path the path to the events log file - * @return an instance of [[ApiEventsMonitor]] - */ - def apply(path: Path): Try[ApiEventsMonitor] = Try { - Files.deleteIfExists(path) - Files.createFile(path) - new ApiEventsMonitor(path, 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 4ac8cc29f121..7bf116fa6bc4 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,7 +2,6 @@ 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 @@ -12,14 +11,15 @@ import org.enso.lockmanager.server.LockManagerService import org.enso.logger.akka.ActorMessageLogging import org.enso.polyglot.runtime.Runtime import org.enso.polyglot.runtime.Runtime.{Api, ApiEnvelope} +import org.enso.profiling.events.EventsMonitor import org.graalvm.polyglot.io.MessageEndpoint import java.nio.ByteBuffer /** An actor managing a connection to Enso's runtime server. */ -class RuntimeConnector( +final class RuntimeConnector( handlers: Map[Class[_], ActorRef], - eventsMonitor: EventsMonitor + initialEventsMonitor: EventsMonitor ) extends Actor with LazyLogging with ActorMessageLogging @@ -37,17 +37,15 @@ class RuntimeConnector( engine ) unstashAll() - context.become(waitingOnEndpoint(engine)) + context.become(waitingOnEndpoint(engine, initialEventsMonitor)) case _ => stash() } - private def registerEvent: PartialFunction[Any, Any] = { case event => - eventsMonitor.registerEvent(event) - event - } - - private def waitingOnEndpoint(engine: MessageEndpoint): Receive = - registerEvent.andThen(LoggingReceive { + private def waitingOnEndpoint( + engine: MessageEndpoint, + eventsMonitor: EventsMonitor + ): Receive = + registerEvent(eventsMonitor).andThen(LoggingReceive { case MessageFromRuntime( Runtime.Api.Response(None, Api.InitializedNotification()) ) => @@ -56,7 +54,12 @@ class RuntimeConnector( engine ) unstashAll() - context.become(initialized(engine, Map())) + context.become(initialized(engine, eventsMonitor, Map())) + + case RuntimeConnector.RegisterEventsMonitor(newEventsMonitor) => + eventsMonitor.close() + context.become(waitingOnEndpoint(engine, newEventsMonitor)) + case _ => stash() }) @@ -81,20 +84,30 @@ class RuntimeConnector( * the runtime are forwarded to one of the registered handlers. * * @param engine endpoint of a runtime + * @param eventsMonitor the current events monitor * @param senders request ids with corresponding senders */ def initialized( engine: MessageEndpoint, + eventsMonitor: EventsMonitor, senders: Map[Runtime.Api.RequestId, ActorRef] - ): Receive = registerEvent.andThen(LoggingReceive { - case Destroy => context.stop(self) + ): Receive = registerEvent(eventsMonitor).andThen(LoggingReceive { + case Destroy => + eventsMonitor.close() + context.stop(self) + + case RuntimeConnector.RegisterEventsMonitor(newEventsMonitor) => + eventsMonitor.close() + context.become(initialized(engine, newEventsMonitor, senders)) case msg: Runtime.ApiEnvelope => engine.sendBinary(Runtime.Api.serialize(msg)) msg match { case Api.Request(Some(id), _) => - context.become(initialized(engine, senders + (id -> sender()))) + context.become( + initialized(engine, eventsMonitor, senders + (id -> sender())) + ) case _ => } @@ -125,8 +138,21 @@ class RuntimeConnector( payload.getClass.getCanonicalName ) } - context.become(initialized(engine, senders - correlationId)) + context.become( + initialized(engine, eventsMonitor, senders - correlationId) + ) }) + + /** Register event in the events monitor + * + * @param eventsMonitor the current events monitor + */ + private def registerEvent( + eventsMonitor: EventsMonitor + ): PartialFunction[Any, Any] = { event => + eventsMonitor.registerEvent(event) + event + } } object RuntimeConnector { @@ -137,6 +163,12 @@ object RuntimeConnector { */ case class Initialize(engineConnection: MessageEndpoint) + /** Protocol message to register new events monitor in the runtime connector. + * + * @param eventsMonitor the events monitor to register + */ + case class RegisterEventsMonitor(eventsMonitor: EventsMonitor) + /** Protocol message to inform the actor about the connection being closed. */ case object Destroy diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala index f0b4167023cc..c2a22e84fb1d 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala @@ -32,6 +32,7 @@ import org.enso.languageserver.filemanager._ import org.enso.languageserver.io._ import org.enso.languageserver.libraries._ import org.enso.languageserver.monitoring.IdlenessMonitor +import org.enso.languageserver.profiling.ProfilingManager import org.enso.languageserver.protocol.json.{ JsonConnectionControllerFactory, JsonRpcProtocolFactory @@ -61,6 +62,7 @@ import org.slf4j.event.Level import java.nio.file.{Files, Path} import java.util.UUID + import scala.concurrent.duration._ class BaseServerTest @@ -315,6 +317,14 @@ class BaseServerTest ) ) + val profilingManager = system.actorOf( + ProfilingManager.props( + runtimeConnectorProbe.ref, + distributionManager, + clock + ) + ) + val libraryConfig = LibraryConfig( localLibraryManager = localLibraryManager, editionReferenceResolver = editionReferenceResolver, @@ -345,6 +355,7 @@ class BaseServerTest runtimeConnector = runtimeConnectorProbe.ref, idlenessMonitor = idlenessMonitor, projectSettingsManager = projectSettingsManager, + profilingManager = profilingManager, libraryConfig = libraryConfig, config = config ) diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ProfilingJsonMessages.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ProfilingJsonMessages.scala new file mode 100644 index 000000000000..7293b6839c5c --- /dev/null +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ProfilingJsonMessages.scala @@ -0,0 +1,29 @@ +package org.enso.languageserver.websocket.json + +import io.circe.literal._ + +object ProfilingJsonMessages { + + def ok(reqId: Int) = + json""" + { "jsonrpc": "2.0", + "id": $reqId, + "result": null + }""" + def profilingStart(reqId: Int) = + json""" + { "jsonrpc": "2.0", + "method": "profiling/start", + "id": $reqId, + "params": null + }""" + + def profilingStop(reqId: Int) = + json""" + { "jsonrpc": "2.0", + "method": "profiling/stop", + "id": $reqId, + "params": null + }""" + +} diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ProfilingManagerTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ProfilingManagerTest.scala new file mode 100644 index 000000000000..42925d2f5c89 --- /dev/null +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ProfilingManagerTest.scala @@ -0,0 +1,53 @@ +package org.enso.languageserver.websocket.json + +import org.enso.distribution.DistributionManager +import org.enso.languageserver.profiling.ProfilingManager +import org.enso.languageserver.runtime.RuntimeConnector + +import java.nio.file.Files + +class ProfilingManagerTest extends BaseServerTest { + + private val json = ProfilingJsonMessages + + def getDistributionManager = + new DistributionManager(fakeInstalledEnvironment()) + + "ProfilingManager" should { + + "save profiling data " in { + val client = getInitialisedWsClient() + + client.send(json.profilingStart(1)) + runtimeConnectorProbe.receiveN(1).head match { + case _: RuntimeConnector.RegisterEventsMonitor => + // Ok + case other => + fail(s"Unexpected message: $other") + } + client.expectJson(json.ok(1)) + + client.send(json.profilingStop(2)) + runtimeConnectorProbe.receiveN(1).head match { + case _: RuntimeConnector.RegisterEventsMonitor => + // Ok + case other => + fail(s"Unexpected message: $other") + } + client.expectJson(json.ok(2)) + + val distributionManager = getDistributionManager + val instant = clock.instant + val samplesFile = distributionManager.paths.profiling.resolve( + ProfilingManager.createSamplesFileName(instant) + ) + val eventsFile = distributionManager.paths.profiling.resolve( + ProfilingManager.createEventsFileName(instant) + ) + + Files.exists(samplesFile) shouldEqual true + Files.exists(eventsFile) shouldEqual true + } + } + +} diff --git a/engine/runner/src/main/scala/org/enso/runner/Main.scala b/engine/runner/src/main/scala/org/enso/runner/Main.scala index 565c605e9801..71497fc28e17 100644 --- a/engine/runner/src/main/scala/org/enso/runner/Main.scala +++ b/engine/runner/src/main/scala/org/enso/runner/Main.scala @@ -17,7 +17,7 @@ import org.enso.libraryupload.LibraryUploader.UploadFailedError import org.slf4j.event.Level import org.enso.pkg.{Contact, PackageManager, Template} import org.enso.polyglot.{HostEnsoUtils, LanguageInfo, Module, PolyglotContext} -import org.enso.profiling.{FileSampler, NoopSampler} +import org.enso.profiling.sampler.{NoopSampler, OutputStreamSampler} import org.enso.version.VersionDescription import org.graalvm.polyglot.PolyglotException @@ -1293,12 +1293,14 @@ object Main { )(main: => A): A = { val sampler = profilingConfig.profilingPath match { case Some(path) => - new FileSampler(path.toFile) + OutputStreamSampler.ofFile(path.toFile) case None => - NoopSampler() + new NoopSampler() } sampler.start() - profilingConfig.profilingTime.foreach(sampler.stop(_)(executor)) + profilingConfig.profilingTime.foreach(timeout => + sampler.scheduleStop(timeout.length, timeout.unit, executor) + ) sys.addShutdownHook(sampler.stop()) try { diff --git a/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/DistributionManager.scala b/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/DistributionManager.scala index f7ee44764572..a28797bf272d 100644 --- a/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/DistributionManager.scala +++ b/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/DistributionManager.scala @@ -26,6 +26,7 @@ import scala.util.control.NonFatal * @param locks a directory for storing lockfiles that are used to synchronize * access to the various components * @param logs a directory for storing logs + * @param profiling a directory for storing profiling information * @param unsafeTemporaryDirectory path to the temporary directory, should not * be used directly, see * [[TemporaryDirectoryManager]] @@ -43,6 +44,7 @@ case class DistributionPaths( runRoot: Path, locks: Path, logs: Path, + profiling: Path, unsafeTemporaryDirectory: Path, customEditions: Seq[Path], localLibrariesSearchPaths: Seq[Path], @@ -60,7 +62,8 @@ case class DistributionPaths( | locks = ${mask(locks)}, | logs = ${mask(logs)}, | tmp = ${mask(unsafeTemporaryDirectory)}, - | ensoHome = ${mask(ensoHome)}, + | profiling = ${mask(profiling)}, + | ensoHome = ${mask(ensoHome)}, | customEditions = ${mask(customEditions)}, | localLibrariesSearchpaths = ${mask(localLibrariesSearchPaths)} |)""".stripMargin @@ -154,6 +157,7 @@ class DistributionManager(val env: Environment) { runRoot = runRoot, locks = runRoot / LOCK_DIRECTORY, logs = LocallyInstalledDirectories.logDirectory, + profiling = dataRoot / PROFILING_DIRECTORY, unsafeTemporaryDirectory = dataRoot / TMP_DIRECTORY, customEditions = detectCustomEditionPaths(home), localLibrariesSearchPaths = detectLocalLibraryPaths(home), @@ -446,6 +450,7 @@ object DistributionManager { val TMP_DIRECTORY = "tmp" val EDITIONS_DIRECTORY = "editions" val LIBRARIES_DIRECTORY = "lib" + val PROFILING_DIRECTORY = "profiling" /** Defines paths inside of the ENSO_HOME directory. */ object Home { diff --git a/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/PortableDistributionManager.scala b/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/PortableDistributionManager.scala index 13f810d6d699..a14549f2167e 100644 --- a/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/PortableDistributionManager.scala +++ b/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/PortableDistributionManager.scala @@ -23,7 +23,7 @@ class PortableDistributionManager(env: Environment) * This directory is checked for [[PORTABLE_MARK_FILENAME]]. If the mark file * is present, portable mode is selected. */ - def possiblePortableRoot: Path = + private def possiblePortableRoot: Path = env.getPathToRunningExecutable.getParent.getParent /** Specifies whether the program has been run as a portable distribution or @@ -79,6 +79,7 @@ class PortableDistributionManager(env: Environment) runRoot = root, locks = root / LOCK_DIRECTORY, logs = root / LOG_DIRECTORY, + profiling = root / PROFILING_DIRECTORY, unsafeTemporaryDirectory = root / TMP_DIRECTORY, customEditions = detectCustomEditionPaths(home), localLibrariesSearchPaths = detectLocalLibraryPaths(home), diff --git a/lib/scala/profiling-utils/src/main/java/org/enso/profiling/events/EventsMonitor.java b/lib/scala/profiling-utils/src/main/java/org/enso/profiling/events/EventsMonitor.java new file mode 100644 index 000000000000..c532a9522e76 --- /dev/null +++ b/lib/scala/profiling-utils/src/main/java/org/enso/profiling/events/EventsMonitor.java @@ -0,0 +1,14 @@ +package org.enso.profiling.events; + +import java.io.Closeable; + +/** Diagnostic tool that processes event messages. Used for debugging or performance review. */ +public interface EventsMonitor extends Closeable { + + /** + * Process the event message. + * + * @param event the event to register. + */ + void registerEvent(Object event); +} diff --git a/lib/scala/profiling-utils/src/main/java/org/enso/profiling/events/NoopEventsMonitor.java b/lib/scala/profiling-utils/src/main/java/org/enso/profiling/events/NoopEventsMonitor.java new file mode 100644 index 000000000000..2e87ba1f83b3 --- /dev/null +++ b/lib/scala/profiling-utils/src/main/java/org/enso/profiling/events/NoopEventsMonitor.java @@ -0,0 +1,13 @@ +package org.enso.profiling.events; + +import java.io.IOException; + +/** Events monitor that does nothing. */ +public final class NoopEventsMonitor implements EventsMonitor { + + @Override + public void registerEvent(Object event) {} + + @Override + public void close() throws IOException {} +} diff --git a/lib/scala/profiling-utils/src/main/java/org/enso/profiling/package-info.java b/lib/scala/profiling-utils/src/main/java/org/enso/profiling/package-info.java deleted file mode 100644 index 6583d1c6373c..000000000000 --- a/lib/scala/profiling-utils/src/main/java/org/enso/profiling/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package org.enso.profiling; diff --git a/lib/scala/profiling-utils/src/main/java/org/enso/profiling/sampler/MethodsSampler.java b/lib/scala/profiling-utils/src/main/java/org/enso/profiling/sampler/MethodsSampler.java new file mode 100644 index 000000000000..c249c648ce98 --- /dev/null +++ b/lib/scala/profiling-utils/src/main/java/org/enso/profiling/sampler/MethodsSampler.java @@ -0,0 +1,36 @@ +package org.enso.profiling.sampler; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +public interface MethodsSampler { + + /** Start gathering the application statistics. */ + void start(); + + /** Stop gathering the application statistics and write it to the output. */ + void stop() throws IOException; + + /** + * Stop gathering the application statistics after the provided delay and write it to the output. + * + * @param delay the duration to wait before stopping + * @param unit a unit determining how to interpret the delay parameter + * @param executor the executor + */ + default CompletableFuture scheduleStop(long delay, TimeUnit unit, Executor executor) { + Executor delayedExecutor = CompletableFuture.delayedExecutor(delay, unit, executor); + return CompletableFuture.runAsync( + () -> { + try { + this.stop(); + } catch (IOException e) { + throw new CompletionException(e); + } + }, + delayedExecutor); + } +} diff --git a/lib/scala/profiling-utils/src/main/java/org/enso/profiling/sampler/NoopSampler.java b/lib/scala/profiling-utils/src/main/java/org/enso/profiling/sampler/NoopSampler.java new file mode 100644 index 000000000000..6833496b65fa --- /dev/null +++ b/lib/scala/profiling-utils/src/main/java/org/enso/profiling/sampler/NoopSampler.java @@ -0,0 +1,11 @@ +package org.enso.profiling.sampler; + +/** Sampler that does nothing. */ +public class NoopSampler implements MethodsSampler { + + @Override + public void start() {} + + @Override + public void stop() {} +} diff --git a/lib/scala/profiling-utils/src/main/java/org/enso/profiling/sampler/OutputStreamSampler.java b/lib/scala/profiling-utils/src/main/java/org/enso/profiling/sampler/OutputStreamSampler.java new file mode 100644 index 000000000000..f8013a595bef --- /dev/null +++ b/lib/scala/profiling-utils/src/main/java/org/enso/profiling/sampler/OutputStreamSampler.java @@ -0,0 +1,56 @@ +package org.enso.profiling.sampler; + +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import org.netbeans.modules.sampler.Sampler; + +/** + * Gathers application performance statistics that can be visualised in Java VisualVM, and writes it + * to the provided output. + */ +public final class OutputStreamSampler implements MethodsSampler { + + private final Sampler sampler = Sampler.createSampler(this.getClass().getSimpleName()); + private final OutputStream outputStream; + + private boolean isSamplingStarted = false; + + /** + * Creates the {@link OutputStreamSampler} for provided output stream. + * + * @param outputStream the output stream to write result to. + */ + public OutputStreamSampler(OutputStream outputStream) { + this.outputStream = outputStream; + } + + public static OutputStreamSampler ofFile(File file) throws FileNotFoundException { + return new OutputStreamSampler(new FileOutputStream(file)); + } + + @Override + public void start() { + synchronized (this) { + if (!isSamplingStarted) { + sampler.start(); + isSamplingStarted = true; + } + } + } + + @Override + public void stop() throws IOException { + synchronized (this) { + if (isSamplingStarted) { + try (DataOutputStream dos = new DataOutputStream(outputStream)) { + sampler.stopAndWriteTo(dos); + } + isSamplingStarted = false; + } + } + } +} diff --git a/lib/scala/profiling-utils/src/main/scala/org/enso/profiling/FileSampler.scala b/lib/scala/profiling-utils/src/main/scala/org/enso/profiling/FileSampler.scala deleted file mode 100644 index 5b9b9517c6df..000000000000 --- a/lib/scala/profiling-utils/src/main/scala/org/enso/profiling/FileSampler.scala +++ /dev/null @@ -1,48 +0,0 @@ -package org.enso.profiling - -import org.netbeans.modules.sampler.Sampler - -import java.io.{DataOutputStream, File, FileOutputStream} -import java.util.concurrent.{CompletableFuture, Executor} - -import scala.concurrent.duration.FiniteDuration -import scala.util.Using - -/** Gathers application performance statistics that can be visualised in Java - * VisualVM, and writes it to the provided output. - * - * @param output the output stream to write the collected statistics to - */ -final class FileSampler(output: File) extends MethodsSampler { - - private val sampler: Sampler = Sampler.createSampler(getClass.getSimpleName) - private var samplingStarted: Boolean = false - - /** @inheritdoc */ - def start(): Unit = - this.synchronized { - if (!samplingStarted) { - sampler.start() - samplingStarted = true - } - } - - /** @inheritdoc */ - def stop(): Unit = - this.synchronized { - if (samplingStarted) { - samplingStarted = false - Using.resource(new DataOutputStream(new FileOutputStream(output)))( - sampler.stopAndWriteTo - ) - } - } - - /** @inheritdoc */ - def stop(delay: FiniteDuration)(implicit ec: Executor): Unit = - this.synchronized { - val delayedExecutor = - CompletableFuture.delayedExecutor(delay.length, delay.unit, ec) - CompletableFuture.supplyAsync(() => this.stop(), delayedExecutor) - } -} diff --git a/lib/scala/profiling-utils/src/main/scala/org/enso/profiling/MethodsSampler.scala b/lib/scala/profiling-utils/src/main/scala/org/enso/profiling/MethodsSampler.scala deleted file mode 100644 index 2e4d6a872b54..000000000000 --- a/lib/scala/profiling-utils/src/main/scala/org/enso/profiling/MethodsSampler.scala +++ /dev/null @@ -1,23 +0,0 @@ -package org.enso.profiling - -import java.util.concurrent.Executor - -import scala.concurrent.duration.FiniteDuration - -/** Sampler gathers the application performance statistics. */ -trait MethodsSampler { - - /** Start gathering the application statistics. */ - def start(): Unit - - /** Stop gathering the application statistics and write it to the output. */ - def stop(): Unit - - /** Stop gathering the application statistics after the provided delay and - * write it to the output. - * - * @param delay the duration to wait before stopping - * @param ec the execution context - */ - def stop(delay: FiniteDuration)(implicit ec: Executor): Unit -} diff --git a/lib/scala/profiling-utils/src/main/scala/org/enso/profiling/NoopSampler.scala b/lib/scala/profiling-utils/src/main/scala/org/enso/profiling/NoopSampler.scala deleted file mode 100644 index 9ef0ab06ea42..000000000000 --- a/lib/scala/profiling-utils/src/main/scala/org/enso/profiling/NoopSampler.scala +++ /dev/null @@ -1,24 +0,0 @@ -package org.enso.profiling - -import java.util.concurrent.Executor - -import scala.concurrent.duration.FiniteDuration - -/** Sampler that does nothing. */ -final class NoopSampler extends MethodsSampler { - - /** @inheritdoc */ - override def start(): Unit = () - - /** @inheritdoc */ - override def stop(): Unit = () - - /** @inheritdoc */ - override def stop(delay: FiniteDuration)(implicit ec: Executor): Unit = () -} -object NoopSampler { - - /** Create an instance of [[NoopSampler]]. */ - def apply(): NoopSampler = - new NoopSampler -} diff --git a/lib/scala/testkit/src/main/scala/org/enso/testkit/WithTemporaryDirectory.scala b/lib/scala/testkit/src/main/scala/org/enso/testkit/WithTemporaryDirectory.scala index ecf3569719f2..fdc57deee8ab 100644 --- a/lib/scala/testkit/src/main/scala/org/enso/testkit/WithTemporaryDirectory.scala +++ b/lib/scala/testkit/src/main/scala/org/enso/testkit/WithTemporaryDirectory.scala @@ -38,7 +38,7 @@ trait WithTemporaryDirectory * because if the test runs other executables, they may take a moment to * terminate even after the test completed). */ - def robustDeleteDirectory(dir: File): Unit = { + private def robustDeleteDirectory(dir: File): Unit = { @scala.annotation.tailrec def tryRemoving(retry: Int): Unit = { try {