Skip to content

Value slots, preencoded control flow, and bytecode codegen - #149

Draft
BernardoPe wants to merge 4 commits into
developmentfrom
perf/value-slots
Draft

BernardoPe wants to merge 4 commits into
developmentfrom
perf/value-slots

Conversation

@BernardoPe

@BernardoPe BernardoPe commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

This PR adds value slots and preencoded control flow as a faster alternative to dynamic(), and adds bytecode generation for simple chains.

Value slots (textOf, rawOf, intOf, longOf, doubleOf, boolOf, attrOf, attrOfNullable) let a view read a single value from the model without creating a dynamic() block. The preprocessor stores each slot directly in the chain, so rendering does not need to resolve it as a dynamic block each time.

The same idea is used for forEachOf and whenOf. Their bodies are preencoded once as separate sub-chains, then reused during rendering.

For chains made up of static blocks and value slots, HtmlFlow can also generate a small class that writes directly to the output buffer. This removes the interpretation loop and avoids boxing for numeric slots. The generated bytecode is written directly, so this does not add a bytecode library dependency. If a chain cannot be compiled, it keeps using the interpreted version. Both paths produce the same output.

The changes also work with async views and hot reload.

template-benchmark when adapting the presentations and stocks views to use these features (tested locally):

image image

Here is what the views look like:

public class StocksHtmlFlowSlots {

    public static HtmlView<List<Stock>> view = HtmlFlow.view(StocksHtmlFlowSlots::templateStocks);

    record StockDto(Stock stock, int index) {
        String rowClass()    { return index % 2 == 0 ? "even" : "odd"; }
        String symbol()      { return stock.getSymbol(); }
        String symbolHref()  { return "/stocks/" + stock.getSymbol(); }
        String url()         { return stock.getUrl(); }
        String name()        { return stock.getName(); }
        double price()       { return stock.getPrice(); }
        double change()      { return stock.getChange(); }
        double ratio()       { return stock.getRatio(); }
        String changeClass() { return change() < 0 ? "minus" : null; }
        String ratioClass()  { return ratio() < 0 ? "minus" : null; }
    }

    private static void templateStocks(HtmlPage view) {
        view
            .html()
                .head()
                    // ... unchanged head: title, meta, link, script, style ...
                .__()
                .body()
                    .h1().raw("Stock Prices").__()
                    .table()
                        .thead()
                            .tr()
                                .th().raw("#").__()
                                .th().raw("symbol").__()
                                .th().raw("name").__()
                                .th().raw("price").__()
                                .th().raw("change").__()
                                .th().raw("ratio").__()
                            .__()
                        .__()
                        .tbody()
                        .forEachOf(StocksHtmlFlowSlots::rows, tbody -> tbody
                            .tr()
                                .attrOf("class", StockDto::rowClass)
                                .td()
                                    .intOf(StockDto::index)
                                .__()
                                .td()
                                    .a()
                                        .attrOf("href", StockDto::symbolHref)
                                        .rawOf(StockDto::symbol)
                                    .__()
                                .__()
                                .td()
                                    .a()
                                        .attrOf("href", StockDto::url)
                                        .rawOf(StockDto::name)
                                    .__()
                                .__()
                                .td()
                                    .strong()
                                        .doubleOf(StockDto::price)
                                    .__()
                                .__()
                                .td()
                                    .attrOfNullable("class", StockDto::changeClass)
                                    .doubleOf(StockDto::change)
                                .__()
                                .td()
                                    .attrOfNullable("class", StockDto::ratioClass)
                                    .doubleOf(StockDto::ratio)
                                .__()
                            .__())
                        .__()
                    .__()
                .__()
            .__();
    }

    private static List<StockDto> rows(List<Stock> stocks) {
        return IntStream.range(0, stocks.size())
            .mapToObj(i -> new StockDto(stocks.get(i), i + 1))
            .toList();
    }
}
public class PresentationsHtmlFlowSlots {

    public static HtmlView<Iterator<Presentation>> view =
        HtmlFlow.view(PresentationsHtmlFlowSlots::presentationsView);

    private static void presentationsView(HtmlPage view) {
        view.html()
            .head()
                // ... unchanged head: meta, title, link ...
            .__()
            .body()
                .div().attrClass("container")
                    .div().attrClass("page-header")
                        .h1().raw("JFall 2013 Presentations - htmlApi").__()
                    .__()
                    .forEachOf(PresentationsHtmlFlowSlots::all, div -> div
                        .div().attrClass("panel panel-default")
                            .div().attrClass("panel-heading")
                                .h3()
                                    .attrClass("panel-title")
                                    .rawOf(PresentationsHtmlFlowSlots::heading)
                                .__()
                            .__()
                            .div()
                                .attrClass("panel-body")
                                .rawOf(Presentation::getSummary)
                            .__()
                        .__()
                    )
                .__()
                .script().attrSrc("/webjars/jquery/3.1.1/jquery.min.js").__()
                .script().attrSrc("/webjars/bootstrap/3.3.7-1/js/bootstrap.min.js").__()
            .__()
        .__();
    }

    private static Iterable<Presentation> all(Iterator<Presentation> iter) {
        return () -> iter;
    }

    private static String heading(Presentation p) {
        return p.getTitle() + " - " + p.getSpeakerName();
    }
}

Depends on

This depends on xmlet/xsd2poet#2, which adds the slot methods and Slot<M> to the generated element API.

For now, this branch uses xsd2poet-java:1.0.9-SNAPSHOT, built locally.

@github-actions

Copy link
Copy Markdown
Contributor

Please run mvn spotless:apply locally to fix the formatting issues, then commit and push the changes.

textOf/rawOf/intOf/longOf/doubleOf/boolOf/attrOf/attrOfNullable bind a
single value from the model without opening a dynamic() block; the
preprocessor records each as a node in the same chain it already
builds for static markup. forEachOf and whenOf do the same for loops
and conditionals: each preencodes its body once, as its own sub-chain,
instead of resolving it interpretively on every render.

When a run of the chain holds only static blocks and value slots, it
compiles once into a small generated class that writes straight to the
render buffer, no interpretation loop and no boxing for numeric slots.
The class file is hand-rolled (no bytecode library) since the
generated method never branches and needs no StackMapTable. Anything
that can't compile falls back to the interpreted chain; output is
byte-identical either way.
The async chain hands off to the same synchronous continuation nodes
value slots and forEachOf already use, so nothing async-specific is
needed for them to work in a viewAsync()/viewSuspend() view. These
tests are the proof: value slots on both sides of an await, and a
render that matches the equivalent dynamic() block exactly.
textOf/intOf/attrOf/forEachOf/whenOf only worked through the
preencoding visitor; HtmlViewVisitorHot threw
UnsupportedOperationException for every one of them. Implement all ten
against its existing model field: a slot applies its accessor to the
current model and writes immediately, and forEachOf/whenOf swap the
model in for the body's duration, the same way visitDynamic already
hands it to a whole block.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant