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 {