From 98d30bccf34f9d851d4092b831317f58b007e480 Mon Sep 17 00:00:00 2001 From: Dmitry Bushev Date: Wed, 10 Aug 2022 15:01:33 +0300 Subject: [PATCH] Enable caching in visualization functions (#3618) PR allows to attach metod pointers as a visualization expressions. This way it allows to attach a runtime instrument that enables caching of intermediate expressions. # Important Notes :information_source: API is backward compatible. To attach the visualization with caching support, the same `executionContext/attachVisualisation` method is used, but `VisualisationConfig` message should contain the message pointer. While `VisualisationConfiguration` message has changed, the language server accepts both new and old formats to keep visualisations working in IDE. #### Old format ```json { "executionContextId": "UUID", "visualisationModule": "local.Unnamed.Main", "expression": "x -> x.to_text" } ``` #### New format ```json { "executionContextId": "UUID", "expression": { "module": "local.Unnamed.Main", "definedOnType": "local.Unnamed.Main", "name": "encode" } } ``` --- CHANGELOG.md | 4 +- .../protocol-language-server.md | 15 +- .../runtime/ContextRegistry.scala | 17 +- .../runtime/MethodPointer.scala | 9 +- .../runtime/VisualisationConfiguration.scala | 196 +++++++- .../json/ExecutionContextJsonMessages.scala | 94 +++- .../json/VisualisationOperationsTest.scala | 149 +++--- .../org/enso/polyglot/runtime/Runtime.scala | 64 ++- .../instrument/IdExecutionInstrument.java | 1 + .../test/instrument/RuntimeServerTest.scala | 59 ++- .../RuntimeVisualizationsTest.scala | 432 ++++++++++++++++-- .../FunctionCallInstrumentationNode.java | 8 +- .../interpreter/service/ExecutionService.java | 59 ++- .../instrument/CacheInvalidation.scala | 64 ++- .../instrument/ExecutionContextManager.scala | 45 +- .../instrument/ExecutionContextState.scala | 5 + .../instrument/Visualisation.scala | 11 +- .../instrument/VisualisationHolder.scala | 29 +- .../command/AttachVisualisationCmd.scala | 4 +- .../instrument/command/EditFileCmd.scala | 2 +- .../command/ModifyVisualisationCmd.scala | 4 +- .../instrument/command/RenameProjectCmd.scala | 2 +- .../command/SetExpressionValueCmd.scala | 2 +- .../instrument/job/EnsureCompiledJob.scala | 113 ++++- .../job/ProgramExecutionSupport.scala | 8 +- .../job/UpsertVisualisationJob.scala | 301 ++++++++---- 26 files changed, 1408 insertions(+), 289 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3bbf74f6c5..1260cdfd6c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -302,6 +302,7 @@ - [Explicit `self`][3569] - [Added benchmarking tool for the language server][3578] - [Support module imports using a qualified name][3608] +- [Enable caching in visualisation functions][3618] - [Update Scala compiler and libraries][3631] - [Support importing module methods][3633] @@ -338,7 +339,8 @@ [3562]: https://github.com/enso-org/enso/pull/3562 [3538]: https://github.com/enso-org/enso/pull/3538 [3538]: https://github.com/enso-org/enso/pull/3569 -[3578]: https://github.com/enso-org/enso/pull/3578 +[3618]: https://github.com/enso-org/enso/pull/3618 +[3608]: https://github.com/enso-org/enso/pull/3608 [3608]: https://github.com/enso-org/enso/pull/3608 [3631]: https://github.com/enso-org/enso/pull/3631 [3633]: https://github.com/enso-org/enso/pull/3633 diff --git a/docs/language-server/protocol-language-server.md b/docs/language-server/protocol-language-server.md index ed377206911..919aaead7d2 100644 --- a/docs/language-server/protocol-language-server.md +++ b/docs/language-server/protocol-language-server.md @@ -27,6 +27,7 @@ transport formats, please look [here](./protocol-architecture). - [`ExpressionUpdate`](#expressionupdate) - [`ExpressionUpdatePayload`](#expressionupdatepayload) - [`VisualisationConfiguration`](#visualisationconfiguration) + - [`VisualisationExpression`](#visualisationexpression) - [`SuggestionEntryArgument`](#suggestionentryargument) - [`SuggestionEntry`](#suggestionentry) - [`SuggestionEntryType`](#suggestionentrytype) @@ -378,19 +379,17 @@ A configuration object for properties of the visualisation. ```typescript interface VisualisationConfiguration { - /** - * An execution context of the visualisation. - */ + /** An execution context of the visualisation. */ executionContextId: UUID; + /** * A qualified name of the module containing the expression which creates * visualisation. */ - visualisationModule: String; - /** - * The expression that creates a visualisation. - */ - expression: String; + visualisationModule?: String; + + /** An expression that creates a visualisation. */ + expression: String | MethodPointer; } ``` diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala index 566b02e605d..a8dcc90a86b 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala @@ -243,7 +243,7 @@ final class ContextRegistry( Api.AttachVisualisation( visualisationId, expressionId, - convertVisualisationConfig(cfg) + cfg.toApi ) ) } else { @@ -263,7 +263,7 @@ final class ContextRegistry( Api.AttachVisualisation( visualisationId, expressionId, - convertVisualisationConfig(cfg) + cfg.toApi ) ) } else { @@ -301,25 +301,14 @@ final class ContextRegistry( ) ) - val configuration = convertVisualisationConfig(cfg) - handler.forward( - Api.ModifyVisualisation(visualisationId, configuration) + Api.ModifyVisualisation(visualisationId, cfg.toApi) ) } else { sender() ! AccessDenied } } - private def convertVisualisationConfig( - config: VisualisationConfiguration - ): Api.VisualisationConfiguration = - Api.VisualisationConfiguration( - executionContextId = config.executionContextId, - visualisationModule = config.visualisationModule, - expression = config.expression - ) - private def getRuntimeStackItem( stackItem: StackItem ): Api.StackItem = diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/MethodPointer.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/MethodPointer.scala index 3ccba81b5e1..1a47c411125 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/MethodPointer.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/MethodPointer.scala @@ -1,9 +1,16 @@ package org.enso.languageserver.runtime +import org.enso.polyglot.runtime.Runtime.Api + /** An object pointing to a method definition. * * @param module the module of the method file * @param definedOnType method type * @param name method name */ -case class MethodPointer(module: String, definedOnType: String, name: String) +case class MethodPointer(module: String, definedOnType: String, name: String) { + + /** Convert to corresponding [[Api]] message. */ + def toApi: Api.MethodPointer = + Api.MethodPointer(module, definedOnType, name) +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/VisualisationConfiguration.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/VisualisationConfiguration.scala index 2a28b95b62f..7684627ebbf 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/VisualisationConfiguration.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/VisualisationConfiguration.scala @@ -1,27 +1,201 @@ package org.enso.languageserver.runtime -import java.util.UUID +import io.circe.generic.auto._ +import io.circe.syntax._ +import io.circe.{Decoder, Encoder, Json} +import org.enso.logger.masking.ToLogString +import org.enso.polyglot.runtime.Runtime.Api -import org.enso.logger.masking.{MaskedString, ToLogString} +import java.util.UUID /** A configuration object for properties of the visualisation. * * @param executionContextId an execution context of the visualisation - * @param visualisationModule a qualified name of the module containing - * the expression which creates visualisation - * @param expression the expression that creates a visualisation + * @param expression an expression that creates a visualisation */ case class VisualisationConfiguration( executionContextId: UUID, - visualisationModule: String, - expression: String + expression: VisualisationExpression ) extends ToLogString { + /** A qualified module name containing the expression. */ + def visualisationModule: String = + expression.module + /** @inheritdoc */ override def toLogString(shouldMask: Boolean): String = - "VisualisationConfiguration(" + + s"VisualisationConfiguration(" + s"executionContextId=$executionContextId," + - s"visualisationModule=$visualisationModule,expression=" + - MaskedString(expression).toLogString(shouldMask) + - ")" + s"expression=${expression.toLogString(shouldMask)})" + + /** Convert to corresponding [[Api]] message. */ + def toApi: Api.VisualisationConfiguration = + Api.VisualisationConfiguration( + executionContextId = executionContextId, + expression = expression.toApi + ) + +} +object VisualisationConfiguration { + + /** Create a visualisation configuration. + * + * @param contextId an execution context of the visualisation + * @param module a qualified module name containing the visualisation + * @param expression a visualisation expression + * @return an instance of [[VisualisationConfiguration]] + */ + def apply( + contextId: UUID, + module: String, + expression: String + ): VisualisationConfiguration = + new VisualisationConfiguration( + contextId, + VisualisationExpression.Text(module, expression) + ) + + /** Create a visualisation configuration. + * + * @param contextId an execution context of the visualisation + * @param expression a visualisation expression + * @return an instance of [[VisualisationConfiguration]] + */ + def apply( + contextId: UUID, + expression: MethodPointer + ): VisualisationConfiguration = + new VisualisationConfiguration( + contextId, + VisualisationExpression.ModuleMethod(expression) + ) + + private object CodecField { + + val Expression = "expression" + + val ExecutionContextId = "executionContextId" + + val VisualisationModule = "visualisationModule" + } + + /** Json decoder that supports both old and new formats. */ + implicit val decoder: Decoder[VisualisationConfiguration] = + Decoder.instance { cursor => + cursor.downField(CodecField.Expression).as[String] match { + case Left(_) => + for { + contextId <- cursor + .downField(CodecField.ExecutionContextId) + .as[UUID] + expression <- cursor + .downField(CodecField.Expression) + .as[MethodPointer] + } yield VisualisationConfiguration(contextId, expression) + + case Right(expression) => + for { + contextId <- cursor + .downField(CodecField.ExecutionContextId) + .as[UUID] + visualisationModule <- cursor + .downField(CodecField.VisualisationModule) + .as[String] + } yield VisualisationConfiguration( + contextId, + visualisationModule, + expression + ) + } + } +} + +/** A visualisation expression. */ +sealed trait VisualisationExpression extends ToLogString { + + /** A qualified module name. */ + def module: String + + /** Convert to corresponding [[Api]] message. */ + def toApi: Api.VisualisationExpression +} +object VisualisationExpression { + + /** Visualization expression represented as a text. + * + * @param module a qualified module name containing the expression + * @param expression an expression that creates a visualization + */ + case class Text(module: String, expression: String) + extends VisualisationExpression { + + /** @inheritdoc */ + override def toApi: Api.VisualisationExpression = + Api.VisualisationExpression.Text(module, expression) + + /** @inheritdoc */ + override def toLogString(shouldMask: Boolean): String = + s"Text(module=$module" + + s",expression=" + + (if (shouldMask) STUB else expression) + + ")" + } + + /** Visualization expression represented as a module method. + * + * @param methodPointer a pointer to a method definition + */ + case class ModuleMethod(methodPointer: MethodPointer) + extends VisualisationExpression { + + /** @inheritdoc */ + override val module: String = methodPointer.module + + /** @inheritdoc */ + override def toApi: Api.VisualisationExpression = + Api.VisualisationExpression.ModuleMethod(methodPointer.toApi) + + /** @inheritdoc */ + override def toLogString(shouldMask: Boolean): String = + s"ModuleMethod(methodPointer=$methodPointer)" + } + + private object CodecField { + + val Type = "type" + } + + private object PayloadType { + + val Text = "Text" + + val ModuleMethod = "ModuleMethod" + } + + implicit val encoder: Encoder[VisualisationExpression] = + Encoder.instance[VisualisationExpression] { + case text: VisualisationExpression.Text => + Encoder[VisualisationExpression.Text] + .apply(text) + .deepMerge(Json.obj(CodecField.Type -> PayloadType.Text.asJson)) + + case moduleMethod: VisualisationExpression.ModuleMethod => + Encoder[VisualisationExpression.ModuleMethod] + .apply(moduleMethod) + .deepMerge( + Json.obj(CodecField.Type -> PayloadType.ModuleMethod.asJson) + ) + } + + implicit val decoder: Decoder[VisualisationExpression] = + Decoder.instance { cursor => + cursor.downField(CodecField.Type).as[String].flatMap { + case PayloadType.Text => + Decoder[VisualisationExpression.Text].tryDecode(cursor) + + case PayloadType.ModuleMethod => + Decoder[VisualisationExpression.ModuleMethod].tryDecode(cursor) + } + } + } diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ExecutionContextJsonMessages.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ExecutionContextJsonMessages.scala index d358f1ac595..b5e2e7f2444 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ExecutionContextJsonMessages.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ExecutionContextJsonMessages.scala @@ -2,7 +2,10 @@ package org.enso.languageserver.websocket.json import org.enso.polyglot.runtime.Runtime.Api import io.circe.literal._ -import org.enso.languageserver.runtime.VisualisationConfiguration +import org.enso.languageserver.runtime.{ + VisualisationConfiguration, + VisualisationExpression +} object ExecutionContextJsonMessages { @@ -110,7 +113,25 @@ object ExecutionContextJsonMessages { expressionId: Api.ExpressionId, configuration: VisualisationConfiguration ) = - json""" + configuration.expression match { + case VisualisationExpression.Text(module, expression) => + json""" + { "jsonrpc": "2.0", + "method": "executionContext/executeExpression", + "id": $reqId, + "params": { + "visualisationId": $visualisationId, + "expressionId": $expressionId, + "visualisationConfig": { + "executionContextId": ${configuration.executionContextId}, + "visualisationModule": $module, + "expression": $expression + } + } + } + """ + case VisualisationExpression.ModuleMethod(methodPointer) => + json""" { "jsonrpc": "2.0", "method": "executionContext/executeExpression", "id": $reqId, @@ -119,20 +140,26 @@ object ExecutionContextJsonMessages { "expressionId": $expressionId, "visualisationConfig": { "executionContextId": ${configuration.executionContextId}, - "visualisationModule": ${configuration.visualisationModule}, - "expression": ${configuration.expression} + "expression": { + "module": ${methodPointer.module}, + "definedOnType": ${methodPointer.definedOnType}, + "name": ${methodPointer.name} + } } } } """ + } def executionContextAttachVisualisationRequest( reqId: Int, visualisationId: Api.VisualisationId, expressionId: Api.ExpressionId, configuration: VisualisationConfiguration - ) = - json""" + ) = { + configuration.expression match { + case VisualisationExpression.Text(module, expression) => + json""" { "jsonrpc": "2.0", "method": "executionContext/attachVisualisation", "id": $reqId, @@ -141,12 +168,33 @@ object ExecutionContextJsonMessages { "expressionId": $expressionId, "visualisationConfig": { "executionContextId": ${configuration.executionContextId}, - "visualisationModule": ${configuration.visualisationModule}, - "expression": ${configuration.expression} + "visualisationModule": $module, + "expression": $expression } } } """ + case VisualisationExpression.ModuleMethod(methodPointer) => + json""" + { "jsonrpc": "2.0", + "method": "executionContext/attachVisualisation", + "id": $reqId, + "params": { + "visualisationId": $visualisationId, + "expressionId": $expressionId, + "visualisationConfig": { + "executionContextId": ${configuration.executionContextId}, + "expression": { + "module": ${methodPointer.module}, + "definedOnType": ${methodPointer.definedOnType}, + "name": ${methodPointer.name} + } + } + } + } + """ + } + } def executionContextModuleNotFound( reqId: Int, @@ -215,8 +263,10 @@ object ExecutionContextJsonMessages { reqId: Int, visualisationId: Api.VisualisationId, configuration: VisualisationConfiguration - ) = - json""" + ) = { + configuration.expression match { + case VisualisationExpression.Text(module, expression) => + json""" { "jsonrpc": "2.0", "method": "executionContext/modifyVisualisation", "id": $reqId, @@ -224,12 +274,32 @@ object ExecutionContextJsonMessages { "visualisationId": $visualisationId, "visualisationConfig": { "executionContextId": ${configuration.executionContextId}, - "visualisationModule": ${configuration.visualisationModule}, - "expression": ${configuration.expression} + "visualisationModule": $module, + "expression": $expression } } } """ + case VisualisationExpression.ModuleMethod(methodPointer) => + json""" + { "jsonrpc": "2.0", + "method": "executionContext/modifyVisualisation", + "id": $reqId, + "params": { + "visualisationId": $visualisationId, + "visualisationConfig": { + "executionContextId": ${configuration.executionContextId}, + "expression": { + "module": ${methodPointer.module}, + "definedOnType": ${methodPointer.definedOnType}, + "name": ${methodPointer.name} + } + } + } + } + """ + } + } def executionContextGetComponentGroupsRequest( reqId: Int, diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/VisualisationOperationsTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/VisualisationOperationsTest.scala index c19db5be36c..81ed400c59f 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/VisualisationOperationsTest.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/VisualisationOperationsTest.scala @@ -1,8 +1,10 @@ package org.enso.languageserver.websocket.json import java.util.UUID - import io.circe.literal._ -import org.enso.languageserver.runtime.VisualisationConfiguration +import org.enso.languageserver.runtime.{ + MethodPointer, + VisualisationConfiguration +} import org.enso.polyglot.runtime.Runtime.Api import org.enso.text.editing.model @@ -37,7 +39,60 @@ class VisualisationOperationsTest extends BaseServerTest { config ) ) => - config.expression shouldBe visualisationConfig.expression + config.expression shouldBe visualisationConfig.expression.toApi + config.visualisationModule shouldBe visualisationConfig.visualisationModule + config.executionContextId shouldBe visualisationConfig.executionContextId + requestId + + case msg => + fail(s"Unexpected message: $msg") + } + + runtimeConnectorProbe.lastSender ! Api.Response( + requestId, + Api.VisualisationAttached() + ) + client.expectJson(ExecutionContextJsonMessages.ok(1)) + } + + "allow attaching method pointer as a visualisation expression" in { + val visualisationId = UUID.randomUUID() + val expressionId = UUID.randomUUID() + + val client = getInitialisedWsClient() + val contextId = createExecutionContext(client) + val visualisationModule = "Foo.Bar" + val visualisationMethod = "baz" + val visualisationConfig = + VisualisationConfiguration( + contextId, + MethodPointer( + visualisationModule, + visualisationModule, + visualisationMethod + ) + ) + + client.send( + ExecutionContextJsonMessages.executionContextAttachVisualisationRequest( + 1, + visualisationId, + expressionId, + visualisationConfig + ) + ) + + val requestId = + runtimeConnectorProbe.receiveN(1).head match { + case Api.Request( + requestId, + Api.AttachVisualisation( + `visualisationId`, + `expressionId`, + config + ) + ) => + config.expression shouldBe visualisationConfig.expression.toApi config.visualisationModule shouldBe visualisationConfig.visualisationModule config.executionContextId shouldBe visualisationConfig.executionContextId requestId @@ -106,7 +161,7 @@ class VisualisationOperationsTest extends BaseServerTest { config ) ) => - config.expression shouldBe visualisationConfig.expression + config.expression shouldBe visualisationConfig.expression.toApi config.visualisationModule shouldBe visualisationConfig.visualisationModule config.executionContextId shouldBe visualisationConfig.executionContextId requestId @@ -155,7 +210,7 @@ class VisualisationOperationsTest extends BaseServerTest { config ) ) => - config.expression shouldBe visualisationConfig.expression + config.expression shouldBe visualisationConfig.expression.toApi config.visualisationModule shouldBe visualisationConfig.visualisationModule config.executionContextId shouldBe visualisationConfig.executionContextId requestId @@ -220,17 +275,14 @@ class VisualisationOperationsTest extends BaseServerTest { val expressionId = UUID.randomUUID() val client = getInitialisedWsClient() val contextId = createExecutionContext(client) - client.send(json""" - { "jsonrpc": "2.0", - "method": "executionContext/detachVisualisation", - "id": 1, - "params": { - "contextId": $contextId, - "visualisationId": $visualisationId, - "expressionId": $expressionId - } - } - """) + client.send( + ExecutionContextJsonMessages.executionContextDetachVisualisationRequest( + 1, + contextId, + visualisationId, + expressionId + ) + ) val requestId = runtimeConnectorProbe.receiveN(1).head match { case Api.Request( @@ -259,17 +311,14 @@ class VisualisationOperationsTest extends BaseServerTest { val expressionId = UUID.randomUUID() val contextId = UUID.randomUUID() val client = getInitialisedWsClient() - client.send(json""" - { "jsonrpc": "2.0", - "method": "executionContext/detachVisualisation", - "id": 1, - "params": { - "contextId": $contextId, - "visualisationId": $visualisationId, - "expressionId": $expressionId - } - } - """) + client.send( + ExecutionContextJsonMessages.executionContextDetachVisualisationRequest( + 1, + contextId, + visualisationId, + expressionId + ) + ) client.expectJson(json""" { "jsonrpc": "2.0", "id" : 1, @@ -291,20 +340,13 @@ class VisualisationOperationsTest extends BaseServerTest { val contextId = createExecutionContext(client) val visualisationConfig = VisualisationConfiguration(contextId, "Foo.Bar.baz", "a=x+y") - client.send(json""" - { "jsonrpc": "2.0", - "method": "executionContext/modifyVisualisation", - "id": 1, - "params": { - "visualisationId": $visualisationId, - "visualisationConfig": { - "executionContextId": $contextId, - "visualisationModule": ${visualisationConfig.visualisationModule}, - "expression": ${visualisationConfig.expression} - } - } - } - """) + client.send( + ExecutionContextJsonMessages.executionContextModifyVisualisationRequest( + 1, + visualisationId, + visualisationConfig + ) + ) val requestId = runtimeConnectorProbe.receiveN(1).head match { @@ -312,7 +354,7 @@ class VisualisationOperationsTest extends BaseServerTest { requestId, Api.ModifyVisualisation(`visualisationId`, config) ) => - config.expression shouldBe visualisationConfig.expression + config.expression shouldBe visualisationConfig.expression.toApi config.visualisationModule shouldBe visualisationConfig.visualisationModule config.executionContextId shouldBe visualisationConfig.executionContextId requestId @@ -334,20 +376,14 @@ class VisualisationOperationsTest extends BaseServerTest { val client = getInitialisedWsClient() val visualisationConfig = VisualisationConfiguration(contextId, "Foo.Bar.baz", "a=x+y") - client.send(json""" - { "jsonrpc": "2.0", - "method": "executionContext/modifyVisualisation", - "id": 1, - "params": { - "visualisationId": $visualisationId, - "visualisationConfig": { - "executionContextId": $contextId, - "visualisationModule": ${visualisationConfig.visualisationModule}, - "expression": ${visualisationConfig.expression} - } - } - } - """) + + client.send( + ExecutionContextJsonMessages.executionContextModifyVisualisationRequest( + 1, + visualisationId, + visualisationConfig + ) + ) client.expectJson(json""" { "jsonrpc": "2.0", "id" : 1, @@ -358,7 +394,6 @@ class VisualisationOperationsTest extends BaseServerTest { } """) } - } private def createExecutionContext(client: WsTestClient): UUID = { diff --git a/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala b/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala index 9822397307f..18cf1e2be38 100644 --- a/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala +++ b/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala @@ -477,26 +477,76 @@ object Runtime { expressionId: ExpressionId ) + /** A visualization expression. */ + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") + @JsonSubTypes( + Array( + new JsonSubTypes.Type( + value = classOf[VisualisationExpression.Text], + name = "visualisationExpressionText" + ), + new JsonSubTypes.Type( + value = classOf[VisualisationExpression.ModuleMethod], + name = "visualisationExpressionModuleMethod" + ) + ) + ) + sealed trait VisualisationExpression extends ToLogString { + def module: String + } + object VisualisationExpression { + + /** Visualization expression represented as a text. + * + * @param module a qualified module name containing the expression + * @param expression an expression that creates a visualization + */ + case class Text(module: String, expression: String) + extends VisualisationExpression { + + /** @inheritdoc */ + override def toLogString(shouldMask: Boolean): String = + s"Text(module=$module" + + s",expression=" + + (if (shouldMask) STUB else expression) + + ")" + } + + /** Visualization expression represented as a module method. + * + * @param methodPointer a pointer to a method definition + */ + case class ModuleMethod(methodPointer: MethodPointer) + extends VisualisationExpression { + + /** @inheritdoc */ + override val module: String = methodPointer.module + + /** @inheritdoc */ + override def toLogString(shouldMask: Boolean): String = + s"ModuleMethod(methodPointer=$methodPointer)" + } + } + /** A configuration object for properties of the visualisation. * * @param executionContextId an execution context of the visualisation - * @param visualisationModule a qualified name of the module containing - * the expression which creates visualisation * @param expression the expression that creates a visualisation */ case class VisualisationConfiguration( executionContextId: ContextId, - visualisationModule: String, - expression: String + expression: VisualisationExpression ) extends ToLogString { + /** A qualified module name containing the expression. */ + def visualisationModule: String = + expression.module + /** @inheritdoc */ override def toLogString(shouldMask: Boolean): String = s"VisualisationConfiguration(" + s"executionContextId=$executionContextId," + - s"visualisationModule=$visualisationModule,expression=" + - (if (shouldMask) STUB else expression) + - ")" + s"expression=${expression.toLogString(shouldMask)})" } /** An operation applied to the suggestion argument. */ diff --git a/engine/runtime-instrument-id-execution/src/main/java/org/enso/interpreter/instrument/IdExecutionInstrument.java b/engine/runtime-instrument-id-execution/src/main/java/org/enso/interpreter/instrument/IdExecutionInstrument.java index a4a46a5fdb7..694500df0eb 100644 --- a/engine/runtime-instrument-id-execution/src/main/java/org/enso/interpreter/instrument/IdExecutionInstrument.java +++ b/engine/runtime-instrument-id-execution/src/main/java/org/enso/interpreter/instrument/IdExecutionInstrument.java @@ -330,4 +330,5 @@ public class IdExecutionInstrument extends TruffleInstrument implements IdExecut onExceptionalCallback, timer)); } + } diff --git a/engine/runtime-with-instruments/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala b/engine/runtime-with-instruments/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala index d85f7287612..8c3ce012007 100644 --- a/engine/runtime-with-instruments/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala +++ b/engine/runtime-with-instruments/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala @@ -414,7 +414,60 @@ class RuntimeServerTest ) } - it should "push method with default arguments" in { + it should "push method with default arguments on top of the stack" in { + val contextId = UUID.randomUUID() + val requestId = UUID.randomUUID() + val moduleName = "Enso_Test.Test.Main" + + val metadata = new Metadata + val idFoo = metadata.addItem(35, 6) + + val code = + """import Standard.Base.IO + | + |foo x=0 = x + 42 + | + |main = + | IO.println foo + |""".stripMargin.linesIterator.mkString("\n") + val contents = metadata.appendToCode(code) + val mainFile = context.writeMain(contents) + + // create context + context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) + context.receive shouldEqual Some( + Api.Response(requestId, Api.CreateContextResponse(contextId)) + ) + + // open file + context.send( + Api.Request(Api.OpenFileNotification(mainFile, contents)) + ) + context.receiveNone shouldEqual None + + // push main + context.send( + Api.Request( + requestId, + Api.PushContextRequest( + contextId, + Api.StackItem.ExplicitCall( + Api.MethodPointer(moduleName, "Enso_Test.Test.Main", "foo"), + None, + Vector() + ) + ) + ) + ) + context.receiveNIgnoreStdLib(3) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.PushContextResponse(contextId)), + TestMessages.update(contextId, idFoo, ConstantsGen.INTEGER), + context.executionComplete(contextId) + ) + context.consumeOut shouldEqual List() + } + + it should "push method with default arguments on the stack" in { val contextId = UUID.randomUUID() val requestId = UUID.randomUUID() val moduleName = "Enso_Test.Test.Main" @@ -1013,7 +1066,9 @@ class RuntimeServerTest contextId, mainFoo, ConstantsGen.INTEGER, - Api.MethodPointer("Enso_Test.Test.Main", "Enso_Test.Test.Main", "foo") + Api + .MethodPointer("Enso_Test.Test.Main", "Enso_Test.Test.Main", "foo"), + fromCache = true ), context.executionComplete(contextId) ) diff --git a/engine/runtime-with-instruments/src/test/scala/org/enso/interpreter/test/instrument/RuntimeVisualizationsTest.scala b/engine/runtime-with-instruments/src/test/scala/org/enso/interpreter/test/instrument/RuntimeVisualizationsTest.scala index 1de00b89459..f703a8366fe 100644 --- a/engine/runtime-with-instruments/src/test/scala/org/enso/interpreter/test/instrument/RuntimeVisualizationsTest.scala +++ b/engine/runtime-with-instruments/src/test/scala/org/enso/interpreter/test/instrument/RuntimeVisualizationsTest.scala @@ -23,7 +23,7 @@ import java.util.UUID import scala.io.Source @scala.annotation.nowarn("msg=multiarg infix syntax") -class RuntimeVisualisationsTest +class RuntimeVisualizationsTest extends AnyFlatSpec with Matchers with BeforeAndAfterEach { @@ -234,13 +234,43 @@ class RuntimeVisualisationsTest object Visualisation { + val metadata = new Metadata + val code = - """ - |encode = x -> x.to_text - | - |incAndEncode = x -> encode x+1 - | - |""".stripMargin + metadata.appendToCode( + """ + |encode x = x.to_text + | + |incAndEncode x = + | y = x + 1 + | encode y + | + |""".stripMargin.linesIterator.mkString("\n") + ) + + } + + object AnnotatedVisualisation { + + val metadata = new Metadata + val idIncY = metadata.addItem(50, 5) + val idIncRes = metadata.addItem(66, 8) + val idIncMethod = metadata.addItem(25, 58) + + val code = + metadata.appendToCode( + """import Standard.Base.IO + | + |incAndEncode x = + | y = x + 1 + | res = encode y + | res + | + |encode x = + | IO.println "encoding..." + | x.to_text + |""".stripMargin.linesIterator.mkString("\n") + ) } @@ -311,8 +341,10 @@ class RuntimeVisualisationsTest idMainRes, Api.VisualisationConfiguration( contextId, - "Enso_Test.Test.Visualisation", - "x -> encode x" + Api.VisualisationExpression.Text( + "Enso_Test.Test.Visualisation", + "x -> encode x" + ) ) ) ) @@ -423,8 +455,10 @@ class RuntimeVisualisationsTest context.Main.idMainX, Api.VisualisationConfiguration( contextId, - "Enso_Test.Test.Visualisation", - "x -> encode x" + Api.VisualisationExpression.Text( + "Enso_Test.Test.Visualisation", + "x -> encode x" + ) ) ) ) @@ -552,8 +586,10 @@ class RuntimeVisualisationsTest context.Main.idMainX, Api.VisualisationConfiguration( contextId, - "Enso_Test.Test.Visualisation", - "x -> encode x" + Api.VisualisationExpression.Text( + "Enso_Test.Test.Visualisation", + "x -> encode x" + ) ) ) ) @@ -675,8 +711,10 @@ class RuntimeVisualisationsTest context.Main.idMainZ, Api.VisualisationConfiguration( contextId, - "Enso_Test.Test.Visualisation", - "encode" + Api.VisualisationExpression.Text( + "Enso_Test.Test.Visualisation", + "encode" + ) ) ) ) @@ -796,8 +834,10 @@ class RuntimeVisualisationsTest context.Main.idMainX, Api.VisualisationConfiguration( contextId, - "Enso_Test.Test.Visualisation", - "x -> encode x" + Api.VisualisationExpression.Text( + "Enso_Test.Test.Visualisation", + "x -> encode x" + ) ) ) ) @@ -832,8 +872,10 @@ class RuntimeVisualisationsTest visualisationId, Api.VisualisationConfiguration( contextId, - "Enso_Test.Test.Visualisation", - "x -> incAndEncode x" + Api.VisualisationExpression.Text( + "Enso_Test.Test.Visualisation", + "x -> incAndEncode x" + ) ) ) ) @@ -899,8 +941,10 @@ class RuntimeVisualisationsTest context.Main.idMainX, Api.VisualisationConfiguration( contextId, - "Enso_Test.Test.Visualisation", - "x -> encode x" + Api.VisualisationExpression.Text( + "Enso_Test.Test.Visualisation", + "x -> encode x" + ) ) ) ) @@ -1052,8 +1096,10 @@ class RuntimeVisualisationsTest context.Main.idMainX, Api.VisualisationConfiguration( contextId, - "Enso_Test.Test.Visualisation", - "encode" + Api.VisualisationExpression.Text( + "Enso_Test.Test.Visualisation", + "encode" + ) ) ) ) @@ -1157,8 +1203,10 @@ class RuntimeVisualisationsTest context.Main.idMainX, Api.VisualisationConfiguration( contextId, - "Enso_Test.Test.Visualisation", - "x -> encode x" + Api.VisualisationExpression.Text( + "Enso_Test.Test.Visualisation", + "x -> encode x" + ) ) ) ) @@ -1193,8 +1241,10 @@ class RuntimeVisualisationsTest visualisationId, Api.VisualisationConfiguration( contextId, - "Enso_Test.Test.Visualisation", - "x -> incAndEncode x" + Api.VisualisationExpression.Text( + "Enso_Test.Test.Visualisation", + "x -> incAndEncode x" + ) ) ) ) @@ -1282,8 +1332,10 @@ class RuntimeVisualisationsTest idMain, Api.VisualisationConfiguration( contextId, - "Test.Undefined", - "x -> x" + Api.VisualisationExpression.Text( + "Test.Undefined", + "x -> x" + ) ) ) ) @@ -1342,8 +1394,10 @@ class RuntimeVisualisationsTest idMain, Api.VisualisationConfiguration( contextId, - "Standard.Visualization.Id", - "x -> x.default_visualization.to_text" + Api.VisualisationExpression.Text( + "Standard.Visualization.Id", + "x -> x.default_visualization.to_text" + ) ) ) ) @@ -1429,8 +1483,10 @@ class RuntimeVisualisationsTest idMain, Api.VisualisationConfiguration( contextId, - "Enso_Test.Test.Main", - "Main.does_not_exist" + Api.VisualisationExpression.Text( + "Enso_Test.Test.Main", + "Main.does_not_exist" + ) ) ) ) @@ -1503,8 +1559,10 @@ class RuntimeVisualisationsTest idMain, Api.VisualisationConfiguration( contextId, - moduleName, - "x -> x.visualise_me" + Api.VisualisationExpression.Text( + moduleName, + "x -> x.visualise_me" + ) ) ) ) @@ -1608,8 +1666,10 @@ class RuntimeVisualisationsTest idMain, Api.VisualisationConfiguration( contextId, - "Enso_Test.Test.Visualisation", - "inc_and_encode" + Api.VisualisationExpression.Text( + "Enso_Test.Test.Visualisation", + "inc_and_encode" + ) ) ) ) @@ -1720,8 +1780,10 @@ class RuntimeVisualisationsTest idMain, Api.VisualisationConfiguration( contextId, - moduleName, - "x -> x.catch_primitive _.to_text" + Api.VisualisationExpression.Text( + moduleName, + "x -> x.catch_primitive _.to_text" + ) ) ) ) @@ -1806,8 +1868,10 @@ class RuntimeVisualisationsTest idMain, Api.VisualisationConfiguration( contextId, - moduleName, - "x -> Panic.catch_primitive x caught_panic-> caught_panic.payload.to_text" + Api.VisualisationExpression.Text( + moduleName, + "x -> Panic.catch_primitive x caught_panic-> caught_panic.payload.to_text" + ) ) ) ) @@ -1920,8 +1984,10 @@ class RuntimeVisualisationsTest idMain, Api.VisualisationConfiguration( contextId, - visualisationModule, - visualisationCode + Api.VisualisationExpression.Text( + visualisationModule, + visualisationCode + ) ) ) ) @@ -1948,4 +2014,284 @@ class RuntimeVisualisationsTest val stringified = new String(data) stringified shouldEqual """{ "kind": "Dataflow", "message": "The List is empty."}""" } + + it should "attach method pointer visualisation" in { + val idMainRes = context.Main.metadata.addItem(99, 1) + val contents = context.Main.code + val mainFile = context.writeMain(context.Main.code) + val visualisationFile = + context.writeInSrcDir("Visualisation", context.Visualisation.code) + + context.send( + Api.Request( + Api.OpenFileNotification( + visualisationFile, + context.Visualisation.code + ) + ) + ) + + val contextId = UUID.randomUUID() + val requestId = UUID.randomUUID() + val visualisationId = UUID.randomUUID() + + // create context + context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) + context.receive shouldEqual Some( + Api.Response(requestId, Api.CreateContextResponse(contextId)) + ) + + // Open the new file + context.send( + Api.Request(Api.OpenFileNotification(mainFile, contents)) + ) + context.receiveNone shouldEqual None + + // push main + val item1 = Api.StackItem.ExplicitCall( + Api.MethodPointer("Enso_Test.Test.Main", "Enso_Test.Test.Main", "main"), + None, + Vector() + ) + context.send( + Api.Request(requestId, Api.PushContextRequest(contextId, item1)) + ) + context.receiveNIgnoreStdLib(6) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.PushContextResponse(contextId)), + context.Main.Update.mainX(contextId), + context.Main.Update.mainY(contextId), + context.Main.Update.mainZ(contextId), + TestMessages.update(contextId, idMainRes, ConstantsGen.INTEGER), + context.executionComplete(contextId) + ) + + // attach visualisation + context.send( + Api.Request( + requestId, + Api.AttachVisualisation( + visualisationId, + idMainRes, + Api.VisualisationConfiguration( + contextId, + Api.VisualisationExpression.ModuleMethod( + Api.MethodPointer( + "Enso_Test.Test.Visualisation", + "Enso_Test.Test.Visualisation", + "incAndEncode" + ) + ) + ) + ) + ) + ) + val attachVisualisationResponses = context.receiveN(3) + attachVisualisationResponses should contain allOf ( + Api.Response(requestId, Api.VisualisationAttached()), + context.executionComplete(contextId) + ) + val Some(data) = attachVisualisationResponses.collectFirst { + case Api.Response( + None, + Api.VisualisationUpdate( + Api.VisualisationContext( + `visualisationId`, + `contextId`, + `idMainRes` + ), + data + ) + ) => + data + } + data.sameElements("51".getBytes) shouldBe true + + // recompute + context.send( + Api.Request(requestId, Api.RecomputeContextRequest(contextId, None)) + ) + + val recomputeResponses = context.receiveN(3) + recomputeResponses should contain allOf ( + Api.Response(requestId, Api.RecomputeContextResponse(contextId)), + context.executionComplete(contextId) + ) + val Some(data2) = recomputeResponses.collectFirst { + case Api.Response( + None, + Api.VisualisationUpdate( + Api.VisualisationContext( + `visualisationId`, + `contextId`, + `idMainRes` + ), + data + ) + ) => + data + } + data2.sameElements("51".getBytes) shouldBe true + } + + it should "cache intermediate visualization expressions" in { + val idMainRes = context.Main.metadata.addItem(99, 1) + val contents = context.Main.code + val mainFile = context.writeMain(context.Main.code) + val moduleName = "Enso_Test.Test.Main" + val visualisationFile = + context.writeInSrcDir( + "Visualisation", + context.AnnotatedVisualisation.code + ) + + context.send( + Api.Request( + Api.OpenFileNotification( + visualisationFile, + context.AnnotatedVisualisation.code + ) + ) + ) + + val contextId = UUID.randomUUID() + val requestId = UUID.randomUUID() + val visualisationId = UUID.randomUUID() + + // create context + context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) + context.receive shouldEqual Some( + Api.Response(requestId, Api.CreateContextResponse(contextId)) + ) + + // Open the new file + context.send( + Api.Request(Api.OpenFileNotification(mainFile, contents)) + ) + context.receiveNone shouldEqual None + + // push main + val item1 = Api.StackItem.ExplicitCall( + Api.MethodPointer(moduleName, "Enso_Test.Test.Main", "main"), + None, + Vector() + ) + context.send( + Api.Request(requestId, Api.PushContextRequest(contextId, item1)) + ) + context.receiveNIgnoreStdLib(6) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.PushContextResponse(contextId)), + context.Main.Update.mainX(contextId), + context.Main.Update.mainY(contextId), + context.Main.Update.mainZ(contextId), + TestMessages.update(contextId, idMainRes, ConstantsGen.INTEGER), + context.executionComplete(contextId) + ) + context.consumeOut shouldEqual List() + + // attach visualisation + context.send( + Api.Request( + requestId, + Api.AttachVisualisation( + visualisationId, + idMainRes, + Api.VisualisationConfiguration( + contextId, + Api.VisualisationExpression.ModuleMethod( + Api.MethodPointer( + "Enso_Test.Test.Visualisation", + "Enso_Test.Test.Visualisation", + "incAndEncode" + ) + ) + ) + ) + ) + ) + val attachVisualisationResponses = context.receiveN(3) + attachVisualisationResponses should contain allOf ( + Api.Response(requestId, Api.VisualisationAttached()), + context.executionComplete(contextId) + ) + val Some(data) = attachVisualisationResponses.collectFirst { + case Api.Response( + None, + Api.VisualisationUpdate( + Api.VisualisationContext( + `visualisationId`, + `contextId`, + `idMainRes` + ), + data + ) + ) => + data + } + data.sameElements("51".getBytes) shouldBe true + context.consumeOut shouldEqual List("encoding...") + + // recompute + context.send( + Api.Request(requestId, Api.RecomputeContextRequest(contextId, None)) + ) + + val recomputeResponses = context.receiveN(3) + recomputeResponses should contain allOf ( + Api.Response(requestId, Api.RecomputeContextResponse(contextId)), + context.executionComplete(contextId) + ) + val Some(data2) = recomputeResponses.collectFirst { + case Api.Response( + None, + Api.VisualisationUpdate( + Api.VisualisationContext( + `visualisationId`, + `contextId`, + `idMainRes` + ), + data + ) + ) => + data + } + data2.sameElements("51".getBytes) shouldBe true + context.consumeOut shouldEqual List() + + // Modify the visualization file + context.send( + Api.Request( + Api.EditFileNotification( + visualisationFile, + Seq( + TextEdit( + model.Range(model.Position(3, 12), model.Position(3, 13)), + "2" + ) + ), + execute = true + ) + ) + ) + + val editFileResponse = context.receiveN(2) + editFileResponse should contain( + context.executionComplete(contextId) + ) + val Some(data3) = editFileResponse.collectFirst { + case Api.Response( + None, + Api.VisualisationUpdate( + Api.VisualisationContext( + `visualisationId`, + `contextId`, + `idMainRes` + ), + data + ) + ) => + data + } + data3.sameElements("52".getBytes) shouldBe true + context.consumeOut shouldEqual List("encoding...") + } } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/callable/FunctionCallInstrumentationNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/callable/FunctionCallInstrumentationNode.java index 7c965caf841..35a1abff298 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/node/callable/FunctionCallInstrumentationNode.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/callable/FunctionCallInstrumentationNode.java @@ -19,6 +19,7 @@ import com.oracle.truffle.api.source.SourceSection; import org.enso.interpreter.runtime.callable.function.Function; import org.enso.interpreter.runtime.tag.IdentifiedTag; +import java.util.Arrays; import java.util.UUID; /** @@ -92,8 +93,13 @@ public class FunctionCallInstrumentationNode extends Node implements Instrumenta FunctionCall functionCall, Object[] arguments, @Cached InteropApplicationNode interopApplicationNode) { + Object[] callArguments = + Arrays.copyOf( + functionCall.getArguments(), functionCall.getArguments().length + arguments.length); + System.arraycopy( + arguments, 0, callArguments, functionCall.getArguments().length, arguments.length); return interopApplicationNode.execute( - functionCall.function, functionCall.state, functionCall.arguments); + functionCall.function, functionCall.state, callArguments); } } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/service/ExecutionService.java b/engine/runtime/src/main/java/org/enso/interpreter/service/ExecutionService.java index fe3f9c915ce..8d7f366e849 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/service/ExecutionService.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/service/ExecutionService.java @@ -1,5 +1,6 @@ package org.enso.interpreter.service; +import com.oracle.truffle.api.CallTarget; import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.TruffleLogger; import com.oracle.truffle.api.instrumentation.EventBinding; @@ -48,6 +49,8 @@ import java.util.function.Consumer; * language. */ public class ExecutionService { + + private static final String MAIN_METHOD = "main"; private final Context context; private final Optional idExecutionInstrument; private final NotificationHandler.Forwarder notificationForwarder; @@ -86,7 +89,7 @@ public class ExecutionService { return logger; } - private FunctionCallInstrumentationNode.FunctionCall prepareFunctionCall( + public FunctionCallInstrumentationNode.FunctionCall prepareFunctionCall( Module module, String consName, String methodName) throws ConstructorNotFoundException, MethodNotFoundException { ModuleScope scope = module.compileScope(context); @@ -99,8 +102,9 @@ public class ExecutionService { if (function == null) { throw new MethodNotFoundException(module.getName().toString(), atomConstructor, methodName); } - return new FunctionCallInstrumentationNode.FunctionCall( - function, EmptyMap.create(), new Object[] {}); + Object[] arguments = + MAIN_METHOD.equals(methodName) ? new Object[] {} : new Object[] {atomConstructor}; + return new FunctionCallInstrumentationNode.FunctionCall(function, EmptyMap.create(), arguments); } public void initializeLanguageServerConnection(Endpoint endpoint) { @@ -252,6 +256,55 @@ public class ExecutionService { } } + /** + * Calls a function with the given argument and attaching an execution instrument. + * + * @param module the module providing scope for the function + * @param function the function object + * @param argument the argument applied to the function + * @param cache the runtime cache + * @return the result of calling the function + */ + public Object callFunctionWithInstrument( + Module module, Object function, Object argument, RuntimeCache cache) + throws UnsupportedTypeException, ArityException, UnsupportedMessageException { + UUID nextExecutionItem = null; + CallTarget entryCallTarget = + (function instanceof Function) ? ((Function) function).getCallTarget() : null; + MethodCallsCache methodCallsCache = new MethodCallsCache(); + UpdatesSynchronizationState syncState = new UpdatesSynchronizationState(); + Consumer funCallCallback = + (value) -> context.getLogger().finest("ON_CACHED_CALL " + value.getExpressionId()); + Consumer onComputedCallback = + (value) -> context.getLogger().finest("ON_COMPUTED " + value.getExpressionId()); + Consumer onCachedCallback = + (value) -> context.getLogger().finest("ON_CACHED_VALUE " + value.getExpressionId()); + Consumer onExceptionalCallback = + (value) -> context.getLogger().finest("ON_ERROR " + value); + + Optional> listener = + idExecutionInstrument.map( + service -> + service.bind( + module, + entryCallTarget, + cache, + methodCallsCache, + syncState, + nextExecutionItem, + funCallCallback, + onComputedCallback, + onCachedCallback, + onExceptionalCallback)); + Object p = context.getThreadManager().enter(); + try { + return interopLibrary.execute(function, argument); + } finally { + context.getThreadManager().leave(p); + listener.ifPresent(EventBinding::dispose); + } + } + /** * Sets a module at a given path to use a literal source. * diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/CacheInvalidation.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/CacheInvalidation.scala index e5e0cd060c0..62f12f953c6 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/CacheInvalidation.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/CacheInvalidation.scala @@ -29,7 +29,7 @@ object CacheInvalidation { sealed trait IndexSelector object IndexSelector { - /** Invalidate value from indexes. */ + /** Invalidate value from all indexes. */ case object All extends IndexSelector /** Invalidate the types index. */ @@ -118,6 +118,38 @@ object CacheInvalidation { ): Unit = instructions.foreach(run(stack, _)) + /** Run a sequence of invalidation instructions on all visualisations. + * + * @param visualisations the list of available visualisations + * @param instructions the list of cache invalidation instructions + */ + def runAllVisualisations( + visualisations: Iterable[Visualisation], + instructions: Iterable[CacheInvalidation] + ): Unit = + instructions.foreach { instruction => + runVisualisations( + visualisations, + instruction.command, + instruction.indexes + ) + } + + /** Run cache invalidation of a multiple visualisations + * + * @param visualisations visualisations cache should be invalidated + * @param command the invalidation instruction + * @param indexes the list of indexes to invalidate + */ + def runVisualisations( + visualisations: Iterable[Visualisation], + command: Command, + indexes: Set[IndexSelector] = Set() + ): Unit = + visualisations.foreach { visualisation => + run(visualisation.cache, command, indexes) + } + /** Run a cache invalidation instruction on an execution stack. * * @param stack the runtime stack @@ -148,6 +180,36 @@ object CacheInvalidation { frames.foreach(frame => run(frame.cache, frame.syncState, command, indexes)) } + /** Run cache invalidation of a single instrument frame. + * + * @param cache the cache to invalidate + * @param command the invalidation instruction + * @param indexes the list of indexes to invalidate + */ + private def run( + cache: RuntimeCache, + command: Command, + indexes: Set[IndexSelector] + ): Unit = + command match { + case Command.InvalidateAll => + cache.clear() + indexes.foreach(clearIndex(_, cache)) + case Command.InvalidateKeys(keys) => + keys.foreach { key => + cache.remove(key) + indexes.foreach(clearIndexKey(key, _, cache)) + } + case Command.InvalidateStale(scope) => + val staleKeys = cache.getKeys.asScala.diff(scope.toSet) + staleKeys.foreach { key => + cache.remove(key) + indexes.foreach(clearIndexKey(key, _, cache)) + } + case Command.SetMetadata(metadata) => + cache.setWeights(metadata.asJavaWeights) + } + /** Run cache invalidation of a single instrument frame. * * @param cache the cache to invalidate diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/ExecutionContextManager.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/ExecutionContextManager.scala index 265132215af..742a7ddee16 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/ExecutionContextManager.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/ExecutionContextManager.scala @@ -1,5 +1,6 @@ package org.enso.interpreter.instrument +import org.enso.pkg.QualifiedName import org.enso.polyglot.runtime.Runtime.Api.{ ContextId, ExpressionId, @@ -7,7 +8,7 @@ import org.enso.polyglot.runtime.Runtime.Api.{ VisualisationId } -import scala.collection.mutable.Stack +import scala.collection.mutable /** Storage for active execution contexts. */ @@ -50,16 +51,17 @@ class ExecutionContextManager { * @param id the context id. * @return the stack. */ - def getStack(id: ContextId): Stack[InstrumentFrame] = + def getStack(id: ContextId): mutable.Stack[InstrumentFrame] = synchronized { contexts(id).stack } /** Gets all execution contexts. * - * @return all currently available execution contexsts. + * @return all currently available execution contexts. */ - def getAll: collection.MapView[ContextId, Stack[InstrumentFrame]] = + def getAllContexts + : collection.MapView[ContextId, mutable.Stack[InstrumentFrame]] = synchronized { contexts.view.mapValues(_.stack) } @@ -114,6 +116,22 @@ class ExecutionContextManager { state.visualisations.upsert(visualisation) } + /** Get visualizations of all execution contexts. */ + def getAllVisualisations: Iterable[Visualisation] = + synchronized { + contexts.values.flatMap(_.visualisations.getAll) + } + + /** Get visualisations defined in the module. + * + * @param module the qualified module name + * @return the list of matching visualisations + */ + def getVisualisations(module: QualifiedName): Iterable[Visualisation] = + synchronized { + contexts.values.flatMap(_.visualisations.findByModule(module)) + } + /** Returns a visualisation with the provided id. * * @param contextId the identifier of the execution context @@ -148,6 +166,25 @@ class ExecutionContextManager { } yield visualisation } + /** Get all visualisations invalidated by the provided list of expressions. + * + * @param module the module containing the visualisations + * @param invalidatedExpressions the list of invalidated expressions + * @return a list of matching visualisation + */ + def getInvalidatedVisualisations( + module: QualifiedName, + invalidatedExpressions: Set[ExpressionId] + ): Iterable[Visualisation] = { + for { + state <- contexts.values + visualisation <- state.visualisations.findByModule(module) + if visualisation.visualisationExpressionId.exists( + invalidatedExpressions.contains + ) + } yield visualisation + } + /** Removes a visualisation from the holder. * * @param contextId the identifier of the execution context diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/ExecutionContextState.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/ExecutionContextState.scala index 0cfa1b1092f..de76312c3b6 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/ExecutionContextState.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/ExecutionContextState.scala @@ -37,6 +37,11 @@ case class InstrumentFrame( case object InstrumentFrame { + /** Create an instrument frame. + * + * @param item the stack item + * @return an instance of [[InstrumentFrame]] + */ def apply(item: StackItem): InstrumentFrame = new InstrumentFrame(item, new RuntimeCache, new UpdatesSynchronizationState) } diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/Visualisation.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/Visualisation.scala index bce32bbbfa5..a8649681a51 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/Visualisation.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/Visualisation.scala @@ -1,6 +1,11 @@ package org.enso.interpreter.instrument -import org.enso.polyglot.runtime.Runtime.Api.{ExpressionId, VisualisationId} +import org.enso.interpreter.runtime.Module +import org.enso.polyglot.runtime.Runtime.Api.{ + ExpressionId, + VisualisationConfiguration, + VisualisationId +} /** An object containing visualisation data. * @@ -12,5 +17,9 @@ import org.enso.polyglot.runtime.Runtime.Api.{ExpressionId, VisualisationId} case class Visualisation( id: VisualisationId, expressionId: ExpressionId, + cache: RuntimeCache, + module: Module, + config: VisualisationConfiguration, + visualisationExpressionId: Option[ExpressionId], callback: AnyRef ) diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/VisualisationHolder.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/VisualisationHolder.scala index e3d81f69a15..97d232f9ba2 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/VisualisationHolder.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/VisualisationHolder.scala @@ -1,13 +1,16 @@ package org.enso.interpreter.instrument +import org.enso.pkg.QualifiedName import org.enso.polyglot.runtime.Runtime.Api.{ExpressionId, VisualisationId} +import scala.collection.mutable + /** A mutable holder of all visualisations attached to an execution context. */ class VisualisationHolder() { - private var visualisationMap: Map[ExpressionId, List[Visualisation]] = - Map.empty.withDefaultValue(List.empty) + private val visualisationMap: mutable.Map[ExpressionId, List[Visualisation]] = + mutable.Map.empty.withDefaultValue(List.empty) /** Upserts a visualisation. * @@ -15,8 +18,8 @@ class VisualisationHolder() { */ def upsert(visualisation: Visualisation): Unit = { val visualisations = visualisationMap(visualisation.expressionId) - val removed = visualisations.filterNot(_.id == visualisation.id) - visualisationMap += (visualisation.expressionId -> (visualisation :: removed)) + val rest = visualisations.filterNot(_.id == visualisation.id) + visualisationMap.update(visualisation.expressionId, visualisation :: rest) } /** Removes a visualisation from the holder. @@ -30,8 +33,8 @@ class VisualisationHolder() { expressionId: ExpressionId ): Unit = { val visualisations = visualisationMap(expressionId) - val removed = visualisations.filterNot(_.id == visualisationId) - visualisationMap += (expressionId -> removed) + val rest = visualisations.filterNot(_.id == visualisationId) + visualisationMap.update(expressionId, rest) } /** Finds all visualisations attached to an expression. @@ -42,6 +45,14 @@ class VisualisationHolder() { def find(expressionId: ExpressionId): List[Visualisation] = visualisationMap(expressionId) + /** Finds all visualisations in a given module. + * + * @param module the qualified module name + * @return a list of matching visualisation + */ + def findByModule(module: QualifiedName): Iterable[Visualisation] = + visualisationMap.values.flatten.filter(_.module.getName == module) + /** Returns a visualisation with the provided id. * * @param visualisationId the identifier of visualisation @@ -50,12 +61,14 @@ class VisualisationHolder() { def getById(visualisationId: VisualisationId): Option[Visualisation] = visualisationMap.values.flatten.find(_.id == visualisationId) + /** @return all available visualisations. */ + def getAll: Iterable[Visualisation] = + visualisationMap.values.flatten } object VisualisationHolder { - /** Returns an empty holder. - */ + /** Returns an empty visualisation holder. */ def empty = new VisualisationHolder() } diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/AttachVisualisationCmd.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/AttachVisualisationCmd.scala index 4ec66b42590..60a6c6d25d0 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/AttachVisualisationCmd.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/AttachVisualisationCmd.scala @@ -49,10 +49,10 @@ class AttachVisualisationCmd( ctx.jobProcessor.run( new UpsertVisualisationJob( maybeRequestId, + Api.VisualisationAttached(), request.visualisationId, request.expressionId, - request.visualisationConfig, - Api.VisualisationAttached() + request.visualisationConfig ) ) diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/EditFileCmd.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/EditFileCmd.scala index cb9c8ca4cfe..221fc660392 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/EditFileCmd.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/EditFileCmd.scala @@ -42,7 +42,7 @@ class EditFileCmd(request: Api.EditFileNotification) extends Command(None) { private def executeJobs(implicit ctx: RuntimeContext ): Iterable[ExecuteJob] = { - ctx.contextManager.getAll + ctx.contextManager.getAllContexts .collect { case (contextId, stack) if stack.nonEmpty => new ExecuteJob(contextId, stack.toList) diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/ModifyVisualisationCmd.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/ModifyVisualisationCmd.scala index ba0c6e5d356..fec4fcc85ca 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/ModifyVisualisationCmd.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/ModifyVisualisationCmd.scala @@ -60,10 +60,10 @@ class ModifyVisualisationCmd( ctx.jobProcessor.run( new UpsertVisualisationJob( maybeRequestId, + Api.VisualisationModified(), request.visualisationId, visualisation.expressionId, - request.visualisationConfig, - Api.VisualisationModified() + request.visualisationConfig ) ) maybeFutureExecutable flatMap { diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/RenameProjectCmd.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/RenameProjectCmd.scala index 9ae307c264f..e236fbd804a 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/RenameProjectCmd.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/RenameProjectCmd.scala @@ -39,7 +39,7 @@ class RenameProjectCmd( request.oldName, request.newName ) - ctx.contextManager.getAll.values + ctx.contextManager.getAllContexts.values .foreach(updateMethodPointers(request.newName, _)) reply(Api.ProjectRenamed(request.namespace, request.newName)) logger.log( diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/SetExpressionValueCmd.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/SetExpressionValueCmd.scala index 33cc465b1ad..b9992013301 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/SetExpressionValueCmd.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/SetExpressionValueCmd.scala @@ -47,7 +47,7 @@ class SetExpressionValueCmd(request: Api.SetExpressionValueNotification) private def executeJobs(implicit ctx: RuntimeContext ): Iterable[ExecuteJob] = { - ctx.contextManager.getAll + ctx.contextManager.getAllContexts .collect { case (contextId, stack) if stack.nonEmpty => new ExecuteJob(contextId, stack.toList) diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/EnsureCompiledJob.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/EnsureCompiledJob.scala index 0a0af65e2d2..bbfff6e50cc 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/EnsureCompiledJob.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/EnsureCompiledJob.scala @@ -12,10 +12,16 @@ import org.enso.interpreter.instrument.execution.{ LocationResolver, RuntimeContext } -import org.enso.interpreter.instrument.{CacheInvalidation, InstrumentFrame} +import org.enso.interpreter.instrument.{ + CacheInvalidation, + InstrumentFrame, + Visualisation +} import org.enso.interpreter.runtime.Module import org.enso.interpreter.service.error.ModuleNotFoundForFileException +import org.enso.pkg.QualifiedName import org.enso.polyglot.runtime.Runtime.Api +import org.enso.polyglot.runtime.Runtime.Api.StackItem import org.enso.text.buffer.Rope import java.io.File @@ -39,17 +45,7 @@ final class EnsureCompiledJob(protected val files: Iterable[File]) try { val compilationResult = ensureCompiledFiles(files) - ctx.contextManager.getAll.values.foreach { stack => - getCacheMetadata(stack).foreach { metadata => - CacheInvalidation.run( - stack, - CacheInvalidation( - CacheInvalidation.StackSelector.Top, - CacheInvalidation.Command.SetMetadata(metadata) - ) - ) - } - } + setCacheWeights() compilationResult } finally { ctx.locking.releaseWriteCompilationLock() @@ -89,12 +85,7 @@ final class EnsureCompiledJob(protected val files: Iterable[File]) applyEdits(new File(module.getPath)).map { changeset => compile(module) .map { compilerResult => - val cacheInvalidationCommands = - buildCacheInvalidationCommands( - changeset, - module.getSource.getCharacters - ) - runInvalidationCommands(cacheInvalidationCommands) + invalidateCaches(module, changeset) ctx.jobProcessor.runBackground( AnalyzeModuleInScopeJob( module.getName, @@ -301,18 +292,44 @@ final class EnsureCompiledJob(protected val files: Iterable[File]) /** Run the invalidation commands. * - * @param invalidationCommands the invalidation command to run + * @param module the compiled module + * @param changeset the changeset containing the list of invalidated expressions * @param ctx the runtime context */ - private def runInvalidationCommands( - invalidationCommands: Iterable[CacheInvalidation] + private def invalidateCaches( + module: Module, + changeset: Changeset[_] )(implicit ctx: RuntimeContext): Unit = { - ctx.contextManager.getAll.values + val invalidationCommands = + buildCacheInvalidationCommands( + changeset, + module.getSource.getCharacters + ) + ctx.contextManager.getAllContexts.values .foreach { stack => - if (stack.nonEmpty) { + if (stack.nonEmpty && isStackInModule(module.getName, stack)) { CacheInvalidation.runAll(stack, invalidationCommands) } } + CacheInvalidation.runAllVisualisations( + ctx.contextManager.getVisualisations(module.getName), + invalidationCommands + ) + + val invalidatedVisualisations = + ctx.contextManager.getInvalidatedVisualisations( + module.getName, + changeset.invalidated + ) + invalidatedVisualisations.foreach { visualisation => + UpsertVisualisationJob.upsertVisualisation(visualisation) + } + if (invalidatedVisualisations.nonEmpty) { + ctx.executionService.getLogger.log( + Level.FINE, + s"Invalidated visualisations [${invalidatedVisualisations.map(_.id)}]" + ) + } } /** Send notification about the compilation status. @@ -325,7 +342,7 @@ final class EnsureCompiledJob(protected val files: Iterable[File]) diagnostics: Seq[Api.ExecutionResult.Diagnostic] )(implicit ctx: RuntimeContext): Unit = if (diagnostics.nonEmpty) { - ctx.contextManager.getAll.keys.foreach { contextId => + ctx.contextManager.getAllContexts.keys.foreach { contextId => ctx.endpoint.sendToClient( Api.Response(Api.ExecutionUpdate(contextId, diagnostics)) ) @@ -340,7 +357,7 @@ final class EnsureCompiledJob(protected val files: Iterable[File]) private def sendFailureUpdate( failure: Api.ExecutionResult.Failure )(implicit ctx: RuntimeContext): Unit = - ctx.contextManager.getAll.keys.foreach { contextId => + ctx.contextManager.getAllContexts.keys.foreach { contextId => ctx.endpoint.sendToClient( Api.Response(Api.ExecutionFailed(contextId, failure)) ) @@ -354,6 +371,27 @@ final class EnsureCompiledJob(protected val files: Iterable[File]) else CompilationStatus.Success + private def setCacheWeights()(implicit ctx: RuntimeContext): Unit = { + ctx.contextManager.getAllContexts.values.foreach { stack => + getCacheMetadata(stack).foreach { metadata => + CacheInvalidation.run( + stack, + CacheInvalidation( + CacheInvalidation.StackSelector.Top, + CacheInvalidation.Command.SetMetadata(metadata) + ) + ) + } + } + val visualisations = ctx.contextManager.getAllVisualisations + visualisations.flatMap(getCacheMetadata).foreach { metadata => + CacheInvalidation.runVisualisations( + visualisations, + CacheInvalidation.Command.SetMetadata(metadata) + ) + } + } + private def getCacheMetadata( stack: Iterable[InstrumentFrame] )(implicit ctx: RuntimeContext): Option[CachePreferenceAnalysis.Metadata] = @@ -370,12 +408,37 @@ final class EnsureCompiledJob(protected val files: Iterable[File]) case _ => None } + private def getCacheMetadata( + visualisation: Visualisation + ): Option[CachePreferenceAnalysis.Metadata] = { + val module = visualisation.module + module.getIr.getMetadata(CachePreferenceAnalysis) + } + /** Get all modules in the current compiler scope. */ private def getModulesInScope(implicit ctx: RuntimeContext ): Iterable[Module] = ctx.executionService.getContext.getTopScope.getModules.asScala + /** Check if stack belongs to the provided module. + * + * @param module the qualified module name + * @param stack the execution stack + */ + private def isStackInModule( + module: QualifiedName, + stack: Iterable[InstrumentFrame] + ): Boolean = + stack.headOption match { + case Some( + InstrumentFrame(StackItem.ExplicitCall(methodPointer, _, _), _, _) + ) => + methodPointer.module == module.toString + case _ => + false + } + } object EnsureCompiledJob { diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala index e2d2df78df1..75eccc1eabf 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala @@ -429,12 +429,14 @@ object ProgramExecutionSupport { Either .catchNonFatal { ctx.executionService.getLogger.log( - Level.FINEST, + Level.FINE, s"Executing visualisation ${visualisation.expressionId}" ) - ctx.executionService.callFunction( + ctx.executionService.callFunctionWithInstrument( + visualisation.module, visualisation.callback, - expressionValue + expressionValue, + visualisation.cache ) } .flatMap { diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/UpsertVisualisationJob.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/UpsertVisualisationJob.scala index 67ba920dff9..84f8b431aba 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/UpsertVisualisationJob.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/UpsertVisualisationJob.scala @@ -1,42 +1,45 @@ package org.enso.interpreter.instrument.job -import java.util.logging.Level - import cats.implicits._ -import org.enso.interpreter.instrument.{InstrumentFrame, Visualisation} +import org.enso.compiler.core.IR +import org.enso.compiler.pass.analyse.CachePreferenceAnalysis import org.enso.interpreter.instrument.execution.{Executable, RuntimeContext} import org.enso.interpreter.instrument.job.UpsertVisualisationJob.{ - EvalFailure, EvaluationFailed, - MaxEvaluationRetryCount, + EvaluationResult, ModuleNotFound } +import org.enso.interpreter.instrument.{ + CacheInvalidation, + InstrumentFrame, + RuntimeCache, + Visualisation +} import org.enso.interpreter.runtime.Module import org.enso.interpreter.runtime.control.ThreadInterruptedException -import org.enso.polyglot.runtime.Runtime.Api.{ - ExpressionId, - RequestId, - VisualisationId -} +import org.enso.pkg.QualifiedName +import org.enso.polyglot.runtime.Runtime.Api._ import org.enso.polyglot.runtime.Runtime.{Api, ApiResponse} +import java.util.logging.Level + /** A job that upserts a visualisation. * * @param requestId maybe a request id + * @param response a response used to reply to a client * @param visualisationId an identifier of visualisation * @param expressionId an identifier of expression * @param config a visualisation config - * @param response a response used to reply to a client */ class UpsertVisualisationJob( requestId: Option[RequestId], + response: ApiResponse, visualisationId: VisualisationId, expressionId: ExpressionId, - config: Api.VisualisationConfiguration, - response: ApiResponse + config: Api.VisualisationConfiguration ) extends Job[Option[Executable]]( List(config.executionContextId), - true, + false, false ) { @@ -46,24 +49,36 @@ class UpsertVisualisationJob( ctx.locking.acquireWriteCompilationLock() try { val maybeCallable = - evaluateExpression(config.visualisationModule, config.expression) + UpsertVisualisationJob.evaluateVisualisationExpression( + config.expression + ) maybeCallable match { - case Left(ModuleNotFound) => - replyWithModuleNotFoundError() + case Left(ModuleNotFound(moduleName)) => + replyWithModuleNotFoundError(moduleName) None case Left(EvaluationFailed(message, result)) => replyWithExpressionFailedError(message, result) None - case Right(callable) => - val visualisation = updateVisualisation(callable) + case Right(EvaluationResult(module, callable)) => + val visualisation = + UpsertVisualisationJob.updateVisualisation( + visualisationId, + expressionId, + module, + config, + callable + ) ctx.endpoint.sendToClient(Api.Response(requestId, response)) val stack = ctx.contextManager.getStack(config.executionContextId) val cachedValue = stack.headOption .flatMap(frame => Option(frame.cache.get(expressionId))) - requireVisualisationSynchronization(stack, expressionId) + UpsertVisualisationJob.requireVisualisationSynchronization( + stack, + expressionId + ) cachedValue match { case Some(value) => ProgramExecutionSupport.sendVisualisationUpdate( @@ -84,28 +99,6 @@ class UpsertVisualisationJob( } } - private def requireVisualisationSynchronization( - stack: Iterable[InstrumentFrame], - expressionId: ExpressionId - ): Unit = { - stack.foreach(_.syncState.setVisualisationUnsync(expressionId)) - } - - private def updateVisualisation( - callable: AnyRef - )(implicit ctx: RuntimeContext): Visualisation = { - val visualisation = Visualisation( - visualisationId, - expressionId, - callable - ) - ctx.contextManager.upsertVisualisation( - config.executionContextId, - visualisation - ) - visualisation - } - private def replyWithExpressionFailedError( message: String, executionResult: Option[Api.ExecutionResult.Diagnostic] @@ -118,39 +111,115 @@ class UpsertVisualisationJob( ) } - private def replyWithModuleNotFoundError()(implicit + private def replyWithModuleNotFoundError(module: String)(implicit ctx: RuntimeContext ): Unit = { ctx.endpoint.sendToClient( - Api.Response( - requestId, - Api.ModuleNotFound(config.visualisationModule) - ) + Api.Response(requestId, Api.ModuleNotFound(module)) ) } +} + +object UpsertVisualisationJob { + + /** The number of times to retry the expression evaluation. */ + val MaxEvaluationRetryCount: Int = 5 + + /** Base trait for evaluation failures. + */ + sealed trait EvaluationFailure + + /** Signals that a module cannot be found. + * + * @param moduleName the module name + */ + case class ModuleNotFound(moduleName: String) extends EvaluationFailure + + /** Signals that an evaluation of an expression failed. + * + * @param message the textual reason of a failure + * @param failure the error description + */ + case class EvaluationFailed( + message: String, + failure: Option[Api.ExecutionResult.Diagnostic] + ) extends EvaluationFailure + + case class EvaluationResult(module: Module, callback: AnyRef) + + /** Upsert the provided visualisation. + * + * @param visualisation the visualisation to update + */ + def upsertVisualisation( + visualisation: Visualisation + )(implicit ctx: RuntimeContext): Unit = { + val visualisationConfig = visualisation.config + val expressionId = visualisation.expressionId + val visualisationId = visualisation.id + val maybeCallable = + evaluateVisualisationExpression(visualisation.config.expression) + + maybeCallable.foreach { result => + updateVisualisation( + visualisationId, + expressionId, + result.module, + visualisationConfig, + result.callback + ) + val stack = + ctx.contextManager.getStack(visualisationConfig.executionContextId) + requireVisualisationSynchronization(stack, expressionId) + } + } + + /** Find module by name. + * + * @param moduleName the module name + * @return either the requested module or an error + */ private def findModule( moduleName: String - )(implicit ctx: RuntimeContext): Either[EvalFailure, Module] = { + )(implicit ctx: RuntimeContext): Either[EvaluationFailure, Module] = { val context = ctx.executionService.getContext - // TODO [RW] more specific error when the module cannot be installed (#1861) context.ensureModuleIsLoaded(moduleName) val maybeModule = context.findModule(moduleName) if (maybeModule.isPresent) Right(maybeModule.get()) - else Left(ModuleNotFound) + else Left(ModuleNotFound(moduleName)) } + /** Evaluate the visualisation expression in a given module. + * + * @param module the module where to evaluate the expression + * @param expression the visualisation expression + * @param retryCount the number of attempted retries + * @return either the evaluation result or an evaluation failure + */ private def evaluateModuleExpression( module: Module, - expression: String, + expression: Api.VisualisationExpression, retryCount: Int = 0 )(implicit ctx: RuntimeContext - ): Either[EvalFailure, AnyRef] = + ): Either[EvaluationFailure, EvaluationResult] = Either .catchNonFatal { - ctx.executionService.evaluateExpression(module, expression) + val callback = expression match { + case Api.VisualisationExpression.Text(_, expression) => + ctx.executionService.evaluateExpression(module, expression) + case Api.VisualisationExpression.ModuleMethod( + Api.MethodPointer(_, definedOnType, name) + ) => + ctx.executionService.prepareFunctionCall( + module, + QualifiedName.fromString(definedOnType).item, + name + ) + } + EvaluationResult(module, callback) } .leftFlatMap { case _: ThreadInterruptedException @@ -193,38 +262,110 @@ class UpsertVisualisationJob( ) } - private def evaluateExpression( - moduleName: String, - expression: String - )(implicit ctx: RuntimeContext): Either[EvalFailure, AnyRef] = + /** Evaluate the visualisation expression. + * + * @param expression the visualisation expression to evaluate + * @return either the evaluation result or an evaluation error + */ + private def evaluateVisualisationExpression( + expression: Api.VisualisationExpression + )(implicit + ctx: RuntimeContext + ): Either[EvaluationFailure, EvaluationResult] = { for { - module <- findModule(moduleName) + module <- findModule(expression.module) expression <- evaluateModuleExpression(module, expression) } yield expression + } -} - -object UpsertVisualisationJob { - - /** The number of times to retry the expression evaluation. */ - val MaxEvaluationRetryCount: Int = 5 - - /** Base trait for evaluation failures. - */ - sealed trait EvalFailure - - /** Signals that a module cannot be found. - */ - case object ModuleNotFound extends EvalFailure - - /** Signals that an evaluation of an expression failed. + /** Update the visualisation state. * - * @param message the textual reason of a failure - * @param failure the error description + * @param visualisationId the visualisation identifier + * @param expressionId the expression to which the visualisation is applied + * @param module the module containing the visualisation + * @param visualisationConfig the visualisation configuration + * @param callback the visualisation callback function + * @param ctx the runtime context + * @return the re-evaluated visualisation */ - case class EvaluationFailed( - message: String, - failure: Option[Api.ExecutionResult.Diagnostic] - ) extends EvalFailure + private def updateVisualisation( + visualisationId: VisualisationId, + expressionId: ExpressionId, + module: Module, + visualisationConfig: VisualisationConfiguration, + callback: AnyRef + )(implicit ctx: RuntimeContext): Visualisation = { + val visualisationExpressionId = + findVisualisationExpressionId(module, visualisationConfig.expression) + val visualisation = Visualisation( + visualisationId, + expressionId, + new RuntimeCache(), + module, + visualisationConfig, + visualisationExpressionId, + callback + ) + setCacheWeights(visualisation) + ctx.contextManager.upsertVisualisation( + visualisationConfig.executionContextId, + visualisation + ) + visualisation + } + + /** Find the expressionId of visualisation function. + * + * @param module the module environment + * @param visualisationExpression the visualisation expression + * @return the expression id of required visualisation function + */ + private def findVisualisationExpressionId( + module: Module, + visualisationExpression: VisualisationExpression + ): Option[ExpressionId] = + visualisationExpression match { + case VisualisationExpression.ModuleMethod(methodPointer) => + module.getIr.bindings + .collect { case method: IR.Module.Scope.Definition.Method => + val methodReference = method.methodReference + val methodReferenceName = methodReference.methodName.name + val methodReferenceTypeOpt = methodReference.typePointer.map(_.name) + + method.getExternalId.filter { _ => + methodReferenceName == methodPointer.name && + methodReferenceTypeOpt.isEmpty + } + } + .flatten + .headOption + + case _: VisualisationExpression.Text => None + } + + /** Set the cache weights for the provided visualisation. + * + * @param visualisation the visualisation to update + */ + private def setCacheWeights(visualisation: Visualisation): Unit = { + visualisation.module.getIr.getMetadata(CachePreferenceAnalysis).foreach { + metadata => + CacheInvalidation.runVisualisations( + Seq(visualisation), + CacheInvalidation.Command.SetMetadata(metadata) + ) + } + } + + /** Require to send the visualisation update. + * + * @param stack the execution stack + * @param expressionId the expression id to which the visualisation is applied + */ + private def requireVisualisationSynchronization( + stack: Iterable[InstrumentFrame], + expressionId: ExpressionId + ): Unit = + stack.foreach(_.syncState.setVisualisationUnsync(expressionId)) }