From 2da10f1f22f42d4d2c2c92f7ea3776d58fd2a86b Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:34:54 +0300 Subject: [PATCH 01/13] Switch initializr JavaScript build to the local ParparVM target The initializr javascript module previously built via the remote `javascript` build target (cloud build server). Switch it to the local `local-javascript` target, which routes through CN1BuildMojo#doJavaScriptLocalBuild -> JavaScriptBuilder (ParparVM bytecode -> JavaScript translator). Automate mode still authenticates with the server secret, so the build pipeline is unaffected. Changes (all under scripts/initializr/): - javascript/pom.xml: codename1.defaultBuildTarget javascript -> local-javascript - build.sh / build.bat: explicit -Dcodename1.buildTarget -> local-javascript (these override the pom default) - README.adoc and bundled skill docs (SKILL.md, build-and-run.md, native-interfaces.md): document the JS build as local ParparVM, with the cloud `javascript` target noted as the fallback Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/initializr/README.adoc | 2 +- scripts/initializr/build.bat | 2 +- scripts/initializr/build.sh | 2 +- .../initializr/common/src/main/resources/skill/SKILL.md | 7 +++++-- .../src/main/resources/skill/references/build-and-run.md | 7 ++++--- .../main/resources/skill/references/native-interfaces.md | 2 +- scripts/initializr/javascript/pom.xml | 2 +- 7 files changed, 14 insertions(+), 10 deletions(-) diff --git a/scripts/initializr/README.adoc b/scripts/initializr/README.adoc index 2ccc909727..d18ca6ce90 100644 --- a/scripts/initializr/README.adoc +++ b/scripts/initializr/README.adoc @@ -59,7 +59,7 @@ mvn -DskipTests install [source,bash] ---- cd ../scripts/initializr -./mvnw package -Dcodename1.platform=javascript -Dcodename1.buildTarget=javascript -Dcn1.localWorkspace=true +./mvnw package -Dcodename1.platform=javascript -Dcodename1.buildTarget=local-javascript -Dcn1.localWorkspace=true ---- This switches Initializr to `8.0-SNAPSHOT` so JavaScript builds use your local Codename One code. diff --git a/scripts/initializr/build.bat b/scripts/initializr/build.bat index 622d02e706..40893d8b27 100644 --- a/scripts/initializr/build.bat +++ b/scripts/initializr/build.bat @@ -22,7 +22,7 @@ goto :EOF goto :EOF :javascript -!MVNW! package -DskipTests -Dcodename1.platform^=javascript -Dcodename1.buildTarget^=javascript -U -e +!MVNW! package -DskipTests -Dcodename1.platform^=javascript -Dcodename1.buildTarget^=local-javascript -U -e goto :EOF :android diff --git a/scripts/initializr/build.sh b/scripts/initializr/build.sh index 196d9f1339..54b63ee6e7 100755 --- a/scripts/initializr/build.sh +++ b/scripts/initializr/build.sh @@ -12,7 +12,7 @@ function windows_desktop { } function javascript { - "$MVNW" "package" "-DskipTests" "-Dcodename1.platform=javascript" "-Dcodename1.buildTarget=javascript" "-U" "-e" + "$MVNW" "package" "-DskipTests" "-Dcodename1.platform=javascript" "-Dcodename1.buildTarget=local-javascript" "-U" "-e" } function android { diff --git a/scripts/initializr/common/src/main/resources/skill/SKILL.md b/scripts/initializr/common/src/main/resources/skill/SKILL.md index d71c510541..4c6ca31dcb 100644 --- a/scripts/initializr/common/src/main/resources/skill/SKILL.md +++ b/scripts/initializr/common/src/main/resources/skill/SKILL.md @@ -253,10 +253,13 @@ mvn -pl common cn1:debug # Execute the CN1 test runner mvn -pl common cn1:test -# Cloud build for Android/iOS/JS (requires CN1 build server creds) +# Cloud build for Android/iOS (requires CN1 build server creds) mvn -pl android package -Dcodename1.platform=android -Dcodename1.buildTarget=android-device mvn -pl ios package -Dcodename1.platform=ios -Dcodename1.buildTarget=ios-device -mvn -pl javascript package -Dcodename1.platform=javascript -Dcodename1.buildTarget=javascript + +# JavaScript / web bundle, built locally via the ParparVM → JS translator (Enterprise-gated). +# Use -Dcodename1.buildTarget=javascript instead for the cloud builder. +mvn -pl javascript package -Dcodename1.platform=javascript -Dcodename1.buildTarget=local-javascript ``` See `references/build-and-run.md` for the local-vs-cloud matrix, automated-build mode (Enterprise), iOS local-build prerequisites, and the complete goal list. The full `codename1.arg.*` index lives in `references/build-hints.md`. diff --git a/scripts/initializr/common/src/main/resources/skill/references/build-and-run.md b/scripts/initializr/common/src/main/resources/skill/references/build-and-run.md index 8f3597f324..059bbab3b3 100644 --- a/scripts/initializr/common/src/main/resources/skill/references/build-and-run.md +++ b/scripts/initializr/common/src/main/resources/skill/references/build-and-run.md @@ -13,7 +13,7 @@ A Codename One project can produce four kinds of artifacts. Some build entirely | iOS app | Cloud, **or** locally as an Xcode project via `ios-source` | `mvn -pl ios package -Dcodename1.platform=ios -Dcodename1.buildTarget=ios-device` (cloud) or `…-Dcodename1.buildTarget=ios-source` (local Xcode project) | | Mac Native app (AOT-compiled, same pipeline as iOS) | Cloud, **or** locally as an Xcode project via `mac-source` | `mvn -pl ios package -Dcodename1.platform=ios -Dcodename1.buildTarget=mac-os-x-native` (cloud) or `…-Dcodename1.buildTarget=mac-source` (local Xcode project) | | Native Windows `.exe` (`win32`, ParparVM → clang-cl, no JVM) | Cloud (Linux build server cross-compiles); **also** locally on Windows, or as a project via `windows-source` | `mvn -pl common package -Dcodename1.platform=windows -Dcodename1.buildTarget=windows-device` (cloud) or `…-Dcodename1.buildTarget=local-windows-device` (local). A regular build returns x64 + arm64 release exes; add the `windows.debug` build hint for a single x64 debug exe. | -| JavaScript / web bundle | Cloud | `mvn -pl javascript package -Dcodename1.platform=javascript -Dcodename1.buildTarget=javascript` | +| JavaScript / web bundle | Local (ParparVM → JavaScript translator; Enterprise-gated). Cloud still available via `…-Dcodename1.buildTarget=javascript`. | `mvn -pl javascript package -Dcodename1.platform=javascript -Dcodename1.buildTarget=local-javascript` | The two big "local-only" outputs are the **simulator** and **tests** — those are everything you need for ordinary development and CI feedback loops. You only invoke the cloud builds when you want a deployable native artifact. @@ -102,8 +102,9 @@ mvn -pl ios package -Dcodename1.platform=ios -Dcodename1.buildTarget=mac-source # Native Android APK/AAB. Cloud-built by default. mvn -pl android package -Dcodename1.platform=android -Dcodename1.buildTarget=android-device -# JavaScript / web bundle. Cloud-built. -mvn -pl javascript package -Dcodename1.platform=javascript -Dcodename1.buildTarget=javascript +# JavaScript / web bundle. Built locally via the ParparVM → JavaScript translator (Enterprise-gated). +# Append -Dcodename1.buildTarget=javascript instead to use the cloud builder. +mvn -pl javascript package -Dcodename1.platform=javascript -Dcodename1.buildTarget=local-javascript # Standalone Mac / Windows / Linux desktop app. Cloud-built. mvn -pl javase package -Dcodename1.platform=javase -Dcodename1.buildTarget=mac-os-x-desktop diff --git a/scripts/initializr/common/src/main/resources/skill/references/native-interfaces.md b/scripts/initializr/common/src/main/resources/skill/references/native-interfaces.md index 775489ff22..37edc5cfce 100644 --- a/scripts/initializr/common/src/main/resources/skill/references/native-interfaces.md +++ b/scripts/initializr/common/src/main/resources/skill/references/native-interfaces.md @@ -70,7 +70,7 @@ mvn -pl ios package -Dcodename1.platform=ios -Dcodename1.buildTarget=ios-source mvn -pl android package -Dcodename1.platform=android -Dcodename1.buildTarget=android-device -Dautomated=true # JavaScript — produces a web bundle; open dev tools and confirm the JS impl is included. -mvn -pl javascript package -Dcodename1.platform=javascript -Dcodename1.buildTarget=javascript +mvn -pl javascript package -Dcodename1.platform=javascript -Dcodename1.buildTarget=local-javascript # Desktop simulator — just run cn1:run and observe the bridge boots without errors. mvn -pl common cn1:run diff --git a/scripts/initializr/javascript/pom.xml b/scripts/initializr/javascript/pom.xml index a68c0251d5..4bba4f16eb 100644 --- a/scripts/initializr/javascript/pom.xml +++ b/scripts/initializr/javascript/pom.xml @@ -18,7 +18,7 @@ 1.8 javascript javascript - javascript + local-javascript From 8dff0c289d0907834f91cbcc156f4f1ea0fcc020 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 8 Jun 2026 21:13:25 +0300 Subject: [PATCH 02/13] Authorize local JS build via the logged-in CN1 account The local-javascript build failed in the initializr/website pipeline because JavaScriptBuilder.checkUserLevel() only accepted an explicit javascript.userLevel / CN1_USER_LEVEL / codename1.userLevel property. The website build (build_initializr_for_site -> set_cn1_user_token) authenticates by writing the CN1 user+token to the /com/codename1/ui preferences node (SetUserTokenMojo / cn1:set-user-token), not by setting a userLevel property, so the gate rejected an authenticated build. Honor that login directly: if a CN1 user+token is present in prefs the local JS build is authorized, the same way the cloud build is. The explicit userLevel property remains a fallback for logged-out CLI use. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../codename1/builders/JavaScriptBuilder.java | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java index 0afae0e7db..9b6e3f2e6b 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java @@ -166,6 +166,17 @@ public boolean build(File sourceZip, BuildRequest request) throws BuildException } private boolean checkUserLevel(BuildRequest request) { + // A logged-in Codename One account is the authorization for the local JS + // build, the same way it authorizes the cloud build. The credentials are + // written to the /com/codename1/ui preferences node by `cn1:set-user-token` + // (SetUserTokenMojo) -- e.g. set_cn1_user_token in the website build. Honor + // that login directly so the local target "just works" once you're logged in. + if (hasCodenameOneLogin()) { + log("Local JavaScript builder: authorized via logged-in Codename One account."); + return true; + } + // Fallback for direct CLI invocations that aren't logged in: an explicit + // Enterprise-or-higher user level still unlocks the build. String raw = firstNonEmpty( request.getArg("javascript.userLevel", null), request.getArg("userLevel", null), @@ -179,12 +190,26 @@ private boolean checkUserLevel(BuildRequest request) { return true; } log("ERROR: The local JavaScript build is licensed only to Enterprise and higher tier users. " - + "Set codename1.arg.javascript.userLevel=Enterprise (or a higher tier) in codenameone_settings.properties, " + + "Log in with `cn1:set-user-token -Duser= -Dtoken=`, " + + "set codename1.arg.javascript.userLevel=Enterprise (or a higher tier) in codenameone_settings.properties, " + "or define the CN1_USER_LEVEL environment variable, to enable this preview. " + "See https://www.codenameone.com/pricing.html for tier details."); return false; } + private boolean hasCodenameOneLogin() { + try { + java.util.prefs.Preferences prefs = java.util.prefs.Preferences.userRoot().node("/com/codename1/ui"); + String user = prefs.get("user", null); + String token = prefs.get("token", null); + return user != null && user.trim().length() > 0 + && token != null && token.trim().length() > 0; + } catch (Exception ex) { + // Preferences backing store unavailable -- fall through to the userLevel path. + return false; + } + } + private static int parseUserRank(String raw) { if (raw == null) { return 0; From c5ed5ff920c6c8f5c8ff9410fd137af685688356 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 8 Jun 2026 21:37:25 +0300 Subject: [PATCH 03/13] initializr: declare Enterprise tier for the local JavaScript build The website CI builds the initializr against the released codenameone plugin (cn1.plugin.version=7.0.250), whose JavaScriptBuilder license gate only reads the javascript.userLevel / CN1_USER_LEVEL / codename1.userLevel inputs -- it does not yet honor the logged-in CN1 account. set_cn1_user_token performs the login, but the released gate still rejected the build ("JavaScript build failed"). Declare codename1.arg.javascript.userLevel=Enterprise in the shared initializr settings so the released plugin's gate is satisfied. The javascript module's getCN1ProjectDir() resolves to ../common, so this flows into the build request as the javascript.userLevel arg. The companion plugin change lets newer plugins accept the login directly, at which point this declaration is redundant but harmless. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/initializr/common/codenameone_settings.properties | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/initializr/common/codenameone_settings.properties b/scripts/initializr/common/codenameone_settings.properties index a53c18b350..f2da507a85 100644 --- a/scripts/initializr/common/codenameone_settings.properties +++ b/scripts/initializr/common/codenameone_settings.properties @@ -12,6 +12,11 @@ codename1.arg.and.themeMode=modern codename1.arg.desktop.titleBar=native codename1.arg.desktop.interactiveScrollbars=true codename1.arg.java.version=8 +# Local ParparVM JavaScript build (codename1.buildTarget=local-javascript) is +# gated to Enterprise-tier accounts in the released plugin. The website build +# logs in via set_cn1_user_token; declare the tier so the released plugin's +# license gate is satisfied. Newer plugins also accept the login directly. +codename1.arg.javascript.userLevel=Enterprise codename1.displayName=Initializr codename1.icon=icon.png codename1.ios.appid=Q5GHSKAL2F.com.codename1.initializr From 994ffb9f99617a324b1ccecabb6056bb52b3c330 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 8 Jun 2026 21:47:16 +0300 Subject: [PATCH 04/13] website: flatten the local JavaScript bundle's wrapper directory With codename1.buildTarget=local-javascript the ParparVM builder emits a zip whose entries are nested under a single top-level directory (e.g. Initializr-js/index.html), whereas the cloud result.zip is flat with index.html at the root. The website extraction therefore failed its "missing index.html after extraction" check. After unzip, if index.html isn't at the root, flatten a single top-level wrapper directory so the served initializr-app layout is identical for both the local and cloud builders. No-op for the flat cloud bundles (Playground/Skin Designer still build via the cloud javascript target). Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/website/build.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/scripts/website/build.sh b/scripts/website/build.sh index 4f10267f5e..1c9261c12e 100755 --- a/scripts/website/build.sh +++ b/scripts/website/build.sh @@ -617,6 +617,19 @@ build_initializr_for_site() { mkdir -p "${output_dir}" unzip -q -o "${result_zip}" -d "${output_dir}" + # The cloud result.zip is flat (index.html at the root), but the local + # ParparVM build (codename1.buildTarget=local-javascript) wraps the bundle in + # a single top-level directory (e.g. Initializr-js/). Flatten that wrapper so + # the served layout is identical regardless of which builder produced the zip. + if [ ! -f "${output_dir}/index.html" ]; then + local inner_dir + inner_dir="$(find "${output_dir}" -mindepth 1 -maxdepth 1 -type d | head -n1 || true)" + if [ -n "${inner_dir}" ] && [ -f "${inner_dir}/index.html" ]; then + ( cd "${inner_dir}" && tar cf - . ) | ( cd "${output_dir}" && tar xf - ) + rm -rf "${inner_dir}" + fi + fi + if [ ! -f "${output_dir}/index.html" ]; then echo "Initializr website bundle is missing index.html after extraction." >&2 exit 1 From 0caac58f7e62a41219a7ac20ba8dce8a5b0162da Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 8 Jun 2026 22:00:54 +0300 Subject: [PATCH 05/13] website: include app icon in the local Initializr JS bundle The Initializr page template references /initializr-app/icon.png. The cloud build's bundle shipped that icon; the local ParparVM bundle does not, so the link/image validator failed ("Cannot find file ... initializr-app/icon.png"). Copy scripts/initializr/common/icon.png into the extracted bundle when it is absent, so the served layout matches the cloud build. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/website/build.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/website/build.sh b/scripts/website/build.sh index 1c9261c12e..74d61b191a 100755 --- a/scripts/website/build.sh +++ b/scripts/website/build.sh @@ -634,6 +634,13 @@ build_initializr_for_site() { echo "Initializr website bundle is missing index.html after extraction." >&2 exit 1 fi + + # The Initializr page (layouts/_default/initializr.html) shows the app icon + # from /initializr-app/icon.png. The cloud bundle shipped one; the local + # ParparVM bundle does not, so copy the project icon in when it is absent. + if [ ! -f "${output_dir}/icon.png" ] && [ -f "${REPO_ROOT}/scripts/initializr/common/icon.png" ]; then + cp "${REPO_ROOT}/scripts/initializr/common/icon.png" "${output_dir}/icon.png" + fi } build_playground_for_site() { From b7e602ffc44d0cfd93636118bf3ca9098243c8af Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 9 Jun 2026 03:56:36 +0300 Subject: [PATCH 06/13] js-port(worker): define cn1_get_native_interfaces registry in the worker Native-interface stubs (com__.js) generated by StubGenerator end with `})(cn1_get_native_interfaces());` and self-register into that registry. The accessor is defined only in fontmetrics.js, which loads on the main thread; the worker imports the stubs via importScripts but never loads fontmetrics.js, so the IIFE throws "ReferenceError: cn1_get_native_interfaces is not defined" and aborts worker startup before jvm.start(). Any app with a JS-port NativeInterface (e.g. the Initializr's WebsiteThemeNative) therefore never boots; HelloCodenameOne has none, so the screenshot suite never caught it. Define a worker-local registry before the imports, mirroring the existing getParameterByName worker-local shim. Co-Authored-By: Claude Opus 4.8 (1M context) --- vm/ByteCodeTranslator/src/javascript/worker.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/vm/ByteCodeTranslator/src/javascript/worker.js b/vm/ByteCodeTranslator/src/javascript/worker.js index 2b48303a92..a9daafa9b5 100644 --- a/vm/ByteCodeTranslator/src/javascript/worker.js +++ b/vm/ByteCodeTranslator/src/javascript/worker.js @@ -23,6 +23,18 @@ self.getParameterByName = function(name) { } return decodeURIComponent(results[1].replace(/\+/g, ' ')); }; +// Native-interface stubs (com__.js, imported just below) end with +// ``})(cn1_get_native_interfaces());`` — they self-register into the registry +// returned by that accessor. The accessor is defined in fontmetrics.js, which +// only loads on the main thread; in the worker ``window`` aliases ``self`` and +// fontmetrics.js never loads, so the IIFE throws ReferenceError and aborts +// worker startup before ``jvm.start()`` ever runs — any app with a JS-port +// NativeInterface fails to boot. Define a worker-local registry so the stubs +// register cleanly here too (mirrors the getParameterByName shim above). +self.cn1_native_interfaces = self.cn1_native_interfaces || {}; +self.cn1_get_native_interfaces = self.cn1_get_native_interfaces || function() { + return self.cn1_native_interfaces; +}; /*__IMPORTS__*/ if (typeof self.__parparInstallNativeBindings === 'function') { self.__parparInstallNativeBindings(); From ef67e8c57f85f887f1e3cfd645e5ded43074bc93 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 9 Jun 2026 04:22:11 +0300 Subject: [PATCH 07/13] website: build the Initializr against the local 8.0-SNAPSHOT plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The local ParparVM JavaScript builder (codename1.buildTarget= local-javascript) and its fixes live in the repo's 8.0-SNAPSHOT plugin, not the pinned 7.0.250 release the initializr was building against. So the preview never exercised the repo's builder/translator/JS-port — the native-interface worker fix, the login-aware license gate, etc. could not take effect. Mirror the Playground/Skin Designer path for the initializr: - cn1-local-workspace profile now overrides cn1.version AND cn1.plugin.version to 8.0-SNAPSHOT (was a vestigial 7.0.250 re-pin). - build_initializr_for_site bootstraps the local snapshots and builds with -Dcn1.localWorkspace=true under the bootstrapped JDK 17. - bootstrap_local_cn1_snapshots is now idempotent (the full reactor build runs once even though all three site apps call it). - website-docs.yml sets WEBSITE_BOOTSTRAP_CN1_SNAPSHOTS=true so the preview builds the site apps against repo HEAD. Validated locally: the initializr JS bundle builds against 8.0-SNAPSHOT and its worker.js carries the cn1_get_native_interfaces registry shim. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/website-docs.yml | 5 +++++ scripts/initializr/pom.xml | 8 +++++++- scripts/website/build.sh | 24 +++++++++++++++++++++++- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/.github/workflows/website-docs.yml b/.github/workflows/website-docs.yml index 708698e283..a23dc8ea3b 100644 --- a/.github/workflows/website-docs.yml +++ b/.github/workflows/website-docs.yml @@ -144,6 +144,11 @@ jobs: WEBSITE_INCLUDE_DEVGUIDE: "true" WEBSITE_INCLUDE_INITIALIZR: "auto" WEBSITE_INCLUDE_PLAYGROUND: "auto" + # The Initializr's JavaScript app is built with the local ParparVM + # target, whose builder lives in the repo (8.0-SNAPSHOT) plugin rather + # than the pinned release. Bootstrap the local snapshot artifacts so + # the initializr (and the other site apps) build against repo HEAD. + WEBSITE_BOOTSTRAP_CN1_SNAPSHOTS: "true" # PR previews build with future-dated posts visible so reviewers # can read posts staged for later in the week. Production deploys # (push to master) keep the default so future posts only appear diff --git a/scripts/initializr/pom.xml b/scripts/initializr/pom.xml index 95e782606b..d8552381be 100644 --- a/scripts/initializr/pom.xml +++ b/scripts/initializr/pom.xml @@ -117,8 +117,14 @@ true + - 7.0.250 + 8.0-SNAPSHOT + 8.0-SNAPSHOT diff --git a/scripts/website/build.sh b/scripts/website/build.sh index 74d61b191a..156f3d31f5 100755 --- a/scripts/website/build.sh +++ b/scripts/website/build.sh @@ -48,11 +48,18 @@ bootstrap_local_cn1_snapshots() { return fi + # Each site app (initializr/playground/skindesigner) calls this; the full + # reactor build is expensive, so run setup-workspace.sh only once per build. + if [ "${__CN1_SNAPSHOTS_BOOTSTRAPPED:-}" = "true" ]; then + return + fi + echo "Bootstrapping local Codename One snapshot Maven artifacts..." >&2 ( cd "${REPO_ROOT}" SKIP_CN1_ARCHETYPES=1 ./scripts/setup-workspace.sh -q -DskipTests ) + __CN1_SNAPSHOTS_BOOTSTRAPPED="true" } activate_bootstrapped_java17() { @@ -569,6 +576,13 @@ build_initializr_for_site() { return fi + # The initializr builds the JavaScript app with the local ParparVM target + # (codename1.buildTarget=local-javascript). That builder lives in the repo's + # 8.0-SNAPSHOT plugin, not the pinned release, so bootstrap the local + # snapshots and build with -Dcn1.localWorkspace=true (the cn1-local-workspace + # profile then overrides cn1.version/cn1.plugin.version to the repo build). + bootstrap_local_cn1_snapshots + echo "Building Initializr JavaScript bundle for website..." >&2 ( cd "${REPO_ROOT}/scripts/initializr" @@ -581,7 +595,13 @@ build_initializr_for_site() { fi } - if [ -n "${JAVA_HOME_8_X64:-}" ]; then + local initializr_workspace_args=() + if [ "${WEBSITE_BOOTSTRAP_CN1_SNAPSHOTS}" = "true" ]; then + # Local ParparVM JS build runs the translator + javac; use the + # bootstrapped JDK 17 (matching the Playground/Skin Designer path). + activate_bootstrapped_java17 + initializr_workspace_args+=(-Dcn1.localWorkspace=true) + elif [ -n "${JAVA_HOME_8_X64:-}" ]; then export JAVA_HOME="${JAVA_HOME_8_X64}" export PATH="${JAVA_HOME}/bin:${PATH}" fi @@ -589,6 +609,7 @@ build_initializr_for_site() { # Ensure attached classifier artifact initializr-ZipSupport:jar:common is present # in the local Maven repo before building modules that depend on it (e.g. initializr-common). run_initializr_mvn -q -U -pl cn1libs/ZipSupport -am \ + "${initializr_workspace_args[@]}" \ -DskipTests \ -Dcodename1.platform=javascript \ install @@ -596,6 +617,7 @@ build_initializr_for_site() { set_cn1_user_token "Initializr" run_initializr_mvn -q -U -pl javascript -am \ + "${initializr_workspace_args[@]}" \ -DskipTests \ -Dautomated=true \ -Dcodename1.platform=javascript \ From 424534c4aef5b900a4288ca795bd349548fe811a Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 9 Jun 2026 06:15:11 +0300 Subject: [PATCH 08/13] js-port: implement NativeInterface support (runs on the main thread) NativeLookup.create() returned null on the ParparVM JS port: there was no Impl, the cn1_native_interfaces registry was never read, and the worker imported the JS stub (which crashed on the undefined cn1_get_native_interfaces). Native interfaces simply weren't wired. Implement them end to end, modeled on the cloud builder's JSStubGenerator/NativeLookup.register flow but adapted to the worker/host-call model so the developer's JS impl runs on the MAIN thread (DOM access), as it must: - JavaScriptBuilder: scan staged classes for NativeInterface subtypes and generate a Impl per interface whose methods box their args and delegate to NativeInterfaceBridge.call*. The launcher emits NativeLookup.register(iface, impl) so create() resolves and the optimizer keeps the impl (otherwise reached only reflectively). - NativeInterfaceBridge (com.codename1.impl.platform.js -> HOST_HOOK): call{Boolean,Int,...,String,Object,Void}(iface, method, args). The worker suspends and the call replays on the main thread. - browser_bridge.js: main-thread handlers look up cn1_native_interfaces[iface][method_], invoke it with (args..., { complete, error }) and resume the worker via the host callback. - JavascriptBundleWriter: native-interface stubs (identified by the cn1_get_native_interfaces marker) load on the MAIN thread (index.html) and are excluded from the worker importScripts. - parparvm_runtime: marshal Java String -> JS string in toHostTransferArg so the iface/method names reach the host correctly. - worker.js: drop the earlier worker-local registry shim (the degrade-in-worker approach); stubs no longer load in the worker. The existing JS impl format (cn1_native_interfaces["_"] ["_"](args..., callback)) is preserved so stubs work as-is. Validated locally: the initializr bundle's WebsiteThemeNativeImpl is reachable and emits callBoolean/callVoid host-hooks; browser_bridge registers the handlers; index.html loads the stub and the worker does not. boolean/void/numeric returns are complete; String/Object/long return wrapping is a follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../platform/js/NativeInterfaceBridge.java | 55 ++++ .../codename1/builders/JavaScriptBuilder.java | 282 +++++++++++++++++- .../translator/JavascriptBundleWriter.java | 52 +++- .../src/javascript/browser_bridge.js | 76 +++++ .../src/javascript/index.html | 4 + .../src/javascript/parparvm_runtime.js | 7 + .../src/javascript/worker.js | 12 - 7 files changed, 466 insertions(+), 22 deletions(-) create mode 100644 Ports/JavaScriptPort/src/main/java/com/codename1/impl/platform/js/NativeInterfaceBridge.java diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/platform/js/NativeInterfaceBridge.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/platform/js/NativeInterfaceBridge.java new file mode 100644 index 0000000000..9c8c731890 --- /dev/null +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/platform/js/NativeInterfaceBridge.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026 Codename One and contributors. + * Licensed under the PolyForm Noncommercial License 1.0.0. + * You may use this file only in compliance with that license. + * The license notice for this subtree is available in Ports/JavaScriptPort/LICENSE.md. + */ +package com.codename1.impl.platform.js; + +/** + * Host bridge that dispatches Codename One {@code NativeInterface} method calls + * to their JavaScript implementation registered in {@code cn1_native_interfaces}. + * + *

