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
ℹ️ 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"
}
}
```
This commit is contained in:
Dmitry Bushev 2022-08-10 15:01:33 +03:00 committed by GitHub
parent bd3b778721
commit 98d30bccf3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1408 additions and 289 deletions

View File

@ -302,6 +302,7 @@
- [Explicit `self`][3569] - [Explicit `self`][3569]
- [Added benchmarking tool for the language server][3578] - [Added benchmarking tool for the language server][3578]
- [Support module imports using a qualified name][3608] - [Support module imports using a qualified name][3608]
- [Enable caching in visualisation functions][3618]
- [Update Scala compiler and libraries][3631] - [Update Scala compiler and libraries][3631]
- [Support importing module methods][3633] - [Support importing module methods][3633]
@ -338,7 +339,8 @@
[3562]: https://github.com/enso-org/enso/pull/3562 [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/3538
[3538]: https://github.com/enso-org/enso/pull/3569 [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 [3608]: https://github.com/enso-org/enso/pull/3608
[3631]: https://github.com/enso-org/enso/pull/3631 [3631]: https://github.com/enso-org/enso/pull/3631
[3633]: https://github.com/enso-org/enso/pull/3633 [3633]: https://github.com/enso-org/enso/pull/3633

View File

@ -27,6 +27,7 @@ transport formats, please look [here](./protocol-architecture).
- [`ExpressionUpdate`](#expressionupdate) - [`ExpressionUpdate`](#expressionupdate)
- [`ExpressionUpdatePayload`](#expressionupdatepayload) - [`ExpressionUpdatePayload`](#expressionupdatepayload)
- [`VisualisationConfiguration`](#visualisationconfiguration) - [`VisualisationConfiguration`](#visualisationconfiguration)
- [`VisualisationExpression`](#visualisationexpression)
- [`SuggestionEntryArgument`](#suggestionentryargument) - [`SuggestionEntryArgument`](#suggestionentryargument)
- [`SuggestionEntry`](#suggestionentry) - [`SuggestionEntry`](#suggestionentry)
- [`SuggestionEntryType`](#suggestionentrytype) - [`SuggestionEntryType`](#suggestionentrytype)
@ -378,19 +379,17 @@ A configuration object for properties of the visualisation.
```typescript ```typescript
interface VisualisationConfiguration { interface VisualisationConfiguration {
/** /** An execution context of the visualisation. */
* An execution context of the visualisation.
*/
executionContextId: UUID; executionContextId: UUID;
/** /**
* A qualified name of the module containing the expression which creates * A qualified name of the module containing the expression which creates
* visualisation. * visualisation.
*/ */
visualisationModule: String; visualisationModule?: String;
/**
* The expression that creates a visualisation. /** An expression that creates a visualisation. */
*/ expression: String | MethodPointer;
expression: String;
} }
``` ```

View File

@ -243,7 +243,7 @@ final class ContextRegistry(
Api.AttachVisualisation( Api.AttachVisualisation(
visualisationId, visualisationId,
expressionId, expressionId,
convertVisualisationConfig(cfg) cfg.toApi
) )
) )
} else { } else {
@ -263,7 +263,7 @@ final class ContextRegistry(
Api.AttachVisualisation( Api.AttachVisualisation(
visualisationId, visualisationId,
expressionId, expressionId,
convertVisualisationConfig(cfg) cfg.toApi
) )
) )
} else { } else {
@ -301,25 +301,14 @@ final class ContextRegistry(
) )
) )
val configuration = convertVisualisationConfig(cfg)
handler.forward( handler.forward(
Api.ModifyVisualisation(visualisationId, configuration) Api.ModifyVisualisation(visualisationId, cfg.toApi)
) )
} else { } else {
sender() ! AccessDenied sender() ! AccessDenied
} }
} }
private def convertVisualisationConfig(
config: VisualisationConfiguration
): Api.VisualisationConfiguration =
Api.VisualisationConfiguration(
executionContextId = config.executionContextId,
visualisationModule = config.visualisationModule,
expression = config.expression
)
private def getRuntimeStackItem( private def getRuntimeStackItem(
stackItem: StackItem stackItem: StackItem
): Api.StackItem = ): Api.StackItem =

View File

@ -1,9 +1,16 @@
package org.enso.languageserver.runtime package org.enso.languageserver.runtime
import org.enso.polyglot.runtime.Runtime.Api
/** An object pointing to a method definition. /** An object pointing to a method definition.
* *
* @param module the module of the method file * @param module the module of the method file
* @param definedOnType method type * @param definedOnType method type
* @param name method name * @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)
}

View File

@ -1,27 +1,201 @@
package org.enso.languageserver.runtime 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. /** A configuration object for properties of the visualisation.
* *
* @param executionContextId an execution context of the visualisation * @param executionContextId an execution context of the visualisation
* @param visualisationModule a qualified name of the module containing * @param expression an expression that creates a visualisation
* the expression which creates visualisation
* @param expression the expression that creates a visualisation
*/ */
case class VisualisationConfiguration( case class VisualisationConfiguration(
executionContextId: UUID, executionContextId: UUID,
visualisationModule: String, expression: VisualisationExpression
expression: String
) extends ToLogString { ) extends ToLogString {
/** A qualified module name containing the expression. */
def visualisationModule: String =
expression.module
/** @inheritdoc */ /** @inheritdoc */
override def toLogString(shouldMask: Boolean): String = override def toLogString(shouldMask: Boolean): String =
"VisualisationConfiguration(" + s"VisualisationConfiguration(" +
s"executionContextId=$executionContextId," + s"executionContextId=$executionContextId," +
s"visualisationModule=$visualisationModule,expression=" + s"expression=${expression.toLogString(shouldMask)})"
MaskedString(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)
}
}
} }

View File

@ -2,7 +2,10 @@ package org.enso.languageserver.websocket.json
import org.enso.polyglot.runtime.Runtime.Api import org.enso.polyglot.runtime.Runtime.Api
import io.circe.literal._ import io.circe.literal._
import org.enso.languageserver.runtime.VisualisationConfiguration import org.enso.languageserver.runtime.{
VisualisationConfiguration,
VisualisationExpression
}
object ExecutionContextJsonMessages { object ExecutionContextJsonMessages {
@ -110,7 +113,25 @@ object ExecutionContextJsonMessages {
expressionId: Api.ExpressionId, expressionId: Api.ExpressionId,
configuration: VisualisationConfiguration 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", { "jsonrpc": "2.0",
"method": "executionContext/executeExpression", "method": "executionContext/executeExpression",
"id": $reqId, "id": $reqId,
@ -119,20 +140,26 @@ object ExecutionContextJsonMessages {
"expressionId": $expressionId, "expressionId": $expressionId,
"visualisationConfig": { "visualisationConfig": {
"executionContextId": ${configuration.executionContextId}, "executionContextId": ${configuration.executionContextId},
"visualisationModule": ${configuration.visualisationModule}, "expression": {
"expression": ${configuration.expression} "module": ${methodPointer.module},
"definedOnType": ${methodPointer.definedOnType},
"name": ${methodPointer.name}
}
} }
} }
} }
""" """
}
def executionContextAttachVisualisationRequest( def executionContextAttachVisualisationRequest(
reqId: Int, reqId: Int,
visualisationId: Api.VisualisationId, visualisationId: Api.VisualisationId,
expressionId: Api.ExpressionId, expressionId: Api.ExpressionId,
configuration: VisualisationConfiguration configuration: VisualisationConfiguration
) = ) = {
json""" configuration.expression match {
case VisualisationExpression.Text(module, expression) =>
json"""
{ "jsonrpc": "2.0", { "jsonrpc": "2.0",
"method": "executionContext/attachVisualisation", "method": "executionContext/attachVisualisation",
"id": $reqId, "id": $reqId,
@ -141,12 +168,33 @@ object ExecutionContextJsonMessages {
"expressionId": $expressionId, "expressionId": $expressionId,
"visualisationConfig": { "visualisationConfig": {
"executionContextId": ${configuration.executionContextId}, "executionContextId": ${configuration.executionContextId},
"visualisationModule": ${configuration.visualisationModule}, "visualisationModule": $module,
"expression": ${configuration.expression} "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( def executionContextModuleNotFound(
reqId: Int, reqId: Int,
@ -215,8 +263,10 @@ object ExecutionContextJsonMessages {
reqId: Int, reqId: Int,
visualisationId: Api.VisualisationId, visualisationId: Api.VisualisationId,
configuration: VisualisationConfiguration configuration: VisualisationConfiguration
) = ) = {
json""" configuration.expression match {
case VisualisationExpression.Text(module, expression) =>
json"""
{ "jsonrpc": "2.0", { "jsonrpc": "2.0",
"method": "executionContext/modifyVisualisation", "method": "executionContext/modifyVisualisation",
"id": $reqId, "id": $reqId,
@ -224,12 +274,32 @@ object ExecutionContextJsonMessages {
"visualisationId": $visualisationId, "visualisationId": $visualisationId,
"visualisationConfig": { "visualisationConfig": {
"executionContextId": ${configuration.executionContextId}, "executionContextId": ${configuration.executionContextId},
"visualisationModule": ${configuration.visualisationModule}, "visualisationModule": $module,
"expression": ${configuration.expression} "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( def executionContextGetComponentGroupsRequest(
reqId: Int, reqId: Int,

View File

@ -1,8 +1,10 @@
package org.enso.languageserver.websocket.json package org.enso.languageserver.websocket.json
import java.util.UUID import java.util.UUID
import io.circe.literal._ 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.polyglot.runtime.Runtime.Api
import org.enso.text.editing.model import org.enso.text.editing.model
@ -37,7 +39,60 @@ class VisualisationOperationsTest extends BaseServerTest {
config 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.visualisationModule shouldBe visualisationConfig.visualisationModule
config.executionContextId shouldBe visualisationConfig.executionContextId config.executionContextId shouldBe visualisationConfig.executionContextId
requestId requestId
@ -106,7 +161,7 @@ class VisualisationOperationsTest extends BaseServerTest {
config config
) )
) => ) =>
config.expression shouldBe visualisationConfig.expression config.expression shouldBe visualisationConfig.expression.toApi
config.visualisationModule shouldBe visualisationConfig.visualisationModule config.visualisationModule shouldBe visualisationConfig.visualisationModule
config.executionContextId shouldBe visualisationConfig.executionContextId config.executionContextId shouldBe visualisationConfig.executionContextId
requestId requestId
@ -155,7 +210,7 @@ class VisualisationOperationsTest extends BaseServerTest {
config config
) )
) => ) =>
config.expression shouldBe visualisationConfig.expression config.expression shouldBe visualisationConfig.expression.toApi
config.visualisationModule shouldBe visualisationConfig.visualisationModule config.visualisationModule shouldBe visualisationConfig.visualisationModule
config.executionContextId shouldBe visualisationConfig.executionContextId config.executionContextId shouldBe visualisationConfig.executionContextId
requestId requestId
@ -220,17 +275,14 @@ class VisualisationOperationsTest extends BaseServerTest {
val expressionId = UUID.randomUUID() val expressionId = UUID.randomUUID()
val client = getInitialisedWsClient() val client = getInitialisedWsClient()
val contextId = createExecutionContext(client) val contextId = createExecutionContext(client)
client.send(json""" client.send(
{ "jsonrpc": "2.0", ExecutionContextJsonMessages.executionContextDetachVisualisationRequest(
"method": "executionContext/detachVisualisation", 1,
"id": 1, contextId,
"params": { visualisationId,
"contextId": $contextId, expressionId
"visualisationId": $visualisationId, )
"expressionId": $expressionId )
}
}
""")
val requestId = val requestId =
runtimeConnectorProbe.receiveN(1).head match { runtimeConnectorProbe.receiveN(1).head match {
case Api.Request( case Api.Request(
@ -259,17 +311,14 @@ class VisualisationOperationsTest extends BaseServerTest {
val expressionId = UUID.randomUUID() val expressionId = UUID.randomUUID()
val contextId = UUID.randomUUID() val contextId = UUID.randomUUID()
val client = getInitialisedWsClient() val client = getInitialisedWsClient()
client.send(json""" client.send(
{ "jsonrpc": "2.0", ExecutionContextJsonMessages.executionContextDetachVisualisationRequest(
"method": "executionContext/detachVisualisation", 1,
"id": 1, contextId,
"params": { visualisationId,
"contextId": $contextId, expressionId
"visualisationId": $visualisationId, )
"expressionId": $expressionId )
}
}
""")
client.expectJson(json""" client.expectJson(json"""
{ "jsonrpc": "2.0", { "jsonrpc": "2.0",
"id" : 1, "id" : 1,
@ -291,20 +340,13 @@ class VisualisationOperationsTest extends BaseServerTest {
val contextId = createExecutionContext(client) val contextId = createExecutionContext(client)
val visualisationConfig = val visualisationConfig =
VisualisationConfiguration(contextId, "Foo.Bar.baz", "a=x+y") VisualisationConfiguration(contextId, "Foo.Bar.baz", "a=x+y")
client.send(json""" client.send(
{ "jsonrpc": "2.0", ExecutionContextJsonMessages.executionContextModifyVisualisationRequest(
"method": "executionContext/modifyVisualisation", 1,
"id": 1, visualisationId,
"params": { visualisationConfig
"visualisationId": $visualisationId, )
"visualisationConfig": { )
"executionContextId": $contextId,
"visualisationModule": ${visualisationConfig.visualisationModule},
"expression": ${visualisationConfig.expression}
}
}
}
""")
val requestId = val requestId =
runtimeConnectorProbe.receiveN(1).head match { runtimeConnectorProbe.receiveN(1).head match {
@ -312,7 +354,7 @@ class VisualisationOperationsTest extends BaseServerTest {
requestId, requestId,
Api.ModifyVisualisation(`visualisationId`, config) Api.ModifyVisualisation(`visualisationId`, config)
) => ) =>
config.expression shouldBe visualisationConfig.expression config.expression shouldBe visualisationConfig.expression.toApi
config.visualisationModule shouldBe visualisationConfig.visualisationModule config.visualisationModule shouldBe visualisationConfig.visualisationModule
config.executionContextId shouldBe visualisationConfig.executionContextId config.executionContextId shouldBe visualisationConfig.executionContextId
requestId requestId
@ -334,20 +376,14 @@ class VisualisationOperationsTest extends BaseServerTest {
val client = getInitialisedWsClient() val client = getInitialisedWsClient()
val visualisationConfig = val visualisationConfig =
VisualisationConfiguration(contextId, "Foo.Bar.baz", "a=x+y") VisualisationConfiguration(contextId, "Foo.Bar.baz", "a=x+y")
client.send(json"""
{ "jsonrpc": "2.0", client.send(
"method": "executionContext/modifyVisualisation", ExecutionContextJsonMessages.executionContextModifyVisualisationRequest(
"id": 1, 1,
"params": { visualisationId,
"visualisationId": $visualisationId, visualisationConfig
"visualisationConfig": { )
"executionContextId": $contextId, )
"visualisationModule": ${visualisationConfig.visualisationModule},
"expression": ${visualisationConfig.expression}
}
}
}
""")
client.expectJson(json""" client.expectJson(json"""
{ "jsonrpc": "2.0", { "jsonrpc": "2.0",
"id" : 1, "id" : 1,
@ -358,7 +394,6 @@ class VisualisationOperationsTest extends BaseServerTest {
} }
""") """)
} }
} }
private def createExecutionContext(client: WsTestClient): UUID = { private def createExecutionContext(client: WsTestClient): UUID = {

View File

@ -477,26 +477,76 @@ object Runtime {
expressionId: ExpressionId 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. /** A configuration object for properties of the visualisation.
* *
* @param executionContextId an execution context 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 the expression that creates a visualisation
*/ */
case class VisualisationConfiguration( case class VisualisationConfiguration(
executionContextId: ContextId, executionContextId: ContextId,
visualisationModule: String, expression: VisualisationExpression
expression: String
) extends ToLogString { ) extends ToLogString {
/** A qualified module name containing the expression. */
def visualisationModule: String =
expression.module
/** @inheritdoc */ /** @inheritdoc */
override def toLogString(shouldMask: Boolean): String = override def toLogString(shouldMask: Boolean): String =
s"VisualisationConfiguration(" + s"VisualisationConfiguration(" +
s"executionContextId=$executionContextId," + s"executionContextId=$executionContextId," +
s"visualisationModule=$visualisationModule,expression=" + s"expression=${expression.toLogString(shouldMask)})"
(if (shouldMask) STUB else expression) +
")"
} }
/** An operation applied to the suggestion argument. */ /** An operation applied to the suggestion argument. */

View File

@ -330,4 +330,5 @@ public class IdExecutionInstrument extends TruffleInstrument implements IdExecut
onExceptionalCallback, onExceptionalCallback,
timer)); timer));
} }
} }

View File

@ -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 contextId = UUID.randomUUID()
val requestId = UUID.randomUUID() val requestId = UUID.randomUUID()
val moduleName = "Enso_Test.Test.Main" val moduleName = "Enso_Test.Test.Main"
@ -1013,7 +1066,9 @@ class RuntimeServerTest
contextId, contextId,
mainFoo, mainFoo,
ConstantsGen.INTEGER, 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) context.executionComplete(contextId)
) )

View File

@ -23,7 +23,7 @@ import java.util.UUID
import scala.io.Source import scala.io.Source
@scala.annotation.nowarn("msg=multiarg infix syntax") @scala.annotation.nowarn("msg=multiarg infix syntax")
class RuntimeVisualisationsTest class RuntimeVisualizationsTest
extends AnyFlatSpec extends AnyFlatSpec
with Matchers with Matchers
with BeforeAndAfterEach { with BeforeAndAfterEach {
@ -234,13 +234,43 @@ class RuntimeVisualisationsTest
object Visualisation { object Visualisation {
val metadata = new Metadata
val code = val code =
""" metadata.appendToCode(
|encode = x -> x.to_text """
| |encode x = x.to_text
|incAndEncode = x -> encode x+1 |
| |incAndEncode x =
|""".stripMargin | 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, idMainRes,
Api.VisualisationConfiguration( Api.VisualisationConfiguration(
contextId, contextId,
"Enso_Test.Test.Visualisation", Api.VisualisationExpression.Text(
"x -> encode x" "Enso_Test.Test.Visualisation",
"x -> encode x"
)
) )
) )
) )
@ -423,8 +455,10 @@ class RuntimeVisualisationsTest
context.Main.idMainX, context.Main.idMainX,
Api.VisualisationConfiguration( Api.VisualisationConfiguration(
contextId, contextId,
"Enso_Test.Test.Visualisation", Api.VisualisationExpression.Text(
"x -> encode x" "Enso_Test.Test.Visualisation",
"x -> encode x"
)
) )
) )
) )
@ -552,8 +586,10 @@ class RuntimeVisualisationsTest
context.Main.idMainX, context.Main.idMainX,
Api.VisualisationConfiguration( Api.VisualisationConfiguration(
contextId, contextId,
"Enso_Test.Test.Visualisation", Api.VisualisationExpression.Text(
"x -> encode x" "Enso_Test.Test.Visualisation",
"x -> encode x"
)
) )
) )
) )
@ -675,8 +711,10 @@ class RuntimeVisualisationsTest
context.Main.idMainZ, context.Main.idMainZ,
Api.VisualisationConfiguration( Api.VisualisationConfiguration(
contextId, contextId,
"Enso_Test.Test.Visualisation", Api.VisualisationExpression.Text(
"encode" "Enso_Test.Test.Visualisation",
"encode"
)
) )
) )
) )
@ -796,8 +834,10 @@ class RuntimeVisualisationsTest
context.Main.idMainX, context.Main.idMainX,
Api.VisualisationConfiguration( Api.VisualisationConfiguration(
contextId, contextId,
"Enso_Test.Test.Visualisation", Api.VisualisationExpression.Text(
"x -> encode x" "Enso_Test.Test.Visualisation",
"x -> encode x"
)
) )
) )
) )
@ -832,8 +872,10 @@ class RuntimeVisualisationsTest
visualisationId, visualisationId,
Api.VisualisationConfiguration( Api.VisualisationConfiguration(
contextId, contextId,
"Enso_Test.Test.Visualisation", Api.VisualisationExpression.Text(
"x -> incAndEncode x" "Enso_Test.Test.Visualisation",
"x -> incAndEncode x"
)
) )
) )
) )
@ -899,8 +941,10 @@ class RuntimeVisualisationsTest
context.Main.idMainX, context.Main.idMainX,
Api.VisualisationConfiguration( Api.VisualisationConfiguration(
contextId, contextId,
"Enso_Test.Test.Visualisation", Api.VisualisationExpression.Text(
"x -> encode x" "Enso_Test.Test.Visualisation",
"x -> encode x"
)
) )
) )
) )
@ -1052,8 +1096,10 @@ class RuntimeVisualisationsTest
context.Main.idMainX, context.Main.idMainX,
Api.VisualisationConfiguration( Api.VisualisationConfiguration(
contextId, contextId,
"Enso_Test.Test.Visualisation", Api.VisualisationExpression.Text(
"encode" "Enso_Test.Test.Visualisation",
"encode"
)
) )
) )
) )
@ -1157,8 +1203,10 @@ class RuntimeVisualisationsTest
context.Main.idMainX, context.Main.idMainX,
Api.VisualisationConfiguration( Api.VisualisationConfiguration(
contextId, contextId,
"Enso_Test.Test.Visualisation", Api.VisualisationExpression.Text(
"x -> encode x" "Enso_Test.Test.Visualisation",
"x -> encode x"
)
) )
) )
) )
@ -1193,8 +1241,10 @@ class RuntimeVisualisationsTest
visualisationId, visualisationId,
Api.VisualisationConfiguration( Api.VisualisationConfiguration(
contextId, contextId,
"Enso_Test.Test.Visualisation", Api.VisualisationExpression.Text(
"x -> incAndEncode x" "Enso_Test.Test.Visualisation",
"x -> incAndEncode x"
)
) )
) )
) )
@ -1282,8 +1332,10 @@ class RuntimeVisualisationsTest
idMain, idMain,
Api.VisualisationConfiguration( Api.VisualisationConfiguration(
contextId, contextId,
"Test.Undefined", Api.VisualisationExpression.Text(
"x -> x" "Test.Undefined",
"x -> x"
)
) )
) )
) )
@ -1342,8 +1394,10 @@ class RuntimeVisualisationsTest
idMain, idMain,
Api.VisualisationConfiguration( Api.VisualisationConfiguration(
contextId, contextId,
"Standard.Visualization.Id", Api.VisualisationExpression.Text(
"x -> x.default_visualization.to_text" "Standard.Visualization.Id",
"x -> x.default_visualization.to_text"
)
) )
) )
) )
@ -1429,8 +1483,10 @@ class RuntimeVisualisationsTest
idMain, idMain,
Api.VisualisationConfiguration( Api.VisualisationConfiguration(
contextId, contextId,
"Enso_Test.Test.Main", Api.VisualisationExpression.Text(
"Main.does_not_exist" "Enso_Test.Test.Main",
"Main.does_not_exist"
)
) )
) )
) )
@ -1503,8 +1559,10 @@ class RuntimeVisualisationsTest
idMain, idMain,
Api.VisualisationConfiguration( Api.VisualisationConfiguration(
contextId, contextId,
moduleName, Api.VisualisationExpression.Text(
"x -> x.visualise_me" moduleName,
"x -> x.visualise_me"
)
) )
) )
) )
@ -1608,8 +1666,10 @@ class RuntimeVisualisationsTest
idMain, idMain,
Api.VisualisationConfiguration( Api.VisualisationConfiguration(
contextId, contextId,
"Enso_Test.Test.Visualisation", Api.VisualisationExpression.Text(
"inc_and_encode" "Enso_Test.Test.Visualisation",
"inc_and_encode"
)
) )
) )
) )
@ -1720,8 +1780,10 @@ class RuntimeVisualisationsTest
idMain, idMain,
Api.VisualisationConfiguration( Api.VisualisationConfiguration(
contextId, contextId,
moduleName, Api.VisualisationExpression.Text(
"x -> x.catch_primitive _.to_text" moduleName,
"x -> x.catch_primitive _.to_text"
)
) )
) )
) )
@ -1806,8 +1868,10 @@ class RuntimeVisualisationsTest
idMain, idMain,
Api.VisualisationConfiguration( Api.VisualisationConfiguration(
contextId, contextId,
moduleName, Api.VisualisationExpression.Text(
"x -> Panic.catch_primitive x caught_panic-> caught_panic.payload.to_text" moduleName,
"x -> Panic.catch_primitive x caught_panic-> caught_panic.payload.to_text"
)
) )
) )
) )
@ -1920,8 +1984,10 @@ class RuntimeVisualisationsTest
idMain, idMain,
Api.VisualisationConfiguration( Api.VisualisationConfiguration(
contextId, contextId,
visualisationModule, Api.VisualisationExpression.Text(
visualisationCode visualisationModule,
visualisationCode
)
) )
) )
) )
@ -1948,4 +2014,284 @@ class RuntimeVisualisationsTest
val stringified = new String(data) val stringified = new String(data)
stringified shouldEqual """{ "kind": "Dataflow", "message": "The List is empty."}""" 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...")
}
} }

View File

@ -19,6 +19,7 @@ import com.oracle.truffle.api.source.SourceSection;
import org.enso.interpreter.runtime.callable.function.Function; import org.enso.interpreter.runtime.callable.function.Function;
import org.enso.interpreter.runtime.tag.IdentifiedTag; import org.enso.interpreter.runtime.tag.IdentifiedTag;
import java.util.Arrays;
import java.util.UUID; import java.util.UUID;
/** /**
@ -92,8 +93,13 @@ public class FunctionCallInstrumentationNode extends Node implements Instrumenta
FunctionCall functionCall, FunctionCall functionCall,
Object[] arguments, Object[] arguments,
@Cached InteropApplicationNode interopApplicationNode) { @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( return interopApplicationNode.execute(
functionCall.function, functionCall.state, functionCall.arguments); functionCall.function, functionCall.state, callArguments);
} }
} }

View File

@ -1,5 +1,6 @@
package org.enso.interpreter.service; package org.enso.interpreter.service;
import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.TruffleLogger; import com.oracle.truffle.api.TruffleLogger;
import com.oracle.truffle.api.instrumentation.EventBinding; import com.oracle.truffle.api.instrumentation.EventBinding;
@ -48,6 +49,8 @@ import java.util.function.Consumer;
* language. * language.
*/ */
public class ExecutionService { public class ExecutionService {
private static final String MAIN_METHOD = "main";
private final Context context; private final Context context;
private final Optional<IdExecutionService> idExecutionInstrument; private final Optional<IdExecutionService> idExecutionInstrument;
private final NotificationHandler.Forwarder notificationForwarder; private final NotificationHandler.Forwarder notificationForwarder;
@ -86,7 +89,7 @@ public class ExecutionService {
return logger; return logger;
} }
private FunctionCallInstrumentationNode.FunctionCall prepareFunctionCall( public FunctionCallInstrumentationNode.FunctionCall prepareFunctionCall(
Module module, String consName, String methodName) Module module, String consName, String methodName)
throws ConstructorNotFoundException, MethodNotFoundException { throws ConstructorNotFoundException, MethodNotFoundException {
ModuleScope scope = module.compileScope(context); ModuleScope scope = module.compileScope(context);
@ -99,8 +102,9 @@ public class ExecutionService {
if (function == null) { if (function == null) {
throw new MethodNotFoundException(module.getName().toString(), atomConstructor, methodName); throw new MethodNotFoundException(module.getName().toString(), atomConstructor, methodName);
} }
return new FunctionCallInstrumentationNode.FunctionCall( Object[] arguments =
function, EmptyMap.create(), new Object[] {}); MAIN_METHOD.equals(methodName) ? new Object[] {} : new Object[] {atomConstructor};
return new FunctionCallInstrumentationNode.FunctionCall(function, EmptyMap.create(), arguments);
} }
public void initializeLanguageServerConnection(Endpoint endpoint) { 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<IdExecutionService.ExpressionCall> funCallCallback =
(value) -> context.getLogger().finest("ON_CACHED_CALL " + value.getExpressionId());
Consumer<IdExecutionService.ExpressionValue> onComputedCallback =
(value) -> context.getLogger().finest("ON_COMPUTED " + value.getExpressionId());
Consumer<IdExecutionService.ExpressionValue> onCachedCallback =
(value) -> context.getLogger().finest("ON_CACHED_VALUE " + value.getExpressionId());
Consumer<Exception> onExceptionalCallback =
(value) -> context.getLogger().finest("ON_ERROR " + value);
Optional<EventBinding<ExecutionEventListener>> 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. * Sets a module at a given path to use a literal source.
* *

View File

@ -29,7 +29,7 @@ object CacheInvalidation {
sealed trait IndexSelector sealed trait IndexSelector
object IndexSelector { object IndexSelector {
/** Invalidate value from indexes. */ /** Invalidate value from all indexes. */
case object All extends IndexSelector case object All extends IndexSelector
/** Invalidate the types index. */ /** Invalidate the types index. */
@ -118,6 +118,38 @@ object CacheInvalidation {
): Unit = ): Unit =
instructions.foreach(run(stack, _)) 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. /** Run a cache invalidation instruction on an execution stack.
* *
* @param stack the runtime stack * @param stack the runtime stack
@ -148,6 +180,36 @@ object CacheInvalidation {
frames.foreach(frame => run(frame.cache, frame.syncState, command, indexes)) 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. /** Run cache invalidation of a single instrument frame.
* *
* @param cache the cache to invalidate * @param cache the cache to invalidate

View File

@ -1,5 +1,6 @@
package org.enso.interpreter.instrument package org.enso.interpreter.instrument
import org.enso.pkg.QualifiedName
import org.enso.polyglot.runtime.Runtime.Api.{ import org.enso.polyglot.runtime.Runtime.Api.{
ContextId, ContextId,
ExpressionId, ExpressionId,
@ -7,7 +8,7 @@ import org.enso.polyglot.runtime.Runtime.Api.{
VisualisationId VisualisationId
} }
import scala.collection.mutable.Stack import scala.collection.mutable
/** Storage for active execution contexts. /** Storage for active execution contexts.
*/ */
@ -50,16 +51,17 @@ class ExecutionContextManager {
* @param id the context id. * @param id the context id.
* @return the stack. * @return the stack.
*/ */
def getStack(id: ContextId): Stack[InstrumentFrame] = def getStack(id: ContextId): mutable.Stack[InstrumentFrame] =
synchronized { synchronized {
contexts(id).stack contexts(id).stack
} }
/** Gets all execution contexts. /** 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 { synchronized {
contexts.view.mapValues(_.stack) contexts.view.mapValues(_.stack)
} }
@ -114,6 +116,22 @@ class ExecutionContextManager {
state.visualisations.upsert(visualisation) 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. /** Returns a visualisation with the provided id.
* *
* @param contextId the identifier of the execution context * @param contextId the identifier of the execution context
@ -148,6 +166,25 @@ class ExecutionContextManager {
} yield visualisation } 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. /** Removes a visualisation from the holder.
* *
* @param contextId the identifier of the execution context * @param contextId the identifier of the execution context

View File

@ -37,6 +37,11 @@ case class InstrumentFrame(
case object InstrumentFrame { case object InstrumentFrame {
/** Create an instrument frame.
*
* @param item the stack item
* @return an instance of [[InstrumentFrame]]
*/
def apply(item: StackItem): InstrumentFrame = def apply(item: StackItem): InstrumentFrame =
new InstrumentFrame(item, new RuntimeCache, new UpdatesSynchronizationState) new InstrumentFrame(item, new RuntimeCache, new UpdatesSynchronizationState)
} }

View File

@ -1,6 +1,11 @@
package org.enso.interpreter.instrument 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. /** An object containing visualisation data.
* *
@ -12,5 +17,9 @@ import org.enso.polyglot.runtime.Runtime.Api.{ExpressionId, VisualisationId}
case class Visualisation( case class Visualisation(
id: VisualisationId, id: VisualisationId,
expressionId: ExpressionId, expressionId: ExpressionId,
cache: RuntimeCache,
module: Module,
config: VisualisationConfiguration,
visualisationExpressionId: Option[ExpressionId],
callback: AnyRef callback: AnyRef
) )

View File

@ -1,13 +1,16 @@
package org.enso.interpreter.instrument package org.enso.interpreter.instrument
import org.enso.pkg.QualifiedName
import org.enso.polyglot.runtime.Runtime.Api.{ExpressionId, VisualisationId} import org.enso.polyglot.runtime.Runtime.Api.{ExpressionId, VisualisationId}
import scala.collection.mutable
/** A mutable holder of all visualisations attached to an execution context. /** A mutable holder of all visualisations attached to an execution context.
*/ */
class VisualisationHolder() { class VisualisationHolder() {
private var visualisationMap: Map[ExpressionId, List[Visualisation]] = private val visualisationMap: mutable.Map[ExpressionId, List[Visualisation]] =
Map.empty.withDefaultValue(List.empty) mutable.Map.empty.withDefaultValue(List.empty)
/** Upserts a visualisation. /** Upserts a visualisation.
* *
@ -15,8 +18,8 @@ class VisualisationHolder() {
*/ */
def upsert(visualisation: Visualisation): Unit = { def upsert(visualisation: Visualisation): Unit = {
val visualisations = visualisationMap(visualisation.expressionId) val visualisations = visualisationMap(visualisation.expressionId)
val removed = visualisations.filterNot(_.id == visualisation.id) val rest = visualisations.filterNot(_.id == visualisation.id)
visualisationMap += (visualisation.expressionId -> (visualisation :: removed)) visualisationMap.update(visualisation.expressionId, visualisation :: rest)
} }
/** Removes a visualisation from the holder. /** Removes a visualisation from the holder.
@ -30,8 +33,8 @@ class VisualisationHolder() {
expressionId: ExpressionId expressionId: ExpressionId
): Unit = { ): Unit = {
val visualisations = visualisationMap(expressionId) val visualisations = visualisationMap(expressionId)
val removed = visualisations.filterNot(_.id == visualisationId) val rest = visualisations.filterNot(_.id == visualisationId)
visualisationMap += (expressionId -> removed) visualisationMap.update(expressionId, rest)
} }
/** Finds all visualisations attached to an expression. /** Finds all visualisations attached to an expression.
@ -42,6 +45,14 @@ class VisualisationHolder() {
def find(expressionId: ExpressionId): List[Visualisation] = def find(expressionId: ExpressionId): List[Visualisation] =
visualisationMap(expressionId) 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. /** Returns a visualisation with the provided id.
* *
* @param visualisationId the identifier of visualisation * @param visualisationId the identifier of visualisation
@ -50,12 +61,14 @@ class VisualisationHolder() {
def getById(visualisationId: VisualisationId): Option[Visualisation] = def getById(visualisationId: VisualisationId): Option[Visualisation] =
visualisationMap.values.flatten.find(_.id == visualisationId) visualisationMap.values.flatten.find(_.id == visualisationId)
/** @return all available visualisations. */
def getAll: Iterable[Visualisation] =
visualisationMap.values.flatten
} }
object VisualisationHolder { object VisualisationHolder {
/** Returns an empty holder. /** Returns an empty visualisation holder. */
*/
def empty = new VisualisationHolder() def empty = new VisualisationHolder()
} }

View File

@ -49,10 +49,10 @@ class AttachVisualisationCmd(
ctx.jobProcessor.run( ctx.jobProcessor.run(
new UpsertVisualisationJob( new UpsertVisualisationJob(
maybeRequestId, maybeRequestId,
Api.VisualisationAttached(),
request.visualisationId, request.visualisationId,
request.expressionId, request.expressionId,
request.visualisationConfig, request.visualisationConfig
Api.VisualisationAttached()
) )
) )

View File

@ -42,7 +42,7 @@ class EditFileCmd(request: Api.EditFileNotification) extends Command(None) {
private def executeJobs(implicit private def executeJobs(implicit
ctx: RuntimeContext ctx: RuntimeContext
): Iterable[ExecuteJob] = { ): Iterable[ExecuteJob] = {
ctx.contextManager.getAll ctx.contextManager.getAllContexts
.collect { .collect {
case (contextId, stack) if stack.nonEmpty => case (contextId, stack) if stack.nonEmpty =>
new ExecuteJob(contextId, stack.toList) new ExecuteJob(contextId, stack.toList)

View File

@ -60,10 +60,10 @@ class ModifyVisualisationCmd(
ctx.jobProcessor.run( ctx.jobProcessor.run(
new UpsertVisualisationJob( new UpsertVisualisationJob(
maybeRequestId, maybeRequestId,
Api.VisualisationModified(),
request.visualisationId, request.visualisationId,
visualisation.expressionId, visualisation.expressionId,
request.visualisationConfig, request.visualisationConfig
Api.VisualisationModified()
) )
) )
maybeFutureExecutable flatMap { maybeFutureExecutable flatMap {

View File

@ -39,7 +39,7 @@ class RenameProjectCmd(
request.oldName, request.oldName,
request.newName request.newName
) )
ctx.contextManager.getAll.values ctx.contextManager.getAllContexts.values
.foreach(updateMethodPointers(request.newName, _)) .foreach(updateMethodPointers(request.newName, _))
reply(Api.ProjectRenamed(request.namespace, request.newName)) reply(Api.ProjectRenamed(request.namespace, request.newName))
logger.log( logger.log(

View File

@ -47,7 +47,7 @@ class SetExpressionValueCmd(request: Api.SetExpressionValueNotification)
private def executeJobs(implicit private def executeJobs(implicit
ctx: RuntimeContext ctx: RuntimeContext
): Iterable[ExecuteJob] = { ): Iterable[ExecuteJob] = {
ctx.contextManager.getAll ctx.contextManager.getAllContexts
.collect { .collect {
case (contextId, stack) if stack.nonEmpty => case (contextId, stack) if stack.nonEmpty =>
new ExecuteJob(contextId, stack.toList) new ExecuteJob(contextId, stack.toList)

View File

@ -12,10 +12,16 @@ import org.enso.interpreter.instrument.execution.{
LocationResolver, LocationResolver,
RuntimeContext 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.runtime.Module
import org.enso.interpreter.service.error.ModuleNotFoundForFileException 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
import org.enso.polyglot.runtime.Runtime.Api.StackItem
import org.enso.text.buffer.Rope import org.enso.text.buffer.Rope
import java.io.File import java.io.File
@ -39,17 +45,7 @@ final class EnsureCompiledJob(protected val files: Iterable[File])
try { try {
val compilationResult = ensureCompiledFiles(files) val compilationResult = ensureCompiledFiles(files)
ctx.contextManager.getAll.values.foreach { stack => setCacheWeights()
getCacheMetadata(stack).foreach { metadata =>
CacheInvalidation.run(
stack,
CacheInvalidation(
CacheInvalidation.StackSelector.Top,
CacheInvalidation.Command.SetMetadata(metadata)
)
)
}
}
compilationResult compilationResult
} finally { } finally {
ctx.locking.releaseWriteCompilationLock() ctx.locking.releaseWriteCompilationLock()
@ -89,12 +85,7 @@ final class EnsureCompiledJob(protected val files: Iterable[File])
applyEdits(new File(module.getPath)).map { changeset => applyEdits(new File(module.getPath)).map { changeset =>
compile(module) compile(module)
.map { compilerResult => .map { compilerResult =>
val cacheInvalidationCommands = invalidateCaches(module, changeset)
buildCacheInvalidationCommands(
changeset,
module.getSource.getCharacters
)
runInvalidationCommands(cacheInvalidationCommands)
ctx.jobProcessor.runBackground( ctx.jobProcessor.runBackground(
AnalyzeModuleInScopeJob( AnalyzeModuleInScopeJob(
module.getName, module.getName,
@ -301,18 +292,44 @@ final class EnsureCompiledJob(protected val files: Iterable[File])
/** Run the invalidation commands. /** 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 * @param ctx the runtime context
*/ */
private def runInvalidationCommands( private def invalidateCaches(
invalidationCommands: Iterable[CacheInvalidation] module: Module,
changeset: Changeset[_]
)(implicit ctx: RuntimeContext): Unit = { )(implicit ctx: RuntimeContext): Unit = {
ctx.contextManager.getAll.values val invalidationCommands =
buildCacheInvalidationCommands(
changeset,
module.getSource.getCharacters
)
ctx.contextManager.getAllContexts.values
.foreach { stack => .foreach { stack =>
if (stack.nonEmpty) { if (stack.nonEmpty && isStackInModule(module.getName, stack)) {
CacheInvalidation.runAll(stack, invalidationCommands) 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. /** Send notification about the compilation status.
@ -325,7 +342,7 @@ final class EnsureCompiledJob(protected val files: Iterable[File])
diagnostics: Seq[Api.ExecutionResult.Diagnostic] diagnostics: Seq[Api.ExecutionResult.Diagnostic]
)(implicit ctx: RuntimeContext): Unit = )(implicit ctx: RuntimeContext): Unit =
if (diagnostics.nonEmpty) { if (diagnostics.nonEmpty) {
ctx.contextManager.getAll.keys.foreach { contextId => ctx.contextManager.getAllContexts.keys.foreach { contextId =>
ctx.endpoint.sendToClient( ctx.endpoint.sendToClient(
Api.Response(Api.ExecutionUpdate(contextId, diagnostics)) Api.Response(Api.ExecutionUpdate(contextId, diagnostics))
) )
@ -340,7 +357,7 @@ final class EnsureCompiledJob(protected val files: Iterable[File])
private def sendFailureUpdate( private def sendFailureUpdate(
failure: Api.ExecutionResult.Failure failure: Api.ExecutionResult.Failure
)(implicit ctx: RuntimeContext): Unit = )(implicit ctx: RuntimeContext): Unit =
ctx.contextManager.getAll.keys.foreach { contextId => ctx.contextManager.getAllContexts.keys.foreach { contextId =>
ctx.endpoint.sendToClient( ctx.endpoint.sendToClient(
Api.Response(Api.ExecutionFailed(contextId, failure)) Api.Response(Api.ExecutionFailed(contextId, failure))
) )
@ -354,6 +371,27 @@ final class EnsureCompiledJob(protected val files: Iterable[File])
else else
CompilationStatus.Success 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( private def getCacheMetadata(
stack: Iterable[InstrumentFrame] stack: Iterable[InstrumentFrame]
)(implicit ctx: RuntimeContext): Option[CachePreferenceAnalysis.Metadata] = )(implicit ctx: RuntimeContext): Option[CachePreferenceAnalysis.Metadata] =
@ -370,12 +408,37 @@ final class EnsureCompiledJob(protected val files: Iterable[File])
case _ => None 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. */ /** Get all modules in the current compiler scope. */
private def getModulesInScope(implicit private def getModulesInScope(implicit
ctx: RuntimeContext ctx: RuntimeContext
): Iterable[Module] = ): Iterable[Module] =
ctx.executionService.getContext.getTopScope.getModules.asScala 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 { object EnsureCompiledJob {

View File

@ -429,12 +429,14 @@ object ProgramExecutionSupport {
Either Either
.catchNonFatal { .catchNonFatal {
ctx.executionService.getLogger.log( ctx.executionService.getLogger.log(
Level.FINEST, Level.FINE,
s"Executing visualisation ${visualisation.expressionId}" s"Executing visualisation ${visualisation.expressionId}"
) )
ctx.executionService.callFunction( ctx.executionService.callFunctionWithInstrument(
visualisation.module,
visualisation.callback, visualisation.callback,
expressionValue expressionValue,
visualisation.cache
) )
} }
.flatMap { .flatMap {

View File

@ -1,42 +1,45 @@
package org.enso.interpreter.instrument.job package org.enso.interpreter.instrument.job
import java.util.logging.Level
import cats.implicits._ 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.execution.{Executable, RuntimeContext}
import org.enso.interpreter.instrument.job.UpsertVisualisationJob.{ import org.enso.interpreter.instrument.job.UpsertVisualisationJob.{
EvalFailure,
EvaluationFailed, EvaluationFailed,
MaxEvaluationRetryCount, EvaluationResult,
ModuleNotFound ModuleNotFound
} }
import org.enso.interpreter.instrument.{
CacheInvalidation,
InstrumentFrame,
RuntimeCache,
Visualisation
}
import org.enso.interpreter.runtime.Module import org.enso.interpreter.runtime.Module
import org.enso.interpreter.runtime.control.ThreadInterruptedException import org.enso.interpreter.runtime.control.ThreadInterruptedException
import org.enso.polyglot.runtime.Runtime.Api.{ import org.enso.pkg.QualifiedName
ExpressionId, import org.enso.polyglot.runtime.Runtime.Api._
RequestId,
VisualisationId
}
import org.enso.polyglot.runtime.Runtime.{Api, ApiResponse} import org.enso.polyglot.runtime.Runtime.{Api, ApiResponse}
import java.util.logging.Level
/** A job that upserts a visualisation. /** A job that upserts a visualisation.
* *
* @param requestId maybe a request id * @param requestId maybe a request id
* @param response a response used to reply to a client
* @param visualisationId an identifier of visualisation * @param visualisationId an identifier of visualisation
* @param expressionId an identifier of expression * @param expressionId an identifier of expression
* @param config a visualisation config * @param config a visualisation config
* @param response a response used to reply to a client
*/ */
class UpsertVisualisationJob( class UpsertVisualisationJob(
requestId: Option[RequestId], requestId: Option[RequestId],
response: ApiResponse,
visualisationId: VisualisationId, visualisationId: VisualisationId,
expressionId: ExpressionId, expressionId: ExpressionId,
config: Api.VisualisationConfiguration, config: Api.VisualisationConfiguration
response: ApiResponse
) extends Job[Option[Executable]]( ) extends Job[Option[Executable]](
List(config.executionContextId), List(config.executionContextId),
true, false,
false false
) { ) {
@ -46,24 +49,36 @@ class UpsertVisualisationJob(
ctx.locking.acquireWriteCompilationLock() ctx.locking.acquireWriteCompilationLock()
try { try {
val maybeCallable = val maybeCallable =
evaluateExpression(config.visualisationModule, config.expression) UpsertVisualisationJob.evaluateVisualisationExpression(
config.expression
)
maybeCallable match { maybeCallable match {
case Left(ModuleNotFound) => case Left(ModuleNotFound(moduleName)) =>
replyWithModuleNotFoundError() replyWithModuleNotFoundError(moduleName)
None None
case Left(EvaluationFailed(message, result)) => case Left(EvaluationFailed(message, result)) =>
replyWithExpressionFailedError(message, result) replyWithExpressionFailedError(message, result)
None None
case Right(callable) => case Right(EvaluationResult(module, callable)) =>
val visualisation = updateVisualisation(callable) val visualisation =
UpsertVisualisationJob.updateVisualisation(
visualisationId,
expressionId,
module,
config,
callable
)
ctx.endpoint.sendToClient(Api.Response(requestId, response)) ctx.endpoint.sendToClient(Api.Response(requestId, response))
val stack = ctx.contextManager.getStack(config.executionContextId) val stack = ctx.contextManager.getStack(config.executionContextId)
val cachedValue = stack.headOption val cachedValue = stack.headOption
.flatMap(frame => Option(frame.cache.get(expressionId))) .flatMap(frame => Option(frame.cache.get(expressionId)))
requireVisualisationSynchronization(stack, expressionId) UpsertVisualisationJob.requireVisualisationSynchronization(
stack,
expressionId
)
cachedValue match { cachedValue match {
case Some(value) => case Some(value) =>
ProgramExecutionSupport.sendVisualisationUpdate( 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( private def replyWithExpressionFailedError(
message: String, message: String,
executionResult: Option[Api.ExecutionResult.Diagnostic] executionResult: Option[Api.ExecutionResult.Diagnostic]
@ -118,39 +111,115 @@ class UpsertVisualisationJob(
) )
} }
private def replyWithModuleNotFoundError()(implicit private def replyWithModuleNotFoundError(module: String)(implicit
ctx: RuntimeContext ctx: RuntimeContext
): Unit = { ): Unit = {
ctx.endpoint.sendToClient( ctx.endpoint.sendToClient(
Api.Response( Api.Response(requestId, Api.ModuleNotFound(module))
requestId,
Api.ModuleNotFound(config.visualisationModule)
)
) )
} }
}
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( private def findModule(
moduleName: String moduleName: String
)(implicit ctx: RuntimeContext): Either[EvalFailure, Module] = { )(implicit ctx: RuntimeContext): Either[EvaluationFailure, Module] = {
val context = ctx.executionService.getContext val context = ctx.executionService.getContext
// TODO [RW] more specific error when the module cannot be installed (#1861)
context.ensureModuleIsLoaded(moduleName) context.ensureModuleIsLoaded(moduleName)
val maybeModule = context.findModule(moduleName) val maybeModule = context.findModule(moduleName)
if (maybeModule.isPresent) Right(maybeModule.get()) 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( private def evaluateModuleExpression(
module: Module, module: Module,
expression: String, expression: Api.VisualisationExpression,
retryCount: Int = 0 retryCount: Int = 0
)(implicit )(implicit
ctx: RuntimeContext ctx: RuntimeContext
): Either[EvalFailure, AnyRef] = ): Either[EvaluationFailure, EvaluationResult] =
Either Either
.catchNonFatal { .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 { .leftFlatMap {
case _: ThreadInterruptedException case _: ThreadInterruptedException
@ -193,38 +262,110 @@ class UpsertVisualisationJob(
) )
} }
private def evaluateExpression( /** Evaluate the visualisation expression.
moduleName: String, *
expression: String * @param expression the visualisation expression to evaluate
)(implicit ctx: RuntimeContext): Either[EvalFailure, AnyRef] = * @return either the evaluation result or an evaluation error
*/
private def evaluateVisualisationExpression(
expression: Api.VisualisationExpression
)(implicit
ctx: RuntimeContext
): Either[EvaluationFailure, EvaluationResult] = {
for { for {
module <- findModule(moduleName) module <- findModule(expression.module)
expression <- evaluateModuleExpression(module, expression) expression <- evaluateModuleExpression(module, expression)
} yield expression } yield expression
}
} /** Update the visualisation state.
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.
* *
* @param message the textual reason of a failure * @param visualisationId the visualisation identifier
* @param failure the error description * @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( private def updateVisualisation(
message: String, visualisationId: VisualisationId,
failure: Option[Api.ExecutionResult.Diagnostic] expressionId: ExpressionId,
) extends EvalFailure 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))
} }