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]
- [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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,6 +113,8 @@ object ExecutionContextJsonMessages {
expressionId: Api.ExpressionId,
configuration: VisualisationConfiguration
) =
configuration.expression match {
case VisualisationExpression.Text(module, expression) =>
json"""
{ "jsonrpc": "2.0",
"method": "executionContext/executeExpression",
@ -119,19 +124,41 @@ 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/executeExpression",
"id": $reqId,
"params": {
"visualisationId": $visualisationId,
"expressionId": $expressionId,
"visualisationConfig": {
"executionContextId": ${configuration.executionContextId},
"expression": {
"module": ${methodPointer.module},
"definedOnType": ${methodPointer.definedOnType},
"name": ${methodPointer.name}
}
}
}
}
"""
}
def executionContextAttachVisualisationRequest(
reqId: Int,
visualisationId: Api.VisualisationId,
expressionId: Api.ExpressionId,
configuration: VisualisationConfiguration
) =
) = {
configuration.expression match {
case VisualisationExpression.Text(module, expression) =>
json"""
{ "jsonrpc": "2.0",
"method": "executionContext/attachVisualisation",
@ -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,7 +263,9 @@ object ExecutionContextJsonMessages {
reqId: Int,
visualisationId: Api.VisualisationId,
configuration: VisualisationConfiguration
) =
) = {
configuration.expression match {
case VisualisationExpression.Text(module, expression) =>
json"""
{ "jsonrpc": "2.0",
"method": "executionContext/modifyVisualisation",
@ -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,

View File

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

View File

@ -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. */

View File

@ -330,4 +330,5 @@ public class IdExecutionInstrument extends TruffleInstrument implements IdExecut
onExceptionalCallback,
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 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)
)

View File

@ -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 =
metadata.appendToCode(
"""
|encode = x -> x.to_text
|encode x = x.to_text
|
|incAndEncode = x -> encode x+1
|incAndEncode x =
| y = x + 1
| encode y
|
|""".stripMargin
|""".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,12 +341,14 @@ class RuntimeVisualisationsTest
idMainRes,
Api.VisualisationConfiguration(
contextId,
Api.VisualisationExpression.Text(
"Enso_Test.Test.Visualisation",
"x -> encode x"
)
)
)
)
)
val attachVisualisationResponses = context.receiveN(3)
attachVisualisationResponses should contain allOf (
Api.Response(requestId, Api.VisualisationAttached()),
@ -423,12 +455,14 @@ class RuntimeVisualisationsTest
context.Main.idMainX,
Api.VisualisationConfiguration(
contextId,
Api.VisualisationExpression.Text(
"Enso_Test.Test.Visualisation",
"x -> encode x"
)
)
)
)
)
val attachVisualisationResponses = context.receiveN(2)
attachVisualisationResponses should contain(
Api.Response(requestId, Api.VisualisationAttached())
@ -552,12 +586,14 @@ class RuntimeVisualisationsTest
context.Main.idMainX,
Api.VisualisationConfiguration(
contextId,
Api.VisualisationExpression.Text(
"Enso_Test.Test.Visualisation",
"x -> encode x"
)
)
)
)
)
val attachVisualisationResponses = context.receiveN(2)
attachVisualisationResponses should contain(
Api.Response(requestId, Api.VisualisationAttached())
@ -675,12 +711,14 @@ class RuntimeVisualisationsTest
context.Main.idMainZ,
Api.VisualisationConfiguration(
contextId,
Api.VisualisationExpression.Text(
"Enso_Test.Test.Visualisation",
"encode"
)
)
)
)
)
val attachVisualisationResponses = context.receiveN(2)
attachVisualisationResponses should contain(
Api.Response(requestId, Api.VisualisationAttached())
@ -796,12 +834,14 @@ class RuntimeVisualisationsTest
context.Main.idMainX,
Api.VisualisationConfiguration(
contextId,
Api.VisualisationExpression.Text(
"Enso_Test.Test.Visualisation",
"x -> encode x"
)
)
)
)
)
val attachVisualisationResponses = context.receiveN(2)
attachVisualisationResponses should contain(
@ -832,12 +872,14 @@ class RuntimeVisualisationsTest
visualisationId,
Api.VisualisationConfiguration(
contextId,
Api.VisualisationExpression.Text(
"Enso_Test.Test.Visualisation",
"x -> incAndEncode x"
)
)
)
)
)
val modifyVisualisationResponses = context.receiveN(2)
modifyVisualisationResponses should contain(
Api.Response(requestId, Api.VisualisationModified())
@ -899,12 +941,14 @@ class RuntimeVisualisationsTest
context.Main.idMainX,
Api.VisualisationConfiguration(
contextId,
Api.VisualisationExpression.Text(
"Enso_Test.Test.Visualisation",
"x -> encode x"
)
)
)
)
)
context.receiveN(3) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.VisualisationAttached()),
Api.Response(
@ -1052,12 +1096,14 @@ class RuntimeVisualisationsTest
context.Main.idMainX,
Api.VisualisationConfiguration(
contextId,
Api.VisualisationExpression.Text(
"Enso_Test.Test.Visualisation",
"encode"
)
)
)
)
)
val attachVisualisationResponses = context.receiveN(2)
attachVisualisationResponses should contain(
Api.Response(requestId, Api.VisualisationAttached())
@ -1157,12 +1203,14 @@ class RuntimeVisualisationsTest
context.Main.idMainX,
Api.VisualisationConfiguration(
contextId,
Api.VisualisationExpression.Text(
"Enso_Test.Test.Visualisation",
"x -> encode x"
)
)
)
)
)
val attachVisualisationResponses = context.receiveN(2)
attachVisualisationResponses should contain(
@ -1193,12 +1241,14 @@ class RuntimeVisualisationsTest
visualisationId,
Api.VisualisationConfiguration(
contextId,
Api.VisualisationExpression.Text(
"Enso_Test.Test.Visualisation",
"x -> incAndEncode x"
)
)
)
)
)
// detach visualisation
context.send(
Api.Request(
@ -1282,12 +1332,14 @@ class RuntimeVisualisationsTest
idMain,
Api.VisualisationConfiguration(
contextId,
Api.VisualisationExpression.Text(
"Test.Undefined",
"x -> x"
)
)
)
)
)
context.receiveN(1) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.ModuleNotFound("Test.Undefined"))
)
@ -1342,12 +1394,14 @@ class RuntimeVisualisationsTest
idMain,
Api.VisualisationConfiguration(
contextId,
Api.VisualisationExpression.Text(
"Standard.Visualization.Id",
"x -> x.default_visualization.to_text"
)
)
)
)
)
val attachVisualisationResponses = context.receiveN(4)
attachVisualisationResponses should contain allOf (
@ -1429,12 +1483,14 @@ class RuntimeVisualisationsTest
idMain,
Api.VisualisationConfiguration(
contextId,
Api.VisualisationExpression.Text(
"Enso_Test.Test.Main",
"Main.does_not_exist"
)
)
)
)
)
context.receiveN(1) should contain theSameElementsAs Seq(
Api.Response(
requestId,
@ -1503,12 +1559,14 @@ class RuntimeVisualisationsTest
idMain,
Api.VisualisationConfiguration(
contextId,
Api.VisualisationExpression.Text(
moduleName,
"x -> x.visualise_me"
)
)
)
)
)
context.receiveN(3) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.VisualisationAttached()),
Api.Response(
@ -1608,12 +1666,14 @@ class RuntimeVisualisationsTest
idMain,
Api.VisualisationConfiguration(
contextId,
Api.VisualisationExpression.Text(
"Enso_Test.Test.Visualisation",
"inc_and_encode"
)
)
)
)
)
context.receiveN(3) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.VisualisationAttached()),
Api.Response(
@ -1720,12 +1780,14 @@ class RuntimeVisualisationsTest
idMain,
Api.VisualisationConfiguration(
contextId,
Api.VisualisationExpression.Text(
moduleName,
"x -> x.catch_primitive _.to_text"
)
)
)
)
)
val attachVisualisationResponses = context.receiveN(3)
attachVisualisationResponses should contain allOf (
Api.Response(requestId, Api.VisualisationAttached()),
@ -1806,12 +1868,14 @@ class RuntimeVisualisationsTest
idMain,
Api.VisualisationConfiguration(
contextId,
Api.VisualisationExpression.Text(
moduleName,
"x -> Panic.catch_primitive x caught_panic-> caught_panic.payload.to_text"
)
)
)
)
)
context.receiveN(4) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.VisualisationAttached()),
TestMessages.panic(
@ -1920,12 +1984,14 @@ class RuntimeVisualisationsTest
idMain,
Api.VisualisationConfiguration(
contextId,
Api.VisualisationExpression.Text(
visualisationModule,
visualisationCode
)
)
)
)
)
val attachVisualisationResponses = context.receiveN(3)
attachVisualisationResponses should contain allOf (
Api.Response(requestId, Api.VisualisationAttached()),
@ -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...")
}
}

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

View File

@ -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<IdExecutionService> 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<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.
*

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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