Start Sooner Wait Less

A margin calculation is an odd place to find a startup stall. It sounds like arithmetic. In our native Mac profile, it could wait for the operating system’s main thread.

Following that call led to screen-scale queries repeated across components, a theme scan repeated across styles, and a switch generating blurred artwork just to say how big it was. None of those costs required a complicated screen. They happened while constructing an ordinary one.

The native profiling work started by asking what each call actually needed from the platform. Later, we found the same kind of overreach in the JavaScript compiler: preparing calls to suspend even when they could finish synchronously.

Publish screen state when it changes

Pixel conversion needs the screen scale. AppKit owns the window-to-screen relationship, but the relationship does not change for every padding calculation.

The port now publishes screen identity and scale together when a window is created or moves. Readers consume that state atomically. Publishing them as one value also avoids pairing the screen from one update with the scale from another.

flowchart LR A[Window created or moved] --> B[Publish screen and scale together] B --> C[Atomic state] C --> D[Pixel conversion] D --> E[Padding and margin layout]

The old path accounted for 35 ms of blocked event-dispatch-thread time per launch in the Mac profile. Installing a window observer had another synchronous dispatch, even though the caller needed no result. That accounted for 37 ms.

The profile gave us two calls to remove from the waiting path. Their overlap and scheduling still matter to the total launch time.

Try the lock before announcing a park

ParparVM’s monitor entry announced a GC park before it knew whether acquiring the lock would block. An uncontended lock could therefore wait for the collector’s handshake despite having no competing owner.

The implementation now tries the mutex first. Only the path that actually needs to wait enters the park protocol. That handshake had cost 8.7 ms in the startup profile. Instrumentation also had to change: the stall report previously missed that handshake loop and reported zero.

The old stall report said zero because it missed the handshake loop. Fixing that blind spot let us see the wait we were trying to remove.

Style construction repeated a global query

A UIID selects component styling. Before using a dark variant, UIManager needed to know whether one existed. It answered by scanning the whole theme table once for every distinct UIID.

The fix indexes dark keys once per theme generation. A new UIID then performs a lookup against that index, while loading a new theme invalidates the old answer.

Native Mac measurementBeforeAfter
First-use dark-variant lookup111,955 ns17,378 ns

A first screen often introduces many distinct UIIDs. Each new style had been paying for that global search.

For an application-side diagnostic, measure construction separately from the first paint and use a fresh process for cold samples:

import com.codename1.components.Switch;
import com.codename1.ui.Form;
import com.codename1.ui.Label;
import com.codename1.ui.layouts.BoxLayout;

long started = System.currentTimeMillis();
Form form = new Form("Settings", BoxLayout.y());
form.add(new Label("Notifications"));
form.add(new Switch());
long constructionMs = System.currentTimeMillis() - started;
System.out.println("Form construction: " + constructionMs + " ms");
form.show();

This separates construction from show(). Use a profiler for the individual lookup and first-frame timings.

Preferred size should not generate artwork

Switch.getPreferredSize() used to build the artwork, including a Gaussian blur. Layout can ask for a size even if the component will never be painted. A hidden or discarded switch therefore paid for pixels nobody would see.

The preferred size now comes directly from the same dimensions used to draw the switch. Artwork generation stays on the path that needs artwork. This is the sort of optimization a per-frame rendering benchmark misses because the unwanted work happened before the first frame.

Keep the image on the GPU path

The Metal pipeline also stopped round-tripping each picture through the CPU and retaining a decoded EncodedImage copy beside its GPU texture. Rounded corners can be handled in the shader instead of generating another rounded image.

That connects startup to steady-state memory. Loading a screen full of pictures can otherwise create several representations of each picture before the user has interacted with it. Removing a representation removes its construction cost and its lifetime from the memory profile.

This work complements collector-managed reference retention. Avoiding an unnecessary copy and deciding when to evict a useful copy are different jobs; both matter for an image-heavy application.

The same profiling pass found correctness defects

A null socket handle was being unboxed into a long. An image creation path needed its one-pass premultiplication restored. A mismatched native symbol name had left rounded drawing inactive without a linker error. Forked Maven runs also failed to inherit maven.repo.local, which could make a developer run stale artifacts while believing a fix was under test.

The affected modules built cleanly, and 6,145 core tests passed with the changes.

JavaScript was waiting for the wrong reason too

On the native Mac path, a margin calculation waited for screen information that could already have been published. The JavaScript compiler had a different unnecessary wait: an unrelated blocking method could make a synchronous call into a suspension point.

