diff --git a/build.sbt b/build.sbt index 2d27eb7caec..3e9b05c7a97 100644 --- a/build.sbt +++ b/build.sbt @@ -708,7 +708,9 @@ lazy val `profiling-utils` = project exclude ("org.netbeans.api", "org-openide-util-ui") exclude ("org.netbeans.api", "org-openide-awt") exclude ("org.netbeans.api", "org-openide-modules") - exclude ("org.netbeans.api", "org-netbeans-api-annotations-common") + exclude ("org.netbeans.api", "org-netbeans-api-annotations-common"), + "junit" % "junit" % junitVersion % Test, + "com.github.sbt" % "junit-interface" % junitIfVersion % Test ) ) 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 index ed584b6323a..a216cd522a5 100644 --- 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 @@ -28,4 +28,15 @@ object ProfilingApi { } } + case object ProfilingSnapshot extends Method("profiling/snapshot") { + + 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 index 0384643e2de..b9d9779dae9 100644 --- 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 @@ -5,8 +5,10 @@ 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.logger.masking.MaskedPath import org.enso.profiling.events.NoopEventsMonitor import org.enso.profiling.sampler.{MethodsSampler, OutputStreamSampler} +import org.enso.profiling.snapshot.{HeapDumpSnapshot, ProfilingSnapshot} import java.io.{ByteArrayOutputStream, PrintStream} import java.nio.charset.StandardCharsets @@ -21,11 +23,13 @@ import scala.util.{Failure, Success, Try} * * @param runtimeConnector the connection to runtime * @param distributionManager the distribution manager + * @param profilingSnapshot the profiling snapshot generator * @param clock the system clock */ final class ProfilingManager( runtimeConnector: ActorRef, distributionManager: DistributionManager, + profilingSnapshot: ProfilingSnapshot, clock: Clock ) extends Actor with LazyLogging { @@ -71,7 +75,10 @@ final class ProfilingManager( 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) + logger.trace( + "Saved the sampler's result to [{}].", + MaskedPath(samplesPath) + ) } runtimeConnector ! RuntimeConnector.RegisterEventsMonitor( @@ -83,6 +90,21 @@ final class ProfilingManager( case None => sender() ! ProfilingProtocol.ProfilingStopResponse } + + case ProfilingProtocol.ProfilingSnapshotRequest => + val instant = clock.instant() + + Try(saveHeapDump(instant)) match { + case Failure(exception) => + logger.error("Failed to save the memory snapshot.", exception) + case Success(heapDumpPath) => + logger.trace( + "Saved the memory snapshot to [{}].", + MaskedPath(heapDumpPath) + ) + } + + sender() ! ProfilingProtocol.ProfilingSnapshotResponse } private def saveSamplerResult( @@ -98,6 +120,16 @@ final class ProfilingManager( samplesPath } + private def saveHeapDump(instant: Instant): Path = { + val heapDumpFileName = createHeapDumpFileName(instant) + val heapDumpPath = + distributionManager.paths.profiling.resolve(heapDumpFileName) + + profilingSnapshot.generateSnapshot(heapDumpPath) + + heapDumpPath + } + private def createEventsMonitor(instant: Instant): RuntimeEventsMonitor = { val eventsLogFileName = createEventsFileName(instant) val eventsLogPath = @@ -112,6 +144,7 @@ object ProfilingManager { private val PROFILING_FILE_PREFIX = "enso-language-server" private val SAMPLES_FILE_EXT = ".npss" private val EVENTS_FILE_EXT = ".log" + private val HEAP_DUMP_FILE_EXT = ".hprof" private val PROFILING_FILE_DATE_PART_FORMATTER = new DateTimeFormatterBuilder() @@ -148,16 +181,30 @@ object ProfilingManager { s"$baseName$EVENTS_FILE_EXT" } + def createHeapDumpFileName(instant: Instant): String = { + val baseName = createProfilingFileName(instant) + s"$baseName$HEAP_DUMP_FILE_EXT" + } + /** Creates the configuration object used to create a [[ProfilingManager]]. * * @param runtimeConnector the connection to runtime * @param distributionManager the distribution manager + * @param profilingSnapshot the profiling snapshot generator * @param clock the system clock */ def props( runtimeConnector: ActorRef, distributionManager: DistributionManager, - clock: Clock = Clock.systemUTC() + profilingSnapshot: ProfilingSnapshot = new HeapDumpSnapshot(), + clock: Clock = Clock.systemUTC() ): Props = - Props(new ProfilingManager(runtimeConnector, distributionManager, clock)) + Props( + new ProfilingManager( + runtimeConnector, + distributionManager, + profilingSnapshot, + 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 index 040f7deee32..956e13dae0b 100644 --- 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 @@ -14,4 +14,10 @@ object ProfilingProtocol { /** A response to request to stop the profiling. */ case object ProfilingStopResponse + /** A request to create a memory snapshot. */ + case object ProfilingSnapshotRequest + + /** A response to request to create a memory snapshot. */ + case object ProfilingSnapshotResponse + } 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 d2c5ce6e44f..edaf80729b8 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 @@ -36,6 +36,7 @@ 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.{ + ProfilingSnapshot, ProfilingStart, ProfilingStop } @@ -52,6 +53,7 @@ import org.enso.languageserver.requesthandler.monitoring.{ PingHandler } import org.enso.languageserver.requesthandler.profiling.{ + ProfilingSnapshotHandler, ProfilingStartHandler, ProfilingStopHandler } @@ -645,6 +647,10 @@ class JsonConnectionController( ProfilingStop -> ProfilingStopHandler.props( requestTimeout, profilingManager + ), + ProfilingSnapshot -> ProfilingSnapshotHandler.props( + requestTimeout, + profilingManager ) ) } 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 ff62b66d390..3040aadef43 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 @@ -25,6 +25,7 @@ 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.{ + ProfilingSnapshot, ProfilingStart, ProfilingStop } @@ -108,6 +109,7 @@ object JsonRpc { .registerRequest(RuntimeGetComponentGroups) .registerRequest(ProfilingStart) .registerRequest(ProfilingStop) + .registerRequest(ProfilingSnapshot) .registerNotification(TaskStarted) .registerNotification(TaskProgressUpdate) .registerNotification(TaskFinished) diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/profiling/ProfilingSnapshotHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/profiling/ProfilingSnapshotHandler.scala new file mode 100644 index 00000000000..34c09e01cad --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/profiling/ProfilingSnapshotHandler.scala @@ -0,0 +1,70 @@ +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/snapshot` commands. + * + * @param timeout a request timeout + * @param profilingManager a reference to the profiling manager + */ +class ProfilingSnapshotHandler( + 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.ProfilingSnapshot, id, _) => + profilingManager ! ProfilingProtocol.ProfilingSnapshotRequest + 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.ProfilingSnapshotResponse => + replyTo ! ResponseResult(ProfilingApi.ProfilingSnapshot, id, Unused) + cancellable.cancel() + context.stop(self) + } + +} + +object ProfilingSnapshotHandler { + + /** Creates configuration object used to create a [[ProfilingSnapshotHandler]]. + * + * @param timeout request timeout + * @param profilingManager reference to the profiling manager + */ + def props(timeout: FiniteDuration, profilingManager: ActorRef): Props = + Props(new ProfilingSnapshotHandler(timeout, profilingManager)) + +} diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/profiling/TestProfilingSnapshot.scala b/engine/language-server/src/test/scala/org/enso/languageserver/profiling/TestProfilingSnapshot.scala new file mode 100644 index 00000000000..0b55b0ed8d1 --- /dev/null +++ b/engine/language-server/src/test/scala/org/enso/languageserver/profiling/TestProfilingSnapshot.scala @@ -0,0 +1,16 @@ +package org.enso.languageserver.profiling + +import org.enso.profiling.snapshot.ProfilingSnapshot + +import java.nio.file.{Files, Path} + +/** Generating a memory dump can take significant time. This test profiling snapshot + * just creates an empty file in the specified location. + */ +final class TestProfilingSnapshot extends ProfilingSnapshot { + + /** @inheritdoc */ + override def generateSnapshot(output: Path): Unit = { + Files.write(output, Array.emptyByteArray) + } +} 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 ab3ca28f067..471e39417e3 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,7 +32,10 @@ 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.profiling.{ + ProfilingManager, + TestProfilingSnapshot +} import org.enso.languageserver.protocol.json.{ JsonConnectionControllerFactory, JsonRpcProtocolFactory @@ -87,6 +90,7 @@ class BaseServerTest val runtimeConnectorProbe = TestProbe() val versionCalculator = Sha3_224VersionCalculator val clock = TestClock() + val profilingSnapshot = new TestProfilingSnapshot val typeGraph: TypeGraph = { val graph = TypeGraph("Any") @@ -351,6 +355,7 @@ class BaseServerTest ProfilingManager.props( runtimeConnectorProbe.ref, distributionManager, + profilingSnapshot, clock ) ) 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 index 7293b6839c5..7175c1a8034 100644 --- 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 @@ -10,6 +10,7 @@ object ProfilingJsonMessages { "id": $reqId, "result": null }""" + def profilingStart(reqId: Int) = json""" { "jsonrpc": "2.0", @@ -26,4 +27,12 @@ object ProfilingJsonMessages { "params": null }""" + def profilingSnapshot(reqId: Int) = + json""" + { "jsonrpc": "2.0", + "method": "profiling/snapshot", + "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 index 42925d2f5c8..eccabecfea2 100644 --- 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 @@ -48,6 +48,21 @@ class ProfilingManagerTest extends BaseServerTest { Files.exists(samplesFile) shouldEqual true Files.exists(eventsFile) shouldEqual true } + + "save memory snapshot" in { + val client = getInitialisedWsClient() + + client.send(json.profilingSnapshot(1)) + client.expectJson(json.ok(1)) + + val distributionManager = getDistributionManager + val instant = clock.instant + val snapshotFile = distributionManager.paths.profiling.resolve( + ProfilingManager.createHeapDumpFileName(instant) + ) + + Files.exists(snapshotFile) shouldEqual true + } } } diff --git a/lib/scala/profiling-utils/src/main/java/org/enso/profiling/snapshot/HeapDumpGenerator.java b/lib/scala/profiling-utils/src/main/java/org/enso/profiling/snapshot/HeapDumpGenerator.java new file mode 100644 index 00000000000..dc13f9640b3 --- /dev/null +++ b/lib/scala/profiling-utils/src/main/java/org/enso/profiling/snapshot/HeapDumpGenerator.java @@ -0,0 +1,38 @@ +package org.enso.profiling.snapshot; + +import com.sun.management.HotSpotDiagnosticMXBean; +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.nio.file.Path; +import javax.management.MBeanServer; + +final class HeapDumpGenerator { + + private static final String HotSpotBeanName = "com.sun.management:type=HotSpotDiagnostic"; + private static volatile HotSpotDiagnosticMXBean hotSpotDiagnosticMXBean; + + private HeapDumpGenerator() {} + + /** + * Store the heap dump in the output file in the same format as the hprof heap dump. + * + * @param output the output file. + * @param dumpOnlyLiveObjects if {@code true} dump only live objects i.e. objects that are + * reachable from others. + */ + public static void generateHeapDump(Path output, boolean dumpOnlyLiveObjects) throws IOException { + if (hotSpotDiagnosticMXBean == null) { + synchronized (HeapDumpGenerator.class) { + hotSpotDiagnosticMXBean = getHotSpotDiagnosticMXBean(); + } + } + + hotSpotDiagnosticMXBean.dumpHeap(output.toString(), dumpOnlyLiveObjects); + } + + private static HotSpotDiagnosticMXBean getHotSpotDiagnosticMXBean() throws IOException { + MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); + return ManagementFactory.newPlatformMXBeanProxy( + mBeanServer, HotSpotBeanName, HotSpotDiagnosticMXBean.class); + } +} diff --git a/lib/scala/profiling-utils/src/main/java/org/enso/profiling/snapshot/HeapDumpSnapshot.java b/lib/scala/profiling-utils/src/main/java/org/enso/profiling/snapshot/HeapDumpSnapshot.java new file mode 100644 index 00000000000..51d0f2b0c10 --- /dev/null +++ b/lib/scala/profiling-utils/src/main/java/org/enso/profiling/snapshot/HeapDumpSnapshot.java @@ -0,0 +1,13 @@ +package org.enso.profiling.snapshot; + +import java.io.IOException; +import java.nio.file.Path; + +/** Generate the heap dump of the current Java process. */ +public final class HeapDumpSnapshot implements ProfilingSnapshot { + + @Override + public void generateSnapshot(Path output) throws IOException { + HeapDumpGenerator.generateHeapDump(output, false); + } +} diff --git a/lib/scala/profiling-utils/src/main/java/org/enso/profiling/snapshot/ProfilingSnapshot.java b/lib/scala/profiling-utils/src/main/java/org/enso/profiling/snapshot/ProfilingSnapshot.java new file mode 100644 index 00000000000..14e01111b8b --- /dev/null +++ b/lib/scala/profiling-utils/src/main/java/org/enso/profiling/snapshot/ProfilingSnapshot.java @@ -0,0 +1,14 @@ +package org.enso.profiling.snapshot; + +import java.io.IOException; +import java.nio.file.Path; + +public interface ProfilingSnapshot { + + /** + * Generate the profiling snapshot in the provided location. + * + * @param output the output file. + */ + void generateSnapshot(Path output) throws IOException; +} diff --git a/lib/scala/profiling-utils/src/test/java/org/enso/profiling/snapshot/HeapDumpGeneratorTest.java b/lib/scala/profiling-utils/src/test/java/org/enso/profiling/snapshot/HeapDumpGeneratorTest.java new file mode 100644 index 00000000000..99dab30caa8 --- /dev/null +++ b/lib/scala/profiling-utils/src/test/java/org/enso/profiling/snapshot/HeapDumpGeneratorTest.java @@ -0,0 +1,25 @@ +package org.enso.profiling.snapshot; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.Assert; +import org.junit.Test; + +public class HeapDumpGeneratorTest { + + @Test + public void generateHeapDump() throws IOException { + String heapDumpFileName = + getClass().getSimpleName() + "-" + System.currentTimeMillis() + ".hprof"; + Path heapDumpFile = Paths.get("").toRealPath().resolve(heapDumpFileName); + + try { + HeapDumpGenerator.generateHeapDump(heapDumpFile, true); + Assert.assertTrue(Files.exists(heapDumpFile)); + } finally { + Files.deleteIfExists(heapDumpFile); + } + } +}