Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Share worker thread among RuntimeServerInstrument and ydoc-polyfill #12528

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from

Conversation

JaroslavTulach
Copy link
Member

@JaroslavTulach JaroslavTulach commented Mar 18, 2025

Pull Request Description

Resolves #11477 by making callbacks from ydoc-polyfill WebSocket emulation on the same ScheduledExecutorService as used by RuntimeServerInstrument. When the pool size is one (set via -Dpolyglot.enso.interpreter.jobParallelism=1 property) there should be no parallel executions (under the assumption that all Enso code is executing in the pool controlled by the jobParallelism property).

Important Notes

The EpbLanguage (which injects the WebSocket emulation) locates the RuntimeServerInstrument by its ID and obtains a Supplier<ScheduledExecutorService> from it. 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:

  • All code follows the
    Scala,
    Java,
  • Unit tests have been written where possible.

@JaroslavTulach JaroslavTulach added the CI: No changelog needed Do not require a changelog entry for this PR. label Mar 18, 2025
@JaroslavTulach JaroslavTulach self-assigned this Mar 18, 2025
@JaroslavTulach
Copy link
Member Author

JaroslavTulach commented Mar 18, 2025

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:

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

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 (see below) shall appear.

Verify with node.js

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

@JaroslavTulach JaroslavTulach requested a review from Frizi March 18, 2025 11:16
@JaroslavTulach
Copy link
Member Author

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

log.warn(
"Consider adding `-Dpolyglot.enso.interpreter.jobParallelism=1` to"
+ " ENSO_JVM_OPTS to avoid this error. See #12528");
log.info(dumpStack());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CI: No changelog needed Do not require a changelog entry for this PR.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow to execute Y.js and Insight together
1 participant