Playground: build via local ParparVM JavaScript target#5250
Playground: build via local ParparVM JavaScript target#5250shai-almog wants to merge 14 commits into
Conversation
✅ ByteCodeTranslator Quality ReportTest & Coverage
Benchmark Results
Static Analysis
Generated automatically by the PR CI workflow. |
|
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. |
|
Compared 125 screenshots: 125 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 121 screenshots: 121 matched. |
|
Compared 125 screenshots: 125 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 127 screenshots: 127 matched. |
|
Compared 127 screenshots: 127 matched. |
|
Compared 128 screenshots: 128 matched. Benchmark Results
Detailed Performance Metrics
|
|
Compared 125 screenshots: 125 matched. Benchmark ResultsDetailed Performance Metrics
|
3e84cf7 to
0684ad9
Compare
|
Compared 124 screenshots: 124 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
|
Compared 128 screenshots: 128 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
Migrate the Playground off the pinned old release + cloud `javascript` build target onto the local ParparVM `local-javascript` target, the same path the initializr uses. Playground changes (scripts/cn1playground/): - javascript/pom.xml: codename1.defaultBuildTarget javascript -> local-javascript - build.sh / build.bat: javascript target -> local-javascript - pom.xml: drop the one-release `cn1.registry.version` (7.0.234) pin. The bean-shell access registry now tracks the current API directly. A new cn1.accessRegistry.useLocalSources property (true under the cn1-local-workspace profile) makes the registry generate from the repo's own CN1 sources, since 8.0-SNAPSHOT has no source jars on Maven Central. - common/pom.xml: pass that property through to the registry generator as CN1_ACCESS_USE_LOCAL_SOURCES. - tools/GenerateCN1AccessRegistry.java: remove the security.* / nfc.* class exclusions (they worked around the legacy cloud TeaVM backend lagging the release channel; the local target builds the current sources). Keep the structural exclusions (Accessor/IOAccessor internals, and Simd whose alloca scratch arrays cannot escape the reflection bridge under the bytecode-compliance check). Also exclude the com.codename1.testing.junit package: it is the JavaSE-port JUnit 5 test-extension API (not a runtime API a playground script can use, and not on the common compile classpath), which un-pinning the registry version newly pulled in and broke the build. Fix array-component varargs codegen: emit `new byte[len][]`, not the malformed `new byte[][len]`, for `T[]...`. - tools/generate-cn1-access-registry.sh: drop the registry-version logic. - README / tools/README: document the new path. JS-port translator fixes (vm/ByteCodeTranslator/) needed for the Playground's full-API reflective registry: - JavascriptMethodGenerator: support array class literals (`ldc [B`, `String[].class`, ...) via jvm.getArrayClass(component, dims).classObject. The registry emits these for every array-typed parameter; the JS backend previously only handled object class literals and threw "Unsupported ldc constant". - Parser: rethrow Errors (e.g. OutOfMemoryError) from writeOutput instead of swallowing them, which had let the translator exit 0 with a truncated dist. Plugin / build wiring: - JavaScriptBuilder: let a -Xmx in CN1_TRANSLATOR_OPTS override the default 512m translator heap. - The Playground keeps nearly the whole API reachable, so the JS RTA tree-shaking pass cannot prune much yet runs 100min+. build.sh/build.bat and the website build_playground_for_site set CN1_TRANSLATOR_OPTS to disable RTA (-Dparparvm.js.rta.off) and raise the heap (-Xmx6g); the website extraction flattens the single wrapper dir so index.html lands at the served root. Verified locally: the local-javascript build produces a complete bundle (index.html, worker.js, translated_app + chunks, parparvm_runtime.js, browser_bridge.js, js/ assets, themes, editor html.tar, native-interface stub) that loads in headless Chromium with no fatal JS/VM errors, and the playground language smoke tests pass (registry regenerated in release mode, 325/325 syntax, 312/312 paint, 20/20 preview). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
0684ad9 to
ebbb018
Compare
Cloudflare Preview
|
JavascriptBundleWriter.hoistStringConstants emits top-level `const _q<alias>="..."` aliases for repeated string literals, but it reset the alias counter to 0 for every chunk. The translated_app chunks are concatenated into one worker scope via importScripts, so two chunks each declaring e.g. `const _q0O` (with different string values) is a `SyntaxError: redeclaration of const _q0O` that aborts worker startup. This only triggered once a bundle was large enough to split into multiple chunks (>20 MB) -- the Playground is the first such app; smaller local-javascript apps (initializr, HelloCodenameOne) are single-chunk and were unaffected. Thread one shared alias counter through every chunk so each gets a disjoint alias range -- exactly the naming a single un-split chunk would produce. Verified: node --check on the concatenated 57 MB Playground bundle parses cleanly (0 cross-chunk duplicate const names). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ParparVM localforage shim (synchronous-callback, localStorage-backed)
bailed out if a real `window.localforage` was already present:
if (window.localforage && typeof window.localforage.setItem === "function") return;
But fontmetrics.js bundles a real localForage 1.7.3 and globally exposes
it, and it loads BEFORE the shim. So the shim bailed, CN1 Storage hit the
real localForage's Promise-based callback path, and the worker-bridged
callback failed with `TypeError: b is not a function` (localForage's
executeCallback invoking a non-function) right after the app's main()
ran -- the Playground is the first local-javascript app to touch Storage
on boot.
The worker can't pump the async microtask loop the real localForage
relies on, so the synchronous shim MUST own window.localforage. Install
unconditionally, overriding any real localForage; only skip re-installing
over ourselves (idempotent via a __cn1ShimInstalled marker). fontmetrics
keeps its own bundled instance (referenced through its module closure) for
font-metric caching.
Verified: the Playground bundle boots in headless Chromium with no
`b is not a function` (0 fatal JS errors) where it previously threw on
every Storage access.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ootstrap race
Getting the Playground editor (a Monaco BrowserComponent) working on the
local ParparVM JavaScript target surfaced several JS-port issues that
don't bite TeaVM (which runs on the main thread, not a worker):
- HTML5BrowserComponent.isCORSRestricted: was an @JSBody, which on the
ParparVM port runs in the worker where the iframe arg is a host-ref proxy
with no live DOM (iframe.contentWindow is undefined) -- so it always threw
and reported EVERY BrowserComponent as CORS-restricted, blocking execute().
Reimplemented via the JSO bridge (getContentWindow().getDocument()) so the
probe runs on the main thread: same-origin -> not restricted; truly
cross-origin -> caught and restricted. Verified: "Is NOT cors restricted".
- JavaScriptBuilder: BrowserComponent.setURLHierarchy loads from
assets/cn1html/<path>, but the app's bundled HTML hierarchy only shipped
packed in html.tar (the runtime unpacks it into FileSystemStorage, which an
iframe can't fetch over HTTP). Unpack html.tar into assets/cn1html/ in the
bundle so the editor iframe URL resolves to a real same-origin file.
- editor.js: Monaco worker baseUrl was relative ("monaco/min/vs/"), which is
unparseable inside the data:-URL worker ("Failed to parse URL") and wrong
(duplicated vs/vs/). Use the absolute parent of vs/. Also force a layout()
via a ResizeObserver on the iframe body + after bootstrap, because the editor
is created in a hidden 0x0 peer iframe and Monaco's automaticLayout misses
the later resize (it stayed laid out at ~5x5 = blank pane).
- PlaygroundBrowserEditor.flush(): the BrowserComponent ready event can fire
before editor.js has defined window.PlaygroundEditor, so the bootstrap
no-oped. Inject a self-retrying bootstrap that waits inside the iframe until
PlaygroundEditor exists. editor.js also self-signals readiness.
Verified in headless Chromium: the Playground boots, renders its full UI
(Code/CSS/Preview tabs, nav), the editor iframe loads same-origin, and Monaco
renders syntax-highlighted code when bootstrapped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BrowserComponent.onMessage never fired on the ParparVM port, so the Playground editor (Monaco in a BrowserComponent) could never be bootstrapped or send edits back. Two root causes, both from the worker model: - browser_bridge.js serializeEventForWorker() omitted MessageEvent fields, so the worker-side MessageEvent arrived with getDataAsString()==null and no source. Serialize data/dataAsString/origin/source (source as a host-ref). - HTML5BrowserComponent.messageListener only forwarded a message when getEventSource(e) == iframe.getContentWindow(); on the worker those are two different wrappers for the same window (always false), so every message was dropped. The worker can't compare window identity, so forward any message that carries a source and let multi-frame apps disambiguate themselves. Playground side (the editor hosts two BrowserComponents, Java + CSS, and the port can't match a message to a specific iframe): - editor.js tags "change" messages with the editor language and self-signals "ready" until bootstrapped (the host's one-time browser.ready can miss a freshly (re)created peer iframe). - PlaygroundBrowserEditor routes "change" by language and injects a self-retrying bootstrap that waits for window.PlaygroundEditor. Verified in headless Chromium: loading the Playground with ?sample=hello-world boots, renders the full UI, and the Monaco editor displays the sample's code with syntax highlighting at full size (0 page errors). Running the code (Preview) still surfaces a separate beanshell-run error -- tracked separately. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ierarchy The Playground editor is loaded into a peer BrowserComponent via a relative "assets/cn1html/.." src. A relative iframe src is resolved against the host document's base URL at the instant it is set, and the Playground mutates its own location (share links, history.pushState), so the editor iframe intermittently resolved against the wrong base and 404'd. Build the URL absolutely from location.origin + current directory so the load is deterministic. Verified reliable across repeated Playwright runs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…p poll
CN1 Storage on the JavaScript port is backed by LocalForage, which wrapped
every async localforage callback in a `while(!done){Thread.sleep(20)}`
busy-wait. The app runs in a Web Worker; the localforage shim (main thread,
synchronous localStorage) delivers its result back to the worker as a
`worker-callback` message. The tight Thread.sleep timer loop STARVES the
worker's self.onmessage, so the callback (and, fatally, all pointer events)
were never processed until the 10s poll timeout fired. The Playground reads 7
keys synchronously on the EDT at startup and keeps saving state, so the EDT was
permanently busy-waiting -> the whole app was frozen to input and boot took
~94s (7 reads x 10s timeouts).
Fix: the shim's localStorage ops are already synchronous, so add value-
returning *Sync methods (getItemSync/setItemSync/removeItemSync/clearSync/
lengthSync/keysSync) and call them as ordinary BLOCKING JSO host calls that
return the value directly. The worker parks on HOST_CALL and resumes on
HOST_CALLBACK -- a path that is not starved -- so there is no poll, no
Thread.sleep, and no message starvation. Removed awaitLocalForageDone.
Verified in Playwright: boot to editor-visible dropped 94s -> ~4s, clicks now
repaint (Samples/Inspector panels open, tabs switch), 0 page errors. Fixes the
JS-port input freeze that made the migrated Playground non-interactive.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…previews run
The live preview interprets the user's script with BeanShell. The runner wraps a
loose script as `Object build(ctx){...} build(ctx);` (and lifecycle samples
declare `void init(Object)` / `start()`). On the ParparVM JavaScript port bsh's
default imports (java.lang/java.util/java.io) were not applied to this eval
namespace, so resolving the `Object` return/param type threw "Class: Object not
found in namespace". The method declaration was skipped, the method was never
installed, `build(ctx);` became a no-op, no component was ever constructed or
captured, and resolveComponent reported "Script must return a Component..." —
the preview showed "Preview paused" for every sample.
Import the core JRE packages (java.lang/java.util/java.io) and java.lang.Object/
String explicitly in PlaygroundRunner.bindGlobals so script methods install and
run. Verified in Playwright: the hello-world/Welcome preview now renders the full
form (title, labels, buttons, switch, checkbox, text field, FAB) and the
lifecycle sample's init/start + form.show() path runs; 0 page errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…5MB gz)
The editor only ever uses Java (basic-language) and CSS (language service), but
the bundled Monaco shipped the full TypeScript compiler language service (5.5MB)
plus the HTML and JSON services and all 81 basic-languages. Monaco lazy-loads
these on demand via a generic worker, so removing the unused ones is safe — they
were never activated. Keep language/css and basic-languages/{java,css}.
Saves ~6MB raw / ~1.45MB gz of editor page weight. Verified in Playwright: Java
highlighting and the CSS editor both work, 0 Monaco 404s, 0 page errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r, GPU/physics demos Four fixes to make the local-javascript Playground fully interactive: 1. Editor & panel clicks were dead. The peer pointer-events toggle yielded the canvas to a peer iframe whenever the iframe's BOX was under the cursor, but the Samples/Inspector sheets paint OPAQUELY on the canvas over the editor iframe -- so every panel/list click was swallowed by the hidden editor. The toggle now also requires the canvas pixel under the cursor to be (near-)transparent (an actual punched hole) before yielding, so panels get their clicks and the editor still gets clicks where it shows through. 2. Deep links (?sample=, ?code=, ?css=) never worked. CN.getProperty( "browser.window.location.href") is inlined/devirtualised past HTML5Implementation.getProperty to the base impl (returns the default), so the override was never reached. Route the host-page URL through the WebsiteThemeNative native interface (host-call bridge, not devirtualised), reading window.location and falling back to the parent frame when embedded. Keeps CN.getProperty as the off-browser fallback. Also forward the full host href to the worker (__cn1LocationHref) and prefer it in getProperty's location branches. 3. Unified the two loaders. The app posts cn1-app-ready once its own boot splash clears; the website wrapper holds its overlay until then (with a generous timeout fallback) so the bare "Loading..." splash no longer flashes mid-boot. 4. Added Physics and 3D / GPU sample demos. Verified on a locally built bundle (Playwright): selecting Physics via the panel loads it; ?sample=physics / ?sample=3d-gpu load directly and via a parent-frame query (production embed); cn1-app-ready hides the wrapper loader; Monaco is editable; 0 page errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (typed chars erased)
Once the pointer-events toggle let clicks/keys actually reach the Monaco editor
peer, every interaction wiped the typed character. Root cause: HTML5Peer and
HTML5BrowserComponent both gate their peer DOM management on documentContains(),
which was an @JSBody. On the ParparVM worker the script runs in the worker where
the element argument is a host-ref proxy with no live DOM, so
doc.documentElement.contains(el) ALWAYS returned false. initComponent() then
re-appended the iframe on every call -- and appendChild() of an already-attached
iframe MOVES it, which reloads it: editor.js re-runs, Monaco re-bootstraps from the
stale source, and the user's edits vanish. A click drove initComponent repeatedly,
so it reloaded on every keystroke/click ("typed character erased immediately" /
"no interaction").
Fix: replace both documentContains() @JSBody copies with a JSO-bridge probe
(getParentNode() runs on the main thread and reliably reports detached vs attached),
mirroring the existing isCORSRestricted() worker-proxy fix. initComponent now sees
the peer is already in the DOM and skips the re-append, so the iframe is never
moved/reloaded. Verified (Playwright): click no longer detaches the frame or churns
the DOM, typing persists and accumulates, no NPE flood, and sample switching still
loads (the editor stays in the DOM; it's only hidden behind panels).
Also: guard the iframe eventRouter -- when the canvas is pointer-events:none the
event belongs to the peer, so don't re-dispatch a synthetic copy into CN1 (caused
relayout churn), and bail out when getBoundingClientRect() returns null on the
worker bridge (was an NPE on every event). And keep PlaygroundBrowserEditor's
pendingSource in sync with typed text so any future re-bootstrap can't regress to
stale content.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…every keystroke After the iframe-reload fix, typing still broke: a second after typing, the caret jumped to line 1 col 1 and text could only be entered at the very start. Cause: the editor re-signals "ready" every 400ms until bootstrapped, and on the JS port a "ready" from ANY editor iframe is delivered to EVERY editor's host handler, so the host kept calling flush() -> bootstrap() -> setSource() -> model.setValue(), which resets Monaco's caret to 1:1 and (inside the 280ms change-debounce window) re-pushed stale text -- a sync loop that reverted edits. Fix: - editor.js bootstrap(): only seed the source on the FIRST bootstrap. Later bootstraps (from repeated/ cross-editor "ready") refresh markers/messages/theme only and never call setValue, so the caret and content are preserved. - PlaygroundBrowserEditor: ignore "ready" once already bootstrapped, so the host flushes exactly once. Verified (Playwright): place caret at end, type, wait 3.2s past all retries/debounce -> caret stays at end (line 33, not 1:1), further typing appends contiguously (AAABBB), no revert, 0 NPE. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Benchmarked the edit->preview pipeline (instrumented PlaygroundRunner/executeRunScript): the fixed debounce was the largest controllable chunk -- 280ms (editor change notification) + 850ms (auto-run) = 1130ms before the BeanShell eval even starts. The eval itself is ~450-540ms (warm) and is bsh-interpretation bound (amplified by the JS port's per-method generator model -- architectural, not tunable here); render is ~12ms and editor/css/persist ~80ms. Cut the change-notification debounce 280->120ms and the auto-run debounce 850->300ms, so the preview reacts ~700ms sooner after you stop typing (~1580ms -> ~870ms total) without thrashing (the auto-run is still ticket-debounced to the last keystroke, and the eval runs on the worker EDT so it never blocks Monaco typing on the main thread). Verified the caret stays stable across the faster runs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…in (~8x) Benchmarked the ParparVM JavaScript translation (it was ~22 min, an order of magnitude slower than TeaVM) by timing each phase and sampling the translator with jstack. Breakdown for the Playground (4665 classes / 42,829 methods, RTA off): parse 8s, conservative cull 31s, suspension 0s, codegen+write 1266s. Codegen was ~30ms/method and 29/36 stack samples sat in JavascriptMethodGenerator.findSimpleGotoSource, called from the inline-goto fold peephole (inlineUniqueSourceCases): each pass calls findSimpleGotoSource -- a full O(regionLen) scan -- for every unique-source case and rebuilds the whole switch-body string per fold, so the cost is ~O(folds * cases * regionLen). On the largest generated state-machine methods this single peephole dominated the entire build. The fold is a pure SIZE optimization (it never affects correctness), so cap it: skip it once a method's switch body exceeds a budget (default 4000 chars, tunable via -Dparparvm.js.inlineFold.maxRegion; <=0 restores unconditional folding). The few huge methods stay slightly larger; everything else still folds. Result (Playground translation): codegen 1266s -> 162s, total ~1310s -> ~186s. And the bundle is actually marginally SMALLER (55.2MB raw / 6.54MB gz vs ~57MB / 6.7MB) -- the fold was barely helping gz size while costing ~18 min. Verified the bundle is correct: editor typing + stable caret, ?sample= deep links, live preview, 0 errors. This halves the website build's dominant cost (it translates Initializr + Playground). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrates the Playground off the pinned old release + cloud
javascriptbuild target onto the local ParparVMlocal-javascripttarget — the same path the initializr uses (#5200) — and removes the class-exclusion list and version pinning that worked around the lagging cloud TeaVM backend.Playground (
scripts/cn1playground/)javascript/pom.xmlcodename1.defaultBuildTargetandbuild.sh/build.batswitchjavascript→local-javascript. The cloudjavascripttarget remains a fallback.cn1.registry.version(7.0.234) pin. The bean-shell access registry now tracks the current API directly. Newcn1.accessRegistry.useLocalSourcesproperty (true under thecn1-local-workspaceprofile) generates the registry from the repo's own CN1 sources (8.0-SNAPSHOT has no source jars on Central);common/pom.xmlpasses it to the generator asCN1_ACCESS_USE_LOCAL_SOURCES.GenerateCN1AccessRegistry.java): removed thesecurity.*/nfc.*exclusions — those were cloud-TeaVM-lag workarounds, and the local target builds current sources. Kept the structural exclusions:Accessor/IOAccessorinternals, andSimd(its alloca scratch arrays may not escape the reflection bridge under the bytecode-compliance check, which the local target enforces too). Also fixed an array-component varargs codegen bug surfaced by re-including NFC: emitnew byte[len][], not the malformednew byte[][len], forT[]...params.JS-port translator (
vm/ByteCodeTranslator/)The Playground's reflective registry references nearly the whole API, exercising paths a normal app doesn't:
ldcclass literals and threwUnsupported ldc constanton array literals (byte[].class,String[].class), which the registry emits for every array-typed parameter. Now lowered tojvm.getArrayClass(component, dims).classObject(cached under the same name array instances use).Parser.writeOutputcaughtThrowablebut only rethrewException, so anOutOfMemoryErrormid-emit let the translator exit 0 with a truncated dist (parparvm_runtime.jsbut noworker.js/translated_app.js). Now rethrowsErrors so the build fails loudly.Plugin / build wiring
JavaScriptBuilder: a-XmxinCN1_TRANSLATOR_OPTSnow overrides the default 512m translator heap.build.sh/build.batand the websitebuild_playground_for_sitesetCN1_TRANSLATOR_OPTS=-Dparparvm.js.rta.off -Xmx6g; the website extraction flattens the single wrapper dir soindex.htmllands at the served root (mirroring the initializr path).Verification
Built locally with the local 8.0-SNAPSHOT plugin: the
local-javascriptbuild produces a complete bundle (index.html,worker.js,translated_app+ 6 chunks,parparvm_runtime.js,browser_bridge.js,js/assets, themes, Monaco editorhtml.tar, native-interface stub) that loads in headless Chromium with no fatal JS/VM errors. The regenerated registry now includescom.codename1.security.*/com.codename1.nfc.*and still excludes the internals + Simd.Note: most of the diff is the regenerated
bsh/cn1/GeneratedCN1Access*.java(checked-in generated source, regenerated each build).🤖 Generated with Claude Code