Repository: enso-org/enso
Pull request #12528: Share `getThreadManager` pool among `RuntimeServerInstrument` and Websocket emulation
Author: login: JaroslavTulach
Labels: name: CI: No changelog needed
description: Do not require a changelog entry for this PR.
Created: 2025-03-18T05:46:43Z | Merged: 2025-04-11T13:59:05Z

### Pull Request Description

Resolves #11477 by making callbacks from `ydoc-polyfill` `WebSocket` emulation via the same `ScheduledExecutorService` as provided by `EnsoLanguage`. In addition to that it registers `threadAccessDeniedHandler` to **coordinate** JavaScript multi threaded execution. 

### Important Notes

The `EpbLanguage` (which injects the `WebSocket` emulation) locates the `EnsoLanguage` by its ID and obtains a reference to `EnsoContext.getThreadManager()` via `lookup(langInfo, ScheduledExecutorService.class)`. Then it uses the obtained executor service for making callbacks into JavaScript whenever a websocket event arrives.

### Checklist

Please ensure that the following checklist has been satisfied before submitting the PR:

- [x] All code follows the
      [Scala](https://github.com/enso-org/enso/blob/develop/docs/style-guide/scala.md),
      [Java](https://github.com/enso-org/enso/blob/develop/docs/style-guide/java.md),
- [x] Manual [testing in the IDE](https://github.com/enso-org/enso/pull/12528#issuecomment-2732738311) works


Linked issue #11477: Allow to execute Y.js and Insight together 
Since the implementation of [reload insight script on file change](https://github.com/enso-org/enso/pull/11415#issuecomment-2453902666) we have a nice and comfortable way to execute **Enso Insight** scripts like [Detect Assign Statements](https://github.com/enso-org/enso/pull/11385#issuecomment-2431344514) in the IDE and observe how engine executes the code.

The next step is to **connect Y.js and Insight** together. Let's do it like this:

- modify [ContextInsightSetup](https://github.com/enso-org/enso/blob/2bbd90970547eed11b4c52f5b73c0bac785379f3/engine/common/src/main/java/org/enso/common/ContextInsightSetup.java#L170) to emulate _necessary JS environment_
- the [emulation has already been written](https://github.com/enso-org/enso/blob/2bbd90970547eed11b4c52f5b73c0bac785379f3/lib/java/ydoc-server/src/main/java/org/enso/ydoc/polyfill/web/WebEnvironment.java) and is part of `ydoc-server` module.
- make sure the **emulation is also initialized** in `ContextInsightSetup` via the `epb` language:

```java
    var epbSrc =
        Source.newBuilder("epb", path.toFile())
            .content("js:0#" + codeToExecute)
            .build();
```

It is essential to use `epb` language as it sets a proper [inner context for JavaScript](https://github.com/enso-org/enso/blob/2bbd90970547eed11b4c52f5b73c0bac785379f3/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/ForeignEvalNode.java#L107) rather than trying to execute JavaScript directly (as it is not ready for multi threaded access).

- all of the above has been **solved** by #11822
- we just **need a demo**:

At the end please demonstrate how a simple script showing co-operation between Enso Insight and Y.js structures can be created and used in the IDE via `-Denso.dev.insight` property.
Linked issue comments: user: login: JaroslavTulach
created_at: 2024-12-10T06:13:25Z
body: @Frizi, @4e6 I guess it is time to specify what:

> demonstrate how a simple script showing co-operation

shall mean?  I've just tried `2024.5.1-nightly.2024.12.10` version:
- if I perform `sbt ydoc-server/test`
- a new file `./lib/java/ydoc-server/target/classes/org/enso/ydoc/server/ydoc.cjs` gets created
- I assume that's the script we want to execute to demonstrate Y-doc server can run together with Insight
- thus we shall [follow the tutorial](https://github.com/enso-org/enso/pull/11415#issuecomment-2453902666) and just set `ENSO_JVM_OPTS=-Denso.dev.insight=$ENSO_ROOT/lib/java/ydoc-server/target/classes/org/enso/ydoc/server/ydoc.cjs`

Is that correct assumption guys? Then the whole script would be:
```bash
enso$ sbt ydoc-server/test
enso$ export ENSO_JVM_OPTS=-Denso.dev.insight=`pwd`/lib/java/ydoc-server/target/classes/org/enso/ydoc/server/ydoc.cjs
enso$ sbt runProjectManagerDistribution
```
and launching IDE in another terminal as `corepack pnpm run dev:gui`.
author_association: MEMBER
user: login: JaroslavTulach
created_at: 2024-12-10T07:11:25Z
body: With `2024.5.1-nightly.2024.12.10` the above steps fail with:
```
[ERROR] [2024-12-10T08:07:51.962] [org.enso.languageserver.protocol.json.JsonConnectionController] Failed to initialize resources.
java.util.concurrent.CompletionException: org.graalvm.polyglot.PolyglotException: ReferenceError: crypto is not defined
        at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:315)
        at java.base/java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:320)
        at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1807)
        at org.enso.akka.wrapper/akka.dispatch.TaskInvocation.run(AbstractDispatcher.scala:49)
        at java.base/java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1423)
        at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:387)
        at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1312)
        at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1843)
        at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1808)
        at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:188)
Caused by: org.graalvm.polyglot.PolyglotException: ReferenceError: crypto is not defined
        at <js>.poly_enso_eval(ydoc.cjs:3858)
        at <epb>.(Unknown)
        at org.graalvm.polyglot/org.graalvm.polyglot.Context.initialize(Context.java:579)
        at org.enso.language.server/org.enso.languageserver.boot.resource.TruffleContextInitialization.initComponent(TruffleContextInitialization.java:47)
        at org.enso.language.server/org.enso.languageserver.boot.resource.LockedInitialization.lambda$init$0(LockedInitialization.java:37)
        at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1804)
```
We need to modify [ContextInsightSetup](https://github.com/enso-org/enso/blob/2bbd90970547eed11b4c52f5b73c0bac785379f3/engine/common/src/main/java/org/enso/common/ContextInsightSetup.java#L170) to emulate necessary JS environment as provided by `ydoc-polyfill`.

- #11822 

Created a #11822 PR to turn on the `ydoc-polyfill`.
author_association: MEMBER
user: login: JaroslavTulach
created_at: 2024-12-17T14:11:36Z
body: Once I asked _Where is the (type script?) code representing an AST element? Is there information about offset/length? What code needs to be added lib/java/ydoc-server/target/classes/org/enso/ydoc/server/ydoc.cjs to print all AST that shall be updated with expressionUpdates and their offset/length? Having a diff doing this printing would be a good entry point for me._

@Frizi wrote:

> There is no offset/length in AST now. It is a mutable tree structure first. Here are the primary parse methods: 
>
> https://github.com/enso-org/enso/blob/9cf7833de085f66c8a840e73e68a22af15937b2d/app/ydoc-shared/src/ast/parse.ts#L448-L457
> 
> This is where we use the parser inside the ydoc server currently:
> 
> https://github.com/enso-org/enso/blob/9cf7833de085f66c8a840e73e68a22af15937b2d/app/ydoc-server/src/languageServerSession.ts#L605
>
author_association: MEMBER
user: login: 4e6
created_at: 2024-12-20T15:06:07Z
body: I was not able to get enough information from the insight script to prepare update messages that could be exchanged with GUI. For example,

```js
insight.on("return", function(ctx, frame) {
  print(`[return] ctx='${JSON.stringify(ctx)}'`)
  print(`[return] frame='${JSON.stringify(frame)}'`)
}, {
  expressions: true,
  statements: true,
  roots: true,
  rootNameFilter: ".*main"
});
```
#### `ctx`

```json
{
   "name":"local.NewProject1.Main::local.NewProject1.Main::main",
   "source":{
      "characters":"from Standard.Base import all\nfrom Standard.Table import all\nfrom Standard.Database import all\nfrom Standard.AWS import all\nfrom Standard.Google_Api import all\nfrom Standard.Snowflake import all\nimport Standard.Examples\nimport Standard.Visualization\n\n## ICON group\nfirst_column file1 =\n    vector1 = file1.column_names\n    any1 = vector1.first\n    any1\n\nmain =\n    file1 = Data.read Examples.welcome\n    any1 = Main.first_column file1\n    any3 = file1.at 'Column 1'\n\n\n\n#### METADATA ####\n[[{\"index\":{\"value\":278},\"size\":{\"value\":5}},\"b9b7d881-fed9-49cc-a3c0-feb247635f89\"],[{\"index\":{\"value\":300},\"size\":{\"value\":18}},\"2bbf7b4a-6866-4783-a793-22b72c1d5161\"],[{\"index\":{\"value\":330},\"size\":{\"value\":13}},\"c3dedc01-bbbc-4660-9dad-7a0f5f50d59a\"],[{\"index\":{\"value\":348},\"size\":{\"value\":4}},\"17961ddd-0306-4c20-a7f5-82eace2f7e5e\"],[{\"index\":{\"value\":373},\"size\":{\"value\":26}},\"fcf199e6-d2b1-44b7-be92-a47545d6a593\"],[{\"index\":{\"value\":411},\"size\":{\"value\":23}},\"3d29652c-e20c-4436-addb-1060cc55013a\"],[{\"index\":{\"value\":446},\"size\":{\"value\":8}},\"fce3a9bd-d7c7-40d9-887c-eceb0ee25240\"],[{\"index\":{\"value\":446},\"size\":{\"value\":19}},\"dfc7341d-aba3-49c6-be18-f5f9e5562d83\"]]\n{\"ide\":{\"node\":{\"b9b7d881-fed9-49cc-a3c0-feb247635f89\":{\"position\":{\"vector\":[-388,282]}},\"2bbf7b4a-6866-4783-a793-22b72c1d5161\":{\"position\":{\"vector\":[-388,210]}},\"c3dedc01-bbbc-4660-9dad-7a0f5f50d59a\":{\"position\":{\"vector\":[-388,138]}},\"17961ddd-0306-4c20-a7f5-82eace2f7e5e\":{\"position\":{\"vector\":[-388,66]}},\"fcf199e6-d2b1-44b7-be92-a47545d6a593\":{\"position\":{\"vector\":[-388,282]},\"visualization\":{\"show\":false,\"width\":1027.671875,\"height\":193}},\"3d29652c-e20c-4436-addb-1060cc55013a\":{\"position\":{\"vector\":[-443,177]}},\"dfc7341d-aba3-49c6-be18-f5f9e5562d83\":{\"position\":{\"vector\":[-145,177]}},\"fce3a9bd-d7c7-40d9-887c-eceb0ee25240\":{\"position\":{\"vector\":[-145,177]}}},\"widget\":{},\"import\":{},\"snapshot\":\"eJx9kD0LwjAQhvf8igMHt4A4O/iFOKhDRcdytokEL7mSplb99ba2FotopiTPk/deoj1biAK6FH0qZ5grMDZjHwCJhO7RPZ7oD15gwNPfgOkx+g1XzGdS8TQzv53IcakJL70Z7baTlje0Gan8CxxMXiCZBwbDTojBANbz3RbOnotMaOPzECdMhXWgDakRTARU66qSwL46NbeyUWKHthpRC+juNW09+QrqgBAWjWuj2liof0p6hSm8u8pSUcJWfQZuqofyu9ZbGXeNMMBw3hijoXgCQg+d7w==\"}}",
      "name":"local.NewProject1.Main",
      "language":"enso",
      "mimeType":null,
      "uri":"truffle:51f7bf11a1e5144c224e96c1c245f7b479c22bbc0fc4fb757bfe5e72ae5c58b4/local.NewProject1.Main"
   },
   "characters":"main =\n    file1 = Data.read Examples.welcome\n    any1 = Main.first_column file1\n    any3 = file1.at 'Column 1'",
   "line":16,
   "startLine":16,
   "endLine":19,
   "column":1,
   "startColumn":1,
   "endColumn":30,
   "charIndex":354,
   "charLength":111,
   "charEndIndex":465
}
```

#### `frame`

```json
{
   "file1":{},
   "any1":"Column 1",
   "any3":{}
}
author_association: CONTRIBUTOR
user: login: 4e6
created_at: 2024-12-20T15:09:06Z
body: If the issue is just about running `ydoc-server` from the insight script, then you can already do it since #11822 by pointing `enso.dev.insight` property to the `ydoc-server-polyglot` artifact:

```
ENSO_JVM_OPTS="-Denso.dev.insight=$(pwd)/app/ydoc-server-polyglot/dist/main.cjs" java -jar project-manager.jar
```
author_association: CONTRIBUTOR
user: login: JaroslavTulach
created_at: 2024-12-21T12:23:38Z
body: The goal I'd like to see is:
- modify the `app/ydoc-server-polyglot/dist/main.cjs` to update the AST somehow
- modify the IDE to read/use such updated AST somehow

Btw. Use `ctx.returnValue`: https://www.graalvm.org/jdk20/tools/graalvm-insight/manual/#intercepting-and-altering-execution

<blockquote><img src="/jdk20/resources/img/graalvm.png" width="48" align="right"><div><strong><a href="https://www.graalvm.org/jdk20/tools/graalvm-insight/manual/#intercepting-and-altering-execution">GraalVM</a></strong></div><div>GraalVM is an advanced JDK with ahead-of-time Native Image compilation.</div></blockquote>
author_association: MEMBER
user: login: enso-bot[bot]
created_at: 2024-12-27T18:50:43Z
body: **Dmitry Bushev** reports a new **STANDUP** for yesterday (2024-12-26):

**Progress:** [11477] Added ability to debug JS in Chrome Dev Tools. Implemented parsing of the span map. Implemented setting of the ast metadata from the insight script. It should be finished by 2025-01-03.

**Next Day:** Next day I will be working on the [#11477 task](https://github.com/enso-org/enso/issues/11477). Continue working on the task
author_association: NONE
user: login: enso-bot[bot]
created_at: 2024-12-27T18:52:16Z
body: **Dmitry Bushev** reports a new **STANDUP** for today (2024-12-27):

**Progress:** [11477] Debugging the ast representation in chrome inspector. Fixed an issue with re-setting the metadata. Looking into Yjs connection to ydoc server. It should be finished by 2025-01-03.

**Next Day:** Next day I will be working on the [#11477 task](https://github.com/enso-org/enso/issues/11477). Continue working on the task
author_association: NONE
user: login: JaroslavTulach
created_at: 2024-12-28T05:53:34Z
body: > Added ability to debug JS in Chrome Dev Tools.

Do you mean something other than `enso --inspect` which has already been working?
author_association: MEMBER
user: login: enso-bot[bot]
created_at: 2024-12-31T21:16:48Z
body: **Dmitry Bushev** reports a new **STANDUP** for the provided date (2024-12-30):

**Progress:** [10923] Looking into performance regression. Removed the truffle boundaries. Updated the PR. Trying to produce the nodes graph for an alternative fix [11477] Started implementing the logic that attaches the Yjs websocket provider. It should be finished by 2025-01-03.

**Next Day:** Next day I will be working on the [#11477 task](https://github.com/enso-org/enso/issues/11477). Continue working on the task
author_association: NONE
user: login: enso-bot[bot]
created_at: 2024-12-31T21:21:43Z
body: **Dmitry Bushev** reports a new **STANDUP** for yesterday (2024-12-31):

**Progress:** [11477] Implemented the Yjs provider that connects to Ydoc and listens to subdocks to mimic the gui behavior. Debugging issues with the websocket connection. It should be finished by 2025-01-03.

**Next Day:** Next day I will be working on the [#11477 task](https://github.com/enso-org/enso/issues/11477). Continue working on the task
author_association: NONE
user: login: enso-bot[bot]
created_at: 2025-01-02T18:29:42Z
body: **Dmitry Bushev** reports a new **STANDUP** for yesterday (2025-01-01):

**Progress:** [11938] Inspecting the truffle asts to discover differences in AtomConstructor implementation. Re-running benchmarks. Merged the PR [11477] Debugging the Yjs websocket provider. It should be finished by 2025-01-03.

**Next Day:** Next day I will be working on the [#11477 task](https://github.com/enso-org/enso/issues/11477). Continue working on the task
author_association: NONE
user: login: enso-bot[bot]
created_at: 2025-01-02T18:42:32Z
body: **Dmitry Bushev** reports a new **STANDUP** for today (2025-01-02):

**Progress:** [11477] Debugging the websocket implementation. Fixed an issue with establishing the connection. Debugging the issue with reading the response. Reduced the test example to a simple websocket connection. Created a draft PR It should be finished by 2025-01-03.

**Next Day:** Next day I will be working on the [#11477 task](https://github.com/enso-org/enso/issues/11477). Continue working on the task
author_association: NONE
user: login: enso-bot[bot]
created_at: 2025-03-14T03:06:50Z
body: **Jaroslav Tulach** reports a new **STANDUP** for yesterday (2025-03-13):

**Progress:** .

- Debugging Y.doc in Chrome dev tools: https://github.com/enso-org/enso/pull/11965
- Found multi threaded access problem in Ydoc emulation: https://github.com/enso-org/enso/pull/11965#issuecomment-2721007201
- `Standard.Generic_JDBC` library warning discussion
- numpy is working with version 24.0.0: https://discord.com/channels/@me/948519891961544714/1349750906895335486 It should be finished by 2025-03-18.

<blockquote><img src="https://cdn.discordapp.com/assets/og_img_discord_home.png" width="48" align="right"><div><img src="/assets/favicon.ico" height="14"> Discord</div><div><strong><a href="https://discord.com/channels/@me/948519891961544714/1349750906895335486">Discord - Group Chat That’s All Fun & Games</a></strong></div><div>Discord is great for playing games and chilling with friends, or even building a worldwide community. Customize your own space to talk, play, and hang out.</div></blockquote>
author_association: NONE
user: login: enso-bot[bot]
created_at: 2025-03-15T04:57:12Z
body: **Jaroslav Tulach** reports a new **STANDUP** for yesterday (2025-03-14):

**Progress:** .

- bit of Y.doc server: https://github.com/enso-org/enso/pull/11965#discussion_r1995127770
- anxious PR to upgrade Truffle: https://github.com/enso-org/enso/pull/12500
- **opt-in** via `ENSO_JVM_OPTS`: https://github.com/enso-org/enso/issues/12483#issuecomment-2723433165
- verifying the **opt-in**: https://github.com/enso-org/enso/issues/12483#issuecomment-2723495768
- debugging **opt-in** PR: https://github.com/enso-org/enso/pull/12501
- standup It should be finished by 2025-03-18.
author_association: NONE
user: login: enso-bot[bot]
created_at: 2025-03-17T04:57:59Z
body: **Jaroslav Tulach** reports a new **STANDUP** for yesterday (2025-03-16):

**Progress:** .

- rebranding https://github.com/enso-org/enso/pull/12501 as change to _native image mode_
- watching cloud introduction: https://discord.com/channels/401396655599124480/1344961565522395146/1350515730994692117 It should be finished by 2025-03-18.

<blockquote><img src="https://cdn.discordapp.com/assets/og_img_discord_home.png" width="48" align="right"><div><img src="/assets/favicon.ico" height="14"> Discord</div><div><strong><a href="https://discord.com/channels/401396655599124480/1344961565522395146/1350515730994692117">Discord - Group Chat That’s All Fun & Games</a></strong></div><div>Discord is great for playing games and chilling with friends, or even building a worldwide community. Customize your own space to talk, play, and hang out.</div></blockquote>
author_association: NONE
user: login: enso-bot[bot]
created_at: 2025-03-18T04:00:23Z
body: **Jaroslav Tulach** reports a new **STANDUP** for yesterday (2025-03-17):

**Progress:** .

- +1 to `--jvm` option: https://github.com/enso-org/enso/pull/12515#issuecomment-2729236286
- testing and closing `ENSO_JVM_OPTS` PR: https://github.com/enso-org/enso/pull/12501#issuecomment-2728891175
- phillipic against exposing `ENSO_LAUNCHER` to end users: https://github.com/enso-org/enso/pull/12515#pullrequestreview-2689159319
- reviewed parser change: https://github.com/enso-org/enso/pull/12505#pullrequestreview-2689253456
- Y.doc server: sharing `ScheduledExecutorService` among `EpbLanguage` and `RuntimeServerInstrument`
- (demo )meetings It should be finished by 2025-03-18.
author_association: NONE
user: login: enso-bot[bot]
created_at: 2025-03-19T04:35:10Z
body: **Jaroslav Tulach** reports a new **STANDUP** for yesterday (2025-03-18):

**Progress:** .

- `ydoc-polyfill` thread sharing: https://github.com/enso-org/enso/pull/12528
- detecting violations to multi threaded access: https://github.com/enso-org/enso/pull/12528#discussion_r2000854166
- adjusting to GraalVM 24.2 being out: https://github.com/enso-org/enso/pull/12500#issuecomment-2733293967
- fixing `Base_Tests`, `test`
- review appender: https://github.com/enso-org/enso/pull/12488#pullrequestreview-2696136897
- planning meeting It should be finished by 2025-03-18.
author_association: NONE

Diff:
diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
index be9793c8835c..80809913ecb0 100644
--- a/MODULE.bazel.lock
+++ b/MODULE.bazel.lock
@@ -616,7 +616,7 @@
     },
     "@@rules_rust+//crate_universe:extension.bzl%crate": {
       "general": {
-        "bzlTransitiveDigest": "t0LacG+6Y3sh8Y9kRThzg2d+viLv86e7ACVSHudAhlA=",
+        "bzlTransitiveDigest": "FBJ8rEFIAaJxNKbI8PBvdmePmLfLri38dvzRcNI+6RE=",
         "usagesDigest": "3QyfiyrJKYb+RnTA85xhz1ecQjw78S7PpkjVgmLoLxA=",
         "recordedFileInputs": {
           "@@//app/rust-ffi/Cargo.toml": "b17bf21f56720ffba5031fdf70296acb53ff018adcf17a8cdcec86b46103d1d8",
diff --git a/engine/runtime-instrument-common/src/main/java/org/enso/interpreter/instrument/InterpreterContext.java b/engine/runtime-instrument-common/src/main/java/org/enso/interpreter/instrument/InterpreterContext.java
new file mode 100644
index 000000000000..963a282ad54a
--- /dev/null
+++ b/engine/runtime-instrument-common/src/main/java/org/enso/interpreter/instrument/InterpreterContext.java
@@ -0,0 +1,18 @@
+package org.enso.interpreter.instrument;
+
+import com.oracle.truffle.api.TruffleContext;
+import org.enso.interpreter.service.ExecutionService;
+
+/**
+ * Contains suppliers of services that provide interpreter specific functionality.
+ *
+ * @param executionService a service allowing externally-triggered code execution
+ * @param contextManager a storage for active execution contexts
+ * @param endpoint a message endpoint
+ * @param truffleContext a context of a set of Truffle languages
+ */
+public record InterpreterContext(
+    ExecutionService executionService,
+    ExecutionContextManager contextManager,
+    Endpoint endpoint,
+    TruffleContext truffleContext) {}
diff --git a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/Handler.scala b/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/Handler.scala
index d2f3f448084e..4405761d714c 100644
--- a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/Handler.scala
+++ b/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/Handler.scala
@@ -38,13 +38,12 @@ abstract class Handler {
     executionService: ExecutionService,
     truffleContext: TruffleContext
   ): Unit = {
-    val interpreterCtx =
-      InterpreterContext(
-        executionService,
-        contextManager,
-        endpoint,
-        truffleContext
-      )
+    val interpreterCtx = new InterpreterContext(
+      executionService,
+      contextManager,
+      endpoint,
+      truffleContext
+    )
     val commandProcessor = new CommandExecutionEngine(interpreterCtx)
     ctx = HandlersContext(
       executionService,
diff --git a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/InterpreterContext.scala b/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/InterpreterContext.scala
deleted file mode 100644
index dd933476f718..000000000000
--- a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/InterpreterContext.scala
+++ /dev/null
@@ -1,20 +0,0 @@
-package org.enso.interpreter.instrument
-
-import com.oracle.truffle.api.TruffleContext
-import org.enso.interpreter.service.ExecutionService
-
-/** Contains suppliers of services that provide interpreter specific
-  * functionality.
-  *
-  * @param executionService a service allowing externally-triggered code
-  *                         execution
-  * @param contextManager a storage for active execution contexts
-  * @param endpoint a message endpoint
-  * @param truffleContext a context of a set of Truffle languages
-  */
-case class InterpreterContext(
-  executionService: ExecutionService,
-  contextManager: ExecutionContextManager,
-  endpoint: Endpoint,
-  truffleContext: TruffleContext
-)
diff --git a/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/EpbContext.java b/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/EpbContext.java
index c688e1264595..a9d26a1a15cb 100644
--- a/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/EpbContext.java
+++ b/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/EpbContext.java
@@ -8,7 +8,8 @@
 import com.oracle.truffle.api.source.Source;
 import java.io.IOException;
 import java.net.URL;
-import java.util.concurrent.Executors;
+import java.util.Random;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.function.Function;
 import java.util.logging.Level;
 import org.enso.ydoc.polyfill.web.WebEnvironment;
@@ -28,6 +29,7 @@ final class EpbContext {
   private final TruffleLanguage.Env env;
   private @CompilationFinal TruffleContext innerContext;
   private final TruffleLogger log;
+  private final Random delayer = new Random();
   private boolean polyfillInitialized;
 
   /**
@@ -54,6 +56,7 @@ public void initialize(String preInitializeLanguages) {
             env.newInnerContextBuilder()
                 .initializeCreatorContext(true)
                 .inheritAllAccess(true)
+                .threadAccessDeniedHandler(this::handleMultiAccess)
                 .config(INNER_OPTION, "yes")
                 .build();
       }
@@ -87,7 +90,9 @@ public void log(Level level, String msg, Object... args) {
   final void initializePolyfill(Node node, TruffleContext ctx) {
     if (!polyfillInitialized) {
       polyfillInitialized = true;
-      var exec = Executors.newSingleThreadScheduledExecutor();
+      var ensoLanguage = getEnv().getInternalLanguages().get("enso");
+      var exec = getEnv().lookup(ensoLanguage, ScheduledExecutorService.class);
+      assert exec != null : "Need executor from " + ensoLanguage;
       Function<URL, Value> eval =
           (url) -> {
             try {
@@ -101,4 +106,43 @@ final void initializePolyfill(Node node, TruffleContext ctx) {
       WebEnvironment.initialize(eval, exec);
     }
   }
+
+  final void handleMultiAccess(String msg) {
+    try {
+      var ms = delayer.nextInt(10, 1000);
+      // dump stack when assertions on
+      assert dumpStack(ms);
+      Thread.sleep(ms);
+    } catch (InterruptedException ex) {
+      Thread.currentThread().interrupt();
+    }
+  }
+
+  private boolean dumpStack(int ms) {
+    var sb = new StringBuilder("Polyglot access failed. Waiting " + ms + " ms. Threaddump:\n");
+    var traces = Thread.getAllStackTraces();
+    for (var entry : traces.entrySet()) {
+      var thread = new StringBuilder();
+      thread.append(entry.getKey().getName()).append("\n");
+      var keep = false;
+      for (var e : entry.getValue()) {
+        keep |= e.getClassName().contains("com.oracle.truffle");
+        thread
+            .append("    ")
+            .append(e.getClassName())
+            .append(".")
+            .append(e.getMethodName())
+            .append("(")
+            .append(e.getFileName())
+            .append(":")
+            .append(e.getLineNumber())
+            .append(")\n");
+      }
+      if (keep) {
+        sb.append(thread.toString());
+      }
+    }
+    log(Level.WARNING, sb.toString());
+    return true;
+  }
 }
diff --git a/engine/runtime/src/main/java/org/enso/interpreter/EnsoLanguage.java b/engine/runtime/src/main/java/org/enso/interpreter/EnsoLanguage.java
index 0968178d9650..d64df047ac31 100644
--- a/engine/runtime/src/main/java/org/enso/interpreter/EnsoLanguage.java
+++ b/engine/runtime/src/main/java/org/enso/interpreter/EnsoLanguage.java
@@ -20,6 +20,7 @@
 import java.time.ZoneId;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.logging.Level;
 import org.enso.common.LanguageInfo;
 import org.enso.common.RuntimeOptions;
@@ -94,7 +95,12 @@
     contextPolicy = TruffleLanguage.ContextPolicy.EXCLUSIVE,
     dependentLanguages = {"epb"},
     fileTypeDetectors = FileDetector.class,
-    services = {Timer.class, NotificationHandler.Forwarder.class, LockManager.class})
+    services = {
+      Timer.class,
+      NotificationHandler.Forwarder.class,
+      LockManager.class,
+      ScheduledExecutorService.class
+    })
 @ProvidedTags({
   DebuggerTags.AlwaysHalt.class,
   StandardTags.CallTag.class,
@@ -170,6 +176,7 @@ protected EnsoContext createContext(Env env) {
         new EnsoContext(
             this, getLanguageHome(), env, notificationHandler, lockManager, distributionManager);
 
+    env.registerService(context.getThreadManager());
     return context;
   }
 
diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/EnsoRootNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/EnsoRootNode.java
index 147677d77c56..ebe7f2b3024a 100644
--- a/engine/runtime/src/main/java/org/enso/interpreter/node/EnsoRootNode.java
+++ b/engine/runtime/src/main/java/org/enso/interpreter/node/EnsoRootNode.java
@@ -1,6 +1,5 @@
 package org.enso.interpreter.node;
 
-import com.oracle.truffle.api.TruffleLogger;
 import com.oracle.truffle.api.frame.FrameDescriptor;
 import com.oracle.truffle.api.frame.FrameSlotKind;
 import com.oracle.truffle.api.nodes.NodeInfo;
@@ -8,9 +7,6 @@
 import com.oracle.truffle.api.source.Source;
 import com.oracle.truffle.api.source.SourceSection;
 import java.util.Objects;
-import java.util.function.BiFunction;
-import java.util.logging.Level;
-import org.enso.common.LanguageInfo;
 import org.enso.compiler.context.LocalScope;
 import org.enso.interpreter.EnsoLanguage;
 import org.enso.interpreter.runtime.EnsoContext;
@@ -20,8 +16,6 @@
 /** A common base class for all kinds of root node in Enso. */
 @NodeInfo(shortName = "Root", description = "A root node for Enso computations")
 public abstract class EnsoRootNode extends RootNode {
-  private static final TruffleLogger LOGGER = TruffleLogger.getLogger(LanguageInfo.ID);
-
   private final String name;
   private final int sourceStartIndex;
   private final int sourceLength;
@@ -44,7 +38,7 @@ protected EnsoRootNode(
       ModuleScope moduleScope,
       String name,
       SourceSection sourceSection) {
-    super(language, buildFrameDescriptor(name, localScope, LOGGER));
+    super(language, buildFrameDescriptor(name, localScope));
     Objects.requireNonNull(language);
     Objects.requireNonNull(localScope);
     Objects.requireNonNull(moduleScope);
@@ -67,19 +61,11 @@ protected EnsoRootNode(
    *
    * @return {@link FrameDescriptor} built from the variable definitions in the local localScope.
    */
-  private static FrameDescriptor buildFrameDescriptor(
-      String name, LocalScope localScope, TruffleLogger log) {
+  private static FrameDescriptor buildFrameDescriptor(String name, LocalScope localScope) {
     var descriptorBuilder = FrameDescriptor.newBuilder();
     descriptorBuilder.addSlot(FrameSlotKind.Object, LocalScope.monadicStateSlotName(), null);
 
-    BiFunction<String, Object[], Void> logFnOrNull =
-        log.isLoggable(Level.FINE)
-            ? (msg, args) -> {
-              log.log(Level.FINE, msg, args);
-              return null;
-            }
-            : null;
-    var allDefs = localScope.allSymbols(name, logFnOrNull);
+    var allDefs = localScope.allSymbols(name, null);
     for (var definition : allDefs) {
       descriptorBuilder.addSlot(FrameSlotKind.Illegal, definition, null);
     }
diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/EnsoContext.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/EnsoContext.java
index 92aeca95527d..210cc27770e6 100644
--- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/EnsoContext.java
+++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/EnsoContext.java
@@ -792,7 +792,7 @@ public boolean isGlobalSuggestionsEnabled() {
 
   /** The job parallelism or 1 */
   public int getJobParallelism() {
-    int n = getOption(RuntimeOptions.JOB_PARALLELISM_KEY);
+    var n = getOption(RuntimeOptions.JOB_PARALLELISM_KEY);
     return Math.max(1, n);
   }
 
diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/GuestCodeExecutor.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/GuestCodeExecutor.java
new file mode 100644
index 000000000000..2351098d6e9c
--- /dev/null
+++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/GuestCodeExecutor.java
@@ -0,0 +1,111 @@
+package org.enso.interpreter.runtime;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+abstract class GuestCodeExecutor implements ScheduledExecutorService {
+  final ScheduledExecutorService guestCode;
+
+  GuestCodeExecutor(ScheduledExecutorService delegate) {
+    this.guestCode = delegate;
+  }
+
+  protected abstract Runnable wrap(Runnable r);
+
+  protected abstract <V> Callable<V> wrap(Callable<V> c);
+
+  private <T> List<Callable<T>> wrap(Collection<? extends Callable<T>> tasks) {
+    return tasks.stream().map(this::wrap).toList();
+  }
+
+  @Override
+  public final ScheduledFuture<?> schedule(Runnable r, long l, TimeUnit tu) {
+    return guestCode.schedule(wrap(r), l, tu);
+  }
+
+  @Override
+  public final <V> ScheduledFuture<V> schedule(Callable<V> clbl, long l, TimeUnit tu) {
+    return guestCode.schedule(wrap(clbl), l, tu);
+  }
+
+  @Override
+  public final ScheduledFuture<?> scheduleAtFixedRate(Runnable r, long l, long l1, TimeUnit tu) {
+    return guestCode.scheduleAtFixedRate(wrap(r), l, l1, tu);
+  }
+
+  @Override
+  public final ScheduledFuture<?> scheduleWithFixedDelay(Runnable r, long l, long l1, TimeUnit tu) {
+    return guestCode.scheduleWithFixedDelay(wrap(r), l, l1, tu);
+  }
+
+  @Override
+  public final List<Runnable> shutdownNow() {
+    return guestCode.shutdownNow();
+  }
+
+  @Override
+  public final boolean isShutdown() {
+    return guestCode.isShutdown();
+  }
+
+  @Override
+  public final boolean isTerminated() {
+    return guestCode.isTerminated();
+  }
+
+  @Override
+  public final boolean awaitTermination(long l, TimeUnit tu) throws InterruptedException {
+    return guestCode.awaitTermination(l, tu);
+  }
+
+  @Override
+  public final <T> Future<T> submit(Callable<T> clbl) {
+    return guestCode.submit(wrap(clbl));
+  }
+
+  @Override
+  public final <T> Future<T> submit(Runnable r, T t) {
+    return guestCode.submit(wrap(r), t);
+  }
+
+  @Override
+  public final Future<?> submit(Runnable r) {
+    return guestCode.submit(r);
+  }
+
+  @Override
+  public final <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
+      throws InterruptedException {
+    return guestCode.invokeAll(wrap(tasks));
+  }
+
+  @Override
+  public final <T> List<Future<T>> invokeAll(
+      Collection<? extends Callable<T>> clctn, long l, TimeUnit tu) throws InterruptedException {
+    return guestCode.invokeAll(wrap(clctn), l, tu);
+  }
+
+  @Override
+  public final <T> T invokeAny(Collection<? extends Callable<T>> tasks)
+      throws InterruptedException, ExecutionException {
+    return guestCode.invokeAny(wrap(tasks));
+  }
+
+  @Override
+  public final <T> T invokeAny(Collection<? extends Callable<T>> clctn, long l, TimeUnit tu)
+      throws InterruptedException, ExecutionException, TimeoutException {
+    return guestCode.invokeAny(wrap(clctn), l, tu);
+  }
+
+  @Override
+  public final void execute(Runnable command) {
+    guestCode.execute(wrap(command));
+  }
+}
diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/ThreadExecutors.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/ThreadExecutors.java
index 335b6057de62..533d6262c5b9 100644
--- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/ThreadExecutors.java
+++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/ThreadExecutors.java
@@ -39,8 +39,8 @@ ExecutorService newCachedThreadPool(
     return s;
   }
 
-  ExecutorService newFixedThreadPool(int cnt, String name, boolean systemThread) {
-    var s = Executors.newFixedThreadPool(cnt, new Factory(name, systemThread));
+  ScheduledExecutorService newScheduledThreadPool(int cnt, String name, boolean systemThread) {
+    var s = Executors.newScheduledThreadPool(cnt, new Factory(name, systemThread));
     pools.put(s, name);
     return s;
   }
diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/ThreadManager.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/ThreadManager.java
index fedfdd6a8c75..60a3a85f74d4 100644
--- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/ThreadManager.java
+++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/ThreadManager.java
@@ -2,22 +2,61 @@
 
 import com.oracle.truffle.api.ThreadLocalAction;
 import com.oracle.truffle.api.TruffleLanguage.Env;
+import java.util.concurrent.Callable;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.function.Supplier;
 import org.enso.interpreter.runtime.control.ThreadInterruptedException;
 
 /** Manages threads running guest code, exposing a safepoint-like functionality. */
-public final class ThreadManager {
+public final class ThreadManager extends GuestCodeExecutor {
   private final ThreadExecutors threads;
-  private final ExecutorService questCode;
   private final ConcurrentHashMap<Thread, Boolean> interruptFlags = new ConcurrentHashMap<>();
 
   ThreadManager(ThreadExecutors th, int throughput, Env env) {
+    super(th.newScheduledThreadPool(throughput, "guest-code", false));
     this.threads = th;
-    this.questCode = th.newFixedThreadPool(throughput, "guest-code", false);
+  }
+
+  @Override
+  protected Runnable wrap(Runnable r) {
+    Runnable wrap =
+        () -> {
+          var notYetEntered = interruptFlags.get(Thread.currentThread()) == null;
+          if (notYetEntered) {
+            interruptFlags.put(Thread.currentThread(), false);
+            try {
+              r.run();
+            } finally {
+              interruptFlags.remove(Thread.currentThread());
+            }
+          } else {
+            r.run();
+          }
+        };
+    return wrap;
+  }
+
+  @Override
+  protected <V> Callable<V> wrap(Callable<V> c) {
+    Callable<V> wrap =
+        () -> {
+          var notYetEntered = interruptFlags.get(Thread.currentThread()) == null;
+          if (notYetEntered) {
+            interruptFlags.put(Thread.currentThread(), false);
+            try {
+              return c.call();
+            } finally {
+              interruptFlags.remove(Thread.currentThread());
+            }
+          } else {
+            return c.call();
+          }
+        };
+    return wrap;
   }
 
   /**
@@ -39,7 +78,7 @@ public final <T> CompletableFuture<T> submit(Supplier<T> action) {
               interruptFlags.remove(Thread.currentThread());
             }
           };
-      return CompletableFuture.supplyAsync(wrap, questCode);
+      return CompletableFuture.supplyAsync(wrap, guestCode);
     } else {
       try {
         return CompletableFuture.completedFuture(action.get());
@@ -71,10 +110,10 @@ public ExecutorService newCachedThreadPool(String name, int min, int max, int ma
    * @param name human-readable name of the pool
    * @return new execution service for this context
    */
-  public ExecutorService newFixedThreadPool(int parallel, String name) {
+  public ScheduledExecutorService newFixedThreadPool(int parallel, String name) {
     // only allow creation of systemThreads
     // non-system threads have to be managed and controlled internally
-    return threads.newFixedThreadPool(parallel, name, true);
+    return threads.newScheduledThreadPool(parallel, name, true);
   }
 
   /**
@@ -103,6 +142,7 @@ protected void perform(ThreadLocalAction.Access access) {
   }
 
   /** Requests that all threads are shutdown. */
+  @Override
   public final void shutdown() {
     threads.shutdown();
     var hasBeenInterrupted = Thread.interrupted();
diff --git a/lib/java/ydoc-polyfill/src/main/java/org/enso/ydoc/polyfill/web/WebSocket.java b/lib/java/ydoc-polyfill/src/main/java/org/enso/ydoc/polyfill/web/WebSocket.java
index 1c0f4dd40721..fe426dd4eab4 100644
--- a/lib/java/ydoc-polyfill/src/main/java/org/enso/ydoc/polyfill/web/WebSocket.java
+++ b/lib/java/ydoc-polyfill/src/main/java/org/enso/ydoc/polyfill/web/WebSocket.java
@@ -279,14 +279,14 @@ public void onMessage(WsSession session, BufferData buffer, boolean last) {
 
       // Passing byte sequence to JS requires `HostAccess.allowBufferAccess()`
       var bytes = ByteSequence.create(buffer.readBytes());
-      executor.execute(() -> handleMessage.executeVoid(bytes));
+      handleCallback(() -> handleMessage.executeVoid(bytes));
     }
 
     @Override
     public void onMessage(WsSession session, String text, boolean last) {
       log.debug("onMessage [{}]", text);
 
-      executor.execute(() -> handleMessage.executeVoid(text));
+      handleCallback(() -> handleMessage.executeVoid(text));
     }
 
     @Override
@@ -294,7 +294,7 @@ public void onPing(WsSession session, BufferData buffer) {
       log.debug("onPing [{}]", buffer);
 
       var bytes = ByteSequence.create(buffer.readBytes());
-      executor.execute(() -> handlePing.executeVoid(bytes));
+      handleCallback(() -> handlePing.executeVoid(bytes));
     }
 
     @Override
@@ -302,7 +302,7 @@ public void onPong(WsSession session, BufferData buffer) {
       log.debug("onPong [{}]", buffer);
 
       var bytes = ByteSequence.create(buffer.readBytes());
-      executor.execute(() -> handlePong.executeVoid(bytes));
+      handleCallback(() -> handlePong.executeVoid(bytes));
     }
 
     @Override
@@ -311,14 +311,14 @@ public void onOpen(WsSession session) {
 
       this.session = session;
 
-      executor.execute(() -> handleOpen.executeVoid());
+      handleCallback(() -> handleOpen.executeVoid());
     }
 
     @Override
     public void onClose(WsSession session, int status, String reason) {
       log.debug("onClose [{}] [{}]", status, reason);
 
-      executor.execute(() -> handleClose.executeVoid(status, reason));
+      handleCallback(() -> handleClose.executeVoid(status, reason));
       this.session = null;
     }
 
@@ -326,7 +326,7 @@ public void onClose(WsSession session, int status, String reason) {
     public void onError(WsSession session, Throwable t) {
       log.error("onError ", t);
 
-      executor.execute(() -> handleError.executeVoid(t.getMessage()));
+      handleCallback(() -> handleError.executeVoid(t.getMessage()));
     }
 
     @Override
@@ -334,9 +334,13 @@ public Optional<Headers> onHttpUpgrade(HttpPrologue prologue, Headers headers) {
       log.debug("onHttpUpgrade [{}]", prologue);
 
       var url = new URL(prologue);
-      executor.execute(() -> handleUpgrade.executeVoid(url));
+      handleCallback(() -> handleUpgrade.executeVoid(url));
 
       return Optional.empty();
     }
+
+    private void handleCallback(Runnable code) {
+      executor.execute(code);
+    }
   }
 }
diff --git a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala
index bc0c2eba8961..1ef99a4d1bc6 100644
--- a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala
+++ b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala
@@ -174,9 +174,8 @@ class Runner(
     def prepareAndRunCommand(engine: Engine, cmd: ExecCommand): R = {
       val jvmOptsFromEnvironment = environment.getEnvVar(JVM_OPTIONS_ENV_VAR)
       jvmOptsFromEnvironment.foreach { opts =>
-        logger.debug(
-          "Picking up additional JVM options [{}] from the " +
-          "[{}] environment variable.",
+        logger.info(
+          "Additional JVM options [{}] from the {} environment variable.",
           MaskedString(opts),
           JVM_OPTIONS_ENV_VAR
         )
diff --git a/project/DistributionPackage.scala b/project/DistributionPackage.scala
index 0064bb8c5feb..5e9e96788f76 100644
--- a/project/DistributionPackage.scala
+++ b/project/DistributionPackage.scala
@@ -327,6 +327,27 @@ object DistributionPackage {
     }
   }
 
+  private def reduceArgs(
+    args: java.util.List[String],
+    jvmOptName: String,
+    envToFill: java.util.Map[String, String]
+  ): Unit = {
+    var prevValue = System.getenv(jvmOptName)
+    if (prevValue == null) {
+      prevValue = "-ea";
+    }
+
+    val at = args.indexOf("--debug")
+    if (at >= 0) {
+      args.set(at, "--jvm")
+      val newValue =
+        prevValue + " " + WithDebugCommand.DEBUG_OPTION
+      envToFill.put(jvmOptName, newValue)
+    } else {
+      envToFill.put(jvmOptName, prevValue)
+    }
+  }
+
   def runEnginePackage(
     distributionRoot: File,
     args: Seq[String],
@@ -351,13 +372,7 @@ object DistributionPackage {
 
     all.add(enso.getAbsolutePath)
     all.addAll(args.asJava)
-
-    if (args.contains("--debug")) {
-      all.remove("--debug")
-      pb.environment().put("JAVA_OPTS", "-ea " + WithDebugCommand.DEBUG_OPTION)
-    } else {
-      pb.environment().put("JAVA_OPTS", "-ea")
-    }
+    reduceArgs(all, "JAVA_OPTS", pb.environment)
     if (disablePrivateCheck) {
       all.add("--disable-private-check")
     }
@@ -445,10 +460,7 @@ object DistributionPackage {
     pb.command(all)
     pb.environment().put("ENSO_ENGINE_PATH", engineRoot.toString())
     pb.environment().put("ENSO_JVM_PATH", System.getProperty("java.home"))
-    if (args.contains("--debug")) {
-      all.remove("--debug")
-      pb.environment().put("ENSO_JVM_OPTS", WithDebugCommand.DEBUG_OPTION)
-    }
+    reduceArgs(all, "ENSO_JVM_OPTS", pb.environment)
     pb.inheritIO()
     val p        = pb.start()
     val exitCode = p.waitFor()


Reviews:
user: login: JaroslavTulach
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T11:27:09Z
user: login: JaroslavTulach
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T11:39:45Z
user: login: JaroslavTulach
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T04:23:54Z
user: login: JaroslavTulach
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T04:29:49Z
user: login: JaroslavTulach
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T04:30:11Z
user: login: JaroslavTulach
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T12:57:29Z
user: login: hubertp
body: OK
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T13:16:37Z
user: login: JaroslavTulach
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T13:56:38Z

Review comments:
diff_hunk: @@ -311,32 +311,81 @@ public void onOpen(WsSession session) {
 
       this.session = session;
 
-      executor.execute(() -> handleOpen.executeVoid());
+      handleCallback(() -> handleOpen.executeVoid());
     }
 
     @Override
     public void onClose(WsSession session, int status, String reason) {
       log.debug("onClose [{}] [{}]", status, reason);
 
-      executor.execute(() -> handleClose.executeVoid(status, reason));
+      handleCallback(() -> handleClose.executeVoid(status, reason));
       this.session = null;
     }
 
     @Override
     public void onError(WsSession session, Throwable t) {
       log.error("onError ", t);
 
-      executor.execute(() -> handleError.executeVoid(t.getMessage()));
+      handleCallback(() -> handleError.executeVoid(t.getMessage()));
     }
 
     @Override
     public Optional<Headers> onHttpUpgrade(HttpPrologue prologue, Headers headers) {
       log.debug("onHttpUpgrade [{}]", prologue);
 
       var url = new URL(prologue);
-      executor.execute(() -> handleUpgrade.executeVoid(url));
+      handleCallback(() -> handleUpgrade.executeVoid(url));
 
       return Optional.empty();
     }
+
+    private void handleCallback(Runnable code) {
+      Runnable safeCode =
+          () -> {
+            try {
+              code.run();
+            } catch (Throwable t) {
+              // use threadAccessDeniedHandler instead - once #12500 is integrated - instead of
+              // following check
+              if (t instanceof IllegalStateException
+                  && t.getMessage() instanceof String msg
+                  && msg.startsWith("Multi threaded access requested by thread")) {
+                log.warn(
+                    "Consider adding `-Dpolyglot.enso.interpreter.jobParallelism=1` to"
+                        + " ENSO_JVM_OPTS to avoid this error. See #12528");
+                log.info(dumpStack());
path: lib/java/ydoc-polyfill/src/main/java/org/enso/ydoc/polyfill/web/WebSocket.java
user: login: JaroslavTulach
body: When Truffle system detects _multi threaded access_, chances are high we can inspect all thread stack traces and find the other thread that is also using JavaScript.
created_at: 2025-03-18T11:27:09Z
author_association: MEMBER
diff_hunk: @@ -311,32 +311,81 @@ public void onOpen(WsSession session) {
 
       this.session = session;
 
-      executor.execute(() -> handleOpen.executeVoid());
+      handleCallback(() -> handleOpen.executeVoid());
     }
 
     @Override
     public void onClose(WsSession session, int status, String reason) {
       log.debug("onClose [{}] [{}]", status, reason);
 
-      executor.execute(() -> handleClose.executeVoid(status, reason));
+      handleCallback(() -> handleClose.executeVoid(status, reason));
       this.session = null;
     }
 
     @Override
     public void onError(WsSession session, Throwable t) {
       log.error("onError ", t);
 
-      executor.execute(() -> handleError.executeVoid(t.getMessage()));
+      handleCallback(() -> handleError.executeVoid(t.getMessage()));
     }
 
     @Override
     public Optional<Headers> onHttpUpgrade(HttpPrologue prologue, Headers headers) {
       log.debug("onHttpUpgrade [{}]", prologue);
 
       var url = new URL(prologue);
-      executor.execute(() -> handleUpgrade.executeVoid(url));
+      handleCallback(() -> handleUpgrade.executeVoid(url));
 
       return Optional.empty();
     }
+
+    private void handleCallback(Runnable code) {
+      Runnable safeCode =
+          () -> {
+            try {
+              code.run();
+            } catch (Throwable t) {
+              // use threadAccessDeniedHandler instead - once #12500 is integrated - instead of
+              // following check
+              if (t instanceof IllegalStateException
+                  && t.getMessage() instanceof String msg
+                  && msg.startsWith("Multi threaded access requested by thread")) {
+                log.warn(
+                    "Consider adding `-Dpolyglot.enso.interpreter.jobParallelism=1` to"
+                        + " ENSO_JVM_OPTS to avoid this error. See #12528");
+                log.info(dumpStack());
path: lib/java/ydoc-polyfill/src/main/java/org/enso/ydoc/polyfill/web/WebSocket.java
user: login: JaroslavTulach
body: The violator in the first accident is `scala-execution-context-global-56` thread. This is the thread dump:
```
job-pool-1
    java.lang.Thread.dumpThreads(Thread.java:-2)
    java.lang.Thread.getAllStackTraces(Thread.java:2521)
    org.enso.ydoc.polyfill.web.WebSocket.dumpStack(WebSocket.java:367)
    org.enso.ydoc.polyfill.web.WebSocket$WebSocketConnection.lambda$handleCallback$8(WebSocket.java:356)
    org.enso.ydoc.polyfill.web.WebSocket$WebSocketConnection$$Lambda/0x0000708124b85770.run(null:-1)
    java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
    java.util.concurrent.FutureTask.run(FutureTask.java:317)
    java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
    java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
    java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
    java.lang.Thread.runWith(Thread.java:1596)
    java.lang.Thread.run(Thread.java:1583)
    com.oracle.truffle.polyglot.PolyglotThread.access$001(PolyglotThread.java:53)
    com.oracle.truffle.polyglot.PolyglotThread$1.execute(PolyglotThread.java:106)
    com.oracle.truffle.polyglot.PolyglotThread$ThreadSpawnRootNode.executeImpl(PolyglotThread.java:140)
    com.oracle.truffle.polyglot.PolyglotThread$ThreadSpawnRootNode.execute(PolyglotThread.java:131)
    com.oracle.truffle.runtime.OptimizedCallTarget.executeRootNode(OptimizedCallTarget.java:745)
    com.oracle.truffle.runtime.OptimizedCallTarget.profiledPERoot(OptimizedCallTarget.java:669)
    com.oracle.truffle.runtime.OptimizedCallTarget.callBoundary(OptimizedCallTarget.java:602)
    com.oracle.truffle.runtime.OptimizedCallTarget.doInvoke(OptimizedCallTarget.java:586)
    com.oracle.truffle.runtime.OptimizedCallTarget.callIndirect(OptimizedCallTarget.java:519)
    com.oracle.truffle.runtime.OptimizedCallTarget.call(OptimizedCallTarget.java:500)
    com.oracle.truffle.polyglot.PolyglotThread.run(PolyglotThread.java:102)
job-pool-2
    java.lang.Thread.dumpThreads(Thread.java:-2)
    java.lang.Thread.getAllStackTraces(Thread.java:2521)
    org.enso.ydoc.polyfill.web.WebSocket.dumpStack(WebSocket.java:367)
    org.enso.ydoc.polyfill.web.WebSocket$WebSocketConnection.lambda$handleCallback$8(WebSocket.java:356)
    org.enso.ydoc.polyfill.web.WebSocket$WebSocketConnection$$Lambda/0x0000708124b85770.run(null:-1)
    java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
    java.util.concurrent.FutureTask.run(FutureTask.java:317)
    java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
    java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
    java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
    java.lang.Thread.runWith(Thread.java:1596)
    java.lang.Thread.run(Thread.java:1583)
    com.oracle.truffle.polyglot.PolyglotThread.access$001(PolyglotThread.java:53)
    com.oracle.truffle.polyglot.PolyglotThread$1.execute(PolyglotThread.java:106)
    com.oracle.truffle.polyglot.PolyglotThread$ThreadSpawnRootNode.executeImpl(PolyglotThread.java:140)
    com.oracle.truffle.polyglot.PolyglotThread$ThreadSpawnRootNode.execute(PolyglotThread.java:131)
    com.oracle.truffle.runtime.OptimizedCallTarget.executeRootNode(OptimizedCallTarget.java:745)
    com.oracle.truffle.runtime.OptimizedCallTarget.profiledPERoot(OptimizedCallTarget.java:669)
    com.oracle.truffle.runtime.OptimizedCallTarget.callBoundary(OptimizedCallTarget.java:602)
    com.oracle.truffle.runtime.OptimizedCallTarget.doInvoke(OptimizedCallTarget.java:586)
    com.oracle.truffle.runtime.OptimizedCallTarget.callIndirect(OptimizedCallTarget.java:519)
    com.oracle.truffle.runtime.OptimizedCallTarget.call(OptimizedCallTarget.java:500)
    com.oracle.truffle.polyglot.PolyglotThread.run(PolyglotThread.java:102)
scala-execution-context-global-56
    java.lang.ClassLoader.defineClass1(ClassLoader.java:-2)
    java.lang.ClassLoader.defineClass(ClassLoader.java:1027)
    java.lang.ClassLoader.defineClass(ClassLoader.java:1105)
    java.security.SecureClassLoader.defineClass(SecureClassLoader.java:182)
    jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:821)
    jdk.internal.loader.BuiltinClassLoader.findClassInModuleOrNull(BuiltinClassLoader.java:741)
    jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:665)
    jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:639)
    jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
    java.lang.ClassLoader.loadClass(ClassLoader.java:526)
    com.oracle.truffle.host.HostObjectGen$InteropLibraryExports$Uncached.isString(HostObjectGen.java:9002)
    com.oracle.truffle.api.interop.InteropLibraryGen$UncachedDispatch.isString(InteropLibraryGen.java:7076)
    com.oracle.truffle.polyglot.PolyglotValueDispatch$InteropValue.isString(PolyglotValueDispatch.java:2729)
    org.graalvm.polyglot.Value.isString(Value.java:1035)
    org.enso.ydoc.polyfill.Arguments.lambda$toString$0(Arguments.java:13)
    org.enso.ydoc.polyfill.Arguments.toString(Arguments.java:14)
    org.enso.ydoc.polyfill.web.EventTarget.execute(EventTarget.java:38)
    org.graalvm.polyglot.Engine$APIAccessImpl.callProxyExecutableExecute(Engine.java:1370)
    com.oracle.truffle.host.GuestToHostCodeCache$1.executeImpl(GuestToHostCodeCache.java:126)
    com.oracle.truffle.host.GuestToHostRootNode.execute(GuestToHostRootNode.java:80)
    com.oracle.truffle.runtime.OptimizedCallTarget.executeRootNode(OptimizedCallTarget.java:745)
    com.oracle.truffle.runtime.OptimizedCallTarget.callInlined(OptimizedCallTarget.java:550)
    com.oracle.truffle.runtime.OptimizedRuntimeSupport.callInlined(OptimizedRuntimeSupport.java:250)
    com.oracle.truffle.host.GuestToHostRootNode.guestToHostCall(GuestToHostRootNode.java:102)
    com.oracle.truffle.host.HostProxy.execute(HostProxy.java:139)
    com.oracle.truffle.host.HostProxyGen$InteropLibraryExports$Cached.executeNode_AndSpecialize(HostProxyGen.java:401)
    com.oracle.truffle.host.HostProxyGen$InteropLibraryExports$Cached.execute(HostProxyGen.java:377)
    com.oracle.truffle.api.interop.InteropLibraryGen$CachedDispatch.execute(InteropLibraryGen.java:7952)
    com.oracle.truffle.js.nodes.function.JSFunctionCallNode$ForeignExecuteNode.executeCall(JSFunctionCallNode.java:1539)
    ...
    com.oracle.truffle.js.runtime.objects.JSObject.invokeMember(JSObject.java:245)
    com.oracle.truffle.js.runtime.objects.JSObjectGen$InteropLibraryExports$Uncached.invokeMember(JSObjectGen.java:1020)
    com.oracle.truffle.api.interop.InteropLibraryGen.genericDispatch(InteropLibraryGen.java:329)
    com.oracle.truffle.api.library.ReflectionLibraryDefault$Send.doSendGeneric(ReflectionLibrary.java:193)
    com.oracle.truffle.api.library.ReflectionLibraryDefaultGen$ReflectionLibraryExports$Uncached.send(ReflectionLibraryDefaultGen.java:230)
    com.oracle.truffle.polyglot.OtherContextGuestObject.sendImpl(OtherContextGuestObject.java:157)
    com.oracle.truffle.polyglot.OtherContextGuestObject$Send.doSlowPath(OtherContextGuestObject.java:121)
    com.oracle.truffle.polyglot.OtherContextGuestObjectGen$ReflectionLibraryExports$Cached.executeAndSpecialize(OtherContextGuestObjectGen.java:194)
    com.oracle.truffle.polyglot.OtherContextGuestObjectGen$ReflectionLibraryExports$Cached.send(OtherContextGuestObjectGen.java:136)
    com.oracle.truffle.api.interop.InteropLibraryGen$Proxy.invokeMember(InteropLibraryGen.java:2910)
    org.enso.interpreter.epb.JsForeignNode.doExecute(JsForeignNode.java:40)
    org.enso.interpreter.epb.JsForeignNodeGen.executeAndSpecialize(JsForeignNodeGen.java:77)
    org.enso.interpreter.epb.JsForeignNodeGen.execute(JsForeignNodeGen.java:65)
    org.enso.interpreter.epb.ForeignEvalNode.execute(ForeignEvalNode.java:107)
    com.oracle.truffle.runtime.OptimizedCallTarget.executeRootNode(OptimizedCallTarget.java:745)
    com.oracle.truffle.runtime.OptimizedCallTarget.profiledPERoot(OptimizedCallTarget.java:669)
    com.oracle.truffle.runtime.OptimizedCallTarget.callBoundary(OptimizedCallTarget.java:602)
    com.oracle.truffle.runtime.OptimizedCallTarget.doInvoke(OptimizedCallTarget.java:586)
    com.oracle.truffle.runtime.OptimizedCallTarget.callIndirect(OptimizedCallTarget.java:519)
    com.oracle.truffle.runtime.OptimizedCallTarget.call(OptimizedCallTarget.java:500)
    com.oracle.truffle.tools.agentscript.impl.InsightPerSource.initializeAgent(InsightPerSource.java:120)
    com.oracle.truffle.tools.agentscript.impl.InsightPerSource.onLanguageContextInitialized(InsightPerSource.java:190)
    com.oracle.truffle.api.instrumentation.InstrumentationHandler.notifyLanguageContextInitialized(InstrumentationHandler.java:1049)
    com.oracle.truffle.api.instrumentation.InstrumentAccessor$InstrumentImpl.notifyLanguageContextInitialized(InstrumentAccessor.java:269)
    com.oracle.truffle.polyglot.PolyglotLanguageContext.ensureInitialized(PolyglotLanguageContext.java:796)
    com.oracle.truffle.polyglot.PolyglotContextImpl.initializeLanguage(PolyglotContextImpl.java:1629)
    com.oracle.truffle.polyglot.PolyglotContextImpl.initializeLanguage(PolyglotContextImpl.java:1638)
    com.oracle.truffle.polyglot.PolyglotContextDispatch.initializeLanguage(PolyglotContextDispatch.java:55)
    org.graalvm.polyglot.Context.initialize(Context.java:579)
    org.enso.languageserver.boot.resource.TruffleContextInitialization.initComponent(TruffleContextInitialization.java:47)
    org.enso.languageserver.boot.resource.LockedInitialization.lambda$init$0(LockedInitialization.java:37)
    org.enso.languageserver.boot.resource.LockedInitialization$$Lambda/0x0000708124546ac8.run(null:-1)
    java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1804)
    akka.dispatch.TaskInvocation.run(AbstractDispatcher.scala:49)
    java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1423)
    java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:387)
    java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1312)
    java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1843)
    java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1808)
    java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:188)
