On-deman backend heap and thread dump (#8320)

close #8249

Changelog:
- add: `profiling/snapshot` request that takes a heap dump of the language server and puts it in the `ENSO_DATA_DIRECTORY/profiling` direcotry
This commit is contained in:
Dmitry Bushev 2023-11-20 11:41:01 +00:00 committed by GitHub
parent b8ebed69c3
commit b224f95639
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 284 additions and 5 deletions

View File

@ -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
)
)

View File

@ -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
}
}
}

View File

@ -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
)
)
}

View File

@ -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
}

View File

@ -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
)
)
}

View File

@ -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)

View File

@ -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))
}

View File

@ -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)
}
}

View File

@ -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
)
)

View File

@ -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
}"""
}

View File

@ -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
}
}
}

View File

@ -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 <i>live</i> 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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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);
}
}
}