From 4b3ba78b52146c0820429fb97e8cc5be84966300 Mon Sep 17 00:00:00 2001 From: Dmitry Bushev Date: Thu, 23 Nov 2023 15:31:17 +0000 Subject: [PATCH] Add shortcuts to start and stop the backend profiling (#8358) close #8329 Changelog: - add: `cmd`+`shift`+`,` and `cmd`+`shift`+`.` shortcuts to start and stop the backend profiling. Profiling data is stored on disk. --- .../engine-protocol/src/language_server.rs | 8 ++++ app/gui/docs/product/shortcuts.md | 2 + app/gui/src/controller/graph/executed.rs | 12 ++++++ app/gui/src/model/execution_context.rs | 8 ++++ app/gui/src/model/execution_context/plain.rs | 8 ++++ .../model/execution_context/synchronized.rs | 17 +++++++++ app/gui/src/presenter/project.rs | 27 +++++++++++++ app/gui/view/src/project.rs | 6 +++ .../protocol-language-server.md | 5 ++- .../profiling/ProfilingApi.scala | 6 ++- .../profiling/ProfilingManager.scala | 37 +++++++++++------- .../profiling/ProfilingProtocol.scala | 7 +++- .../profiling/ProfilingStartHandler.scala | 11 ++++-- .../json/ProfilingJsonMessages.scala | 6 ++- .../websocket/json/ProfilingManagerTest.scala | 38 +++++++++++++++++++ 15 files changed, 173 insertions(+), 25 deletions(-) diff --git a/app/gui/controller/engine-protocol/src/language_server.rs b/app/gui/controller/engine-protocol/src/language_server.rs index ea026dbed7..6ebe340387 100644 --- a/app/gui/controller/engine-protocol/src/language_server.rs +++ b/app/gui/controller/engine-protocol/src/language_server.rs @@ -159,6 +159,14 @@ trait API { #[MethodInput=RecomputeInput, rpc_name="executionContext/recompute"] fn recompute(&self, context_id: ContextId, invalidated_expressions: InvalidatedExpressions, execution_environment: Option) -> (); + /// Start the profiling of the language server. + #[MethodInput=ProfilingStartInput, rpc_name="profiling/start"] + fn profiling_start(&self, memory_snapshot: Option) -> (); + + /// Stop the profiling of the language server. + #[MethodInput=ProfilingStopInput, rpc_name="profiling/stop"] + fn profiling_stop(&self) -> (); + /// Obtain the full suggestions database. #[MethodInput=GetSuggestionsDatabaseInput, rpc_name="search/getSuggestionsDatabase"] fn get_suggestions_database(&self) -> response::GetSuggestionDatabase; diff --git a/app/gui/docs/product/shortcuts.md b/app/gui/docs/product/shortcuts.md index f41e8a8faa..6aac5ed7db 100644 --- a/app/gui/docs/product/shortcuts.md +++ b/app/gui/docs/product/shortcuts.md @@ -129,6 +129,8 @@ broken and require further investigation. | Shortcut | Action | | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | +| ctrl + alt + , | Start the language server profiling. | +| ctrl + alt + . | Stop the language server profiling and save the collected data. | | ctrl + shift + x | Force reloading file in the backend. May fix some issues with synchronization if they appear. | | ctrl + shift + d | Toggle Debug Mode. All actions below are only possible when it is activated. | | ctrl + alt + shift + i | Open the developer console. | diff --git a/app/gui/src/controller/graph/executed.rs b/app/gui/src/controller/graph/executed.rs index fc447d071d..fe38dffee6 100644 --- a/app/gui/src/controller/graph/executed.rs +++ b/app/gui/src/controller/graph/executed.rs @@ -395,6 +395,18 @@ impl Handle { } } + /// Command to start gathering the profiling info in the connected language server. + pub async fn start_language_server_profiling(&self) -> FallibleResult { + self.execution_ctx.start_profiling().await?; + Ok(()) + } + + /// Command to stop gathering the profiling info in the connected language server. + pub async fn stop_language_server_profiling(&self) -> FallibleResult { + self.execution_ctx.stop_profiling().await?; + Ok(()) + } + /// Get the current call stack frames. pub fn call_stack(&self) -> Vec { self.execution_ctx.stack_items().collect() diff --git a/app/gui/src/model/execution_context.rs b/app/gui/src/model/execution_context.rs index 65250b744e..5e3054a61a 100644 --- a/app/gui/src/model/execution_context.rs +++ b/app/gui/src/model/execution_context.rs @@ -568,6 +568,14 @@ pub trait API: Debug { #[allow(clippy::needless_lifetimes)] // Note: Needless lifetimes fn restart<'a>(&'a self) -> BoxFuture<'a, FallibleResult>; + /// Start the profiling of the language server. + #[allow(clippy::needless_lifetimes)] // Note: Needless lifetimes + fn start_profiling<'a>(&'a self) -> BoxFuture<'a, FallibleResult>; + + /// Stop the profiling of the language server. + #[allow(clippy::needless_lifetimes)] // Note: Needless lifetimes + fn stop_profiling<'a>(&'a self) -> BoxFuture<'a, FallibleResult>; + /// Adjust method pointers after the project rename action. fn rename_method_pointers(&self, old_project_name: String, new_project_name: String); diff --git a/app/gui/src/model/execution_context/plain.rs b/app/gui/src/model/execution_context/plain.rs index 3f77df7838..6e300e83c3 100644 --- a/app/gui/src/model/execution_context/plain.rs +++ b/app/gui/src/model/execution_context/plain.rs @@ -298,6 +298,14 @@ impl model::execution_context::API for ExecutionContext { futures::future::ready(Ok(())).boxed_local() } + fn start_profiling(&self) -> BoxFuture { + futures::future::ready(Ok(())).boxed_local() + } + + fn stop_profiling(&self) -> BoxFuture { + futures::future::ready(Ok(())).boxed_local() + } + fn rename_method_pointers(&self, old_project_name: String, new_project_name: String) { let update_method_pointer = |method_pointer: &mut MethodPointer| { let module = method_pointer.module.replacen(&old_project_name, &new_project_name, 1); diff --git a/app/gui/src/model/execution_context/synchronized.rs b/app/gui/src/model/execution_context/synchronized.rs index 76631a11ab..6357413978 100644 --- a/app/gui/src/model/execution_context/synchronized.rs +++ b/app/gui/src/model/execution_context/synchronized.rs @@ -328,6 +328,23 @@ impl model::execution_context::API for ExecutionContext { .boxed_local() } + fn start_profiling(&self) -> BoxFuture { + async move { + let memory_snapshot = Some(true); + self.language_server.client.profiling_start(&memory_snapshot).await?; + Ok(()) + } + .boxed_local() + } + + fn stop_profiling(&self) -> BoxFuture { + async move { + self.language_server.client.profiling_stop().await?; + Ok(()) + } + .boxed_local() + } + fn rename_method_pointers(&self, old_project_name: String, new_project_name: String) { self.model.rename_method_pointers(old_project_name, new_project_name); } diff --git a/app/gui/src/presenter/project.rs b/app/gui/src/presenter/project.rs index 5f9994378d..c758c456f5 100644 --- a/app/gui/src/presenter/project.rs +++ b/app/gui/src/presenter/project.rs @@ -278,6 +278,30 @@ impl Model { }) } + fn start_language_server_profiling(&self) { + let controller = self.graph_controller.clone_ref(); + let status_notifications = self.controller.status_notifications.clone_ref(); + executor::global::spawn(async move { + if let Err(err) = controller.start_language_server_profiling().await { + error!("Error starting the language server profiling: {err}"); + } else { + status_notifications.publish_event("Backend profiling started."); + } + }) + } + + fn stop_language_server_profiling(&self) { + let controller = self.graph_controller.clone_ref(); + let status_notifications = self.controller.status_notifications.clone_ref(); + executor::global::spawn(async move { + status_notifications.publish_event("Stopping backend profiling ..."); + if let Err(err) = controller.stop_language_server_profiling().await { + error!("Error stopping the language server profiling: {err}"); + } + status_notifications.publish_event("Backend profiling stopped."); + }) + } + /// Prepare a list of projects to display in the Open Project dialog. fn project_list_opened(&self, project_list_ready: frp::Source<()>) { let controller = self.ide_controller.clone_ref(); @@ -463,6 +487,9 @@ impl Project { eval graph_view.execution_environment((env) model.execution_environment_changed(*env)); eval_ graph_view.execution_environment_play_button_pressed( model.trigger_clean_live_execution()); + eval_ view.start_language_server_profiling(model.start_language_server_profiling()); + eval_ view.stop_language_server_profiling(model.stop_language_server_profiling()); + eval view.current_shortcut ((shortcut) model.handled_shortcut_changed(shortcut)); } diff --git a/app/gui/view/src/project.rs b/app/gui/view/src/project.rs index 2bfbaf858f..afacf5ffb1 100644 --- a/app/gui/view/src/project.rs +++ b/app/gui/view/src/project.rs @@ -151,6 +151,10 @@ ensogl::define_endpoints! { accept_searcher_input(), /// Dump the suggestion database in JSON to the console. dump_suggestion_database(), + /// Start the language server profiling + start_language_server_profiling(), + /// Stop the language server profiling + stop_language_server_profiling(), } Output { @@ -812,6 +816,8 @@ impl application::View for View { (Press, "debug_mode", "ctrl shift enter", "debug_push_breadcrumb"), (Press, "debug_mode", "ctrl shift b", "debug_pop_breadcrumb"), (Press, "debug_mode", "ctrl shift u", "dump_suggestion_database"), + (Press, "", "cmd alt ,", "start_language_server_profiling"), + (Press, "", "cmd alt .", "stop_language_server_profiling"), ] .iter() .map(|(a, b, c, d)| Self::self_shortcut_when(*a, *c, *d, *b)) diff --git a/docs/language-server/protocol-language-server.md b/docs/language-server/protocol-language-server.md index 0194470b11..0b03f90f20 100644 --- a/docs/language-server/protocol-language-server.md +++ b/docs/language-server/protocol-language-server.md @@ -5111,7 +5111,10 @@ request to store the gathered data. After the profiling is started, subsequent #### Parameters ```typescript -interface ProfilingStartParameters {} +interface ProfilingStartParameters { + /** Also take a memory snapshot when the profiling is stopped. */ + memorySnapshot?: boolean; +} ``` #### Result 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 a216cd522a..23ad87625e 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 @@ -6,9 +6,11 @@ object ProfilingApi { case object ProfilingStart extends Method("profiling/start") { - implicit val hasParams: HasParams.Aux[this.type, Unused.type] = + case class Params(memorySnapshot: Option[Boolean]) + + implicit val hasParams: HasParams.Aux[this.type, ProfilingStart.Params] = new HasParams[this.type] { - type Params = Unused.type + type Params = ProfilingStart.Params } implicit val hasResult: HasResult.Aux[this.type, Unused.type] = new HasResult[this.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 b9d9779dae..a9dfc6a7eb 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 @@ -44,7 +44,7 @@ final class ProfilingManager( initialized(None) private def initialized(sampler: Option[RunningSampler]): Receive = { - case ProfilingProtocol.ProfilingStartRequest => + case ProfilingProtocol.ProfilingStartRequest(memorySnapshot) => sampler match { case Some(_) => sender() ! ProfilingProtocol.ProfilingStartResponse @@ -62,13 +62,15 @@ final class ProfilingManager( sender() ! ProfilingProtocol.ProfilingStartResponse context.become( - initialized(Some(RunningSampler(instant, sampler, result))) + initialized( + Some(RunningSampler(instant, sampler, result, memorySnapshot)) + ) ) } case ProfilingProtocol.ProfilingStopRequest => sampler match { - case Some(RunningSampler(instant, sampler, result)) => + case Some(RunningSampler(instant, sampler, result, memorySnapshot)) => sampler.stop() Try(saveSamplerResult(result.toByteArray, instant)) match { @@ -81,6 +83,10 @@ final class ProfilingManager( ) } + if (memorySnapshot) { + saveMemorySnapshot(instant) + } + runtimeConnector ! RuntimeConnector.RegisterEventsMonitor( new NoopEventsMonitor ) @@ -93,20 +99,22 @@ final class ProfilingManager( 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) - ) - } + saveMemorySnapshot(instant) sender() ! ProfilingProtocol.ProfilingSnapshotResponse } + private def saveMemorySnapshot(instant: Instant): Unit = + 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) + ) + } + private def saveSamplerResult( result: Array[Byte], instant: Instant @@ -163,7 +171,8 @@ object ProfilingManager { private case class RunningSampler( instant: Instant, sampler: MethodsSampler, - result: ByteArrayOutputStream + result: ByteArrayOutputStream, + memorySnapshot: Boolean ) private def createProfilingFileName(instant: Instant): String = { 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 956e13dae0..244cb915bd 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 @@ -2,8 +2,11 @@ package org.enso.languageserver.profiling object ProfilingProtocol { - /** A request to start the profiling. */ - case object ProfilingStartRequest + /** A request to start the profiling. + * + * @param memorySnapshot take memory snapshot + */ + case class ProfilingStartRequest(memorySnapshot: Boolean) /** A response to request to start the profiling. */ case object ProfilingStartResponse diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/profiling/ProfilingStartHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/profiling/ProfilingStartHandler.scala index 570a6d43b5..7e300fd93b 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/profiling/ProfilingStartHandler.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/profiling/ProfilingStartHandler.scala @@ -3,7 +3,8 @@ 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.profiling.ProfilingApi.ProfilingStart +import org.enso.languageserver.profiling.ProfilingProtocol import org.enso.languageserver.requesthandler.RequestTimeout import org.enso.languageserver.util.UnhandledLogging @@ -24,8 +25,10 @@ class ProfilingStartHandler(timeout: FiniteDuration, profilingManager: ActorRef) override def receive: Receive = requestStage private def requestStage: Receive = { - case Request(ProfilingApi.ProfilingStart, id, _) => - profilingManager ! ProfilingProtocol.ProfilingStartRequest + case Request(ProfilingStart, id, ProfilingStart.Params(memorySnapshot)) => + profilingManager ! ProfilingProtocol.ProfilingStartRequest( + memorySnapshot.getOrElse(false) + ) val cancellable = context.system.scheduler.scheduleOnce(timeout, self, RequestTimeout) context.become( @@ -48,7 +51,7 @@ class ProfilingStartHandler(timeout: FiniteDuration, profilingManager: ActorRef) context.stop(self) case ProfilingProtocol.ProfilingStartResponse => - replyTo ! ResponseResult(ProfilingApi.ProfilingStart, id, Unused) + replyTo ! ResponseResult(ProfilingStart, id, Unused) cancellable.cancel() context.stop(self) } 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 7175c1a803..ec8a89a8b8 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 @@ -11,12 +11,14 @@ object ProfilingJsonMessages { "result": null }""" - def profilingStart(reqId: Int) = + def profilingStart(reqId: Int, memorySnapshot: Boolean = false) = json""" { "jsonrpc": "2.0", "method": "profiling/start", "id": $reqId, - "params": null + "params": { + "memorySnapshot": $memorySnapshot + } }""" def profilingStop(reqId: Int) = 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 eccabecfea..8b705e00df 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 @@ -49,6 +49,44 @@ class ProfilingManagerTest extends BaseServerTest { Files.exists(eventsFile) shouldEqual true } + "save profiling with memory snapshot " in { + val client = getInitialisedWsClient() + + client.send(json.profilingStart(1, memorySnapshot = true)) + runtimeConnectorProbe.receiveN(1).head match { + case _: RuntimeConnector.RegisterEventsMonitor => + // Ok + case other => + fail(s"Unexpected message: $other") + } + client.expectJson(json.ok(1)) + + client.send(json.profilingStop(2)) + runtimeConnectorProbe.receiveN(1).head match { + case _: RuntimeConnector.RegisterEventsMonitor => + // Ok + case other => + fail(s"Unexpected message: $other") + } + client.expectJson(json.ok(2)) + + val distributionManager = getDistributionManager + val instant = clock.instant + val samplesFile = distributionManager.paths.profiling.resolve( + ProfilingManager.createSamplesFileName(instant) + ) + val eventsFile = distributionManager.paths.profiling.resolve( + ProfilingManager.createEventsFileName(instant) + ) + val snapshotFile = distributionManager.paths.profiling.resolve( + ProfilingManager.createHeapDumpFileName(instant) + ) + + Files.exists(samplesFile) shouldEqual true + Files.exists(eventsFile) shouldEqual true + Files.exists(snapshotFile) shouldEqual true + } + "save memory snapshot" in { val client = getInitialisedWsClient()