```
I can fix it by doing the call to `Context.initialize` at
```
org.enso.languageserver.boot.resource.TruffleContextInitialization.initComponent(TruffleContextInitialization.java:47)
```
in the `ScheduledExecutorService`. Is it good idea, @hubertp?
created_at: 2025-03-18T11:39:45Z
author_association: MEMBER
diff_hunk: @@ -311,32 +311,81 @@ public void onOpen(WsSession session) {
 
       this.session = session;
 
-      executor.execute(() -> handleOpen.executeVoid());
+      handleCallback(() -> handleOpen.executeVoid());
     }
 
     @Override
     public void onClose(WsSession session, int status, String reason) {
       log.debug("onClose [{}] [{}]", status, reason);
 
-      executor.execute(() -> handleClose.executeVoid(status, reason));
+      handleCallback(() -> handleClose.executeVoid(status, reason));
       this.session = null;
     }
 
     @Override
     public void onError(WsSession session, Throwable t) {
       log.error("onError ", t);
 
-      executor.execute(() -> handleError.executeVoid(t.getMessage()));
+      handleCallback(() -> handleError.executeVoid(t.getMessage()));
     }
 
     @Override
     public Optional<Headers> onHttpUpgrade(HttpPrologue prologue, Headers headers) {
       log.debug("onHttpUpgrade [{}]", prologue);
 
       var url = new URL(prologue);
-      executor.execute(() -> handleUpgrade.executeVoid(url));
+      handleCallback(() -> handleUpgrade.executeVoid(url));
 
       return Optional.empty();
     }
+
+    private void handleCallback(Runnable code) {
+      Runnable safeCode =
+          () -> {
+            try {
+              code.run();
+            } catch (Throwable t) {
+              // use threadAccessDeniedHandler instead - once #12500 is integrated - instead of
+              // following check
+              if (t instanceof IllegalStateException
+                  && t.getMessage() instanceof String msg
+                  && msg.startsWith("Multi threaded access requested by thread")) {
+                log.warn(
+                    "Consider adding `-Dpolyglot.enso.interpreter.jobParallelism=1` to"
+                        + " ENSO_JVM_OPTS to avoid this error. See #12528");
+                log.info(dumpStack());
path: lib/java/ydoc-polyfill/src/main/java/org/enso/ydoc/polyfill/web/WebSocket.java
user: login: JaroslavTulach
body: > Is it good idea...?

No, it is not. During the discussions we agree that we want Enso to be multi-threaded. As such this _multi threaded access detector_ has been moved to `EpbLanguage` itself in 0e03731 - e.g. multi threaded access to JavaScript is **coordinated** and thus **allowed**.
created_at: 2025-04-10T04:23:54Z
author_association: MEMBER
diff_hunk: @@ -134,6 +143,9 @@ protected void onCreate(Env env) {
 
     initializerEventBinding =
         env.getInstrumenter().attachContextsListener(new Initializer(this) {}, true);
+
+    Supplier<ScheduledExecutorService> supply = this::getJobExecutor;
+    env.registerService(supply);
path: engine/runtime-instrument-runtime-server/src/main/java/org/enso/interpreter/instrument/runtime/server/RuntimeServerInstrument.java
user: login: JaroslavTulach
body: Registers `getThreadManager` as `Supplier<ScheduledExecutorService>` for use by https://github.com/enso-org/enso/pull/12528/files#r2036474840
created_at: 2025-04-10T04:29:48Z
author_association: MEMBER
diff_hunk: @@ -87,7 +91,11 @@ public void log(Level level, String msg, Object... args) {
   final void initializePolyfill(Node node, TruffleContext ctx) {
     if (!polyfillInitialized) {
       polyfillInitialized = true;
-      var exec = Executors.newSingleThreadScheduledExecutor();
+      var instr = getEnv().getInstruments().get("enso-runtime-server");
+      var suppl = getEnv().lookup(instr, Supplier.class);
+      var service = suppl == null ? null : suppl.get();
+      assert service instanceof ScheduledExecutorService;
+      var exec = (ScheduledExecutorService) service;
path: engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/EpbContext.java
user: login: JaroslavTulach
body: Uses `EnsoContext.getThreadManager` registered by https://github.com/enso-org/enso/pull/12528/files#r2036474590
created_at: 2025-04-10T04:30:11Z
author_association: MEMBER
diff_hunk: @@ -67,19 +61,11 @@ protected EnsoRootNode(
    *
    * @return {@link FrameDescriptor} built from the variable definitions in the local localScope.
    */
-  private static FrameDescriptor buildFrameDescriptor(
-      String name, LocalScope localScope, TruffleLogger log) {
+  private static FrameDescriptor buildFrameDescriptor(String name, LocalScope localScope) {
     var descriptorBuilder = FrameDescriptor.newBuilder();
     descriptorBuilder.addSlot(FrameSlotKind.Object, LocalScope.monadicStateSlotName(), null);
 
-    BiFunction<String, Object[], Void> logFnOrNull =
-        log.isLoggable(Level.FINE)
-            ? (msg, args) -> {
-              log.log(Level.FINE, msg, args);
-              return null;
-            }
-            : null;
-    var allDefs = localScope.allSymbols(name, logFnOrNull);
+    var allDefs = localScope.allSymbols(name, null);
path: engine/runtime/src/main/java/org/enso/interpreter/node/EnsoRootNode.java
user: login: JaroslavTulach
body: b5b430a9798a4df306f3524657c956aab509518c avoids usage of `TruffleLogger` from non-guest code threads.
created_at: 2025-04-11T12:57:29Z
author_association: MEMBER
diff_hunk: @@ -101,4 +106,43 @@ final void initializePolyfill(Node node, TruffleContext ctx) {
       WebEnvironment.initialize(eval, exec);
     }
   }
+
+  final void handleMultiAccess(String msg) {
+    try {
+      var ms = delayer.nextInt(10, 1000);
path: engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/EpbContext.java
user: login: hubertp
body: shouldn't we log this event **always**?
created_at: 2025-04-11T13:13:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -101,4 +106,43 @@ final void initializePolyfill(Node node, TruffleContext ctx) {
       WebEnvironment.initialize(eval, exec);
     }
   }
+
+  final void handleMultiAccess(String msg) {
+    try {
+      var ms = delayer.nextInt(10, 1000);
path: engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/EpbContext.java
user: login: JaroslavTulach
body: - I was thinking about some heuristics to decide when to log, but 
- at the end I decided to leave it out of this PR
- I am not sure logging (and dumping all the thread stacks) always is what we want 
- there might be plenty of events like this
- a one line note in the log when verbosity is high might be OK
- so far I haven't seen many clashes even I am running with `-ea` on
created_at: 2025-04-11T13:56:38Z
author_association: MEMBER

PR comments:
user: login: JaroslavTulach
created_at: 2025-03-18T11:04:20Z
body: ### Manual Testing

Let's use a simple script that connects to echo service at http://websocket.org to verify how well the support for `WebSocket` API works in Enso IDE:
```js
var counter = 0;
function app() {
    print("Connecting to websocket.org...");
    let ws  = new WebSocket('wss://echo.websocket.org');
    function sayHi() {
      ws.send(`Hi there! #${counter++}`);
    }
    ws.onopen = function(ev) {
      print("Open: " + ev);
    };
    ws.onmessage = function(ev) {
      print("Msg   : " + Object.getOwnPropertyNames(ev));
      print("  data: " + ev.data);
      setTimeout(sayHi, 3000);
    };
    ws.onclose = function(ev) {
      print("Close: " + Object.getOwnPropertyNames(ev));
    };
    ws.onerror = function(ev) {
      print("Err: " + Object.getOwnPropertyNames(ev));
    };
}