The generated {@code Impl} classes (emitted by the JavaScript + * builder) delegate every interface method to one of the {@code call*} natives + * below. Because this class lives in {@code com.codename1.impl.platform.js}, the + * translator categorizes these natives as HOST_HOOK: the worker suspends and the + * call is replayed on the main thread (via {@code browser_bridge.js}), + * where the developer-authored JS stub runs with full DOM access and completes + * the call through its callback. The worker resumes with the returned value.

+ * + *

This preserves the existing JS native-interface impl format + * ({@code cn1_native_interfaces["_"]["_"](args..., callback)}) + * so existing stubs work unchanged.

+ * + *

{@code iface} is the interface class name with dots replaced by underscores + * (the {@code cn1_native_interfaces} registry key) and {@code method} is the + * trailing-underscore method key (e.g. {@code "isDarkMode_"}). {@code args} + * holds the (boxed) Java arguments, or an empty array for a no-arg method.

+ */ +public final class NativeInterfaceBridge { + private NativeInterfaceBridge() { + } + + public static native boolean callBoolean(String iface, String method, Object[] args); + + public static native int callInt(String iface, String method, Object[] args); + + public static native long callLong(String iface, String method, Object[] args); + + public static native double callDouble(String iface, String method, Object[] args); + + public static native float callFloat(String iface, String method, Object[] args); + + public static native byte callByte(String iface, String method, Object[] args); + + public static native short callShort(String iface, String method, Object[] args); + + public static native char callChar(String iface, String method, Object[] args); + + public static native String callString(String iface, String method, Object[] args); + + public static native Object callObject(String iface, String method, Object[] args); + + public static native void callVoid(String iface, String method, Object[] args); +} diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java index 9b6e3f2e6b..0e1a25998b 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java @@ -29,6 +29,10 @@ import java.io.InputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.net.URLClassLoader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -123,9 +127,17 @@ public boolean build(File sourceZip, BuildRequest request) throws BuildException File portSources = locateJavaScriptPortSources(request); File portClassesStaged = stageJavaScriptPort(request, portSources, stageClasses, portClasses); + // For every NativeInterface in the app, generate a Impl whose + // methods bridge to the developer's JS stub on the MAIN thread (via + // NativeInterfaceBridge -> browser_bridge.js -> cn1_native_interfaces). The + // launcher registers each impl with NativeLookup so create() resolves and the + // optimizer keeps the impl (it is otherwise only reached reflectively). + List> nativeInterfaces = findNativeInterfaces(stageClasses); + List generatedImpls = generateNativeInterfaceImpls(buildDir, nativeInterfaces); + String translatorAppName = sanitizeIdentifier(request.getMainClass()) + "JavaScriptMain"; - File launcherJava = writeLauncher(buildDir, translatorAppName, request.getPackageName(), request.getMainClass(), stageClasses); - compileLauncher(launcherJava, stageClasses, portClassesStaged); + File launcherJava = writeLauncher(buildDir, translatorAppName, request.getPackageName(), request.getMainClass(), stageClasses, nativeInterfaces); + compileLauncher(launcherJava, generatedImpls, stageClasses, portClassesStaged); File parparvmCompilerJar = extractParparVMCompiler(); @@ -371,7 +383,8 @@ private String resolveJavac() { return "javac"; } - private File writeLauncher(File workDir, String launcherName, String packageName, String mainClass, File stageClasses) throws IOException { + private File writeLauncher(File workDir, String launcherName, String packageName, String mainClass, File stageClasses, + List> nativeInterfaces) throws IOException { // If the build-time SVG transcoder generated com.codename1.generated.svg.SVGRegistry // for this app, register the transcoded SVGs at startup -- the JS-port analogue of // JavaSEPort.init's reflective installGlobal(). A DIRECT call (not reflection) is @@ -390,6 +403,16 @@ private File writeLauncher(File workDir, String launcherName, String packageName if (hasGeneratedSvg) { pw.println(" com.codename1.generated.svg.SVGRegistry.installGlobal();"); } + // Register the generated native interface implementations. The DIRECT + // class references (not reflection) also keep the optimizer from culling + // the *Impl classes, which are otherwise reached only via NativeLookup. + if (nativeInterfaces != null) { + for (Class iface : nativeInterfaces) { + String ifaceName = iface.getName(); + pw.println(" com.codename1.system.NativeLookup.register(" + + ifaceName + ".class, " + ifaceName + "Impl.class);"); + } + } pw.println(" ParparVMBootstrap.bootstrap(new " + mainClass + "());"); pw.println(" }"); pw.println("}"); @@ -399,15 +422,256 @@ private File writeLauncher(File workDir, String launcherName, String packageName return f; } - private void compileLauncher(File launcherJava, File stageClasses, File portClasses) throws Exception { + private void compileLauncher(File launcherJava, List generatedImpls, File stageClasses, File portClasses) throws Exception { String javac = resolveJavac(); - boolean ok = exec(tmpDir, -1, javac, "-source", "8", "-target", "8", - "-cp", stageClasses.getAbsolutePath() + File.pathSeparator + portClasses.getAbsolutePath(), - "-d", stageClasses.getAbsolutePath(), - launcherJava.getAbsolutePath()); + List cmd = new ArrayList(); + cmd.add(javac); + cmd.add("-source"); cmd.add("8"); + cmd.add("-target"); cmd.add("8"); + cmd.add("-cp"); cmd.add(stageClasses.getAbsolutePath() + File.pathSeparator + portClasses.getAbsolutePath()); + cmd.add("-d"); cmd.add(stageClasses.getAbsolutePath()); + cmd.add(launcherJava.getAbsolutePath()); + if (generatedImpls != null) { + for (File impl : generatedImpls) { + cmd.add(impl.getAbsolutePath()); + } + } + boolean ok = exec(tmpDir, -1, cmd.toArray(new String[cmd.size()])); if (!ok) { - throw new BuildException("Failed to compile JavaScript launcher class"); + throw new BuildException("Failed to compile JavaScript launcher / native interface impl classes"); + } + } + + // ----- Native interface binding -------------------------------------------------- + // Scans the staged app classes for com.codename1.system.NativeInterface subtypes and + // generates, per interface, a Impl whose methods delegate to + // NativeInterfaceBridge.call* (a HOST_HOOK native). At runtime those calls suspend the + // worker and run the developer's JS stub (cn1_native_interfaces[...][method_]) on the + // MAIN thread, then resume the worker with the result. Mirrors the cloud builder's + // JSStubGenerator + NativeLookup.register flow, adapted to the worker/host-call model. + + private List> findNativeInterfaces(File stageClasses) { + List> result = new ArrayList>(); + URLClassLoader loader = null; + try { + loader = new URLClassLoader(new URL[]{ stageClasses.toURI().toURL() }, + JavaScriptBuilder.class.getClassLoader()); + Class niClass; + try { + niClass = loader.loadClass("com.codename1.system.NativeInterface"); + } catch (Throwable t) { + log("com.codename1.system.NativeInterface not on the classpath; no native interfaces to bind"); + return result; + } + List classFiles = new ArrayList(); + collectClassFiles(stageClasses, classFiles); + for (File cf : classFiles) { + // Cheap pre-filter: only classes whose bytes mention the marker interface + // are candidates (native interfaces extend it directly). Avoids loading the + // thousands of unrelated core/runtime classes. + byte[] bytes; + try { + bytes = java.nio.file.Files.readAllBytes(cf.toPath()); + } catch (Throwable t) { + continue; + } + if (!new String(bytes, StandardCharsets.ISO_8859_1).contains("com/codename1/system/NativeInterface")) { + continue; + } + String cn = classNameFor(stageClasses, cf); + if (cn == null) { + continue; + } + try { + Class c = loader.loadClass(cn); + if (c.isInterface() && !c.equals(niClass) && niClass.isAssignableFrom(c)) { + result.add(c); + log("Found native interface: " + c.getName()); + } + } catch (Throwable ignore) { + // class not loadable in isolation (missing deps) -- not a native interface we can bind + } + } + } catch (Throwable t) { + log("Failed scanning for native interfaces: " + t); + } finally { + if (loader != null) { + try { + loader.close(); + } catch (Throwable ignore) { + } + } + } + return result; + } + + private static void collectClassFiles(File dir, List out) { + File[] children = dir.listFiles(); + if (children == null) return; + for (File f : children) { + if (f.isDirectory()) { + collectClassFiles(f, out); + } else if (f.getName().endsWith(".class") && f.getName().indexOf('$') < 0) { + out.add(f); + } + } + } + + private static String classNameFor(File root, File classFile) { + String rootPath = root.getAbsolutePath(); + String filePath = classFile.getAbsolutePath(); + if (!filePath.startsWith(rootPath)) { + return null; + } + String rel = filePath.substring(rootPath.length()); + if (rel.startsWith(File.separator)) { + rel = rel.substring(1); + } + if (!rel.endsWith(".class")) { + return null; + } + rel = rel.substring(0, rel.length() - ".class".length()); + return rel.replace(File.separatorChar, '.').replace('/', '.'); + } + + private List generateNativeInterfaceImpls(File buildDir, List> nativeInterfaces) throws IOException { + List generated = new ArrayList(); + if (nativeInterfaces == null || nativeInterfaces.isEmpty()) { + return generated; + } + File genDir = new File(buildDir, "generated-native-impls"); + genDir.mkdirs(); + for (Class iface : nativeInterfaces) { + File jf = writeNativeInterfaceImpl(genDir, iface); + if (jf != null) { + generated.add(jf); + } + } + return generated; + } + + private File writeNativeInterfaceImpl(File genDir, Class iface) throws IOException { + String pkg = iface.getPackage() != null ? iface.getPackage().getName() : ""; + String simpleImpl = iface.getSimpleName() + "Impl"; + String registryKey = iface.getName().replace('.', '_'); + + File pkgDir = pkg.isEmpty() ? genDir : new File(genDir, pkg.replace('.', File.separatorChar)); + pkgDir.mkdirs(); + File out = new File(pkgDir, simpleImpl + ".java"); + + StringBuilder sb = new StringBuilder(); + if (!pkg.isEmpty()) { + sb.append("package ").append(pkg).append(";\n\n"); + } + sb.append("public class ").append(simpleImpl) + .append(" implements ").append(iface.getName()).append(" {\n"); + sb.append(" private static final String __NI = \"").append(registryKey).append("\";\n\n"); + + for (Method m : iface.getMethods()) { + if (Modifier.isStatic(m.getModifiers())) { + continue; + } + appendNativeInterfaceImplMethod(sb, m); + } + sb.append("}\n"); + + PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(out), StandardCharsets.UTF_8)); + try { + pw.print(sb.toString()); + } finally { + pw.close(); + } + return out; + } + + private void appendNativeInterfaceImplMethod(StringBuilder sb, Method m) { + Class[] params = m.getParameterTypes(); + Class ret = m.getReturnType(); + String methodKey = nativeInterfaceMethodKey(m); + + sb.append(" public ").append(ret.getCanonicalName()).append(" ").append(m.getName()).append("("); + for (int i = 0; i < params.length; i++) { + if (i > 0) sb.append(", "); + sb.append(params[i].getCanonicalName()).append(" p").append(i); + } + sb.append(") {\n"); + + // Build the boxed argument array. + StringBuilder args = new StringBuilder(); + if (params.length == 0) { + args.append("new Object[0]"); + } else { + args.append("new Object[]{ "); + for (int i = 0; i < params.length; i++) { + if (i > 0) args.append(", "); + args.append(boxArgExpression(params[i], "p" + i)); + } + args.append(" }"); + } + + String call = "com.codename1.impl.platform.js.NativeInterfaceBridge."; + String invokeArgs = "__NI, \"" + methodKey + "\", " + args.toString(); + + if (ret == void.class) { + sb.append(" ").append(call).append("callVoid(").append(invokeArgs).append(");\n"); + } else if (ret == boolean.class) { + sb.append(" return ").append(call).append("callBoolean(").append(invokeArgs).append(");\n"); + } else if (ret == int.class) { + sb.append(" return ").append(call).append("callInt(").append(invokeArgs).append(");\n"); + } else if (ret == long.class) { + sb.append(" return ").append(call).append("callLong(").append(invokeArgs).append(");\n"); + } else if (ret == double.class) { + sb.append(" return ").append(call).append("callDouble(").append(invokeArgs).append(");\n"); + } else if (ret == float.class) { + sb.append(" return ").append(call).append("callFloat(").append(invokeArgs).append(");\n"); + } else if (ret == byte.class) { + sb.append(" return ").append(call).append("callByte(").append(invokeArgs).append(");\n"); + } else if (ret == short.class) { + sb.append(" return ").append(call).append("callShort(").append(invokeArgs).append(");\n"); + } else if (ret == char.class) { + sb.append(" return ").append(call).append("callChar(").append(invokeArgs).append(");\n"); + } else if (ret == String.class) { + sb.append(" return ").append(call).append("callString(").append(invokeArgs).append(");\n"); + } else { + sb.append(" return (").append(ret.getCanonicalName()).append(") ") + .append(call).append("callObject(").append(invokeArgs).append(");\n"); + } + sb.append(" }\n\n"); + } + + private static String boxArgExpression(Class type, String var) { + if (type == int.class) return "Integer.valueOf(" + var + ")"; + if (type == long.class) return "Long.valueOf(" + var + ")"; + if (type == double.class) return "Double.valueOf(" + var + ")"; + if (type == float.class) return "Float.valueOf(" + var + ")"; + if (type == boolean.class) return "Boolean.valueOf(" + var + ")"; + if (type == byte.class) return "Byte.valueOf(" + var + ")"; + if (type == short.class) return "Short.valueOf(" + var + ")"; + if (type == char.class) return "Character.valueOf(" + var + ")"; + return var; + } + + // Mirrors StubGenerator's JS stub key: methodName + "_" + ("_" + xmlvmType) per param. + private static String nativeInterfaceMethodKey(Method m) { + StringBuilder key = new StringBuilder(m.getName()).append("_"); + for (Class p : m.getParameterTypes()) { + if ("com.codename1.ui.PeerComponent".equals(p.getName())) { + key.append("_com_codename1_ui_PeerComponent"); + } else { + key.append("_").append(xmlvmTypeName(p)); + } + } + return key.toString(); + } + + private static String xmlvmTypeName(Class type) { + if (type.isArray()) { + return xmlvmTypeName(type.getComponentType()) + "_1ARRAY"; + } + if (type.isPrimitive()) { + return type.getName(); } + return type.getName().replace('.', '_'); } private File extractParparVMCompiler() throws BuildException { diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java index 0bbaba57fe..35033264ae 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java @@ -536,6 +536,10 @@ private static void writeWorker(File outputDirectory) throws IOException { // call) but *after* other runtime helpers / native shims. if (name.startsWith("translated_app_") && name.endsWith(".js")) { classChunkScripts.add(name); + } else if (isNativeInterfaceStub(file)) { + // Native-interface implementations run on the MAIN thread (index.html), + // not in the worker -- they need DOM access. Skip them here. + continue; } else { nativeScripts.add(name); } @@ -560,7 +564,53 @@ private static void writeWorker(File outputDirectory) throws IOException { } private static void writeIndex(File outputDirectory) throws IOException { - writeResource(outputDirectory, "index.html", "index.html"); + String index = loadResource("index.html"); + StringBuilder stubs = new StringBuilder(); + for (String stub : collectNativeInterfaceStubs(outputDirectory)) { + stubs.append("\n"); + } + index = index.replace("", stubs.toString().trim()); + Files.write(new File(outputDirectory, "index.html").toPath(), index.getBytes(StandardCharsets.UTF_8)); + } + + /** + * Native-interface JS implementations self-register into + * {@code cn1_native_interfaces} (they end with {@code })(cn1_get_native_interfaces());}). + * They run on the MAIN thread so their DOM access works, and are dispatched from the + * worker via the host-call bridge. Identify them by that content marker so the worker + * importScripts list excludes them and index.html loads them on the page instead. + */ + private static List collectNativeInterfaceStubs(File outputDirectory) { + List stubs = new ArrayList(); + File[] files = outputDirectory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.getName().endsWith(".js") && isNativeInterfaceStub(file)) { + stubs.add(file.getName()); + } + } + } + Collections.sort(stubs); + return stubs; + } + + private static boolean isNativeInterfaceStub(File jsFile) { + String name = jsFile.getName(); + if ("parparvm_runtime.js".equals(name) + || "translated_app.js".equals(name) + || "worker.js".equals(name) + || "sw.js".equals(name) + || "port.js".equals(name) + || "browser_bridge.js".equals(name) + || name.startsWith("translated_app_")) { + return false; + } + try { + String content = new String(Files.readAllBytes(jsFile.toPath()), StandardCharsets.UTF_8); + return content.contains("cn1_get_native_interfaces"); + } catch (IOException ex) { + return false; + } } private static void writeBrowserBridge(File outputDirectory) throws IOException { diff --git a/vm/ByteCodeTranslator/src/javascript/browser_bridge.js b/vm/ByteCodeTranslator/src/javascript/browser_bridge.js index 8693dca4b4..5f9e0da2e5 100644 --- a/vm/ByteCodeTranslator/src/javascript/browser_bridge.js +++ b/vm/ByteCodeTranslator/src/javascript/browser_bridge.js @@ -164,6 +164,82 @@ } }; + // ---- Native interface dispatch ------------------------------------------------- + // Codename One NativeInterface calls arrive here (on the MAIN thread) from the + // worker via the generated Impl -> NativeInterfaceBridge.call* host-hooks. + // We look up the developer's JS implementation in cn1_native_interfaces (the + // registry the stub self-registers into, populated on the main thread by the + // + + diff --git a/vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js b/vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js index fddb772c2a..b752778d78 100644 --- a/vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js +++ b/vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js @@ -2393,6 +2393,13 @@ const jvm = { } return out; } + // A Java String is a VM object, not a JS primitive. Host code (e.g. the + // native-interface bridge, which passes the interface/method names) expects + // the actual text, so marshal it to a plain JS string rather than letting it + // fall through to the opaque object-iteration path below. + if (value.__class === "java_lang_String") { + return this.toNativeString(value); + } if (value.__cn1HostRef != null) { return value.__cn1HostClass ? { __cn1HostRef: value.__cn1HostRef, __cn1HostClass: value.__cn1HostClass } diff --git a/vm/ByteCodeTranslator/src/javascript/worker.js b/vm/ByteCodeTranslator/src/javascript/worker.js index a9daafa9b5..2b48303a92 100644 --- a/vm/ByteCodeTranslator/src/javascript/worker.js +++ b/vm/ByteCodeTranslator/src/javascript/worker.js @@ -23,18 +23,6 @@ self.getParameterByName = function(name) { } return decodeURIComponent(results[1].replace(/\+/g, ' ')); }; -// Native-interface stubs (com__.js, imported just below) end with -// ``})(cn1_get_native_interfaces());`` — they self-register into the registry -// returned by that accessor. The accessor is defined in fontmetrics.js, which -// only loads on the main thread; in the worker ``window`` aliases ``self`` and -// fontmetrics.js never loads, so the IIFE throws ReferenceError and aborts -// worker startup before ``jvm.start()`` ever runs — any app with a JS-port -// NativeInterface fails to boot. Define a worker-local registry so the stubs -// register cleanly here too (mirrors the getParameterByName shim above). -self.cn1_native_interfaces = self.cn1_native_interfaces || {}; -self.cn1_get_native_interfaces = self.cn1_get_native_interfaces || function() { - return self.cn1_native_interfaces; -}; /*__IMPORTS__*/ if (typeof self.__parparInstallNativeBindings === 'function') { self.__parparInstallNativeBindings(); From 88ff91f5b668bb5ae78eada71a15f79b0a61e4e9 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 9 Jun 2026 08:01:59 +0300 Subject: [PATCH 09/13] js-port: full type marshalling for native interfaces Extend native-interface support to every NativeInterface type (all primitives, String, the primitive arrays byte[]/int[]/long[]/double[]/ float[]/boolean[]/char[]/short[], and String[]) in both directions. - NativeInterfaceBridge: per-primitive call* + callString/callVoid/ callObject, plus a typed callArray(iface, method, args, componentToken) for array returns. - JavaScriptBuilder: Impl return-type dispatch routes arrays to callArray with the element's component token (JAVA_INT.../ java_lang_String) and boxes primitive args into the Object[]. - JavascriptNativeRegistry: the call* symbols are RUNTIME_IMPLEMENTED so the translator defers to the worker-side wrappers. - parparvm_runtime.js: bindNative wrappers funnel through the single __cn1_native_interface_call__ host hook and coerce the resolved JS value to the declared Java type -- createJavaString (String/String[]), _LfromNumber (long/long[]), jvm.newArray(token) (typed arrays), int/short/byte/char/float/double normalisation. toHostTransferArg now unboxes argument values too (boxed Integer/Long/.../Boolean -> JS primitive, long {__l} -> number) so primitive/long/array arguments marshal correctly to the host. - browser_bridge.js: one __cn1_native_interface_call__ handler dispatches to cn1_native_interfaces[iface][method](args..., callback). Verified in the local initializr bundle: no "Missing javascript native method" stubs remain, the bindNative wrappers/array builder are present, and WebsiteThemeNativeImpl is reachable. PeerComponent returns route through callObject (best-effort) pending dedicated peer wrapping. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../platform/js/NativeInterfaceBridge.java | 45 +++++---- .../codename1/builders/JavaScriptBuilder.java | 22 +++++ .../translator/JavascriptNativeRegistry.java | 18 +++- .../src/javascript/browser_bridge.js | 32 ++----- .../src/javascript/parparvm_runtime.js | 94 +++++++++++++++++++ 5 files changed, 168 insertions(+), 43 deletions(-) diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/platform/js/NativeInterfaceBridge.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/platform/js/NativeInterfaceBridge.java index 9c8c731890..e919b3b908 100644 --- a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/platform/js/NativeInterfaceBridge.java +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/platform/js/NativeInterfaceBridge.java @@ -12,19 +12,20 @@ * *

