Update profiling CLI arguments (#3461)

This commit is contained in:
Dmitry Bushev 2022-05-24 16:01:26 +03:00 committed by GitHub
parent 9a188344a8
commit f9d2964e83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 499 additions and 175 deletions

View File

@ -235,6 +235,7 @@
[3444]: https://github.com/enso-org/enso/pull/3444 [3444]: https://github.com/enso-org/enso/pull/3444
[3453]: https://github.com/enso-org/enso/pull/3453 [3453]: https://github.com/enso-org/enso/pull/3453
[3454]: https://github.com/enso-org/enso/pull/3454 [3454]: https://github.com/enso-org/enso/pull/3454
[3461]: https://github.com/enso-org/enso/pull/3461
[3465]: https://github.com/enso-org/enso/pull/3465 [3465]: https://github.com/enso-org/enso/pull/3465
# Enso 2.0.0-alpha.18 (2021-10-12) # Enso 2.0.0-alpha.18 (2021-10-12)

View File

@ -905,9 +905,10 @@ lazy val `json-rpc-server` = project
libraryDependencies ++= akka ++ akkaTest, libraryDependencies ++= akka ++ akkaTest,
libraryDependencies ++= circe, libraryDependencies ++= circe,
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"io.circe" %% "circe-literal" % circeVersion, "io.circe" %% "circe-literal" % circeVersion,
akkaTestkit % Test, "com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion,
"org.scalatest" %% "scalatest" % scalatestVersion % Test akkaTestkit % Test,
"org.scalatest" %% "scalatest" % scalatestVersion % Test
) )
) )

View File