if (typeof insight === "undefined") {
    globalThis.print = console.log;
    if (typeof WebSocket === "undefined") {
        let r = require("ws");
        globalThis.WebSocket = r.WebSocket;
    }
    app();
} else {
    app();
}
```
The script is generic. You can use it in [node.js](http://nodejs.org) as well as Enso IDE or command line interface.

#### Enso GUI

Store the above script as `/tmp/wstest.js` and then execute following commands in the root of Enso repository:
```bash
enso$ export ENSO_JVM_OPTS="-Denso.dev.insight=/tmp/wstest.js -Dpolyglot.enso.interpreter.jobParallelism=1"
enso$ sbt runProjectManagerDistribution
....
Connecting to websocket.org...
...
Msg   : 
  data: Hi there! #2
...
```
and in another terminal execute `corepack pnpm dev:gui`. Connect your browser to http://localhost:5173/, open a project and observe messages printed by the `sbt runProjectManagerDistribution`. The same output as in case of [node.js](http://nodejs.org) (see below) shall appear.

#### Verify with [node.js](http://nodejs.org)

Save the above script into an empty directory `ws` as `wstest.js` and execute:
```
ws$ npm install ws
ws$ node wstest.js 
Connecting to websocket.org...
Open: [object Event]
Msg   : 
  data: Request served by 1781505b56ee58
