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.
This commit is contained in:
Dmitry Bushev 2023-11-23 15:31:17 +00:00 committed by GitHub
parent 87dfb57f53
commit 4b3ba78b52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 173 additions and 25 deletions

View File

@ -159,6 +159,14 @@ trait API {
#[MethodInput=RecomputeInput, rpc_name="executionContext/recompute"]
fn recompute(&self, context_id: ContextId, invalidated_expressions: InvalidatedExpressions, execution_environment: Option<ExecutionEnvironment>) -> ();
/// Start the profiling of the language server.
#[MethodInput=ProfilingStartInput, rpc_name="profiling/start"]
fn profiling_start(&self, memory_snapshot: Option<bool>) -> ();
/// 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;

View File

@ -129,6 +129,8 @@ broken and require further investigation.
| Shortcut | Action |
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| <kbd>ctrl</kbd> + <kbd>alt</kbd> + <kbd>,</kbd> | Start the language server profiling. |
| <kbd>ctrl</kbd> + <kbd>alt</kbd> + <kbd>.</kbd> | Stop the language server profiling and save the collected data. |
| <kbd>ctrl</kbd> + <kbd>shift</kbd> + <kbd>x</kbd> | Force reloading file in the backend. May fix some issues with synchronization if they appear. |
| <kbd>ctrl</kbd> + <kbd>shift</kbd> + <kbd>d</kbd> | Toggle Debug Mode. All actions below are only possible when it is activated. |
| <kbd>ctrl</kbd> + <kbd>alt</kbd> + <kbd>shift</kbd> + <kbd>i</kbd> | Open the developer console. |

View File

@ -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<LocalCall> {
self.execution_ctx.stack_items().collect()

View File

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

View File

@ -298,6 +298,14 @@ impl model::execution_context::API for ExecutionContext {
futures::future::ready(Ok(())).boxed_local()
}
fn start_profiling(&self) -> BoxFuture<FallibleResult> {
futures::future::ready(Ok(())).boxed_local()
}
fn stop_profiling(&self) -> BoxFuture<FallibleResult> {
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);

View File

@ -328,6 +328,23 @@ impl model::execution_context::API for ExecutionContext {
.boxed_local()
}
fn start_profiling(&self) -> BoxFuture<FallibleResult> {
async move {
let memory_snapshot = Some(true);
self.language_server.client.profiling_start(&memory_snapshot).await?;
Ok(())
}
.boxed_local()
}
fn stop_profiling(&self) -> BoxFuture<FallibleResult> {
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);
}

View File

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

View File

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

View File

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

View File

@ -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] {

View File

@ -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 = {

View File

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

View File

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

View File

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

View File

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