@ -15,9 +15,10 @@ import org.enso.languageserver.runtime.RuntimeKiller.{
ShutDownRuntime ShutDownRuntime
} }
import org.enso.loggingservice.LogLevel import org.enso.loggingservice.LogLevel
import org.enso.profiling.{FileSampler, MethodsSampler, NoopSampler}
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.concurrent.{Await, Future} import scala.concurrent.{Await, ExecutionContextExecutor, Future}
/** A lifecycle component used to start and stop a Language Server. /** A lifecycle component used to start and stop a Language Server.
* *
@ -31,11 +32,14 @@ class LanguageServerComponent(config: LanguageServerConfig, logLevel: LogLevel)
@volatile @volatile
private var maybeServerCtx: Option[ServerContext] = None private var maybeServerCtx: Option[ServerContext] = None
implicit private val ec = config.computeExecutionContext implicit private val ec: ExecutionContextExecutor =
config.computeExecutionContext
/** @inheritdoc */ /** @inheritdoc */
override def start(): Future[ComponentStarted.type] = { override def start(): Future[ComponentStarted.type] = {
logger.info("Starting Language Server...") logger.info("Starting Language Server...")
val sampler = startSampling(config)
logger.debug(s"Started ${sampler.getClass.getName}.")
val module = new MainModule(config, logLevel) val module = new MainModule(config, logLevel)
val bindJsonServer = val bindJsonServer =
for { for {
@ -51,7 +55,8 @@ class LanguageServerComponent(config: LanguageServerConfig, logLevel: LogLevel)
jsonBinding <- bindJsonServer jsonBinding <- bindJsonServer
binaryBinding <- bindBinaryServer binaryBinding <- bindBinaryServer
_ <- Future { _ <- Future {
maybeServerCtx = Some(ServerContext(module, jsonBinding, binaryBinding)) maybeServerCtx =
Some(ServerContext(sampler, module, jsonBinding, binaryBinding))
} }
_ <- Future { _ <- Future {
logger.info( logger.info(
@ -62,14 +67,29 @@ class LanguageServerComponent(config: LanguageServerConfig, logLevel: LogLevel)
} yield ComponentStarted } yield ComponentStarted
} }
/** Start the application sampling. */
private def startSampling(config: LanguageServerConfig): MethodsSampler = {
val sampler = config.profilingConfig.profilingPath match {
case Some(path) =>
new FileSampler(path.toFile)
case None =>
NoopSampler()
}
sampler.start()
config.profilingConfig.profilingTime.foreach(sampler.stop(_))
sampler
}
/** @inheritdoc */ /** @inheritdoc */
override def stop(): Future[ComponentStopped.type] = override def stop(): Future[ComponentStopped.type] =
maybeServerCtx match { maybeServerCtx match {
case None => case None =>
Future.failed(new Exception("Server isn't running")) Future.successful(ComponentStopped)
case Some(serverContext) => case Some(serverContext) =>
for { for {
_ <- stopSampling(serverContext)
_ <- terminateTruffle(serverContext) _ <- terminateTruffle(serverContext)
_ <- terminateAkka(serverContext) _ <- terminateAkka(serverContext)
_ <- releaseResources(serverContext) _ <- releaseResources(serverContext)
@ -77,6 +97,9 @@ class LanguageServerComponent(config: LanguageServerConfig, logLevel: LogLevel)
} yield ComponentStopped } yield ComponentStopped
} }
private def stopSampling(serverContext: ServerContext): Future[Unit] =
Future(serverContext.sampler.stop()).recover(logError)
private def releaseResources(serverContext: ServerContext): Future[Unit] = private def releaseResources(serverContext: ServerContext): Future[Unit] =
for { for {
_ <- Future(serverContext.mainModule.close()).recover(logError) _ <- Future(serverContext.mainModule.close()).recover(logError)
@ -101,7 +124,7 @@ class LanguageServerComponent(config: LanguageServerConfig, logLevel: LogLevel)
} }
private def terminateTruffle(serverContext: ServerContext): Future[Unit] = { private def terminateTruffle(serverContext: ServerContext): Future[Unit] = {
implicit val askTimeout = Timeout(12.seconds) implicit val askTimeout: Timeout = Timeout(12.seconds)
val killFiber = val killFiber =
(serverContext.mainModule.runtimeKiller ? ShutDownRuntime) (serverContext.mainModule.runtimeKiller ? ShutDownRuntime)
.mapTo[RuntimeShutdownResult] .mapTo[RuntimeShutdownResult]
@ -129,11 +152,13 @@ object LanguageServerComponent {
/** A running server context. /** A running server context.
* *
* @param sampler a sampler gathering the application performance statistics
* @param mainModule a main module containing all components of the server * @param mainModule a main module containing all components of the server
* @param jsonBinding a http binding for rpc protocol * @param jsonBinding a http binding for rpc protocol
* @param binaryBinding a http binding for data protocol * @param binaryBinding a http binding for data protocol
*/ */
case class ServerContext( case class ServerContext(
sampler: MethodsSampler,
mainModule: MainModule, mainModule: MainModule,
jsonBinding: Http.ServerBinding, jsonBinding: Http.ServerBinding,
binaryBinding: Http.ServerBinding binaryBinding: Http.ServerBinding

View File

@ -2,7 +2,7 @@ package org.enso.languageserver.boot
import java.util.UUID import java.util.UUID
import scala.concurrent.ExecutionContext import scala.concurrent.{ExecutionContext, ExecutionContextExecutor}
/** The config of the running Language Server instance. /** The config of the running Language Server instance.
* *
@ -11,7 +11,7 @@ import scala.concurrent.ExecutionContext
* @param dataPort a data port that the server listen to * @param dataPort a data port that the server listen to
* @param contentRootUuid an id of content root * @param contentRootUuid an id of content root
* @param contentRootPath a path to the content root * @param contentRootPath a path to the content root
* @param isProfilingEnabled is the application profiling enabled * @param profilingConfig an application profiling configuration
*/ */
case class LanguageServerConfig( case class LanguageServerConfig(
interface: String, interface: String,
@ -19,7 +19,7 @@ case class LanguageServerConfig(
dataPort: Int, dataPort: Int,
contentRootUuid: UUID, contentRootUuid: UUID,
contentRootPath: String, contentRootPath: String,
isProfilingEnabled: Boolean, profilingConfig: ProfilingConfig,
name: String = "language-server", name: String = "language-server",
computeExecutionContext: ExecutionContext = ExecutionContext.global computeExecutionContext: ExecutionContextExecutor = ExecutionContext.global
) )

View File

@ -44,7 +44,6 @@ import org.enso.lockmanager.server.LockManagerService
import org.enso.logger.masking.Masking import org.enso.logger.masking.Masking
import org.enso.loggingservice.{JavaLoggingLogHandler, LogLevel} import org.enso.loggingservice.{JavaLoggingLogHandler, LogLevel}
import org.enso.polyglot.{RuntimeOptions, RuntimeServerInfo} import org.enso.polyglot.{RuntimeOptions, RuntimeServerInfo}
import org.enso.profiling.{NoopSampler, TempFileSampler}
import org.enso.searcher.sql.{SqlDatabase, SqlSuggestionsRepo, SqlVersionsRepo} import org.enso.searcher.sql.{SqlDatabase, SqlSuggestionsRepo, SqlVersionsRepo}
import org.enso.text.{ContentBasedVersioning, Sha3_224VersionCalculator} import org.enso.text.{ContentBasedVersioning, Sha3_224VersionCalculator}
import org.graalvm.polyglot.Context import org.graalvm.polyglot.Context
@ -56,6 +55,7 @@ import java.net.URI
import java.time.Clock import java.time.Clock
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.util.{Failure, Success}
/** A main module containing all components of the server. /** A main module containing all components of the server.
* *
@ -83,7 +83,8 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
FileManagerConfig(timeout = 3.seconds), FileManagerConfig(timeout = 3.seconds),
PathWatcherConfig(), PathWatcherConfig(),
ExecutionContextConfig(), ExecutionContextConfig(),
directoriesConfig directoriesConfig,
serverConfig.profilingConfig
) )
log.trace("Created Language Server config [{}].", languageServerConfig) log.trace("Created Language Server config [{}].", languageServerConfig)
@ -148,8 +149,20 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
) )
val runtimeEventsMonitor = val runtimeEventsMonitor =
if (logLevel == LogLevel.Trace) ApiEventsMonitor() languageServerConfig.profiling.runtimeEventsLogPath match {
else new NoopEventsMonitor case Some(path) =>
ApiEventsMonitor(path) match {
case Success(monitor) =>
monitor
case Failure(exception) =>
log.error(
s"Failed to create runtime events monitor for $path ($exception)."
)
new NoopEventsMonitor
}
case None =>
new NoopEventsMonitor
}
log.trace( log.trace(
s"Started runtime events monitor ${runtimeEventsMonitor.getClass.getName}." s"Started runtime events monitor ${runtimeEventsMonitor.getClass.getName}."
) )
@ -228,12 +241,7 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
languageServerConfig, languageServerConfig,
RuntimeFailureMapper(contentRootManagerWrapper), RuntimeFailureMapper(contentRootManagerWrapper),
runtimeConnector, runtimeConnector,
sessionRouter, sessionRouter
if (serverConfig.isProfilingEnabled) {
val s = TempFileSampler("context-registry")
JavaLoggingLogHandler.registerLogFile(s.getSiblingFile(".log"))
s
} else NoopSampler()
), ),
"context-registry" "context-registry"
) )

View File

@ -0,0 +1,18 @@
package org.enso.languageserver.boot
import java.nio.file.Path
import scala.concurrent.duration.FiniteDuration
/** Application profiling configuration.
*
* @param runtimeEventsLogPath the path to the runtime events log file
* @param profilingPath the path to the profiling output file
* @param profilingTime limit the profiling duration, as an infinite profiling
* duration may cause out-of-memory errors.
*/
case class ProfilingConfig(
runtimeEventsLogPath: Option[Path] = None,
profilingPath: Option[Path] = None,
profilingTime: Option[FiniteDuration] = None
)

View File

@ -1,10 +1,12 @@
package org.enso.languageserver.data package org.enso.languageserver.data
import org.enso.languageserver.boot.ProfilingConfig
import org.enso.languageserver.filemanager.ContentRootWithFile import org.enso.languageserver.filemanager.ContentRootWithFile
import org.enso.logger.masking.{MaskingUtils, ToLogString} import org.enso.logger.masking.{MaskingUtils, ToLogString}
import java.io.File import java.io.File
import java.nio.file.Files import java.nio.file.Files
import scala.concurrent.duration._ import scala.concurrent.duration._
/** Configuration of the path watcher. /** Configuration of the path watcher.
@ -122,13 +124,15 @@ object ProjectDirectoriesConfig {
* @param pathWatcher the path watcher config * @param pathWatcher the path watcher config
* @param executionContext the executionContext config * @param executionContext the executionContext config
* @param directories the configuration of internal directories * @param directories the configuration of internal directories
* @param profiling the profiling configuration
*/ */
case class Config( case class Config(
projectContentRoot: ContentRootWithFile, projectContentRoot: ContentRootWithFile,
fileManager: FileManagerConfig, fileManager: FileManagerConfig,
pathWatcher: PathWatcherConfig, pathWatcher: PathWatcherConfig,
executionContext: ExecutionContextConfig, executionContext: ExecutionContextConfig,
directories: ProjectDirectoriesConfig directories: ProjectDirectoriesConfig,
profiling: ProfilingConfig
) extends ToLogString { ) extends ToLogString {
/** @inheritdoc */ /** @inheritdoc */

View File

@ -8,6 +8,8 @@ import java.nio.charset.StandardCharsets
import java.nio.file.{Files, Path, StandardOpenOption} import java.nio.file.{Files, Path, StandardOpenOption}
import java.time.Clock import java.time.Clock
import scala.util.Try
/** Gather messages between the language server and the runtime and write them /** Gather messages between the language server and the runtime and write them
* to the provided file in CSV format. * to the provided file in CSV format.
* *
@ -61,12 +63,16 @@ final class ApiEventsMonitor(path: Path, clock: Clock) extends EventsMonitor {
} }
object ApiEventsMonitor { object ApiEventsMonitor {
/** Create default instance of [[ApiEventsMonitor]]. */ /** Create default instance of [[ApiEventsMonitor]].
def apply(): ApiEventsMonitor = *
new ApiEventsMonitor( * @param path the path to the events log file
Files.createTempFile("enso-api-events-", ".csv"), * @return an instance of [[ApiEventsMonitor]]
Clock.systemUTC() */
) def apply(path: Path): Try[ApiEventsMonitor] = Try {
Files.deleteIfExists(path)
Files.createFile(path)
new ApiEventsMonitor(path, Clock.systemUTC())
}
/** Direction of the message. */ /** Direction of the message. */
sealed trait Direction sealed trait Direction

View File

@ -13,7 +13,6 @@ import org.enso.languageserver.util.UnhandledLogging
import org.enso.logger.akka.ActorMessageLogging import org.enso.logger.akka.ActorMessageLogging
import org.enso.polyglot.runtime.Runtime.Api import org.enso.polyglot.runtime.Runtime.Api
import org.enso.polyglot.runtime.Runtime.Api.ContextId import org.enso.polyglot.runtime.Runtime.Api.ContextId
import org.enso.profiling.MethodsSampler
import org.enso.searcher.SuggestionsRepo import org.enso.searcher.SuggestionsRepo
import java.util.UUID import java.util.UUID
@ -59,15 +58,13 @@ import scala.concurrent.duration._
* @param runtimeFailureMapper mapper for runtime failures * @param runtimeFailureMapper mapper for runtime failures
* @param runtime reference to the [[RuntimeConnector]] * @param runtime reference to the [[RuntimeConnector]]
* @param sessionRouter the session router * @param sessionRouter the session router
* @param sampler the methods sampler
*/ */
final class ContextRegistry( final class ContextRegistry(
repo: SuggestionsRepo[Future], repo: SuggestionsRepo[Future],
config: Config, config: Config,
runtimeFailureMapper: RuntimeFailureMapper, runtimeFailureMapper: RuntimeFailureMapper,
runtime: ActorRef, runtime: ActorRef,
sessionRouter: ActorRef, sessionRouter: ActorRef
sampler: MethodsSampler
) extends Actor ) extends Actor
with LazyLogging with LazyLogging
with ActorMessageLogging with ActorMessageLogging
@ -75,7 +72,8 @@ final class ContextRegistry(
import ContextRegistryProtocol._ import ContextRegistryProtocol._
private val timeout: FiniteDuration = config.executionContext.requestTimeout private val timeout: FiniteDuration =
config.executionContext.requestTimeout
override def preStart(): Unit = { override def preStart(): Unit = {
context.system.eventStream context.system.eventStream
@ -110,11 +108,9 @@ final class ContextRegistry(
.foreach(_ ! update) .foreach(_ ! update)
case update: Api.ExecutionFailed => case update: Api.ExecutionFailed =>
sampler.stop(6.seconds)(context.dispatcher)
store.getListener(update.contextId).foreach(_ ! update) store.getListener(update.contextId).foreach(_ ! update)
case update: Api.ExecutionComplete => case update: Api.ExecutionComplete =>
sampler.stop(6.seconds)(context.dispatcher)
store.getListener(update.contextId).foreach(_ ! update) store.getListener(update.contextId).foreach(_ ! update)
case update: Api.ExecutionUpdate => case update: Api.ExecutionUpdate =>
@ -166,7 +162,6 @@ final class ContextRegistry(
} }
case PushContextRequest(client, contextId, stackItem) => case PushContextRequest(client, contextId, stackItem) =>
sampler.start()
if (store.hasContext(client.clientId, contextId)) { if (store.hasContext(client.clientId, contextId)) {
val item = getRuntimeStackItem(stackItem) val item = getRuntimeStackItem(stackItem)
val handler = val handler =
@ -400,15 +395,13 @@ object ContextRegistry {
* @param runtimeFailureMapper mapper for runtime failures * @param runtimeFailureMapper mapper for runtime failures
* @param runtime reference to the [[RuntimeConnector]] * @param runtime reference to the [[RuntimeConnector]]
* @param sessionRouter the session router * @param sessionRouter the session router
* @param sampler the methods sampler
*/ */
def props( def props(
repo: SuggestionsRepo[Future], repo: SuggestionsRepo[Future],
config: Config, config: Config,
runtimeFailureMapper: RuntimeFailureMapper, runtimeFailureMapper: RuntimeFailureMapper,
runtime: ActorRef, runtime: ActorRef,
sessionRouter: ActorRef, sessionRouter: ActorRef
sampler: MethodsSampler
): Props = ): Props =
Props( Props(
new ContextRegistry( new ContextRegistry(
@ -416,8 +409,7 @@ object ContextRegistry {
config, config,
runtimeFailureMapper, runtimeFailureMapper,
runtime, runtime,
sessionRouter, sessionRouter
sampler
) )
) )
} }

View File

@ -3,6 +3,7 @@ package org.enso.languageserver.boot.resource
import akka.actor.ActorSystem import akka.actor.ActorSystem
import akka.testkit._ import akka.testkit._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.enso.languageserver.boot.ProfilingConfig
import org.enso.languageserver.data._ import org.enso.languageserver.data._
import org.enso.languageserver.event.InitializedEvent import org.enso.languageserver.event.InitializedEvent
import org.enso.languageserver.filemanager.{ContentRoot, ContentRootWithFile} import org.enso.languageserver.filemanager.{ContentRoot, ContentRootWithFile}
@ -20,6 +21,7 @@ import org.sqlite.SQLiteException
import java.nio.file.{Files, StandardOpenOption} import java.nio.file.{Files, StandardOpenOption}
import java.util.UUID import java.util.UUID
import scala.concurrent.Await import scala.concurrent.Await
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -210,7 +212,8 @@ class RepoInitializationSpec
FileManagerConfig(timeout = 3.seconds.dilated), FileManagerConfig(timeout = 3.seconds.dilated),
PathWatcherConfig(), PathWatcherConfig(),
ExecutionContextConfig(requestTimeout = 3.seconds.dilated), ExecutionContextConfig(requestTimeout = 3.seconds.dilated),
ProjectDirectoriesConfig.initialize(root.file) ProjectDirectoriesConfig.initialize(root.file),
ProfilingConfig()
) )
} }

View File

@ -3,6 +3,7 @@ package org.enso.languageserver.filemanager
import akka.actor.{ActorRef, ActorSystem} import akka.actor.{ActorRef, ActorSystem}
import akka.testkit.{TestDuration, TestKit, TestProbe} import akka.testkit.{TestDuration, TestKit, TestProbe}
import org.apache.commons.lang3.SystemUtils import org.apache.commons.lang3.SystemUtils
import org.enso.languageserver.boot.ProfilingConfig
import org.enso.languageserver.data._ import org.enso.languageserver.data._
import org.enso.languageserver.filemanager.ContentRootManagerProtocol.{ import org.enso.languageserver.filemanager.ContentRootManagerProtocol.{
ContentRootsAddedNotification, ContentRootsAddedNotification,
@ -19,6 +20,7 @@ import org.scalatest.{Inside, OptionValues}
import java.io.File import java.io.File
import java.nio.file.{Path => JPath} import java.nio.file.{Path => JPath}
import java.util.UUID import java.util.UUID
import scala.concurrent.duration.DurationInt import scala.concurrent.duration.DurationInt
class ContentRootManagerSpec class ContentRootManagerSpec
@ -46,7 +48,8 @@ class ContentRootManagerSpec
FileManagerConfig(timeout = 3.seconds.dilated), FileManagerConfig(timeout = 3.seconds.dilated),
PathWatcherConfig(), PathWatcherConfig(),
ExecutionContextConfig(requestTimeout = 3.seconds.dilated), ExecutionContextConfig(requestTimeout = 3.seconds.dilated),
ProjectDirectoriesConfig.initialize(root.file) ProjectDirectoriesConfig.initialize(root.file),
ProfilingConfig()
) )
rootActor = system.actorOf(ContentRootManagerActor.props(config)) rootActor = system.actorOf(ContentRootManagerActor.props(config))
rootManager = new ContentRootManagerWrapper(config, rootActor) rootManager = new ContentRootManagerWrapper(config, rootActor)

View File

@ -3,6 +3,7 @@ package org.enso.languageserver.runtime
import akka.actor.{ActorRef, ActorSystem} import akka.actor.{ActorRef, ActorSystem}
import akka.testkit.{ImplicitSender, TestKit, TestProbe} import akka.testkit.{ImplicitSender, TestKit, TestProbe}
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.enso.languageserver.boot.ProfilingConfig
import org.enso.languageserver.data._ import org.enso.languageserver.data._
import org.enso.languageserver.event.InitializedEvent import org.enso.languageserver.event.InitializedEvent
import org.enso.languageserver.filemanager.{ import org.enso.languageserver.filemanager.{
@ -29,6 +30,7 @@ import org.scalatest.wordspec.AnyWordSpecLike
import java.nio.file.Files import java.nio.file.Files
import java.util.UUID import java.util.UUID
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.concurrent.{Await, Future} import scala.concurrent.{Await, Future}
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
@ -453,7 +455,8 @@ class ContextEventsListenerSpec
FileManagerConfig(timeout = 3.seconds), FileManagerConfig(timeout = 3.seconds),
PathWatcherConfig(), PathWatcherConfig(),
ExecutionContextConfig(requestTimeout = 3.seconds), ExecutionContextConfig(requestTimeout = 3.seconds),
ProjectDirectoriesConfig.initialize(root.file) ProjectDirectoriesConfig.initialize(root.file),
ProfilingConfig()
) )
} }

View File

@ -4,6 +4,7 @@ import akka.actor.{ActorRef, ActorSystem}
import akka.testkit.{ImplicitSender, TestKit, TestProbe} import akka.testkit.{ImplicitSender, TestKit, TestProbe}
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.enso.docs.generator.DocsGenerator import org.enso.docs.generator.DocsGenerator
import org.enso.languageserver.boot.ProfilingConfig
import org.enso.languageserver.capability.CapabilityProtocol.{ import org.enso.languageserver.capability.CapabilityProtocol.{
AcquireCapability, AcquireCapability,
CapabilityAcquired CapabilityAcquired
@ -1107,7 +1108,8 @@ class SuggestionsHandlerSpec
FileManagerConfig(timeout = 3.seconds), FileManagerConfig(timeout = 3.seconds),
PathWatcherConfig(), PathWatcherConfig(),
ExecutionContextConfig(requestTimeout = 3.seconds), ExecutionContextConfig(requestTimeout = 3.seconds),
ProjectDirectoriesConfig.initialize(root.file) ProjectDirectoriesConfig.initialize(root.file),
ProfilingConfig()
) )
} }

View File

@ -6,6 +6,7 @@ import akka.actor.{ActorRef, Props}
import akka.http.scaladsl.model.RemoteAddress import akka.http.scaladsl.model.RemoteAddress
import com.google.flatbuffers.FlatBufferBuilder import com.google.flatbuffers.FlatBufferBuilder
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.enso.languageserver.boot.ProfilingConfig
import org.enso.languageserver.data.{ import org.enso.languageserver.data.{
Config, Config,
ExecutionContextConfig, ExecutionContextConfig,
@ -45,7 +46,8 @@ class BaseBinaryServerTest extends BinaryServerTestKit {
FileManagerConfig(timeout = 3.seconds), FileManagerConfig(timeout = 3.seconds),
PathWatcherConfig(), PathWatcherConfig(),
ExecutionContextConfig(requestTimeout = 3.seconds), ExecutionContextConfig(requestTimeout = 3.seconds),
ProjectDirectoriesConfig.initialize(testContentRoot.file) ProjectDirectoriesConfig.initialize(testContentRoot.file),
ProfilingConfig()
) )
sys.addShutdownHook(FileUtils.deleteQuietly(testContentRoot.file)) sys.addShutdownHook(FileUtils.deleteQuietly(testContentRoot.file))

View File

@ -12,6 +12,7 @@ import org.enso.editions.{EditionResolver, Editions}
import org.enso.jsonrpc.test.JsonRpcServerTestKit import org.enso.jsonrpc.test.JsonRpcServerTestKit
import org.enso.jsonrpc.{ClientControllerFactory, Protocol} import org.enso.jsonrpc.{ClientControllerFactory, Protocol}
import org.enso.languageserver.TestClock import org.enso.languageserver.TestClock
import org.enso.languageserver.boot.ProfilingConfig
import org.enso.languageserver.boot.resource.{ import org.enso.languageserver.boot.resource.{
DirectoriesInitialization, DirectoriesInitialization,
RepoInitialization, RepoInitialization,
@ -41,7 +42,6 @@ import org.enso.loggingservice.LogLevel
import org.enso.pkg.PackageManager import org.enso.pkg.PackageManager
import org.enso.polyglot.data.TypeGraph import org.enso.polyglot.data.TypeGraph
import org.enso.polyglot.runtime.Runtime.Api import org.enso.polyglot.runtime.Runtime.Api
import org.enso.profiling.NoopSampler
import org.enso.runtimeversionmanager.test.{ import org.enso.runtimeversionmanager.test.{
FakeEnvironment, FakeEnvironment,
TestableThreadSafeFileLockManager TestableThreadSafeFileLockManager
@ -93,7 +93,8 @@ class BaseServerTest
FileManagerConfig(timeout = 3.seconds), FileManagerConfig(timeout = 3.seconds),
PathWatcherConfig(), PathWatcherConfig(),
ExecutionContextConfig(requestTimeout = 3.seconds), ExecutionContextConfig(requestTimeout = 3.seconds),
ProjectDirectoriesConfig(testContentRoot.file) ProjectDirectoriesConfig(testContentRoot.file),
ProfilingConfig()
) )
override def protocol: Protocol = JsonRpc.protocol override def protocol: Protocol = JsonRpc.protocol
@ -189,8 +190,7 @@ class BaseServerTest
config, config,
RuntimeFailureMapper(contentRootManagerWrapper), RuntimeFailureMapper(contentRootManagerWrapper),
runtimeConnectorProbe.ref, runtimeConnectorProbe.ref,
sessionRouter, sessionRouter
NoopSampler()
) )
) )

View File

@ -1,19 +1,20 @@
package org.enso.languageserver.websocket.json package org.enso.languageserver.websocket.json
import io.circe.literal._
import io.circe.parser.parse
import org.apache.commons.io.FileUtils
import org.bouncycastle.util.encoders.Hex
import org.enso.languageserver.boot.ProfilingConfig
import org.enso.languageserver.data._
import org.enso.polyglot.runtime.Runtime.Api
import org.enso.testkit.RetrySpec
import java.io.File import java.io.File
import java.nio.file.attribute.BasicFileAttributes import java.nio.file.attribute.BasicFileAttributes
import java.nio.file.{Files, Paths} import java.nio.file.{Files, Paths}
import java.security.MessageDigest import java.security.MessageDigest
import java.util.UUID import java.util.UUID
import io.circe.literal._
import io.circe.parser.parse
import org.apache.commons.io.FileUtils
import org.bouncycastle.util.encoders.Hex
import org.enso.languageserver.data._
import org.enso.polyglot.runtime.Runtime.Api
import org.enso.testkit.RetrySpec
import scala.concurrent.duration._ import scala.concurrent.duration._
class FileManagerTest extends BaseServerTest with RetrySpec { class FileManagerTest extends BaseServerTest with RetrySpec {
@ -26,7 +27,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
FileManagerConfig(timeout = 3.seconds), FileManagerConfig(timeout = 3.seconds),
PathWatcherConfig(), PathWatcherConfig(),
ExecutionContextConfig(requestTimeout = 3.seconds), ExecutionContextConfig(requestTimeout = 3.seconds),
ProjectDirectoriesConfig.initialize(testContentRoot.file) ProjectDirectoriesConfig.initialize(testContentRoot.file),
ProfilingConfig()
) )
} }

View File

@ -679,7 +679,8 @@ object LauncherApplication {
logLevel, logLevel,
connectLogger, connectLogger,
globalCLIOptions.colorMode, globalCLIOptions.colorMode,
!disableLogMasking !disableLogMasking,
None
) )
initializeApp() initializeApp()

View File

@ -6,7 +6,9 @@ import org.enso.languageserver.boot.{
} }
import org.enso.loggingservice.LogLevel import org.enso.loggingservice.LogLevel
import scala.concurrent.Await import java.util.concurrent.Semaphore
import scala.concurrent.{Await, ExecutionContext, Future}
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.io.StdIn import scala.io.StdIn
@ -14,6 +16,8 @@ import scala.io.StdIn
*/ */
object LanguageServerApp { object LanguageServerApp {
private val semaphore = new Semaphore(1)
/** Runs a Language Server /** Runs a Language Server
* *
* @param config the application config * @param config the application config
@ -27,7 +31,7 @@ object LanguageServerApp {
): Unit = { ): Unit = {
val server = new LanguageServerComponent(config, logLevel) val server = new LanguageServerComponent(config, logLevel)
Runtime.getRuntime.addShutdownHook(new Thread(() => { Runtime.getRuntime.addShutdownHook(new Thread(() => {
Await.result(server.stop(), 40.seconds) stop(server)(config.computeExecutionContext)
})) }))
Await.result(server.start(), 1.minute) Await.result(server.start(), 1.minute)
if (deamonize) { if (deamonize) {
@ -37,7 +41,31 @@ object LanguageServerApp {
} }
} else { } else {
StdIn.readLine() StdIn.readLine()
stop(server)(config.computeExecutionContext)
} }
} }
/** Stops the language server.
*
* @param server the language server component
* @param ec the execution context
*/
private def stop(
server: LanguageServerComponent
)(implicit ec: ExecutionContext): Unit = {
Await.ready(synchronize(server.stop()), 40.seconds)
}
/** Makes sure that the calls to the provided future are synchronized. */
private def synchronize[A](
fut: => Future[A]
)(implicit ec: ExecutionContext): Future[A] = {
val task = for {
_ <- Future { semaphore.acquire() }
result <- fut
} yield result
task.onComplete(_ => semaphore.release())
task
}
} }

View File

@ -7,7 +7,7 @@ import com.typesafe.scalalogging.Logger
import org.apache.commons.cli.{Option => CliOption, _} import org.apache.commons.cli.{Option => CliOption, _}
import org.enso.editions.DefaultEdition import org.enso.editions.DefaultEdition
import org.enso.languageserver.boot import org.enso.languageserver.boot
import org.enso.languageserver.boot.LanguageServerConfig import org.enso.languageserver.boot.{LanguageServerConfig, ProfilingConfig}
import org.enso.libraryupload.LibraryUploader.UploadFailedError import org.enso.libraryupload.LibraryUploader.UploadFailedError
import org.enso.loggingservice.LogLevel import org.enso.loggingservice.LogLevel
import org.enso.pkg.{Contact, PackageManager, Template} import org.enso.pkg.{Contact, PackageManager, Template}
@ -16,14 +16,14 @@ import org.enso.version.VersionDescription
import org.graalvm.polyglot.PolyglotException import org.graalvm.polyglot.PolyglotException
import java.io.File import java.io.File
import java.nio.file.Path import java.nio.file.{Path, Paths}
import java.util.UUID import java.util.{Collections, UUID}
import scala.Console.err import scala.Console.err
import scala.concurrent.duration._
import scala.jdk.CollectionConverters._ import scala.jdk.CollectionConverters._
import scala.util.{Failure, Success, Try}
import scala.util.control.NonFatal import scala.util.control.NonFatal
import java.util.Collections import scala.util.{Failure, Success, Try}
/** The main CLI entry point class. */ /** The main CLI entry point class. */
object Main { object Main {
@ -40,7 +40,10 @@ object Main {
private val DOCS_OPTION = "docs" private val DOCS_OPTION = "docs"
private val PREINSTALL_OPTION = "preinstall-dependencies" private val PREINSTALL_OPTION = "preinstall-dependencies"
private val LANGUAGE_SERVER_OPTION = "server" private val LANGUAGE_SERVER_OPTION = "server"
private val LANGUAGE_SERVER_PROFILING = "server-profiling" private val LANGUAGE_SERVER_PROFILING_PATH = "server-profiling-path"
private val LANGUAGE_SERVER_PROFILING_TIME = "server-profiling-time"
private val LANGUAGE_SERVER_PROFILING_EVENTS_LOG_PATH =
"server-profiling-events-log-path"
private val DAEMONIZE_OPTION = "daemon" private val DAEMONIZE_OPTION = "daemon"
private val INTERFACE_OPTION = "interface" private val INTERFACE_OPTION = "interface"
private val RPC_PORT_OPTION = "rpc-port" private val RPC_PORT_OPTION = "rpc-port"
@ -153,11 +156,26 @@ object Main {
.longOpt(LANGUAGE_SERVER_OPTION) .longOpt(LANGUAGE_SERVER_OPTION)
.desc("Runs Language Server") .desc("Runs Language Server")
.build() .build()
val lsProfilingOption = CliOption.builder val lsProfilingPathOption = CliOption.builder
.longOpt(LANGUAGE_SERVER_PROFILING) .hasArg(true)
.desc( .numberOfArgs(1)
"Enables the Language Server profiling. The output is written to system temp directory." .argName("file")
) .longOpt(LANGUAGE_SERVER_PROFILING_PATH)
.desc("The path to the Language Server profiling file.")
.build()
val lsProfilingTimeOption = CliOption.builder
.hasArg(true)
.numberOfArgs(1)
.argName("seconds")
.longOpt(LANGUAGE_SERVER_PROFILING_TIME)
.desc("The duration in seconds limiting the profiling time.")
.build()
val lsProfilingEventsLogPathOption = CliOption.builder
.hasArg(true)
.numberOfArgs(1)
.argName("file")
.longOpt(LANGUAGE_SERVER_PROFILING_EVENTS_LOG_PATH)
.desc("The path to the runtime events log file.")
.build() .build()
val deamonizeOption = CliOption.builder val deamonizeOption = CliOption.builder
.longOpt(DAEMONIZE_OPTION) .longOpt(DAEMONIZE_OPTION)
@ -335,7 +353,9 @@ object Main {
.addOption(newProjectAuthorNameOpt) .addOption(newProjectAuthorNameOpt)
.addOption(newProjectAuthorEmailOpt) .addOption(newProjectAuthorEmailOpt)
.addOption(lsOption) .addOption(lsOption)
.addOption(lsProfilingOption) .addOption(lsProfilingPathOption)
.addOption(lsProfilingTimeOption)
.addOption(lsProfilingEventsLogPathOption)
.addOption(deamonizeOption) .addOption(deamonizeOption)
.addOption(interfaceOption) .addOption(interfaceOption)
.addOption(rpcPortOption) .addOption(rpcPortOption)
@ -823,14 +843,29 @@ object Main {
dataPort <- Either dataPort <- Either
.catchNonFatal(dataPortStr.toInt) .catchNonFatal(dataPortStr.toInt)
.leftMap(_ => "Port must be integer") .leftMap(_ => "Port must be integer")
profilingEnabled = line.hasOption(LANGUAGE_SERVER_PROFILING) profilingPathStr =
Option(line.getOptionValue(LANGUAGE_SERVER_PROFILING_PATH))
profilingPath <- Either
.catchNonFatal(profilingPathStr.map(Paths.get(_)))
.leftMap(_ => "Profiling path is invalid")
profilingTimeStr = Option(
line.getOptionValue(LANGUAGE_SERVER_PROFILING_TIME)
)
profilingTime <- Either
.catchNonFatal(profilingTimeStr.map(_.toInt.seconds))
.leftMap(_ => "Profiling time should be an integer")
profilingEventsLogPathStr =
Option(line.getOptionValue(LANGUAGE_SERVER_PROFILING_EVENTS_LOG_PATH))
profilingEventsLogPath <- Either
.catchNonFatal(profilingEventsLogPathStr.map(Paths.get(_)))
.leftMap(_ => "Profiling events log path is invalid")
} yield boot.LanguageServerConfig( } yield boot.LanguageServerConfig(
interface, interface,
rpcPort, rpcPort,
dataPort, dataPort,
rootId, rootId,
rootPath, rootPath,
profilingEnabled ProfilingConfig(profilingEventsLogPath, profilingPath, profilingTime)
) )
/** Prints the version of the Enso executable. /** Prints the version of the Enso executable.

View File

@ -1,7 +1,6 @@
package org.enso.jsonrpc package org.enso.jsonrpc
import java.util.UUID import java.util.UUID
import akka.NotUsed import akka.NotUsed
import akka.actor.{ActorRef, ActorSystem, Props} import akka.actor.{ActorRef, ActorSystem, Props}
import akka.http.scaladsl.Http import akka.http.scaladsl.Http
@ -10,6 +9,7 @@ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.Route
import akka.stream.scaladsl.{Flow, Sink, Source} import akka.stream.scaladsl.{Flow, Sink, Source}
import akka.stream.{Materializer, OverflowStrategy} import akka.stream.{Materializer, OverflowStrategy}
import com.typesafe.scalalogging.LazyLogging
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
@ -31,7 +31,7 @@ class JsonRpcServer(
)( )(
implicit val system: ActorSystem, implicit val system: ActorSystem,
implicit val materializer: Materializer implicit val materializer: Materializer
) { ) extends LazyLogging {
implicit val ec: ExecutionContext = system.dispatcher implicit val ec: ExecutionContext = system.dispatcher
@ -55,6 +55,9 @@ class JsonRpcServer(
_.toStrict(config.lazyMessageTimeout) _.toStrict(config.lazyMessageTimeout)
.map(msg => MessageHandler.WebMessage(msg.text)) .map(msg => MessageHandler.WebMessage(msg.text))
) )
.wireTap { webMessage =>
logger.trace(s"Received text message: ${webMessage.message}.")
}
.to( .to(
Sink.actorRef[MessageHandler.WebMessage]( Sink.actorRef[MessageHandler.WebMessage](
messageHandler, messageHandler,
@ -78,6 +81,9 @@ class JsonRpcServer(
NotUsed NotUsed
} }
.map((outMsg: MessageHandler.WebMessage) => TextMessage(outMsg.message)) .map((outMsg: MessageHandler.WebMessage) => TextMessage(outMsg.message))
.wireTap { textMessage =>
logger.trace(s"Sent text message ${textMessage.text}.")
}
Flow.fromSinkAndSource(incomingMessages, outgoingMessages) Flow.fromSinkAndSource(incomingMessages, outgoingMessages)
} }

View File

@ -2,9 +2,7 @@ package org.enso.loggingservice
import org.enso.loggingservice.internal.{InternalLogMessage, LoggerConnection} import org.enso.loggingservice.internal.{InternalLogMessage, LoggerConnection}
import java.io.{File, FileWriter, Writer} import java.util.logging.{Handler, Level, LogRecord}
import java.nio.charset.StandardCharsets
import java.util.logging.{Handler, Level, LogRecord, XMLFormatter}
/** A [[Handler]] implementation that allows to use the logging service as a /** A [[Handler]] implementation that allows to use the logging service as a
* backend for [[java.util.logging]]. * backend for [[java.util.logging]].
@ -27,12 +25,6 @@ class JavaLoggingLogHandler(
exception = Option(record.getThrown) exception = Option(record.getThrown)
) )
connection.send(message) connection.send(message)
val w = JavaLoggingLogHandler.log;
if (w != null) {
val f = new XMLFormatter()
val out = f.format(record)
w.write(out);
}
} }
} }
@ -81,17 +73,4 @@ object JavaLoggingLogHandler {
case LogLevel.Debug => Level.FINE case LogLevel.Debug => Level.FINE
case LogLevel.Trace => Level.ALL case LogLevel.Trace => Level.ALL
} }
var log: Writer = null
def registerLogFile(file: File): Unit = {
if (this.log != null) {
this.log.close()
}
val w = new FileWriter(file, StandardCharsets.UTF_8, false)
w.write(
"<?xml version='1.0' encoding='UTF-8'?><uigestures version='1.0'>\n"
)
this.log = w
}
} }

View File

@ -132,4 +132,16 @@ object LogLevel {
case Debug => akka.event.Logging.DebugLevel case Debug => akka.event.Logging.DebugLevel
case Trace => akka.event.Logging.DebugLevel case Trace => akka.event.Logging.DebugLevel
} }
/** Converts our internal [[LogLevel]] to the corresponding instance of
* Java log level.
*/
def toJava(logLevel: LogLevel): java.util.logging.Level = logLevel match {
case Off => java.util.logging.Level.OFF
case Error => java.util.logging.Level.SEVERE
case Warning => java.util.logging.Level.WARNING
case Info => java.util.logging.Level.INFO
case Debug => java.util.logging.Level.FINER
case Trace => java.util.logging.Level.FINEST
}
} }

View File

@ -1,16 +1,11 @@
package org.enso.loggingservice package org.enso.loggingservice
import java.nio.file.Path
import akka.http.scaladsl.model.Uri import akka.http.scaladsl.model.Uri
import com.typesafe.scalalogging.Logger import com.typesafe.scalalogging.Logger
import org.enso.logger.masking.Masking import org.enso.logger.masking.Masking
import org.enso.loggingservice.printers.{ import org.enso.loggingservice.printers._
FileOutputPrinter,
Printer, import java.nio.file.Path
StderrPrinter,
StderrPrinterWithColors
}
import scala.concurrent.duration.DurationInt import scala.concurrent.duration.DurationInt
import scala.concurrent.{Await, ExecutionContext, Future, Promise} import scala.concurrent.{Await, ExecutionContext, Future, Promise}
@ -54,7 +49,8 @@ abstract class LoggingServiceSetupHelper(implicit
logLevel: Option[LogLevel], logLevel: Option[LogLevel],
connectToExternalLogger: Option[Uri], connectToExternalLogger: Option[Uri],
colorMode: ColorMode, colorMode: ColorMode,
logMasking: Boolean logMasking: Boolean,
profilingLog: Option[Path]
): Unit = { ): Unit = {
val actualLogLevel = logLevel.getOrElse(defaultLogLevel) val actualLogLevel = logLevel.getOrElse(defaultLogLevel)
Masking.setup(logMasking) Masking.setup(logMasking)
@ -62,7 +58,7 @@ abstract class LoggingServiceSetupHelper(implicit
case Some(uri) => case Some(uri) =>
setupLoggingConnection(uri, actualLogLevel) setupLoggingConnection(uri, actualLogLevel)
case None => case None =>
setupLoggingServer(actualLogLevel, colorMode) setupLoggingServer(actualLogLevel, colorMode, profilingLog)
} }
} }
@ -114,7 +110,8 @@ abstract class LoggingServiceSetupHelper(implicit
private def setupLoggingServer( private def setupLoggingServer(
logLevel: LogLevel, logLevel: LogLevel,
colorMode: ColorMode colorMode: ColorMode,
profilingLog: Option[Path]
): Unit = { ): Unit = {
val printExceptionsInStderr = val printExceptionsInStderr =
implicitly[Ordering[LogLevel]].compare(logLevel, LogLevel.Debug) >= 0 implicitly[Ordering[LogLevel]].compare(logLevel, LogLevel.Debug) >= 0
@ -132,10 +129,11 @@ abstract class LoggingServiceSetupHelper(implicit
suffix = logFileSuffix, suffix = logFileSuffix,
printExceptions = true printExceptions = true
) )
val profilingPrinterOpt = profilingLog.map(new FileXmlPrinter(_))
Seq( Seq(
stderrPrinter(colorMode, printExceptionsInStderr), stderrPrinter(colorMode, printExceptionsInStderr),
filePrinter filePrinter
) ) ++ profilingPrinterOpt
} catch { } catch {
case NonFatal(error) => case NonFatal(error) =>
logger.error( logger.error(

View File

@ -70,7 +70,7 @@ object FileOutputPrinter {
def create( def create(
logDirectory: Path, logDirectory: Path,
suffix: String, suffix: String,
printExceptions: Boolean = true printExceptions: Boolean
): FileOutputPrinter = ): FileOutputPrinter =
new FileOutputPrinter(logDirectory, suffix, printExceptions) new FileOutputPrinter(logDirectory, suffix, printExceptions)
} }

View File

@ -0,0 +1,61 @@
package org.enso.loggingservice.printers
import org.enso.loggingservice.LogLevel
import org.enso.loggingservice.internal.protocol.WSLogMessage
import java.io.PrintWriter
import java.nio.file.{Files, Path, StandardOpenOption}
import java.util.logging.{LogRecord, XMLFormatter}
/** Creates a new file in [[logPath]] and writes incoming log messages to
* this file in XML format.
*
* @param logPath the file path to log
*/
class FileXmlPrinter(logPath: Path) extends Printer {
private val writer = initializeWriter()
private val formatter = new XMLFormatter()
/** @inheritdoc */
override def print(message: WSLogMessage): Unit = {
val lines = formatter.format(toLogRecord(message))
writer.print(lines)
}
/** @inheritdoc */
override def shutdown(): Unit = {
writer.flush()
writer.close()
}
/** Opens the log file for writing. */
private def initializeWriter(): PrintWriter = {
Option(logPath.getParent).foreach(Files.createDirectories(_))
val writer = new PrintWriter(
Files.newBufferedWriter(
logPath,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.WRITE
)
)
writer.println(FileXmlPrinter.Header)
writer
}
/** Converts [[WSLogMessage]] to java [[LogRecord]]. */
private def toLogRecord(wsLogMessage: WSLogMessage): LogRecord = {
val record =
new LogRecord(LogLevel.toJava(wsLogMessage.level), wsLogMessage.message)
record.setInstant(wsLogMessage.timestamp)
record.setLoggerName(wsLogMessage.group)
record
}
}
object FileXmlPrinter {
private val Header: String =
"<?xml version='1.0' encoding='UTF-8'?><uigestures version='1.0'>"
}

View File

@ -3,10 +3,9 @@ package org.enso.profiling
import org.netbeans.modules.sampler.Sampler import org.netbeans.modules.sampler.Sampler
import java.io.{DataOutputStream, File, FileOutputStream} import java.io.{DataOutputStream, File, FileOutputStream}
import java.nio.file.Files
import java.util.concurrent.{CompletableFuture, Executor, Executors} import java.util.concurrent.{CompletableFuture, Executor, Executors}
import scala.concurrent.duration.Duration import scala.concurrent.duration.FiniteDuration
import scala.util.Using import scala.util.Using
/** Gathers application performance statistics that can be visualised in Java /** Gathers application performance statistics that can be visualised in Java
@ -14,16 +13,11 @@ import scala.util.Using
* *
* @param output the output stream to write the collected statistics to * @param output the output stream to write the collected statistics to
*/ */
final class TempFileSampler(output: File) extends MethodsSampler { final class FileSampler(output: File) extends MethodsSampler {
private val sampler: Sampler = Sampler.createSampler(getClass.getSimpleName) private val sampler: Sampler = Sampler.createSampler(getClass.getSimpleName)
private var samplingStarted: Boolean = false private var samplingStarted: Boolean = false
def getSiblingFile(ext: String): File = {
val newName = output.getName.replace(".npss", ext)
new File(output.getParent, newName)
}
/** @inheritdoc */ /** @inheritdoc */
def start(): Unit = def start(): Unit =
this.synchronized { this.synchronized {
@ -45,7 +39,7 @@ final class TempFileSampler(output: File) extends MethodsSampler {
} }
/** @inheritdoc */ /** @inheritdoc */
def stop(delay: Duration)(implicit ec: Executor): Unit = def stop(delay: FiniteDuration)(implicit ec: Executor): Unit =
this.synchronized { this.synchronized {
val executor = Executors.newSingleThreadScheduledExecutor() val executor = Executors.newSingleThreadScheduledExecutor()
@ -58,22 +52,4 @@ final class TempFileSampler(output: File) extends MethodsSampler {
) )
.whenComplete((_, _) => executor.shutdown()) .whenComplete((_, _) => executor.shutdown())
} }
/** @return `true` if the sampling is started. */
def isSamplingStarted: Boolean =
this.samplingStarted
}
object TempFileSampler {
/** Create an instance of [[MethodsSampler]] that writes the data to the
* temporary `.npss` file with the provided prefix.
*
* @param prefix the prefix of the temp file.
* @return the [[MethodsSampler]] instance
*/
def apply(prefix: String): TempFileSampler = {
val path = Files.createTempFile(s"$prefix-", ".npss")
Files.deleteIfExists(path)
new TempFileSampler(path.toFile)
}
} }

View File

@ -2,7 +2,7 @@ package org.enso.profiling
import java.util.concurrent.Executor import java.util.concurrent.Executor
import scala.concurrent.duration.Duration import scala.concurrent.duration.FiniteDuration
/** Sampler gathers the application performance statistics. */ /** Sampler gathers the application performance statistics. */
trait MethodsSampler { trait MethodsSampler {
@ -19,5 +19,5 @@ trait MethodsSampler {
* @param delay the duration to wait before stopping * @param delay the duration to wait before stopping
* @param ec the execution context * @param ec the execution context
*/ */
def stop(delay: Duration)(implicit ec: Executor): Unit def stop(delay: FiniteDuration)(implicit ec: Executor): Unit
} }

View File

@ -2,7 +2,7 @@ package org.enso.profiling
import java.util.concurrent.Executor import java.util.concurrent.Executor
import scala.concurrent.duration.Duration import scala.concurrent.duration.FiniteDuration
/** Sampler that does nothing. */ /** Sampler that does nothing. */
final class NoopSampler extends MethodsSampler { final class NoopSampler extends MethodsSampler {
@ -14,7 +14,7 @@ final class NoopSampler extends MethodsSampler {
override def stop(): Unit = () override def stop(): Unit = ()
/** @inheritdoc */ /** @inheritdoc */
override def stop(delay: Duration)(implicit ec: Executor): Unit = () override def stop(delay: FiniteDuration)(implicit ec: Executor): Unit = ()
} }
object NoopSampler { object NoopSampler {

View File

@ -7,12 +7,14 @@ import scala.util.Try
object Cli { object Cli {
val JSON_OPTION = "json" val JSON_OPTION = "json"
val HELP_OPTION = "help" val HELP_OPTION = "help"
val NO_LOG_MASKING = "no-log-masking" val NO_LOG_MASKING = "no-log-masking"
val VERBOSE_OPTION = "verbose" val VERBOSE_OPTION = "verbose"
val VERSION_OPTION = "version" val VERSION_OPTION = "version"
val ENABLE_PROFILING = "profiling" val PROFILING_PATH = "profiling-path"
val PROFILING_TIME = "profiling-time"
val PROFILING_EVENTS_LOG_PATH = "profiling-events-log-path"
object option { object option {
@ -47,9 +49,30 @@ object Cli {
) )
.build() .build()
val enableProfiling: cli.Option = cli.Option.builder val profilingPath: cli.Option = cli.Option.builder
.longOpt(ENABLE_PROFILING) .hasArg(true)
.desc("Enables the application profiling.") .numberOfArgs(1)
.argName("file")
.longOpt(PROFILING_PATH)
.desc("The path to profiling file. Enables the application profiling.")
.build()
val profilingTime: cli.Option = cli.Option.builder
.hasArg(true)
.numberOfArgs(1)
.argName("seconds")
.longOpt(PROFILING_TIME)
.desc("The duration in seconds limiting the application profiling time.")
.build()
val profilingEventsLogPath: cli.Option = cli.Option.builder
.hasArg(true)
.numberOfArgs(1)
.argName("file")
.longOpt(PROFILING_EVENTS_LOG_PATH)
.desc(
"The path to the runtime events log file. Enables the runtime events logging."
)
.build() .build()
} }
@ -60,7 +83,9 @@ object Cli {
.addOption(option.version) .addOption(option.version)
.addOption(option.json) .addOption(option.json)
.addOption(option.noLogMasking) .addOption(option.noLogMasking)
.addOption(option.enableProfiling) .addOption(option.profilingPath)
.addOption(option.profilingTime)
.addOption(option.profilingEventsLogPath)
/** Parse the command line options. */ /** Parse the command line options. */
def parse(args: Array[String]): Either[String, cli.CommandLine] = { def parse(args: Array[String]): Either[String, cli.CommandLine] = {

View File

@ -23,6 +23,7 @@ import zio.console._
import zio.interop.catz.core._ import zio.interop.catz.core._
import java.io.IOException import java.io.IOException
import java.nio.file.{FileAlreadyExistsException, Files, Path, Paths}
import java.util.concurrent.ScheduledThreadPoolExecutor import java.util.concurrent.ScheduledThreadPoolExecutor
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -120,23 +121,99 @@ object ProjectManager extends App with LazyLogging {
} }
} }
/** Parses and validates the command line arguments.
*
* @param options the command line arguments
*/
def parseOpts(
options: CommandLine
): ZIO[ZEnv, Throwable, ProjectManagerOptions] = {
val parseProfilingPath = ZIO
.effect {
Option(options.getOptionValue(Cli.PROFILING_PATH))
.map(Paths.get(_).toAbsolutePath)
}
.flatMap {
case pathOpt @ Some(path) =>
ZIO.ifM(ZIO.effect(Files.isDirectory(path)))(
onTrue = putStrLnErr(
s"Error: ${Cli.PROFILING_PATH} is a directory: $path"
) *>
ZIO.fail(new FileAlreadyExistsException(path.toString)),
onFalse = ZIO.succeed(pathOpt)
)
case None =>
ZIO.succeed(None)
}
.catchAll { err =>
putStrLnErr(s"Invalid ${Cli.PROFILING_PATH} argument.") *> ZIO.fail(err)
}
val parseProfilingTime = ZIO
.effect {
Option(options.getOptionValue(Cli.PROFILING_TIME))
.map(_.toInt.seconds)
}
.catchAll { err =>
putStrLnErr(s"Invalid ${Cli.PROFILING_TIME} argument.") *> ZIO.fail(err)
}
val parseProfilingEventsLogPath = ZIO
.effect {
Option(options.getOptionValue(Cli.PROFILING_EVENTS_LOG_PATH))
.map(Paths.get(_).toAbsolutePath)
}
.flatMap {
case pathOpt @ Some(path) =>
ZIO.ifM(ZIO.effect(Files.isDirectory(path)))(
onTrue = putStrLnErr(
s"Error: ${Cli.PROFILING_EVENTS_LOG_PATH} is a directory: $path"
) *>
ZIO.fail(new FileAlreadyExistsException(path.toString)),
onFalse = ZIO.succeed(pathOpt)
)
case None =>
ZIO.succeed(None)
}
.catchAll { err =>
putStrLnErr(s"Invalid ${Cli.PROFILING_EVENTS_LOG_PATH} argument.") *>
ZIO.fail(err)
}
for {
profilingEventsLogPath <- parseProfilingEventsLogPath
profilingPath <- parseProfilingPath
profilingTime <- parseProfilingTime
} yield ProjectManagerOptions(
profilingEventsLogPath,
profilingPath,
profilingTime
)
}
/** The main function of the application, which will be passed the command-line /** The main function of the application, which will be passed the command-line
* arguments to the program and has to return an `IO` with the errors fully handled. * arguments to the program and has to return an `IO` with the errors fully handled.
*/ */
def runOpts(options: CommandLine): ZIO[ZEnv, IOException, ExitCode] = { def runOpts(options: CommandLine): ZIO[ZEnv, Throwable, ExitCode] = {
if (options.hasOption(Cli.HELP_OPTION)) { if (options.hasOption(Cli.HELP_OPTION)) {
ZIO.effectTotal(Cli.printHelp()) *> ZIO.effectTotal(Cli.printHelp()) *>
ZIO.succeed(SuccessExitCode) ZIO.succeed(SuccessExitCode)
} else if (options.hasOption(Cli.VERSION_OPTION)) { } else if (options.hasOption(Cli.VERSION_OPTION)) {
displayVersion(options.hasOption(Cli.JSON_OPTION)) displayVersion(options.hasOption(Cli.JSON_OPTION))
} else { } else {
val verbosity = options.getOptions.count(_ == Cli.option.verbose) val verbosity = options.getOptions.count(_ == Cli.option.verbose)
val logMasking = !options.hasOption(Cli.NO_LOG_MASKING) val logMasking = !options.hasOption(Cli.NO_LOG_MASKING)
val profilingEnabled = options.hasOption(Cli.ENABLE_PROFILING)
logger.info("Starting Project Manager...") logger.info("Starting Project Manager...")
for { for {
logLevel <- setupLogging(verbosity, logMasking) opts <- parseOpts(options)
procConf = MainProcessConfig(logLevel, profilingEnabled) profilingLog = opts.profilingPath.map(getSiblingFile(_, ".log"))
logLevel <- setupLogging(verbosity, logMasking, profilingLog)
procConf = MainProcessConfig(
logLevel,
opts.profilingRuntimeEventsLog,
opts.profilingPath,
opts.profilingTime
)
exitCode <- mainProcess(procConf).fold( exitCode <- mainProcess(procConf).fold(
th => { th => {
logger.error("Main process execution failed.", th) logger.error("Main process execution failed.", th)
@ -150,7 +227,8 @@ object ProjectManager extends App with LazyLogging {
private def setupLogging( private def setupLogging(
verbosityLevel: Int, verbosityLevel: Int,
logMasking: Boolean logMasking: Boolean,
profilingLog: Option[Path]
): ZIO[Console, IOException, LogLevel] = { ): ZIO[Console, IOException, LogLevel] = {
val level = verbosityLevel match { val level = verbosityLevel match {
case 0 => LogLevel.Info case 0 => LogLevel.Info
@ -164,7 +242,7 @@ object ProjectManager extends App with LazyLogging {
ZIO ZIO
.effect { .effect {
Logging.setup(Some(level), None, colorMode, logMasking) Logging.setup(Some(level), None, colorMode, logMasking, profilingLog)
} }
.catchAll { exception => .catchAll { exception =>
putStrLnErr(s"Failed to setup the logger: $exception") putStrLnErr(s"Failed to setup the logger: $exception")
@ -203,4 +281,12 @@ object ProjectManager extends App with LazyLogging {
) )
} }
private def getSiblingFile(file: Path, ext: String): Path = {
val fileName = file.getFileName.toString
val extensionIndex = fileName.lastIndexOf(".")
val newName =
if (extensionIndex > 0) fileName.substring(0, extensionIndex) + ext
else fileName + ext
file.getParent.resolve(newName)
}
} }

View File

@ -0,0 +1,17 @@
package org.enso.projectmanager.boot
import java.nio.file.Path
import scala.concurrent.duration.FiniteDuration
/** The runtime options.
*
* @param profilingRuntimeEventsLog the path to the runtime events log file
* @param profilingPath the path to the profiling output file
* @param profilingTime the time limiting the profiling duration
*/
case class ProjectManagerOptions(
profilingRuntimeEventsLog: Option[Path],
profilingPath: Option[Path],
profilingTime: Option[FiniteDuration]
)

View File

@ -3,6 +3,7 @@ package org.enso.projectmanager.boot
import org.enso.loggingservice.LogLevel import org.enso.loggingservice.LogLevel
import java.io.File import java.io.File
import java.nio.file.Path
import scala.concurrent.duration.FiniteDuration import scala.concurrent.duration.FiniteDuration
@ -12,11 +13,15 @@ object configuration {
* main project manager process. * main project manager process.
* *
* @param logLevel the logging level * @param logLevel the logging level
* @param profilingEnabled if the profiling is enabled * @param profilingEventsLogPath the path to the runtime events log file
* @param profilingPath the path to the profiling out file
* @param profilingTime the time limiting the profiling duration
*/ */
case class MainProcessConfig( case class MainProcessConfig(
logLevel: LogLevel, logLevel: LogLevel,
profilingEnabled: Boolean profilingEventsLogPath: Option[Path],
profilingPath: Option[Path],
profilingTime: Option[FiniteDuration]
) )
/** A configuration object for properties of the Project Manager. /** A configuration object for properties of the Project Manager.

View File

@ -95,8 +95,23 @@ object ExecutorWithUnlimitedPool extends LanguageServerExecutor {
environment = distributionConfiguration.environment, environment = distributionConfiguration.environment,
loggerConnection = descriptor.deferredLoggingServiceEndpoint loggerConnection = descriptor.deferredLoggingServiceEndpoint
) )
val profilingPathArguments =
descriptor.profilingPath.toSeq
.flatMap(path => Seq("--server-profiling-path", path.toString))
val profilingTimeArguments =
descriptor.profilingTime.toSeq
.flatMap(time =>
Seq("--server-profiling-time", time.toSeconds.toString)
)
val profilingEventsLogPathArguments =
descriptor.profilingEventsLogPath.toSeq
.flatMap(path =>
Seq("--server-profiling-events-log-path", path.toString)
)
val additionalArguments = val additionalArguments =
Option.when(descriptor.profilingEnabled)("--server-profiling").toSeq profilingPathArguments ++
profilingTimeArguments ++
profilingEventsLogPathArguments
val runSettings = runner val runSettings = runner
.startLanguageServer( .startLanguageServer(
options = options, options = options,

View File

@ -79,7 +79,9 @@ class LanguageServerController(
engineVersion = engineVersion, engineVersion = engineVersion,
jvmSettings = distributionConfiguration.defaultJVMSettings, jvmSettings = distributionConfiguration.defaultJVMSettings,
discardOutput = distributionConfiguration.shouldDiscardChildOutput, discardOutput = distributionConfiguration.shouldDiscardChildOutput,
profilingEnabled = processConfig.profilingEnabled, profilingEventsLogPath = processConfig.profilingEventsLogPath,
profilingPath = processConfig.profilingPath,
profilingTime = processConfig.profilingTime,
deferredLoggingServiceEndpoint = loggingServiceDescriptor.getEndpoint deferredLoggingServiceEndpoint = loggingServiceDescriptor.getEndpoint
) )

View File

@ -1,14 +1,16 @@
package org.enso.projectmanager.infrastructure.languageserver package org.enso.projectmanager.infrastructure.languageserver
import java.util.UUID
import akka.http.scaladsl.model.Uri import akka.http.scaladsl.model.Uri
import nl.gn0s1s.bump.SemVer import nl.gn0s1s.bump.SemVer
import org.enso.projectmanager.boot.configuration.NetworkConfig import org.enso.projectmanager.boot.configuration.NetworkConfig
import org.enso.projectmanager.versionmanagement.DistributionConfiguration import org.enso.projectmanager.versionmanagement.DistributionConfiguration
import org.enso.runtimeversionmanager.runner.JVMSettings import org.enso.runtimeversionmanager.runner.JVMSettings
import java.nio.file.Path
import java.util.UUID
import scala.concurrent.Future import scala.concurrent.Future
import scala.concurrent.duration.FiniteDuration
/** A descriptor specifying options related to starting a Language Server. /** A descriptor specifying options related to starting a Language Server.
* *
@ -23,7 +25,9 @@ import scala.concurrent.Future
* @param jvmSettings settings to use for the JVM that will host the engine * @param jvmSettings settings to use for the JVM that will host the engine
* @param discardOutput specifies if the process output should be discarded or * @param discardOutput specifies if the process output should be discarded or
* printed to parent's streams * printed to parent's streams
* @param profilingEnabled enables the language server profiling * @param profilingEventsLogPath the path to the runtime events log file
* @param profilingPath the language server profiling file path
* @param profilingTime the time limiting the profiling duration
* @param deferredLoggingServiceEndpoint a future that is completed once the * @param deferredLoggingServiceEndpoint a future that is completed once the
* logging service has been fully set-up; * logging service has been fully set-up;
* if the child component should connect * if the child component should connect
@ -39,6 +43,8 @@ case class LanguageServerDescriptor(
engineVersion: SemVer, engineVersion: SemVer,
jvmSettings: JVMSettings, jvmSettings: JVMSettings,
discardOutput: Boolean, discardOutput: Boolean,
profilingEnabled: Boolean, profilingEventsLogPath: Option[Path],
profilingPath: Option[Path],
profilingTime: Option[FiniteDuration],
deferredLoggingServiceEndpoint: Future[Option[Uri]] deferredLoggingServiceEndpoint: Future[Option[Uri]]
) )

View File

@ -71,7 +71,7 @@ class BaseServerSpec extends JsonRpcServerTestKit with BeforeAndAfterAll {
val debugLogs: Boolean = false val debugLogs: Boolean = false
/** Enables the application profiling. */ /** Enables the application profiling. */
val profilingEnabled: Boolean = false val profilingPath: Option[Path] = None
/** Tests can override this to allow child process output to be displayed. */ /** Tests can override this to allow child process output to be displayed. */
val debugChildLogs: Boolean = false val debugChildLogs: Boolean = false
@ -87,8 +87,10 @@ class BaseServerSpec extends JsonRpcServerTestKit with BeforeAndAfterAll {
val processConfig: MainProcessConfig = val processConfig: MainProcessConfig =
MainProcessConfig( MainProcessConfig(
logLevel = if (debugLogs) LogLevel.Trace else LogLevel.Off, logLevel = if (debugLogs) LogLevel.Trace else LogLevel.Off,
profilingEnabled = profilingEnabled profilingPath = profilingPath,
profilingTime = None,
profilingEventsLogPath = None
) )
val testClock = val testClock =