Msg   : 
  data: Hi there! #0
Msg   : 
  data: Hi there! #1
Msg   : 
  data: Hi there! #2
```
and so on. Every few seconds another _"Hi there!"_ message with an increased counter shall appear. The same output shall be visible in the _Enso GUI_ as well as from CLI.

<!--
#### Enso CLI 

TBD...
-->
author_association: MEMBER
user: login: JaroslavTulach
created_at: 2025-03-18T11:23:49Z
body: @hubertp, @4e6, do you think following assumption:

> the assumption that all Enso code is executing in the pool controlled by the `jobParallelism` property

Is supposed to be true? E.g. if I fix the violations and set `jobParallelism=1` - will JavaScript callbacks be properly serialized one and another and intermixed with regular requests to execute Enso code?

**Update** on Apr 10, 2025:
- since  #12500 there is [GUEST_PARALLELISM](https://github.com/enso-org/enso/blob/5a430d2d216b47a344144c97899c77b952f10ffc/engine/common/src/main/java/org/enso/common/RuntimeOptions.java#L85) option
- by default it is currently one
- but it can be [used to increase](https://github.com/enso-org/enso/blob/5a430d2d216b47a344144c97899c77b952f10ffc/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/semantic/RuntimeManagementTest.scala#L19) the _guest executor thread_ throughput
- since 343f3b9 the `EnsoContext.getThreadManager()` is an `ScheduledExecutorService` 
- the service is also used by `EpbLanguage` emulation - https://github.com/enso-org/enso/pull/12528/files#r2036474840
- since 0e03731 - we are using `threadAccessDeniedHandler` to _coordinate JavaScript execution_ even in multi threaded pool
author_association: MEMBER
user: login: JaroslavTulach
created_at: 2025-04-10T04:56:36Z
body: We seem to have [problems with shutdown](https://github.com/enso-org/enso/actions/runs/14363493432/job/40270663111?pr=12528#step:10:3761):
```
The language did not complete all polyglot threads but should have: [Thread[#546,guest-code-1,5,main]]
```
probably the [shutdown](https://github.com/enso-org/enso/pull/12528/files#diff-4bd4d79f46aee332da04781a4bc0ef217adc50862179660f8a15e68f05ec0403L106) isn't done properly.
author_association: MEMBER
user: login: JaroslavTulach
created_at: 2025-04-11T13:58:57Z
body: Let's merge and continue with
- #11159 
- and #11965
author_association: MEMBER