One blocking run() was enough to affect other run() methods, then their callers. PR #5755 gives suspension analysis the receiver-type information it needs to stop that propagation.

A signature does not identify an implementation

The compiler translates a Java method that can block into a JavaScript generator. Calls that can suspend need yield* so the runtime can resume them later. That is necessary for blocking behavior, but unnecessary generator dispatch adds work to synchronous paths.

The old analysis grouped methods by name and descriptor without the owner class. This small Java example shows the distinction it lost:

final class WaitingTask {
    void run() throws InterruptedException {
        Thread.sleep(10);
    }
}

final class CounterTask {
    private int count;
    void run() {
        count++;
    }
}

Both methods have the same name and argument/return descriptor. Knowing that the receiver is a CounterTask rules out the blocking implementation. Real code adds interfaces, subclasses, and native bridges, so the compiler must conservatively include every implementation that can actually receive the call.

Reuse the type information already computed

JavascriptReachability already computed possible receiver types, but kept that information private. The new dispatch model exposes it to suspension analysis and code generation.

The analysis follows the receiver’s possible implementations instead of every method in the program with the same signature. It also narrows JavaScript-object protection and distinguishes bridge tokens used to replace a method from tokens merely used to look one up.

flowchart TD C[Virtual call site] --> R[Possible receiver types] R --> I[Resolve reachable implementations] I --> Q{Any implementation may block?} Q -->|Yes| G[Generator call path] Q -->|No| S[Synchronous call path] I --> U{Resolution incomplete?} U -->|Yes| F[Keep conservative fallback]

An unresolved receiver cannot simply be skipped. The runtime can search interfaces and fall back to a native table, so static analysis must account for those possibilities too. Both analysis and emitter use the same dispatch model: emitting yield* into a plain JavaScript function is a syntax error.

Why counting bytes was the wrong measure

Translating hellocodenameone showed how far the old assumption had spread:

Generated artifact metricBeforeAfter
yield* sites54,54940,741
Generators13,06811,044
Suspending virtual dispatch sites28,56919,073
Synchronous methods8,4569,875
Translated bundle8,089,807 bytes7,993,916 bytes

A yield site is cheap to spell. Its runtime dispatch cost does not show up proportionally in source size. The 25.3% reduction in yield sites produced only a 1.2% bundle reduction.

Before this work, screenshots could verify rendering and the lifecycle harness could verify milestones. Neither could put an elapsed-time cost on unnecessary generator dispatch. The new JavaScript throughput benchmark translates Java workloads and runs them under Node:

./scripts/run-javascript-throughput-benchmark.sh --help

Use the script’s options to build comparable arms and retain its checksums. Each workload runs in its own process. The comparison refuses changed checksums and counts unexpected generator stepping through the synchronous dispatcher.

The timing results include a regression

We compared the previous and updated revisions with interleaved best-of-three runs. A master-versus-master comparison measured the noise floor at 0.3% to 4.9% across workloads.

WorkloadElapsed-time change
hashCodeHeavy57.6% lower
toStringHeavy18.9% lower
equalsHeavy7.8% lower
mapChurn6.6% lower
iteratorWalk13.7% higher

The iterator regression was reproducible and above its own noise floor. Its emitted body and the inspected iterator functions were byte-identical between arms. We have not found the cause yet. That leaves a specific workload to investigate next, even after the generator count has fallen.

Running under Node isolates compiler/runtime throughput. Browser profiling will tell us how those changes interact with rendering and scheduling on the target device.

A narrower answer must remain correct

The VM tests finished with zero failures across 305 tests and one pre-existing skip. The JavaScript screenshot check matched 181 screenshots after the bridge changes.

Spend the time on the screen the user asked for

A screen-scale lookup should read the current scale. A size query should calculate dimensions. A call that cannot block should not need generator dispatch. Each fix removes work that grew out of a broader assumption than the operation required.

The native and JavaScript investigations both started with work that had spread further than it needed to. Publishing screen state stopped repeated queries; using receiver types stopped unrelated methods from becoming generators. The iterator regression is the next loose end.

Across this week’s release, Codename One is reducing those costs inside the shared implementation. App teams can keep their Java screens and benefit as the ports improve. The compiler still takes the conservative path when it cannot resolve a receiver, and the runtime still coordinates with the collector when a lock must wait. Removing unnecessary work should preserve those safeguards.


Discussion

Which call in your profile spent time waiting when you expected it to finish immediately?