papegaaij opened a new pull request, #1595:
URL: https://github.com/apache/wicket/pull/1595

   Draft, for discussion. This collects the WICKET-6774 component state rework 
together with a
   handful of further allocation and CPU savings found by profiling a real 
application, and adds
   the benchmark module they were all measured with.
   
   ## wicket-benchmarks
   
   A new module, not part of the default reactor build (`-Pbenchmarks`, see its 
README).
   
   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 now holds three kinds of tool:
   
   * **JMH benchmarks** - `ComponentStateBenchmark`, `PageRenderBenchmark`,
     `ResourceNameIteratorBenchmark`, `PageSerializationBenchmark`, 
`PageEncryptionBenchmark`.
   * **`ComponentFootprint`** - retained heap via JOL and serialized size, per 
state shape,
     against an identical stateless tree so the difference isolates the state 
itself.
   * **`WicketContext`** - the shared harness.
   
   ## Component state (WICKET-6774)
   
   A component's state is no longer an `Object[]` with a packing convention, 
but a
   `ComponentState` instance. One final class rather than one subclass per 
combination: every
   unpacking call site otherwise dispatches over four implementations of the 
same six accessors,
   which is past the point where HotSpot stops inlining, and a real page 
interleaves shapes even
   where a benchmark feeding one shape at a time would not show it.
   
   Reading state over an array holding every shape at once, 2 forks, ns/op, 
master -> this branch:
   
   | | master | this branch | |
   |---|---|---|---|
   | `readBehaviorsMixedShapes` | 31.50 | 21.35 | -32% |
   | `readMetaDataMixedShapes`  | 25.27 | 17.66 | -30% |
   | `readModelMixedShapes`     | 30.17 | 16.54 | -45% |
   | `readBehaviors[AJAX_BEHAVIOR]` | 11.15 | 1.97 | -82%, 72 -> 24 B/op |
   | `readBehaviorById` | 2.82 | 1.62 | -43% |
   
   For a component carrying a real `AjaxEventBehavior` the tree is 39.8% 
smaller serialized, -72
   bytes retained heap and -32 bytes serialized per component - comparable to 
the -36.2% reported
   on the issue in 2020.
   
   **Behaviour change:** behavior ids are only maintained for stateful 
behaviors. Ids can change
   for other behaviors, also when combined on the same component. This is 
documented in the guide
   commit on this branch.
   
   ## Resource name iteration
   
   `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()` - a `Locale.of()` cache lookup - even though 
`LocaleResourceNameIterator.next()`
   had just built the identical suffix and thrown it away. 
`ResourceUtil#rejectPathSeparators`
   separately ran `Locale#toString()` on every call.
   
   `nl_NL` without a style, 3 forks:
   
   | | master | this branch | |
   |---|---|---|---|
   | `walkAllCandidates` (miss) | 923.8 ns, 1224 B | 139.9 ns, 672 B | -85% / 
-45% |
   | `firstCandidate` (hit) | 775.2 ns, 600 B | 405.6 ns, 368 B | -48% / -39% |
   
   Both changes were 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), and 4918 locales for 
`rejectPathSeparators`
   - 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.
   
   ## Page store
   
   Serialization and encryption run on the request thread after the response 
has been flushed, so
   this is throughput and thread occupancy rather than user-visible latency.
   
   `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. Wicket already has a `ByteArrayOutputStream` that chains a new buffer 
instead.
   
   `SchemeCrypt` prefixes every ciphertext with a one-byte marker naming the 
scheme, and did so by
   allocating a new array one byte longer and copying the whole payload in - 
and on the way back
   copying everything after the marker out again. `AbstractAesGcmCryptScheme` 
added a third copy.
   `ICryptScheme#encrypt`/`#decrypt` now carry the offsets needed to avoid all 
three; the old
   signatures remain as default methods. The marker stays `SchemeCrypt`'s 
concern, as documented
   in the user guide - a scheme still knows nothing about it, only that the 
first few bytes of
   what it returns are not its own.
   
   | | master | this branch | |
   |---|---|---|---|
   | serialize, 500 components (37,924 bytes) | 256,714 B/op | 214,763 B/op | 
-16% |
   | encrypt, 40kB | 18.28 us, 124,496 B | 12.58 us, 44,416 B | -31% / -64% |
   | decrypt, 40kB | 14.07 us, 84,408 B | 11.17 us, 44,360 B | -21% / -47% |
   
   Encrypting now allocates 1.11x the payload rather than 3.1x. Serialization 
time is unchanged;
   what remains there is `ObjectOutputStream`'s handle table, which scales with 
the number of
   objects in the page graph rather than with its byte count - which is the 
thing the component
   state rework above reduces.
   
   ## How the non-6774 hotspots were found
   
   JFR on a large production application under its Selenium suite, keeping only 
samples whose
   stack contains `WicketFilter.doFilter`, charging JDK frames to the nearest 
non-JDK caller and
   bucketing by package. Inside Wicket request handling: page store 23.2% CPU / 
12.4% allocation,
   render 16.8% / 15.5%, localizer 6.1% / 15.0%.
   
   Two attempts were measured and dropped rather than included: folding the 
resource name
   iterators into precomputed lists (6x slower on the hit path, because 
`getLocale()` costs a
   cache lookup per state and the hit path stops early), and a `reset()`-based 
restart of the same
   iterators (no time win, three new subclass contract hazards).
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to