Switch initializr JavaScript build to local ParparVM target#5200
Switch initializr JavaScript build to local ParparVM target#5200shai-almog wants to merge 22 commits into
Conversation
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
|
Compared 128 screenshots: 128 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
|
Compared 128 screenshots: 128 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
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) <noreply@anthropic.com>
|
Compared 127 screenshots: 127 matched. Benchmark Results
Detailed Performance Metrics
|
Cloudflare Preview
|
|
Compared 124 screenshots: 124 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
|
Compared 121 screenshots: 121 matched. |
Native-interface stubs (com_<pkg>_<Class>.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) <noreply@anthropic.com>
✅ ByteCodeTranslator Quality ReportTest & Coverage
Benchmark Results
Static Analysis
Generated automatically by the PR CI workflow. |
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) <noreply@anthropic.com>
NativeLookup.create() returned null on the ParparVM JS port: there was
no <Iface>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 <Iface>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["<pkg>_<Iface>"]
["<method>_<paramTypes>"](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) <noreply@anthropic.com>
Native Windows portCompared 125 screenshots: 124 matched, 1 updated. |
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: <Iface>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) <noreply@anthropic.com>
Complete the supported-type set with com.codename1.ui.PeerComponent in both directions: - Return: the generated <Iface>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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
4a5cfcf to
5259c7d
Compare
…all 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.<init> 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
bb757e1 to
b283804
Compare
The translator emitted full cn1_<pkg>_<Class>_<method>_<sig> identifiers (avg ~45 chars, ~3.45MB / 35% of the Initializr bundle) at every definition and call site, because esbuild minification isn't in the pipeline. Rename them to short $M* symbols across all chunks. Crucially this is bridge-aware: Codename One has no reflection/ serialization, so name-based resolution IS the JS<->worker bridge. The worker-side runtime/port override native methods by reassigning the global of that exact name (global[name]=nativeFn) AND the constructed global[name+"__impl"] static-body variant. So a renamed native stub would bypass its override and return its placeholder (null/0) -> NPE. We therefore EXCLUDE from the rename every cn1_ name referenced as a string in the runtime sources (parparvm_runtime.js, browser_bridge.js, port.js) plus their "__impl" variants, plus any name referenced as a string in the app bundle (setMain's main method, serialized field manifests), plus constructors/clinit (reconstructed by string). renameTokens never rewrites inside string literals. Initializr translated_app.js: 9.37MB -> 7.50MB raw (gzip 1.33->~1.2MB). Validated by a headless-browser boot of the bundle: boots with no errors, matching the pre-rename baseline (initialized:true, error:""). Kill switch: -Dparparvm.js.minify.idents.off. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit 0a7a804) (cherry picked from commit 7035577)
The previous ident-minification commit broke HelloCodenameOne at runtime (0 screenshots rendered -> the screenshot suite hung): another bridge- overridden native whose stub got renamed, so global[name]=nativeFn override was bypassed. Inferring native names from runtime/port JS text was incomplete. Use the translator's authoritative knowledge instead: every method.isNative() emitted is recorded (with its "__impl" static-body variant) in JavascriptMethodGenerator.NATIVE_METHOD_IDENTIFIERS, and the bundle-writer excludes that set from the rename. Natives are the ONLY functions the JS<->worker bridge overrides by name, so this is complete by construction. Validated by building+booting HelloCodenameOne (the app that regressed): initialized:true, started:true, error:"" -- fully boots and starts (the broken build produced no render). Initializr unchanged (7.50MB, 6241 renamed). No framework or test changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit 85a8e01) (cherry picked from commit f1fcc54)
…tion
The screenshot runner (port.js) dispatches the suite's test lambdas by
building their identifier at runtime via string concatenation, e.g.
"cn1_..._Cn1ssDeviceRunner_lambda_" + methodName + "_" + i + "_" + sig
so the FULL generated identifier (".._lambda_runNextTest_2_<sig>") never
appears as a string literal -- only the 77-char stem does. The exact-match
string-literal exclusion therefore missed these, the minifier renamed the
lambda run methods to $M*, and the bridge's name lookup failed:
`lambda2RunBridge:missingDispatch=1`. With no runnable dispatch the
cooperative scheduler parked every thread (runnable=0) and the suite wedged
~28 tests in -> the javascript-screenshots hang/broken-pipe at 85a8e01.
Fix: treat every scanned cn1_ string token as a PREFIX and protect any
generated def that extends it at an identifier-segment boundary, so the
constructed name keeps its canonical identifier. A length floor (16) keeps
the generic construction roots "cn1_" (-> ___INIT__/___CLINIT__, already
skipped) and "cn1_s_" (dispatch-ids in the _qX table, never a def) out of
the prefix set -- "cn1_" as a prefix would match every def and disable all
minification. Over-protecting only forgoes a little size; under-protecting
breaks a name-resolved bridge.
Validated: HelloCodenameOne JS bundle keeps all 30 Cn1ssDeviceRunner lambda
identifiers verbatim while still minifying 12,836 generated functions
(translated_app.js 11.93MB -> 7.69MB, -36%); the local Playwright screenshot
run progresses past the prior wedge with sinceStepMs resetting (no parked
scheduler) instead of hanging.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 1af5e29)
(cherry picked from commit bb757e1)
…back
Three fixes that make the re-landed identifier minification safe and cure
the Initializr preview's font 404s:
- JavascriptBundleWriter: the string-literal exclusion scanner tracked
quote state character-by-character, which desyncs on apostrophes inside
port.js comments ("doesn't") -- from there it misses most of the ~479
quoted cn1_* literals, so bindCiFallback override targets lost their
rename protection (lambda2RunBridge:missingDispatch, suite dead at
index 45). Chunks (machine-generated, no prose) keep the in-string
scanner; the hand-written bridge JS now uses the regex quoted-token
collector that already guards the suspension analysis.
- build-javascript-port-hellocodenameone.sh: pass
-Dcodename1.javascriptport.webapp explicitly -- the locate walk-up from
a staging CWD can miss the repo, silently degrading the port.js-derived
protections.
- browser_bridge: loadTrueTypeFont falls back from assets/<font> to the
bundle root -- app-resource fonts (Initializr's Inter-*.ttf) are copied
top-level by the translator and 404'd under assets/, dropping the whole
UI to system fonts. Also resolve pending afterPaint frame ticks from
__cn1NudgeVm so hidden-page rAF/timer starvation can't stall settle
waits when an external driver is nudging.
Measured on the test app: raw 10.59MB -> 7.79MB (-26%) with the full
screenshot suite reaching SUITE:FINISHED and zero dispatch failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… transfers Reproduced the reported Initializr symptoms locally (loads extremely slowly, ignores scroll/drag) and fixed three independent causes: - ArrayBufferInputStream.read() dispatched a JSO-bridge call PER BYTE; Resources.load(theme.res) issues hundreds of thousands of single-byte reads through DataInputStream, so boot ran 90+ seconds of bridge round-trips (worker event loop pegged, CDP-paused stack showed parseJsoBridgeMethod under Resources.<init>). A lazily materialised worker-local copy (one bulk readBulkImpl) makes every subsequent read a plain array access. Measured boot: 90s+ (never started) -> 1.0s. - Cooperative budget-yield is now ON by default (opt out with parparvm.js.preemptYield=0) and ALSO checked inside cn1_iv0..N: the per-method-entry check alone cannot slice a loop that stays inside one method and only calls runtime functions. No synchronous stretch can starve the worker's event loop any more. - The canvas booted pointer-events:none and relied on a worker round-trip to restore it on the first event, so the initial pointer DOWN was always lost and the first gesture was swallowed. It now boots pointer-events:auto; the per-event peer toggling still applies when native peers are active. Verified: hit-test lands on the canvas and a synthesized drag crosses the bridge (13 worker-callback events). Regression guard (BridgeBulkTransferGuardTest, runs in the screenshot suite on every platform): the runtime counts JSO dispatches + host calls (jvm.__cn1JsoDispatchCount/__cn1HostCallCount, exposed to tests via a port.js-bound helper), and the test asserts large-volume operations cost bridge calls proportional to OPERATIONS not BYTES -- budgets two orders of magnitude below the per-element cost. First run already measured the fixed stream at 1 bridge call for a 47KB single-byte read-through, and flagged a separate pre-existing gap: a JS-port Storage write is not readable back within 5s (async localforage commit invisible to the synchronous facade) -- that leg logs CN1SS:WARN and self-arms once the round-trip is fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The collapse rules only matched the generator spelling (yield* cn1_iv<N>(...)), so the ~8.5k call sites the CHA proved non-suspending stayed on the verbose stack-shuffle path. Each rule now also applies in the synchronous spelling via applyVirtualRule, which derives the sync pattern/replacement textually (yield\* cn1_iv -> cn1_ivs) -- no capture groups added, so group numbering is identical. -131KB raw on the test app; full screenshot suite green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
{ let __result = <call>; X = __result; } -> X = <call>; (RHS fully
evaluates before the store, so the rewrite is sound even when X appears
in the call expression), plus the adjacent X = Y; Y = X; identity pair.
Modest raw win (-13KB; esbuild already collapses most of these
downstream) but it also shrinks the pre-esbuild chunks the whitespace
budget and chunk splitter operate on. Suite green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Compared 125 screenshots: 125 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 125 screenshots: 125 matched. Benchmark ResultsDetailed Performance Metrics
|

Summary
Switches the initializr's JavaScript build from the remote cloud build target to the local ParparVM-based translator.
CN1BuildMojoalready routes anylocal--prefixed target locally:local-javascript→doJavaScriptLocalBuild()→JavaScriptBuilder(ParparVM bytecode → JavaScript). The plainjavascripttarget went remote (AntcodeNameOnetask → build server → downloadresult.zip). The local path was already fully wired, so this change is purely a build-target switch in the initializr — no Mojo/builder changes.Automate mode still logs in with the server secret, so the pipeline behaves the same as before.
Changes (all under
scripts/initializr/)javascript/pom.xml—codename1.defaultBuildTarget:javascript→local-javascriptbuild.sh/build.bat— these pass-Dcodename1.buildTarget=javascriptexplicitly (overriding the pom default), so both flipped tolocal-javascriptREADME.adoc+ bundled skill docs (SKILL.md,references/build-and-run.md,references/native-interfaces.md) — document the JS build as the local ParparVM target, with the cloudjavascripttarget noted as the fallbackNotes
maven/cn1app-archetypetemplate (shipped to generated user projects) is intentionally not changed here — it still defaults to the cloudjavascripttarget.🤖 Generated with Claude Code