The generated {@code Impl} classes (emitted by the JavaScript * builder) delegate every interface method to one of the {@code call*} natives - * below. Because this class lives in {@code com.codename1.impl.platform.js}, the - * translator categorizes these natives as HOST_HOOK: the worker suspends and the - * call is replayed on the main thread (via {@code browser_bridge.js}), - * where the developer-authored JS stub runs with full DOM access and completes - * the call through its callback. The worker resumes with the returned value.

+ * below, picked by the method's return type. These natives are runtime- + * implemented in {@code parparvm_runtime.js}: the worker suspends, the call is + * replayed on the main thread (via {@code browser_bridge.js}) where the + * developer-authored JS stub runs with full DOM access and completes the call + * through its callback, and the worker resumes with the result coerced to the + * declared Java type.

* - *

This preserves the existing JS native-interface impl format - * ({@code cn1_native_interfaces["_"]["_"](args..., callback)}) - * so existing stubs work unchanged.

+ *

Supported types mirror {@code NativeInterface}: all primitives, {@code String}, + * primitive arrays plus {@code String[]} (via {@link #callArray}), and + * {@code com.codename1.ui.PeerComponent} (routed through {@link #callObject}).

* *

{@code iface} is the interface class name with dots replaced by underscores - * (the {@code cn1_native_interfaces} registry key) and {@code method} is the - * trailing-underscore method key (e.g. {@code "isDarkMode_"}). {@code args} + * (the {@code cn1_native_interfaces} registry key), {@code method} is the + * trailing-underscore method key (e.g. {@code "isDarkMode_"}), and {@code args} * holds the (boxed) Java arguments, or an empty array for a no-arg method.

*/ public final class NativeInterfaceBridge { @@ -33,23 +34,33 @@ private NativeInterfaceBridge() { public static native boolean callBoolean(String iface, String method, Object[] args); - public static native int callInt(String iface, String method, Object[] args); + public static native byte callByte(String iface, String method, Object[] args); - public static native long callLong(String iface, String method, Object[] args); + public static native short callShort(String iface, String method, Object[] args); - public static native double callDouble(String iface, String method, Object[] args); + public static native int callInt(String iface, String method, Object[] args); - public static native float callFloat(String iface, String method, Object[] args); + public static native char callChar(String iface, String method, Object[] args); - public static native byte callByte(String iface, String method, Object[] args); + public static native long callLong(String iface, String method, Object[] args); - public static native short callShort(String iface, String method, Object[] args); + public static native float callFloat(String iface, String method, Object[] args); - public static native char callChar(String iface, String method, Object[] args); + public static native double callDouble(String iface, String method, Object[] args); public static native String callString(String iface, String method, Object[] args); public static native Object callObject(String iface, String method, Object[] args); public static native void callVoid(String iface, String method, Object[] args); + + /** + * Array-returning call. {@code componentToken} identifies the element type so + * the runtime can build the correctly-typed Java array: {@code "JAVA_INT"}, + * {@code "JAVA_BYTE"}, {@code "JAVA_LONG"}, {@code "JAVA_DOUBLE"}, + * {@code "JAVA_FLOAT"}, {@code "JAVA_BOOLEAN"}, {@code "JAVA_CHAR"}, + * {@code "JAVA_SHORT"} or {@code "java_lang_String"}. The caller casts the + * result to the concrete array type. + */ + public static native Object callArray(String iface, String method, Object[] args, String componentToken); } diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java index 0e1a25998b..f02088ec46 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java @@ -632,7 +632,15 @@ private void appendNativeInterfaceImplMethod(StringBuilder sb, Method m) { sb.append(" return ").append(call).append("callChar(").append(invokeArgs).append(");\n"); } else if (ret == String.class) { sb.append(" return ").append(call).append("callString(").append(invokeArgs).append(");\n"); + } else if (ret.isArray()) { + // Primitive arrays + String[]: callArray builds the correctly-typed + // Java array from the JS array the host returns (componentToken picks + // the element type). + sb.append(" return (").append(ret.getCanonicalName()).append(") ") + .append(call).append("callArray(").append(invokeArgs) + .append(", \"").append(arrayComponentToken(ret.getComponentType())).append("\");\n"); } else { + // PeerComponent / other reference types -- best effort passthrough. sb.append(" return (").append(ret.getCanonicalName()).append(") ") .append(call).append("callObject(").append(invokeArgs).append(");\n"); } @@ -664,6 +672,20 @@ private static String nativeInterfaceMethodKey(Method m) { return key.toString(); } + // Runtime newArray() component-class token for an array's element type. + private static String arrayComponentToken(Class component) { + if (component == int.class) return "JAVA_INT"; + if (component == long.class) return "JAVA_LONG"; + if (component == double.class) return "JAVA_DOUBLE"; + if (component == float.class) return "JAVA_FLOAT"; + if (component == boolean.class) return "JAVA_BOOLEAN"; + if (component == byte.class) return "JAVA_BYTE"; + if (component == short.class) return "JAVA_SHORT"; + if (component == char.class) return "JAVA_CHAR"; + if (component == String.class) return "java_lang_String"; + return component.getName().replace('.', '_'); + } + private static String xmlvmTypeName(Class type) { if (type.isArray()) { return xmlvmTypeName(type.getComponentType()) + "_1ARRAY"; diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptNativeRegistry.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptNativeRegistry.java index efa15290cd..2545ceab6b 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptNativeRegistry.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptNativeRegistry.java @@ -117,7 +117,23 @@ enum NativeCategory { "cn1_java_util_TimeZone_getTimezoneRawOffset_java_lang_String_R_int", "cn1_java_util_TimeZone_isTimezoneDST_java_lang_String_long_R_boolean", "cn1_com_codename1_impl_platform_js_VMHost_getLastEventCode_R_int", - "cn1_com_codename1_impl_platform_js_VMHost_pollEventCode_R_int" + "cn1_com_codename1_impl_platform_js_VMHost_pollEventCode_R_int", + // NativeInterface bridge: runtime-implemented in parparvm_runtime.js + // (bindNative). Each forwards to the main thread via the + // __cn1_native_interface_call__ host hook and coerces the result to + // the declared Java type (createJavaString / _LfromNumber / newArray). + "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callBoolean_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_boolean", + "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callByte_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_byte", + "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callShort_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_short", + "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callInt_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_int", + "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callChar_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_char", + "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callLong_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_long", + "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callFloat_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_float", + "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callDouble_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_double", + "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callString_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_java_lang_String", + "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callObject_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_java_lang_Object", + "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callArray_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_java_lang_String_R_java_lang_Object", + "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callVoid_java_lang_String_java_lang_String_java_lang_Object_1ARRAY" )); private static final Set HOST_HOOK_PREFIXES = new HashSet(Arrays.asList( diff --git a/vm/ByteCodeTranslator/src/javascript/browser_bridge.js b/vm/ByteCodeTranslator/src/javascript/browser_bridge.js index 5f9e0da2e5..c491c0dbff 100644 --- a/vm/ByteCodeTranslator/src/javascript/browser_bridge.js +++ b/vm/ByteCodeTranslator/src/javascript/browser_bridge.js @@ -214,31 +214,13 @@ }); } - // One handler per NativeInterfaceBridge.call* native symbol (the suffix encodes - // the bridge method + its (String, String, Object[]) signature and return type; - // void has no _R_). Dispatch is identical across return types -- the worker side - // coerces the resolved value to the declared Java type. - var __cn1NativeInterfaceBridgeSymbols = [ - 'callBoolean_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_boolean', - 'callInt_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_int', - 'callLong_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_long', - 'callDouble_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_double', - 'callFloat_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_float', - 'callByte_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_byte', - 'callShort_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_short', - 'callChar_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_char', - 'callString_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_java_lang_String', - 'callObject_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_java_lang_Object', - 'callVoid_java_lang_String_java_lang_String_java_lang_Object_1ARRAY' - ]; - for (var __cn1NiIdx = 0; __cn1NiIdx < __cn1NativeInterfaceBridgeSymbols.length; __cn1NiIdx++) { - (function(suffix) { - var symbol = 'cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_' + suffix; - hostBridge.register(symbol, function(iface, method, args) { - return cn1InvokeNativeInterface(iface, method, args); - }); - })(__cn1NativeInterfaceBridgeSymbols[__cn1NiIdx]); - } + // Single host hook for every NativeInterfaceBridge.call* native. The worker-side + // bindNative wrappers (parparvm_runtime.js) funnel here with (iface, method, args) + // and coerce the resolved value to the declared Java return type, so dispatch is + // uniform on this side. + hostBridge.register('__cn1_native_interface_call__', function(iface, method, args) { + return cn1InvokeNativeInterface(iface, method, args); + }); var hostRefNextId = 1; var hostRefById = {}; diff --git a/vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js b/vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js index b752778d78..9066830ce8 100644 --- a/vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js +++ b/vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js @@ -2400,6 +2400,23 @@ const jvm = { if (value.__class === "java_lang_String") { return this.toNativeString(value); } + // 64-bit long ({__l:1,l,h}) -> JS number for the host. + if (value.__l === 1) { + return _LtoNumber(value); + } + // Boxed primitives -> their JS value. NativeInterface args arrive boxed in an + // Object[] (Integer.valueOf(...) etc.); the host wants the plain value. + switch (value.__class) { + case "java_lang_Integer": return value.cn1_java_lang_Integer_value | 0; + case "java_lang_Short": return value.cn1_java_lang_Short_value | 0; + case "java_lang_Byte": return value.cn1_java_lang_Byte_value | 0; + case "java_lang_Character": return value.cn1_java_lang_Character_value | 0; + case "java_lang_Boolean": return !!value.cn1_java_lang_Boolean_value; + case "java_lang_Double": return Number(value.cn1_java_lang_Double_value); + case "java_lang_Float": return Number(value.cn1_java_lang_Float_value); + case "java_lang_Long": return _LtoNum(value.cn1_java_lang_Long_value); + default: break; + } if (value.__cn1HostRef != null) { return value.__cn1HostClass ? { __cn1HostRef: value.__cn1HostRef, __cn1HostClass: value.__cn1HostClass } @@ -5159,6 +5176,83 @@ bindNative(["cn1_com_codename1_impl_platform_js_VMHost_pollEventCode_R_int", return event && event.code != null ? (event.code | 0) : -1; }); +// ---- NativeInterface bridge ----------------------------------------------------- +// The generated Impl methods call these NativeInterfaceBridge.call* +// natives. Each forwards the (iface, method, args) tuple to the MAIN thread via +// the shared __cn1_native_interface_call__ host hook (browser_bridge.js runs the +// developer's JS stub with DOM access and resolves through its callback), then +// coerces the JS result to the declared Java return type. Args were already +// unboxed by toHostTransferArg (boxed primitives / Java String / long). +const __NI_PREFIX = "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_"; +const __NI_SIG = "java_lang_String_java_lang_String_java_lang_Object_1ARRAY"; +function* __cn1NativeInterfaceCall(iface, method, args) { + return yield jvm.invokeHostNative("__cn1_native_interface_call__", [iface, method, args]); +} +function __cn1NativeInterfaceArray(v, token) { + if (v == null) { + return null; + } + const len = v.length | 0; + const arr = jvm.newArray(len, token, 1); + for (let i = 0; i < len; i++) { + const e = v[i]; + switch (token) { + case "java_lang_String": arr[i] = (e == null ? null : createJavaString(e)); break; + case "JAVA_LONG": arr[i] = _LfromNumber(Number(e || 0)); break; + case "JAVA_BOOLEAN": arr[i] = !!e; break; + case "JAVA_CHAR": arr[i] = (e | 0) & 0xffff; break; + case "JAVA_BYTE": arr[i] = ((e | 0) << 24) >> 24; break; + case "JAVA_SHORT": arr[i] = ((e | 0) << 16) >> 16; break; + case "JAVA_INT": arr[i] = e | 0; break; + case "JAVA_FLOAT": arr[i] = Math.fround(Number(e || 0)); break; + case "JAVA_DOUBLE": arr[i] = Number(e || 0); break; + default: arr[i] = e; + } + } + return arr; +} +bindNative([__NI_PREFIX + "callBoolean_" + __NI_SIG + "_R_boolean"], function*(iface, method, args) { + return !!(yield* __cn1NativeInterfaceCall(iface, method, args)); +}); +bindNative([__NI_PREFIX + "callByte_" + __NI_SIG + "_R_byte"], function*(iface, method, args) { + const v = yield* __cn1NativeInterfaceCall(iface, method, args); return ((v | 0) << 24) >> 24; +}); +bindNative([__NI_PREFIX + "callShort_" + __NI_SIG + "_R_short"], function*(iface, method, args) { + const v = yield* __cn1NativeInterfaceCall(iface, method, args); return ((v | 0) << 16) >> 16; +}); +bindNative([__NI_PREFIX + "callInt_" + __NI_SIG + "_R_int"], function*(iface, method, args) { + return (yield* __cn1NativeInterfaceCall(iface, method, args)) | 0; +}); +bindNative([__NI_PREFIX + "callChar_" + __NI_SIG + "_R_char"], function*(iface, method, args) { + return ((yield* __cn1NativeInterfaceCall(iface, method, args)) | 0) & 0xffff; +}); +bindNative([__NI_PREFIX + "callLong_" + __NI_SIG + "_R_long"], function*(iface, method, args) { + return _LfromNumber(Number((yield* __cn1NativeInterfaceCall(iface, method, args)) || 0)); +}); +bindNative([__NI_PREFIX + "callFloat_" + __NI_SIG + "_R_float"], function*(iface, method, args) { + return Math.fround(Number((yield* __cn1NativeInterfaceCall(iface, method, args)) || 0)); +}); +bindNative([__NI_PREFIX + "callDouble_" + __NI_SIG + "_R_double"], function*(iface, method, args) { + return Number((yield* __cn1NativeInterfaceCall(iface, method, args)) || 0); +}); +bindNative([__NI_PREFIX + "callString_" + __NI_SIG + "_R_java_lang_String"], function*(iface, method, args) { + const v = yield* __cn1NativeInterfaceCall(iface, method, args); + return v == null ? null : createJavaString(v); +}); +bindNative([__NI_PREFIX + "callObject_" + __NI_SIG + "_R_java_lang_Object"], function*(iface, method, args) { + const v = yield* __cn1NativeInterfaceCall(iface, method, args); + return (typeof v === "string") ? createJavaString(v) : (v == null ? null : v); +}); +bindNative([__NI_PREFIX + "callVoid_" + __NI_SIG], function*(iface, method, args) { + yield* __cn1NativeInterfaceCall(iface, method, args); + return null; +}); +bindNative([__NI_PREFIX + "callArray_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_java_lang_String_R_java_lang_Object"], + function*(iface, method, args, token) { + const v = yield* __cn1NativeInterfaceCall(iface, method, args); + return __cn1NativeInterfaceArray(v, jvm.toNativeString(token)); +}); + // Worker liveness heartbeat (diag-only). If the worker wedges in a synchronous // green-thread step this timer CANNOT fire (single-threaded) and the heartbeat // STOPS; if the worker is merely parked/starved (idle, a host callback not From cdc60217f9380e901c2c3d899c091dd6e6bebe8f Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:31:01 +0300 Subject: [PATCH 10/13] js-port: wrap PeerComponent across the native-interface bridge Complete the supported-type set with com.codename1.ui.PeerComponent in both directions: - Return: the generated Impl wraps the call result with PeerComponent.create(callObject(...)). The host returns the native element; cn1InvokeNativeInterface now host-ref-wraps any non-array object result (hostResult) so the un-cloneable DOM element survives postMessage to the worker and works as an HTMLElement JSO receiver (the JSO bridge dispatches on __cn1HostRef). PeerComponent.create routes to HTML5Implementation.createNativePeer -> new HTML5Peer((HTMLElement)..). - Argument: a PeerComponent param is passed as peer.getNativePeer() (the underlying native element / host-ref), not the Java peer wrapper; toHostTransferArg already marshals the host-ref. The method key uses the _com_codename1_ui_PeerComponent suffix. Validated by temporarily adding int[]/String[]/PeerComponent methods to a native interface and rebuilding: the generated impl compiles (callArray with JAVA_INT/java_lang_String tokens, PeerComponent.create, getNativePeer) -- malformed codegen would have failed javac. Reverted the temp methods; the initializr's WebsiteThemeNative (boolean/void) still builds clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../com/codename1/builders/JavaScriptBuilder.java | 11 ++++++++++- .../src/javascript/browser_bridge.js | 12 +++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java index f02088ec46..846514acad 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java @@ -639,8 +639,12 @@ private void appendNativeInterfaceImplMethod(StringBuilder sb, Method m) { sb.append(" return (").append(ret.getCanonicalName()).append(") ") .append(call).append("callArray(").append(invokeArgs) .append(", \"").append(arrayComponentToken(ret.getComponentType())).append("\");\n"); + } else if ("com.codename1.ui.PeerComponent".equals(ret.getName())) { + // The stub returns a native element (delivered to the worker as a + // host-ref); wrap it as a Codename One peer component. + sb.append(" return com.codename1.ui.PeerComponent.create(") + .append(call).append("callObject(").append(invokeArgs).append("));\n"); } else { - // PeerComponent / other reference types -- best effort passthrough. sb.append(" return (").append(ret.getCanonicalName()).append(") ") .append(call).append("callObject(").append(invokeArgs).append(");\n"); } @@ -648,6 +652,11 @@ private void appendNativeInterfaceImplMethod(StringBuilder sb, Method m) { } private static String boxArgExpression(Class type, String var) { + // Pass a PeerComponent's underlying native element (a host-ref) to the + // stub, not the Java peer wrapper. + if ("com.codename1.ui.PeerComponent".equals(type.getName())) { + return var + ".getNativePeer()"; + } if (type == int.class) return "Integer.valueOf(" + var + ")"; if (type == long.class) return "Long.valueOf(" + var + ")"; if (type == double.class) return "Double.valueOf(" + var + ")"; diff --git a/vm/ByteCodeTranslator/src/javascript/browser_bridge.js b/vm/ByteCodeTranslator/src/javascript/browser_bridge.js index c491c0dbff..d5d862dd99 100644 --- a/vm/ByteCodeTranslator/src/javascript/browser_bridge.js +++ b/vm/ByteCodeTranslator/src/javascript/browser_bridge.js @@ -194,7 +194,17 @@ complete: function(value) { if (settled) return; settled = true; - resolve(value === undefined ? null : value); + if (value === undefined) { + value = null; + } + // A returned host object (e.g. a DOM element backing a PeerComponent) + // is not structured-cloneable; hand the worker a host-ref handle it can + // use as a JSO receiver. Primitives, strings and plain arrays + // (String[]/primitive[]) pass through untouched for worker-side coercion. + if (value !== null && typeof value === 'object' && !Array.isArray(value)) { + value = hostResult(value); + } + resolve(value); }, error: function(err) { if (settled) return; From 5259c7d9c8e3c10addb0578aba0beffc8b766933 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 9 Jun 2026 17:01:15 +0300 Subject: [PATCH 11/13] js-port: minify emitted application JS (strip pretty-print whitespace) The translator emits translated_app.js one statement per line with generous indentation for readability; in a deployed bundle that indentation + blank lines is ~20% dead weight the browser must download and (more importantly) parse. Strip per-line leading/trailing whitespace and drop blank lines before writing each chunk -- safe regardless of ASI or // comments since newlines are preserved (one statement per line). Initializr translated_app.js: 11.93 MB -> 9.58 MB raw (-2.36 MB, ~20%), cutting parse time. Set -Dparparvm.js.pretty=true to keep the readable form for debugging the generated code. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../translator/JavascriptBundleWriter.java | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java index 35033264ae..0dce0e8bcb 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java @@ -179,10 +179,48 @@ public int compare(ByteCodeClass a, ByteCodeClass b) { for (int i = 0; i < leadCount; i++) { String suffix = leadCount >= 10 ? String.format("_%02d", i + 1) : String.format("_%d", i + 1); Files.write(new File(outputDirectory, "translated_app" + suffix + ".js").toPath(), - hoistStringConstants(chunks.get(i).toString()).getBytes(StandardCharsets.UTF_8)); + minifyJs(hoistStringConstants(chunks.get(i).toString())).getBytes(StandardCharsets.UTF_8)); } Files.write(new File(outputDirectory, "translated_app.js").toPath(), - hoistStringConstants(tail.toString()).getBytes(StandardCharsets.UTF_8)); + minifyJs(hoistStringConstants(tail.toString())).getBytes(StandardCharsets.UTF_8)); + } + + /** + * Strips the translator's pretty-printing indentation and blank lines from the + * emitted application JS. The translator emits one statement per line with + * generous indentation for readability; for a deployed bundle that is ~20% dead + * weight that the browser must still download and parse. We keep one statement + * per line (newlines preserved) so the transform is safe regardless of ASI or + * {@code //} comments -- only leading/trailing line whitespace and empty lines + * are removed. Set {@code -Dparparvm.js.pretty=true} to keep the readable form + * for debugging the generated code. + */ + private static String minifyJs(String code) { + if (System.getProperty("parparvm.js.pretty") != null) { + return code; + } + int n = code.length(); + StringBuilder out = new StringBuilder(n); + int i = 0; + while (i < n) { + int eol = code.indexOf('\n', i); + if (eol < 0) { + eol = n; + } + int start = i; + int end = eol; + while (start < end && code.charAt(start) <= ' ') { + start++; + } + while (end > start && code.charAt(end - 1) <= ' ') { + end--; + } + if (end > start) { + out.append(code, start, end).append('\n'); + } + i = eol + 1; + } + return out.toString(); } /** From 998c677a8f0021a1d883fc6e946557e2502dd729 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Wed, 10 Jun 2026 18:40:30 +0300 Subject: [PATCH 12/13] js-port: synchronous virtual dispatch for CHA-proven non-suspending call sites Drop the over-conservative hasVirtualDispatch seed in the suspension analysis: a method containing virtual calls no longer becomes a generator unless one of the dispatched signatures actually has a suspending impl. The emitter now selects a synchronous dispatcher family (cn1_ivs0..4/N, no yield*) at every INVOKEVIRTUAL / INVOKEINTERFACE whose signature the analysis proved entirely synchronous -- ~8.5k of ~40k virtual call sites skip generator ceremony, and ~660 methods become plain functions. Correctness rails (each closes a gap found by the screenshot suite): - cn1_ivs* drives an unexpected generator ONE step and throws a NAMED CHA-unsound error instead of letting the raw generator object leak as a value (the silent-corruption mode that sank prior attempts). - The interpreter-path virtual emission (arity 0-4 fast paths) now consults isInvokeSuspending instead of hardcoding yield* cn1_iv*, which previously emitted yield inside plain functions (ReferenceError: yield is not defined in HashMap. at boot). - JSO-bridge sigs are suspending even when only declared abstractly (Window.getDocument has no translated impl; the runtime installs a function* override that the concrete-impl scan cannot see). - Any method whose identifier / __impl / dispatch-id appears as a string literal in port.js / parparvm_runtime.js / browser_bridge.js is suspending: bindNative / bindCiFallback replace those bodies with generators at runtime. - Diag-only: periodic WORKER_HB_THREADS dump so a single parked thread (EDT still ticking) is observable, not just total freezes. Co-Authored-By: Claude Fable 5 --- .../translator/JavascriptBundleWriter.java | 43 ++++++ .../translator/JavascriptMethodGenerator.java | 72 ++++++---- .../JavascriptSuspensionAnalysis.java | 131 +++++++++++++----- .../src/javascript/parparvm_runtime.js | 81 +++++++++++ 4 files changed, 264 insertions(+), 63 deletions(-) diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java index 0dce0e8bcb..9cde045e3e 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java @@ -739,4 +739,47 @@ private static String loadResource(String resourceName) throws IOException { input.close(); } } + + /** + * Every {@code cn1_*} token referenced as a string literal by the + * hand-written bridge JS (parparvm_runtime.js, browser_bridge.js and + * the JavaScript port's port.js). These are names the bridge resolves + * by string at runtime -- and, in the {@code bindNative} / + * {@code bindCiFallback} case, REPLACES with {@code function*} + * overrides. The suspension analysis must treat the named methods as + * suspending: a translated caller that skipped {@code yield*} (because + * the static body looked synchronous) would receive the installed + * override's raw generator object as its "result" and the override + * body would never run. + */ + static Set collectBridgeReferencedCn1Tokens() { + Set tokens = new HashSet(); + List sources = new ArrayList(); + for (String res : new String[]{ "parparvm_runtime.js", "browser_bridge.js" }) { + try { + sources.add(loadResource(res)); + } catch (IOException ignore) { + // resource absent -- skip + } + } + try { + Path webApp = locateJavaScriptPortWebApp(); + if (webApp != null) { + Path portJs = webApp.resolve("port.js"); + if (Files.exists(portJs)) { + sources.add(new String(Files.readAllBytes(portJs), StandardCharsets.UTF_8)); + } + } + } catch (Exception ignore) { + // port.js unavailable -- skip + } + java.util.regex.Pattern literal = java.util.regex.Pattern.compile("[\"'](cn1_[A-Za-z0-9_]+)[\"']"); + for (String src : sources) { + java.util.regex.Matcher m = literal.matcher(src); + while (m.find()) { + tokens.add(m.group(1)); + } + } + return tokens; + } } diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptMethodGenerator.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptMethodGenerator.java index d76a812071..d1793dd95b 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptMethodGenerator.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptMethodGenerator.java @@ -3388,13 +3388,14 @@ private static boolean appendStraightLineInvokeInstruction(StringBuilder out, In // ``resolveVirtual`` handles inheritance without per-class alias // entries. String dispatchId = JavascriptNameUtil.dispatchMethodIdentifier(invoke.getName(), invoke.getDesc()); + boolean suspending = isInvokeSuspending(invoke); if (hasReturn) { out.append(" {\n"); - appendCompactVirtualDispatch(out, " ", dispatchId, argValues.length, true, target, false, argValues); + appendCompactVirtualDispatch(out, " ", dispatchId, argValues.length, true, target, false, argValues, suspending); out.append(" ").append(ctx.push("__result")).append(";\n"); out.append(" }\n"); } else { - appendCompactVirtualDispatch(out, " ", dispatchId, argValues.length, false, target, false, argValues); + appendCompactVirtualDispatch(out, " ", dispatchId, argValues.length, false, target, false, argValues, suspending); } return true; } @@ -4799,6 +4800,13 @@ private static void appendInvokeInstruction(StringBuilder out, Invoke invoke, in } if (invoke.getOpcode() == Opcodes.INVOKEVIRTUAL || invoke.getOpcode() == Opcodes.INVOKEINTERFACE) { + // CHA verdict for this call site: a sync signature uses the + // non-generator ``cn1_ivs*`` family with no ``yield*`` (so a + // method whose every virtual call is sync can itself be a plain + // ``function``); a suspending signature keeps ``yield* cn1_iv*``. + boolean susp = isInvokeSuspending(invoke); + String iv = susp ? "cn1_iv" : "cn1_ivs"; + String yk = susp ? "yield* " : ""; // Fast path for 0-arg virtual dispatch: inline the // target pop into the iv0 call. Pops TOS inside the // invoke's arg list, so the full block collapses to a @@ -4806,9 +4814,9 @@ private static void appendInvokeInstruction(StringBuilder out, Invoke invoke, in // call sites. if (argCount == 0) { if (hasReturn) { - out.append(" stack.p(yield* cn1_iv0(stack.q(), \"").append(dispatchId).append("\"));\n"); + out.append(" stack.p(").append(yk).append(iv).append("0(stack.q(), \"").append(dispatchId).append("\"));\n"); } else { - out.append(" yield* cn1_iv0(stack.q(), \"").append(dispatchId).append("\");\n"); + out.append(" ").append(yk).append(iv).append("0(stack.q(), \"").append(dispatchId).append("\");\n"); } out.append(" pc = ").append(index + 1).append("; break;\n"); return; @@ -4816,9 +4824,9 @@ private static void appendInvokeInstruction(StringBuilder out, Invoke invoke, in if (argCount == 1) { out.append(" { let __arg0 = stack.q(); "); if (hasReturn) { - out.append("stack.p(yield* cn1_iv1(stack.q(), \"").append(dispatchId).append("\", __arg0));"); + out.append("stack.p(").append(yk).append(iv).append("1(stack.q(), \"").append(dispatchId).append("\", __arg0));"); } else { - out.append("yield* cn1_iv1(stack.q(), \"").append(dispatchId).append("\", __arg0);"); + out.append(yk).append(iv).append("1(stack.q(), \"").append(dispatchId).append("\", __arg0);"); } out.append(" pc = ").append(index + 1).append("; break; }\n"); return; @@ -4826,9 +4834,9 @@ private static void appendInvokeInstruction(StringBuilder out, Invoke invoke, in if (argCount == 2) { out.append(" { let __arg1 = stack.q(); let __arg0 = stack.q(); "); if (hasReturn) { - out.append("stack.p(yield* cn1_iv2(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1));"); + out.append("stack.p(").append(yk).append(iv).append("2(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1));"); } else { - out.append("yield* cn1_iv2(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1);"); + out.append(yk).append(iv).append("2(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1);"); } out.append(" pc = ").append(index + 1).append("; break; }\n"); return; @@ -4836,9 +4844,9 @@ private static void appendInvokeInstruction(StringBuilder out, Invoke invoke, in if (argCount == 3) { out.append(" { let __arg2 = stack.q(); let __arg1 = stack.q(); let __arg0 = stack.q(); "); if (hasReturn) { - out.append("stack.p(yield* cn1_iv3(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1, __arg2));"); + out.append("stack.p(").append(yk).append(iv).append("3(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1, __arg2));"); } else { - out.append("yield* cn1_iv3(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1, __arg2);"); + out.append(yk).append(iv).append("3(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1, __arg2);"); } out.append(" pc = ").append(index + 1).append("; break; }\n"); return; @@ -4846,9 +4854,9 @@ private static void appendInvokeInstruction(StringBuilder out, Invoke invoke, in if (argCount == 4) { out.append(" { let __arg3 = stack.q(); let __arg2 = stack.q(); let __arg1 = stack.q(); let __arg0 = stack.q(); "); if (hasReturn) { - out.append("stack.p(yield* cn1_iv4(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1, __arg2, __arg3));"); + out.append("stack.p(").append(yk).append(iv).append("4(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1, __arg2, __arg3));"); } else { - out.append("yield* cn1_iv4(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1, __arg2, __arg3);"); + out.append(yk).append(iv).append("4(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1, __arg2, __arg3);"); } out.append(" pc = ").append(index + 1).append("; break; }\n"); return; @@ -4863,7 +4871,7 @@ private static void appendInvokeInstruction(StringBuilder out, Invoke invoke, in out.append(" {\n"); appendInvocationArgumentBindings(out, argCount, " ", "stack.q()"); out.append(" let __target = stack.q();\n"); - appendCompactVirtualDispatch(out, " ", dispatchId, argCount, hasReturn, "__target", true); + appendCompactVirtualDispatch(out, " ", dispatchId, argCount, hasReturn, "__target", true, isInvokeSuspending(invoke)); out.append(" pc = ").append(index + 1).append("; break;\n"); out.append(" }\n"); return; @@ -5015,32 +5023,46 @@ private static void appendInvocationArgumentBindings(StringBuilder out, int argC * the arg expressions directly (straight-line path). */ private static void appendCompactVirtualDispatch(StringBuilder out, String indent, String methodId, - int argCount, boolean hasReturn, String targetExpr, boolean argsFromStack) { - appendCompactVirtualDispatch(out, indent, methodId, argCount, hasReturn, targetExpr, argsFromStack, null); + int argCount, boolean hasReturn, String targetExpr, boolean argsFromStack, boolean suspending) { + appendCompactVirtualDispatch(out, indent, methodId, argCount, hasReturn, targetExpr, argsFromStack, null, suspending); } + /** + * @param suspending CHA verdict for the dispatched signature. When true the + * call goes through the generator family + * ({@code yield* cn1_iv*}) so a blocking override can + * suspend the cooperative scheduler. When false the + * analysis proved every impl is synchronous, so we emit + * the synchronous family ({@code cn1_ivs*}, no + * {@code yield*}) -- this is what allows a caller that + * makes only non-suspending virtual calls to itself be a + * plain {@code function}. + */ private static void appendCompactVirtualDispatch(StringBuilder out, String indent, String methodId, - int argCount, boolean hasReturn, String targetExpr, boolean argsFromStack, String[] argExpressions) { + int argCount, boolean hasReturn, String targetExpr, boolean argsFromStack, String[] argExpressions, + boolean suspending) { + String base = suspending ? "cn1_iv" : "cn1_ivs"; + String yieldKw = suspending ? "yield* " : ""; String helper; boolean variadic = false; switch (argCount) { - case 0: helper = "cn1_iv0"; break; - case 1: helper = "cn1_iv1"; break; - case 2: helper = "cn1_iv2"; break; - case 3: helper = "cn1_iv3"; break; - case 4: helper = "cn1_iv4"; break; + case 0: helper = base + "0"; break; + case 1: helper = base + "1"; break; + case 2: helper = base + "2"; break; + case 3: helper = base + "3"; break; + case 4: helper = base + "4"; break; default: - helper = "cn1_ivN"; + helper = base + "N"; variadic = true; break; } out.append(indent); if (hasReturn && argsFromStack) { - out.append("stack.p(yield* ").append(helper).append("(").append(targetExpr).append(", \"").append(methodId).append("\""); + out.append("stack.p(").append(yieldKw).append(helper).append("(").append(targetExpr).append(", \"").append(methodId).append("\""); } else if (hasReturn) { - out.append("let __result = yield* ").append(helper).append("(").append(targetExpr).append(", \"").append(methodId).append("\""); + out.append("let __result = ").append(yieldKw).append(helper).append("(").append(targetExpr).append(", \"").append(methodId).append("\""); } else { - out.append("yield* ").append(helper).append("(").append(targetExpr).append(", \"").append(methodId).append("\""); + out.append(yieldKw).append(helper).append("(").append(targetExpr).append(", \"").append(methodId).append("\""); } if (variadic) { out.append(", ["); diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptSuspensionAnalysis.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptSuspensionAnalysis.java index 2de31ee74f..cda51f131f 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptSuspensionAnalysis.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptSuspensionAnalysis.java @@ -38,12 +38,13 @@ *
  • It is declared {@code synchronized} — monitor acquisition can block.
  • *
  • Its bytecode contains {@code monitorenter} or {@code monitorexit} * (synchronized block) — same reason.
  • - *
  • It contains any {@code invokevirtual} / {@code invokeinterface} - * instruction — the dispatch goes through {@code cn1_iv*} which is - * a generator, so the caller must be ready to {@code yield*}. We - * treat ALL virtuals as suspending rather than doing - * override-set CHA, which keeps the analysis portable and safe - * against future-inherited suspending overrides.
  • + *
  • It contains an {@code invokevirtual} / {@code invokeinterface} + * whose dispatched signature has AT LEAST ONE suspending impl in + * the class hierarchy (override-set CHA). Such sites are emitted as + * {@code yield* cn1_iv*}; sites whose every impl is synchronous use + * the {@code cn1_ivs*} sync dispatcher and do NOT make their caller + * suspending. The suspending-sig set is computed by fixed-point in + * {@link #propagate} and exported via {@link #exportedSuspendingSigs}.
  • *
  • It contains any {@code invokestatic} / {@code invokespecial} whose * resolved target is itself suspending (recursive closure via * fixed-point iteration).
  • @@ -58,6 +59,11 @@ final class JavascriptSuspensionAnalysis { private final Map byName = new HashMap(); private final Set suspending = Collections.newSetFromMap(new IdentityHashMap()); + // Sigs whose runtime impl can be a bindNative-installed generator the + // static concrete-impl scan cannot see: declared (possibly abstractly) + // on JSO-bridge classes, or string-referenced by the bridge JS (see + // seedBridgeReferenced). Unconditionally suspending. + private final Set jsoDeclaredSigs = new java.util.HashSet(); // Sigs (name + descriptor) whose concrete impl set contains AT // LEAST ONE suspending method. Populated during ``propagate`` @@ -73,6 +79,7 @@ static int run(List classes) { JavascriptSuspensionAnalysis a = new JavascriptSuspensionAnalysis(); a.index(classes); a.seedDirectlySuspending(classes); + a.seedBridgeReferenced(classes); a.propagate(classes); return a.applyResults(classes); } @@ -105,6 +112,17 @@ private void seedDirectlySuspending(List classes) { for (ByteCodeClass cls : classes) { boolean clsIsJso = jsoBridgeClasses.contains(cls.getClsName()); for (BytecodeMethod m : cls.getMethods()) { + // JSO-declared SIGNATURES must be suspending even when the + // declaration is abstract (interface methods like + // ``Window.getDocument()`` have NO translated impl at all -- + // the only "impl" is the ``function*`` override bindNative + // installs at runtime, which the concrete-impl scan in + // ``propagate`` can never see). Record the sig here so + // ``propagate`` folds it into ``suspendingSigs`` and every + // dispatching call site keeps its ``yield*``. + if (clsIsJso && !m.isEliminated() && !m.isStatic() && !m.isConstructor()) { + jsoDeclaredSigs.add(m.getMethodName() + m.getSignature()); + } if (m.isEliminated() || m.isAbstract()) { continue; } @@ -119,25 +137,74 @@ private void seedDirectlySuspending(List classes) { // with ``cn1_ivAdapt`` wrappers at every hand-written // ``yield* translatedFn(args)`` call site. // - // ``hasVirtualDispatch`` is required in the seed - // because the emitter hardcodes ``yield* cn1_iv*`` at - // every INVOKEVIRTUAL / INVOKEINTERFACE call site (see - // ``JavascriptMethodGenerator.appendVirtualDispatch`` - // -- there is no ``cn1_ivs*`` synchronous virtual - // dispatcher, and 3 prior attempts to add one all hit - // runtime errors per - // ``project_jsport_suspension_tightening_failure`` - // memory). A method emitted as plain ``function`` - // cannot contain ``yield*``, so any method with even - // ONE virtual call must be a generator. Tightening - // the sync set further requires landing the sync - // virtual dispatcher first. + // Virtual dispatch is NO LONGER an unconditional seed. + // The emitter now has a synchronous virtual-dispatch + // family (``cn1_ivs0..N`` in parparvm_runtime.js) that + // it selects (via ``isInvokeSuspending`` consulting + // ``exportedSuspendingSigs``) for any INVOKEVIRTUAL / + // INVOKEINTERFACE whose CHA impl set is entirely + // synchronous. So a method whose only virtual calls + // target non-suspending sigs can itself be a plain + // ``function``. Suspension still propagates through + // virtual dispatch in ``propagate``: if ANY impl of a + // called sig is suspending, that sig is suspending and + // every caller of it is marked suspending there. The + // earlier sync-dispatcher attempts failed by letting a + // generator leak as a value; ``cn1_ivs*`` drives a + // one-shot and throws a named error on a true gap + // instead (see the runtime helper). if (m.isNative() || m.isSynchronizedMethod() || hasMonitorOps(m) - || clsIsJso - || hasVirtualDispatch(m)) { + || clsIsJso) { + suspending.add(m); + } + } + } + } + + /** + * Any method whose emitted identifier (or its {@code __impl} body, or + * its class-free dispatch id) appears as a string literal in the + * hand-written bridge JS must be suspending. Those strings are how + * {@code bindNative} / {@code bindCiFallback} (port.js, + * parparvm_runtime.js, browser_bridge.js) locate translated methods + * to REPLACE with {@code function*} overrides at runtime. The static + * body may look trivially synchronous, but the override that actually + * runs is a generator -- a caller that skipped {@code yield*} would + * receive the raw generator object as its "result" and the override + * would never execute (observed as the screenshot runner's + * done-callback silently never firing). Over-protecting names the + * bridge merely CALLS (it wraps those in {@code cn1_ivAdapt}, which + * tolerates sync) costs a handful of generators; under-protecting + * breaks the bridge contract silently, so blanket-protect every + * string-referenced name. + */ + private void seedBridgeReferenced(List classes) { + Set tokens = JavascriptBundleWriter.collectBridgeReferencedCn1Tokens(); + if (tokens.isEmpty()) { + return; + } + for (ByteCodeClass cls : classes) { + for (BytecodeMethod m : cls.getMethods()) { + if (m.isEliminated() || m.isAbstract()) { + continue; + } + String full = JavascriptNameUtil.methodIdentifier(cls.getClsName(), m.getMethodName(), m.getSignature()); + boolean referenced = tokens.contains(full) || tokens.contains(full + "__impl"); + boolean dispatchable = !m.isStatic() && !m.isConstructor(); + if (!referenced && dispatchable + && tokens.contains(JavascriptNameUtil.dispatchMethodIdentifier(m.getMethodName(), m.getSignature()))) { + referenced = true; + } + if (referenced) { suspending.add(m); + if (dispatchable) { + // Virtual dispatch can land on the runtime-installed + // override too -- protect the whole signature, same + // as the JSO-declared sigs. + jsoDeclaredSigs.add(m.getMethodName() + m.getSignature()); + } } } } @@ -190,23 +257,6 @@ private static boolean hasMonitorOps(BytecodeMethod m) { return false; } - private static boolean hasVirtualDispatch(BytecodeMethod m) { - List instructions = m.getInstructions(); - if (instructions == null) { - return false; - } - for (Instruction instr : instructions) { - if (!(instr instanceof Invoke)) { - continue; - } - int op = instr.getOpcode(); - if (op == Opcodes.INVOKEVIRTUAL || op == Opcodes.INVOKEINTERFACE) { - return true; - } - } - return false; - } - private void propagate(List classes) { // Build two reverse indexes so a method becoming suspending // can propagate to all its callers without rescanning every @@ -243,6 +293,11 @@ private void propagate(List classes) { } } } + // JSO-bridge declared sigs are suspending regardless of their (often + // absent / abstract) translated impls -- see seedDirectlySuspending. + // Must be folded in BEFORE the caller scan below so dispatching + // callers get escalated. + suspendingSigs.addAll(jsoDeclaredSigs); for (ByteCodeClass cls : classes) { for (BytecodeMethod caller : cls.getMethods()) { if (caller.isEliminated() || caller.isAbstract()) { diff --git a/vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js b/vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js index daf78fbd8c..88e2994a3c 100644 --- a/vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js +++ b/vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js @@ -4053,6 +4053,77 @@ function* cn1_ivN(target, mid, args) { if (r && typeof r.next === "function") { return yield* r; } return r; } +// Synchronous virtual dispatch family (cn1_ivs0..4 / cn1_ivsN). Emitted +// at INVOKEVIRTUAL / INVOKEINTERFACE call sites whose signature the +// suspension analysis (exportedSuspendingSigs) proved has NO suspending +// impl -- so the resolved override is a plain ``function`` returning a +// value, and the caller need not be a generator. This is what lets a +// method that only makes non-suspending virtual calls be emitted as a +// plain ``function`` instead of ``function*`` (no ``yield*`` ceremony), +// removing per-call generator allocation and shrinking the bundle while +// keeping the green-thread model intact for genuinely-blocking paths. +// +// Defensive drive-once: if a target unexpectedly returns a generator (a +// CHA-soundness gap -- e.g. a runtime-installed override the static +// analysis didn't see, or the ``{}`` broken-receiver canvas no-op stubs +// in cn1_ivResolve which are ``function*``), step it ONCE. A body that +// never actually yields completes on the first next() so we return its +// value safely; one that genuinely suspends in this sync context throws +// a NAMED error rather than letting a raw generator object leak +// downstream as the "result" (the silent-corruption failure mode of the +// three earlier sync-dispatcher attempts). +function cn1_ivsDrive(r, mid) { + if (r && typeof r.next === "function") { + const step = r.next(); + if (!step.done) { + throw new Error("cn1_ivs: sync virtual dispatch reached a yielding method (CHA unsound): " + mid); + } + return step.value; + } + return r; +} +function cn1_ivsNpe() { + const ex = jvm.createException("java_lang_NullPointerException"); + if (typeof ex.ctor === "function") { + const cr = ex.ctor(ex.object); + if (cr && typeof cr.next === "function") { + const s = cr.next(); + if (!s.done) { throw new Error("cn1_ivs: NPE constructor yielded in sync dispatch"); } + } + } + throw ex.object; +} +function cn1_ivs0(target, mid) { + if (target == null) { cn1_ivsNpe(); } + return cn1_ivsDrive(cn1_ivResolve(target, mid)(target), mid); +} +function cn1_ivs1(target, mid, a0) { + if (target == null) { cn1_ivsNpe(); } + return cn1_ivsDrive(cn1_ivResolve(target, mid)(target, a0), mid); +} +function cn1_ivs2(target, mid, a0, a1) { + if (target == null) { cn1_ivsNpe(); } + return cn1_ivsDrive(cn1_ivResolve(target, mid)(target, a0, a1), mid); +} +function cn1_ivs3(target, mid, a0, a1, a2) { + if (target == null) { cn1_ivsNpe(); } + return cn1_ivsDrive(cn1_ivResolve(target, mid)(target, a0, a1, a2), mid); +} +function cn1_ivs4(target, mid, a0, a1, a2, a3) { + if (target == null) { cn1_ivsNpe(); } + return cn1_ivsDrive(cn1_ivResolve(target, mid)(target, a0, a1, a2, a3), mid); +} +function cn1_ivsN(target, mid, args) { + if (target == null) { cn1_ivsNpe(); } + const method = cn1_ivResolve(target, mid); + return cn1_ivsDrive(method.apply(null, [target].concat(args)), mid); +} +global.cn1_ivs0 = cn1_ivs0; +global.cn1_ivs1 = cn1_ivs1; +global.cn1_ivs2 = cn1_ivs2; +global.cn1_ivs3 = cn1_ivs3; +global.cn1_ivs4 = cn1_ivs4; +global.cn1_ivsN = cn1_ivsN; global.cn1_iv0 = cn1_iv0; global.cn1_iv1 = cn1_iv1; global.cn1_iv2 = cn1_iv2; @@ -5274,12 +5345,22 @@ bindNative([__NI_PREFIX + "callArray_java_lang_String_java_lang_String_java_lang if (VM_DIAG_ENABLED && typeof setInterval === "function") { let __cn1HbLastResumes = -1; let __cn1HbFrozenStreak = 0; + let __cn1HbTick = 0; setInterval(function() { try { const rc = jvm.__cn1ResumeCount | 0; const frozen = rc === __cn1HbLastResumes; __cn1HbLastResumes = rc; __cn1HbFrozenStreak = frozen ? (__cn1HbFrozenStreak + 1) : 0; + // Periodic full thread dump (every ~20 beats ~= 30s). The FROZEN dump + // below only covers total wedges (resume count stalled); a single + // parked thread with the EDT still ticking -- e.g. a runner waiting on + // a notify that never comes -- never trips it. The periodic dump shows + // every thread's wait target during such partial stalls. + __cn1HbTick++; + if (__cn1HbTick % 20 === 0) { + vmTrace("DIAG:WORKER_HB_THREADS:" + jvm.dumpThreadStates()); + } vmTrace("DIAG:WORKER_HB:resumes=" + rc + ":runnable=" + (jvm.runnable ? jvm.runnable.length : -1) + ":draining=" + (jvm.draining ? 1 : 0) From b283804c6f64f7a11783e40fcb3924f8f3b24c5e Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Wed, 10 Jun 2026 21:33:36 +0300 Subject: [PATCH 13/13] js-port: survive host timer throttling in the green-thread scheduler Headless/hidden Chromium intensively throttles re-armed setTimeout chains (and under wake-up budgeting, worker timers generally) down to ~one firing per minute. ParparVM scheduled EVERY Thread.sleep / Object.wait(timeout) / java.util.Timer wakeup through a single re-armed setTimeout chain, so in quiet phases the whole VM stalled in 12-60s bursts: every green thread parked past its wake deadline (WORKER_HB_THREADS dumps showed sleeps 27-53s overdue with the queue intact and the armed timer never firing) while the suite crawled or wedged. This is the latent stall behind the screenshot suite's late-cluster flakiness -- any change to execution timing (ident minification, sync dispatch) merely moved which test sat inside a throttling window. Defense in depth, all layers verified on the full screenshot suite (SUITE:FINISHED, 0 stranded wakeups): - parparvm_runtime: deliver due timed wakeups opportunistically at every outermost drain() (each host event), bounded-latency 1s backstop pump, distrust an armed wakeup timer whose target is already past (re-arm), per-entry guard in the expired-batch resume loop (one throwing resume can no longer strand the rest of the batch), re-entrancy guard, and diag-only stranded-sleep / thread-dump / arm-fire instrumentation. - browser_bridge: __cn1NudgeVm hook -- posts 'timer-wake' to the worker so an un-throttled external context can keep the VM clock honest. - run-javascript-headless-browser.mjs: disable Chromium background-timer throttling at launch and drive __cn1NudgeVm every 250ms via CDP Runtime.evaluate, which is exempt from visibility throttling. Co-Authored-By: Claude Fable 5 --- scripts/run-javascript-headless-browser.mjs | 27 +++- .../src/javascript/browser_bridge.js | 14 ++ .../src/javascript/parparvm_runtime.js | 143 +++++++++++++++++- 3 files changed, 179 insertions(+), 5 deletions(-) diff --git a/scripts/run-javascript-headless-browser.mjs b/scripts/run-javascript-headless-browser.mjs index c229bf161b..db60110d6e 100755 --- a/scripts/run-javascript-headless-browser.mjs +++ b/scripts/run-javascript-headless-browser.mjs @@ -45,7 +45,20 @@ let finalizeProfile = async () => {}; const launchArgs = [ '--autoplay-policy=no-user-gesture-required', '--disable-web-security', - '--allow-file-access-from-files' + '--allow-file-access-from-files', + // Headless pages count as hidden, so Chromium's background-timer machinery + // (IntensiveWakeUpThrottling in particular) batches re-armed setTimeout + // chains to ~one firing per MINUTE once the page's wake-up budget drains. + // The ParparVM worker schedules every Thread.sleep / Object.wait(timeout) + // through host timers, so the whole green-thread scheduler stalls in + // 12-60s bursts during quiet (no-host-event) phases -- observed as the + // screenshot suite crawling ~60s/test through the theme cluster with every + // thread parked past its wake deadline. Disable the throttling: this + // harness IS the foreground workload. + '--disable-background-timer-throttling', + '--disable-backgrounding-occluded-windows', + '--disable-renderer-backgrounding', + '--disable-features=IntensiveWakeUpThrottling' ]; if (profileWorker) { launchArgs.push(`--remote-debugging-port=${remoteDebugPort}`); @@ -212,6 +225,18 @@ try { append(`goto:${url}`); await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 }); + // VM liveness nudge from the Node side. Headless Chromium intensively + // throttles page AND worker timers (re-armed setTimeout chains batch to + // ~1/min once the hidden page's wake-up budget drains), which starves the + // ParparVM scheduler's sleep/wait wakeups and crawls the suite. CDP + // Runtime.evaluate is exempt from that throttling, so a Node interval + // pinging the bridge's __cn1NudgeVm (worker postMessage 'timer-wake' -> + // drain -> fire due wakeups) keeps the VM clock honest regardless of the + // browser's visibility heuristics. + const nudgeTimer = setInterval(() => { + page.evaluate('window.__cn1NudgeVm && window.__cn1NudgeVm()').catch(() => {}); + }, 250); + nudgeTimer.unref?.(); await page.waitForTimeout(2000); const start = Date.now(); diff --git a/vm/ByteCodeTranslator/src/javascript/browser_bridge.js b/vm/ByteCodeTranslator/src/javascript/browser_bridge.js index daead93bce..f5a4463d43 100644 --- a/vm/ByteCodeTranslator/src/javascript/browser_bridge.js +++ b/vm/ByteCodeTranslator/src/javascript/browser_bridge.js @@ -2706,6 +2706,20 @@ } var worker = new Worker(workerUrl); global.__parparWorker = worker; + // External liveness nudge. Hidden/headless Chromium throttles BOTH the + // page's and the worker's timers (intensive wake-up throttling batches + // re-armed chains to ~1/min), which starves the VM scheduler's + // sleep/wait wakeups -- observed as every green thread parked 12-60s + // past its deadline while the worker idles. postMessage delivery is + // never throttled, and a 'timer-wake' makes the worker drain(), which + // opportunistically fires any due timed wakeups. Test harnesses (or + // embedders that detect background stalls) call this from an + // un-throttled context, e.g. CDP Runtime.evaluate. + global.__cn1NudgeVm = function() { + try { + worker.postMessage({ type: 'timer-wake' }); + } catch (e) { /* worker torn down */ } + }; worker.onmessage = function(event) { handleVmMessage(event.data, worker); }; diff --git a/vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js b/vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js index 88e2994a3c..6a28730608 100644 --- a/vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js +++ b/vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js @@ -2658,6 +2658,15 @@ const jvm = { if (this.draining) { return; } + // Opportunistic wakeup delivery on every outermost drain (i.e. every host + // event that wakes the worker): when the one-shot wakeup timeout is being + // throttled by the host (see _ensureWakeupPump), due sleeps/waits still + // fire with near-zero latency here instead of waiting for the 1s pump. + // O(pending) once per burst; no-op when nothing is due. + if (this.timedWakeups.length && !this._processingWakeups + && this._earliestWakeAt() <= this.schedulerNow() + 1) { + this._processExpiredTimedWakeups(); + } this.draining = true; const deadline = this.schedulerNow() + 8; let steps = 0; @@ -2818,6 +2827,47 @@ const jvm = { _scheduleTimedWakeup(entry) { this.timedWakeups.push(entry); this._refreshTimedWakeupTimer(); + this._ensureWakeupPump(); + }, + // Permanent low-frequency backstop for the one-shot wakeup timer. + // + // Headless/hidden Chromium intensively throttles rapidly re-armed + // setTimeout CHAINS (nesting depth >= 5, short delays) down to ~one + // firing per minute, while >=1s intervals keep firing normally -- + // observed on the screenshot suite as the heartbeat interval beating + // every 1.5s while the armed wakeup timeout sat 12-48s past its target + // (sinceStepMs == wakeFiredAgo == 12771 with every thread parked), so + // every Thread.sleep / Object.wait(timeout) in the VM stalled in + // batches. The pump bounds that worst case at ~1s: a cheap length + + // earliest-deadline check, processing only when something is actually + // due. The one-shot timer remains the precision path; drain() also + // opportunistically processes due wakeups on every host event. + _ensureWakeupPump() { + if (this._wakeupPump != null || typeof setInterval !== "function") { + return; + } + const self = this; + this._wakeupPump = setInterval(function() { + try { + if (self.timedWakeups.length && self._earliestWakeAt() <= self.schedulerNow() + 1) { + self._processExpiredTimedWakeups(); + } + } catch (_e) { + // Never let the backstop kill itself. + } + }, 1000); + // Node harnesses: don't hold the process open for the pump. + if (this._wakeupPump && typeof this._wakeupPump.unref === "function") { + this._wakeupPump.unref(); + } + }, + _earliestWakeAt() { + let earliest = Infinity; + for (let i = 0; i < this.timedWakeups.length; i++) { + const w = this.timedWakeups[i]; + if (!w.cancelled && w.wakeAt < earliest) earliest = w.wakeAt; + } + return earliest; }, _removeTimedWakeup(entry) { if (!entry || entry.cancelled) return; @@ -2840,15 +2890,37 @@ const jvm = { } return; } - if (this._wakeupTimer != null && this._wakeupAt <= earliest) { - // Existing timer fires sooner or at the same moment; keep it. + if (this._wakeupTimer != null && this._wakeupAt <= earliest + && this._wakeupAt > this.schedulerNow() - 100) { + // Existing timer fires sooner or at the same moment; keep it. The + // third clause guards against a ZOMBIE: a timer whose target time is + // already well past yet whose callback never ran (its first statement + // nulls _wakeupTimer, so non-null + past-due means the host lost the + // timeout -- observed on the screenshot suite as every sleeping thread + // stranded 30s+ past its deadline with the queue intact, because a + // past-due _wakeupAt satisfies ``<= earliest`` for EVERY later wakeup + // and this branch then never re-arms). Distrust it and re-arm; if the + // old timer does still fire, the callback's _wakeupTimer-null reset + + // re-entrant processing are idempotent, so the duplicate is harmless. return; } - if (this._wakeupTimer != null) clearTimeout(this._wakeupTimer); + if (this._wakeupTimer != null) { + if (VM_DIAG_ENABLED && this._wakeupAt !== Infinity + && this._wakeupAt <= this.schedulerNow() - 100) { + try { + vmTrace("DIAG:WAKEUP_TIMER_ZOMBIE:rearmed:staleMs=" + + Math.round(this.schedulerNow() - this._wakeupAt)); + } catch (_e) {} + } + clearTimeout(this._wakeupTimer); + } const delay = Math.max(0, earliest - this.schedulerNow()); this._wakeupAt = earliest; const self = this; + this._wakeupArmCount = (this._wakeupArmCount | 0) + 1; this._wakeupTimer = setTimeout(function() { + self._wakeupFireCount = (self._wakeupFireCount | 0) + 1; + self._wakeupLastFiredAt = self.schedulerNow(); self._wakeupTimer = null; self._wakeupAt = Infinity; // ALWAYS reschedule remaining wakeups, even if processing one throws -- @@ -2861,6 +2933,20 @@ const jvm = { }, delay); }, _processExpiredTimedWakeups() { + if (this._processingWakeups) { + // Re-entrancy guard: the resume loop below runs green threads via + // enqueue -> drain, and drain's opportunistic due-check (or the pump / + // a late one-shot) could otherwise re-enter while a batch is mid-resume. + return; + } + this._processingWakeups = true; + try { + this._processExpiredTimedWakeupsInner(); + } finally { + this._processingWakeups = false; + } + }, + _processExpiredTimedWakeupsInner() { const now = this.schedulerNow(); const expired = []; for (let i = this.timedWakeups.length - 1; i >= 0; i--) { @@ -2874,6 +2960,17 @@ const jvm = { if (w.wakeAt <= now + 1) { expired.push(w); this.timedWakeups.splice(i, 1); + } else if (VM_DIAG_ENABLED && !(w.wakeAt > now - 2000)) { + // An entry that is neither due (<= now+1) nor sane-future fails BOTH + // comparisons only when wakeAt isn't an ordinary number (NaN / boxed + // long / string). Print its raw shape -- this is the only way a + // queued, uncancelled, overdue entry can survive processing. + try { + vmTrace("DIAG:WAKEUP_BAD_ENTRY:kind=" + String(w.kind) + + ":typeof=" + (typeof w.wakeAt) + + ":val=" + String(w.wakeAt).slice(0, 30) + + ":thread=" + (w.thread ? w.thread.id : "-")); + } catch (_e) {} } } expired.reverse(); // restore registration order for FIFO fairness @@ -2891,6 +2988,7 @@ const jvm = { if (w.cancelled) { continue; } + try { if (w.kind === "sleep") { this.enqueue(w.thread); } else if (w.kind === "wait") { @@ -2908,6 +3006,16 @@ const jvm = { this.resolveHostCall(w.id, false, null, "host call timed out (jso bridge)"); } } + } catch (resumeErr) { + // Per-entry guard: every entry in this batch is ALREADY spliced out of + // timedWakeups, so an exception escaping one resume would strand every + // remaining entry's thread in a sleep/wait that can never fire again. + // Contain the failure to the one entry and keep resuming the rest. + try { + vmTrace("DIAG:WAKEUP_RESUME_THREW:kind=" + String(w.kind) + + ":err=" + String(resumeErr && resumeErr.message || resumeErr).slice(0, 120)); + } catch (_e) {} + } } this._refreshTimedWakeupTimer(); }, @@ -5361,6 +5469,30 @@ if (VM_DIAG_ENABLED && typeof setInterval === "function") { if (__cn1HbTick % 20 === 0) { vmTrace("DIAG:WORKER_HB_THREADS:" + jvm.dumpThreadStates()); } + // Stranded-sleep detector: a thread parked in sleep PAST its deadline is + // in one of three states, each implicating a different bug: + // queued=1 -- entry still in timedWakeups; the single host timer is + // not firing / mis-armed (_refreshTimedWakeupTimer). + // cancelled=1 -- something _removeTimedWakeup'd it without resuming + // the thread. + // gone -- spliced out of timedWakeups while not cancelled: + // _processExpiredTimedWakeups collected it but the + // enqueue never landed. + var __ths = jvm.threads || []; + for (var __i = 0; __i < __ths.length; __i++) { + var __t = __ths[__i]; + if (__t.done || !__t.waiting || __t.waiting.op !== "sleep" || !__t.waiting.entry) continue; + var __e = __t.waiting.entry; + var __due = __e.wakeAt - jvm.schedulerNow(); + if (__due > -2000) continue; + vmTrace("DIAG:STRANDED_SLEEP:t" + __t.id + + ":dueIn=" + Math.round(__due) + + ":queued=" + (jvm.timedWakeups.indexOf(__e) >= 0 ? 1 : 0) + + ":cancelled=" + (__e.cancelled ? 1 : 0) + + ":wakeupTimerArmed=" + (jvm._wakeupTimer != null ? 1 : 0) + + ":wakeupAt=" + (jvm._wakeupAt === Infinity ? "inf" : Math.round(jvm._wakeupAt - jvm.schedulerNow())) + + ":pendingWakeups=" + jvm.timedWakeups.length); + } vmTrace("DIAG:WORKER_HB:resumes=" + rc + ":runnable=" + (jvm.runnable ? jvm.runnable.length : -1) + ":draining=" + (jvm.draining ? 1 : 0) @@ -5368,7 +5500,10 @@ if (VM_DIAG_ENABLED && typeof setInterval === "function") { + ":frozen=" + (frozen ? 1 : 0) + ":captureGate=" + (jvm.captureGateOwner ? 1 : 0) + ":sinceStepMs=" + (jvm.__cn1LastResumeTs != null ? Math.round(jvm.schedulerNow() - jvm.__cn1LastResumeTs) : -1) - + ":lastThread=" + String(jvm.__cn1LastResumeLabel)); + + ":lastThread=" + String(jvm.__cn1LastResumeLabel) + + ":wakeArm=" + (jvm._wakeupArmCount | 0) + + ":wakeFire=" + (jvm._wakeupFireCount | 0) + + ":wakeFiredAgo=" + (jvm._wakeupLastFiredAt != null ? Math.round(jvm.schedulerNow() - jvm._wakeupLastFiredAt) : -1)); // When the worker is wedged (frozen with nothing runnable) every green // thread is parked. Dump WHAT they are parked on so the lost-response / // deadlock can be isolated without worker-internal tracing (which