Skip to content

Performance improvements 10.x - #1604

Open
reiern70 wants to merge 5 commits into
wicket-10.xfrom
performance-improvements-10.x
Open

reiern70 wants to merge 5 commits into
wicket-10.xfrom
performance-improvements-10.x

Conversation

@reiern70

Copy link
Copy Markdown
Contributor

No description provided.

papegaaij and others added 5 commits September 16, 2026 09:55
A development aid, kept in the default reactor so the benchmarks keep compiling
against the current API. It is never released and has no unit tests; the README
covers how to run it.

The component benchmarks used to investigate WICKET-6774 only ever existed as
attachments on the issue, and they no longer compile: WicketTester has moved to
its own module, and since JDK 23 javac no longer runs annotation processors
found on the classpath, so JMH silently produces no BenchmarkList and the run
executes nothing. Keeping them in the reactor means they keep compiling.

It holds JMH benchmarks for component state, page rendering, resource name
iteration and page serialization, a ComponentFootprint tool that reports
retained heap through JOL and serialized size per state shape, and WicketContext
as the shared harness.

Backported from master. PageEncryptionBenchmark is left out: it measures
SchemeCrypt and ICryptScheme, which this branch does not have. Locale.of() is
Java 19, so the benchmark uses new Locale() here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A component's flexible state - model, behaviors and meta data - is no longer an
Object[] with a packing convention but a ComponentState instance. One final
class rather than one subclass per combination: the combinations all cost the
same 24 bytes on a 64 bit VM with compressed oops, so specialised classes save
nothing, while four implementations of the same six accessors make every
unpacking call site megamorphic and stop it from being inlined.

A behavior's id is now its index in that state rather than a position in a
BehaviorIdList kept in the component's meta data, which removes the list
entirely. Reading meta data on a link or ajax component gets cheaper as a side
effect, because it no longer has to walk past that entry.

Behaviour change: behavior ids are only maintained for stateful behaviors. Ids
can change for other behaviors, also when combined on the same component.
AbstractDefaultAjaxBehavior no longer forces an id at bind time; an id is add
order by construction now, and forcing one would fix the behavior positions of
every ajax-enabled component for its whole life.

Backported from master.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ResourceStreamLocator walks a list of candidate filenames for every property and
markup lookup, once per registered properties loader. Misses are the common
case, because a key is resolved by climbing the component hierarchy, so every
class above the one that declares it contributes a full traversal that finds
nothing.

toString() built each candidate from four prepend() calls, and the locale part
went through getLocale(), which builds a Locale, even though
LocaleResourceNameIterator.next() had just built the identical suffix and thrown
it away. It now keeps that suffix, and caches the Locale per state for the one
caller that does read it back.

Verified by enumeration rather than by argument: every candidate name with its
locale, style, variation and extension over 6 paths x 2 styles x 2 variations x
7 locales x 5 extension lists x strict/non-strict, 9918 names, byte-identical
before and after.

One note for reviewers: toString() now takes the locale segment from next()
instead of from getLocale(). A subclass supplied through the protected
newLocaleResourceNameIterator hook that overrode getLocale() without also
overriding next() would see a difference; nothing in the tree does that.

Backported from master.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ResourceUtil#rejectPathSeparators ran Locale#toString() on every call, and
Locale#toString() builds a new string each time. Every resource lookup validates
the locale, so that is once per lookup for a result that is thrown away.

Without a variant, a script or extensions, Locale#toString() returns nothing but
the language and the country joined by '_', so inspecting those two subtags
directly is equivalent and allocates nothing. Richer locales are rare and still
take the general route, which also keeps the two in agreement about the subtags
that toString() drops.

Verified by enumeration over 4918 locales: byte-identical outcomes before and
after.

Backported from master.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JavaSerializer wrote into a java.io.ByteArrayOutputStream with no initial size,
which starts at 32 bytes and grows by copying everything written so far into a
buffer of twice the size. A page of any substance outgrows that several times
over, and every one of those copies is thrown away again immediately.

Wicket already has a ByteArrayOutputStream that chains a new buffer instead.
Starting it at 4kB means a modest page never has to grow it at all. Measured
over a page of 500 components, 37924 bytes serialized: 256714 -> 214763 bytes
allocated per operation, -16%.

Backported from master.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

2 participants