This is an automated email from the ASF dual-hosted git repository.

papegaaij pushed a commit to branch performance-improvements-10.x
in repository https://gitbox.apache.org/repos/asf/wicket.git

commit 92c69f8828daf9b171c1b2d25779e276ade92eaf
Author: Emond Papegaaij <[email protected]>
AuthorDate: Fri Sep 11 22:04:16 2026 +0200

    Add wicket-benchmarks module
    
    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) <[email protected]>
---
 pom.xml                                            |  19 +
 wicket-benchmarks/README.md                        | 111 +++++
 wicket-benchmarks/pom.xml                          |  86 ++++
 .../wicket/benchmarks/ComponentFootprint.java      | 122 ++++++
 .../wicket/benchmarks/ComponentStateBenchmark.java | 467 +++++++++++++++++++++
 .../wicket/benchmarks/PageRenderBenchmark.java     | 118 ++++++
 .../benchmarks/PageSerializationBenchmark.java     | 124 ++++++
 .../benchmarks/ResourceNameIteratorBenchmark.java  | 126 ++++++
 .../apache/wicket/benchmarks/WicketContext.java    |  53 +++
 9 files changed, 1226 insertions(+)

diff --git a/pom.xml b/pom.xml
index 4dcdbce56d..c4a58a4924 100644
--- a/pom.xml
+++ b/pom.xml
@@ -124,6 +124,7 @@
         <module>wicket-migration</module>
                <module>wicket-tester</module>
                <module>wicket-extensions-tester</module>
+               <module>wicket-benchmarks</module>
     </modules>
        <properties>
                <!-- Encoding -->
@@ -168,6 +169,8 @@
                <jakarta.servlet-api.version>6.0.0</jakarta.servlet-api.version>
                
<jdk-serializable-functional.version>1.9.0</jdk-serializable-functional.version>
                <jetty.version>11.0.24</jetty.version>
+               <jmh.version>1.37</jmh.version>
+               <jol.version>0.17</jol.version>
                <junit.version>5.13.4</junit.version>
                <jsr305.version>3.0.2</jsr305.version>
                <logback.version>1.2.7</logback.version>
@@ -275,6 +278,12 @@
                                <version>${hamcrest.version}</version>
                                <scope>provided</scope>
                        </dependency>
+                       <dependency>
+                               <groupId>org.openjdk.jmh</groupId>
+                               
<artifactId>jmh-generator-annprocess</artifactId>
+                               <version>${jmh.version}</version>
+                               <scope>provided</scope>
+                       </dependency>
                        <dependency>
                                <groupId>com.fasterxml.jackson.core</groupId>
                                <artifactId>jackson-databind</artifactId>
@@ -505,6 +514,16 @@
                                <artifactId>objenesis</artifactId>
                                <version>${objenesis.version}</version>
                        </dependency>
+                       <dependency>
+                               <groupId>org.openjdk.jmh</groupId>
+                               <artifactId>jmh-core</artifactId>
+                               <version>${jmh.version}</version>
+                       </dependency>
+                       <dependency>
+                               <groupId>org.openjdk.jol</groupId>
+                               <artifactId>jol-core</artifactId>
+                               <version>${jol.version}</version>
+                       </dependency>
                        <dependency>
                                <groupId>org.ow2.asm</groupId>
                                <artifactId>asm</artifactId>
diff --git a/wicket-benchmarks/README.md b/wicket-benchmarks/README.md
new file mode 100644
index 0000000000..92125ba863
--- /dev/null
+++ b/wicket-benchmarks/README.md
@@ -0,0 +1,111 @@
+# wicket-benchmarks
+
+Development aid. Never released, contains no unit tests. It lives in the 
reactor so the
+benchmarks keep compiling against the current API — the previous set of 
component benchmarks was
+only ever attached to [WICKET-6774] and had rotted by the time anyone wanted 
to re-run them.
+
+## Running
+
+Benchmarks are in `src/main/java`, so a plain `compile` is enough:
+
+```bash
+# the JS resource optimizer re-minifies its own output, so wicket-core needs a 
clean first
+mvn -o clean -pl wicket-core
+mvn -o -pl wicket-benchmarks -am compile
+
+# classpath for the freshly built classes (not the jars in ~/.m2)
+mvn -o -pl wicket-benchmarks dependency:build-classpath \
+  -Dmdep.outputFile=wicket-benchmarks/target/ext-cp.txt
+CP="wicket-benchmarks/target/classes:wicket-core/target/classes:wicket-util/target/classes:\
+wicket-request/target/classes:wicket-tester/target/classes:\
+$(cat wicket-benchmarks/target/ext-cp.txt)"
+```
+
+Beware: `dependency:build-classpath` lists the **`~/.m2` jars** for 
`wicket-core` and friends.
+If you leave them on the classpath you are measuring whatever was last 
installed, not your working
+copy. Put the `target/classes` directories first, as above, and confirm which 
implementation you
+actually loaded before believing any number.
+
+### Time and allocation
+
+```bash
+java -cp "$CP" org.openjdk.jmh.Main ComponentStateBenchmark -prof gc
+```
+
+**Set the heap deliberately, and size it to the benchmark.** JMH forks inherit 
the default maximum
+heap — a quarter of physical memory, so ~15GB on a 64GB machine — which is 
both larger than needed
+and machine dependent. Pass an explicit `-jvmArgs "-Xmx..."` so a run means 
the same thing on
+another machine.
+
+Do not simply make it small. Too little heap shows up as GC noise, and it can 
be severe enough to
+swamp the measurement entirely: at 1GB `PageRenderBenchmark`, which allocates 
a page per
+invocation, reported 274 ± 437 us/op — an error bar larger than the mean. The 
same measurement at
+4GB is 102 ± 2 us/op. When a result looks noisy, suspect the heap before you 
believe the noise.
+The accessor benchmarks are content with 1GB; the render benchmark wants 4GB.
+
+`-prof gc` is not optional in practice: `gc.alloc.rate.norm` (bytes per 
operation) is the number
+that matters for a framework that keeps many pages in memory, and it is far 
steadier than
+throughput.
+
+### Footprint and serialized size
+
+```bash
+java --add-opens java.base/java.lang=ALL-UNNAMED -cp "$CP" \
+  org.apache.wicket.benchmarks.ComponentFootprint
+```
+
+JOL needs the `--add-opens` to walk the graph. Run it a second time with
+`-XX:+UseCompactObjectHeaders`: that flag moves every object by 4 bytes and 
can change which
+layout wins, so a footprint claim without it is only half the story.
+
+## What is here, and what each part is for
+
+| | measures | use it for |
+|---|---|---|
+| `ComponentStateBenchmark` | ns/op and bytes/op of the per-request state 
accessors | attributing a change to state handling |
+| `ComponentFootprint` | retained heap and serialized bytes per state shape | 
anything about memory |
+| `PageRenderBenchmark` | µs/op of a full 50-child panel render | catching an 
end-to-end regression |
+
+## Reading the results
+
+**Mixed shapes are the interesting ones.** `Component.data` holds a different 
kind of object
+depending on which of model, behaviors and meta data are present. Feed one 
shape at a time and the
+call sites that unpack it are monomorphic and inline, which flatters any 
implementation that
+dispatches on shape. Real pages interleave shapes. A large gap between 
`readMetaData` and
+`readMetaDataMixedShapes` is the signature of dispatch that stopped inlining, 
and it is invisible
+to a per-shape benchmark.
+
+**Measure the case, do not derive it.** Every `read*` benchmark has a 
`baseline*` twin that walks
+the same array with the same blackhole and reads a plain field instead of the 
state; subtract it to
+get the accessor's own cost. And where a path has distinct cases, benchmark 
each directly rather
+than subtracting one array from another: `readModelAllHaveModel` and 
`readModelNoneHaveModel` exist
+because inferring the empty case by subtracting the populated one from the 
mixed one gave an answer
+that sent an investigation after the wrong cause. The arrays differ in length 
and in type profile,
+so the subtraction is not valid.
+
+**Do not measure a mutation repeatedly against one instance.** `detach()` is 
not idempotent: the
+first call detaches models, drops temporary behaviors and compacts the 
behavior array, so every
+later call exercises the already-detached path. The original benchmark did 
exactly this and so
+measured the cheap case with great precision. `buildAndDetach` folds 
construction into the
+operation instead, which keeps every invocation doing real work without paying
+`Level.Invocation` overhead.
+
+**Measure the shape whose cost you are actually arguing about.** The shapes 
are not equally
+interesting, and the difference is not proportional to how exotic they look. 
`STABLE_ID_BEHAVIOR`
+is the case every link and ajax-enabled component hits, and it is where the 
storage layout makes by
+far the largest difference — master keeps a `BehaviorIdList` plus two 
`Object[]` per component to
+record one behavior id, which is ~72 bytes of heap and ~32 bytes of serialized 
form per component.
+A benchmark built only from `AttributeModifier` never creates that structure 
and will report a few
+percent where the real figure is tens of percent. WICKET-6774's own comments 
named this as the
+main win; the first version of this file missed it entirely.
+
+**Weight results by what real pages contain.** From a production app measured 
on WICKET-6774
+(548,285 components across 2,635 pages): 39% carry a model, 35% at least one 
behavior, 8% any meta
+data, and 0.03% more than one meta data entry. An 80% win on a shape that is 
1% of components is
+worth less than a 5% win on models.
+
+**Comparing two implementations** means running the same benchmark source 
against both, because
+these deliberately use public API only. Build each tree separately and keep 
the classpaths
+straight; a git worktree per side is the least error-prone way.
+
+[WICKET-6774]: https://issues.apache.org/jira/browse/WICKET-6774
diff --git a/wicket-benchmarks/pom.xml b/wicket-benchmarks/pom.xml
new file mode 100644
index 0000000000..101617d0ad
--- /dev/null
+++ b/wicket-benchmarks/pom.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/maven-v4_0_0.xsd";>
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.apache.wicket</groupId>
+               <artifactId>wicket-parent</artifactId>
+               <version>10.12.0-SNAPSHOT</version>
+               <relativePath>../pom.xml</relativePath>
+       </parent>
+       <artifactId>wicket-benchmarks</artifactId>
+       <packaging>jar</packaging>
+       <name>Wicket Benchmarks</name>
+       <description>
+               JMH benchmarks and footprint tools for Wicket internals. This 
module is a development
+               aid: it is never released and contains no unit tests. It is 
part of the reactor so that
+               the benchmarks keep compiling against the current API instead 
of rotting outside the
+               repository.
+       </description>
+       <properties>
+               <!-- development aid, never published -->
+               <maven.deploy.skip>true</maven.deploy.skip>
+               <osgi.export.package />
+       </properties>
+       <dependencies>
+               <dependency>
+                       <groupId>org.apache.wicket</groupId>
+                       <artifactId>wicket-core</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.apache.wicket</groupId>
+                       <artifactId>wicket-tester</artifactId>
+                       <scope>compile</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.openjdk.jmh</groupId>
+                       <artifactId>jmh-core</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.openjdk.jol</groupId>
+                       <artifactId>jol-core</artifactId>
+               </dependency>
+       </dependencies>
+       <build>
+               <plugins>
+                       <plugin>
+                               <groupId>org.apache.maven.plugins</groupId>
+                               <artifactId>maven-compiler-plugin</artifactId>
+                               <executions>
+                                       <execution>
+                                               <!--
+                                                       JDK 23 and later no 
longer run annotation processors found on the
+                                                       classpath, so JMH's 
generator has to be named explicitly or no
+                                                       BenchmarkList is 
produced and the jar runs zero benchmarks.
+                                               -->
+                                               <id>default-compile</id>
+                                               <configuration>
+                                                       
<annotationProcessorPaths>
+                                                               <path>
+                                                                       
<groupId>org.openjdk.jmh</groupId>
+                                                                       
<artifactId>jmh-generator-annprocess</artifactId>
+                                                                       
<version>${jmh.version}</version>
+                                                               </path>
+                                                       
</annotationProcessorPaths>
+                                               </configuration>
+                                       </execution>
+                               </executions>
+                       </plugin>
+               </plugins>
+       </build>
+</project>
diff --git 
a/wicket-benchmarks/src/main/java/org/apache/wicket/benchmarks/ComponentFootprint.java
 
b/wicket-benchmarks/src/main/java/org/apache/wicket/benchmarks/ComponentFootprint.java
new file mode 100644
index 0000000000..e949bf0488
--- /dev/null
+++ 
b/wicket-benchmarks/src/main/java/org/apache/wicket/benchmarks/ComponentFootprint.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.wicket.benchmarks;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.benchmarks.ComponentStateBenchmark.Shape;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.openjdk.jol.info.GraphLayout;
+
+/**
+ * Reports what component state actually costs, which is what the single-field 
packing in
+ * {@code Component.data} exists to minimise and what no throughput benchmark 
can tell you.
+ * <p>
+ * Two numbers per state shape, both measured against an identical tree whose 
components carry no
+ * state at all, so the difference isolates the state itself:
+ * <ul>
+ * <li><b>retained heap</b>, via JOL's graph walk - what a live page costs in 
the page cache.
+ * <li><b>serialized bytes</b>, via Java serialization - what it costs in the 
page store, and the
+ * constraint that any extra wrapper class also pays for its class descriptor.
+ * </ul>
+ * Run it twice, with and without {@code -XX:+UseCompactObjectHeaders}: that 
flag shifts every
+ * object by 4 bytes and can change which layout wins.
+ * <p>
+ * Not a JMH benchmark - it measures size, not time, so it is a plain main.
+ */
+public final class ComponentFootprint
+{
+       private static final int CHILDREN = 1_000;
+
+       private ComponentFootprint()
+       {
+       }
+
+       public static void main(String[] args) throws Exception
+       {
+               WicketContext.attach();
+               try
+               {
+                       System.out.printf("Component state footprint, %d 
children per tree%n", CHILDREN);
+                       System.out.printf("compact object headers: %s%n%n", 
compactHeaders());
+
+                       long baseHeap = retained(tree(Shape.NONE));
+                       long baseWire = serialized(tree(Shape.NONE));
+
+                       System.out.printf("%-24s %12s %12s %10s %12s %12s 
%10s%n", "shape", "heap", "heap-Δ",
+                               "Δ/comp", "wire", "wire-Δ", "Δ/comp");
+                       System.out.println("-".repeat(98));
+
+                       for (Shape shape : Shape.values())
+                       {
+                               long heap = retained(tree(shape));
+                               long wire = serialized(tree(shape));
+                               System.out.printf("%-24s %12d %12d %10.1f %12d 
%12d %10.1f%n", shape, heap,
+                                       heap - baseHeap, (heap - baseHeap) / 
(double)CHILDREN, wire, wire - baseWire,
+                                       (wire - baseWire) / (double)CHILDREN);
+                       }
+
+                       Shape detail = args.length > 0 ? Shape.valueOf(args[0])
+                               : Shape.MODEL_BEHAVIOR_METADATA;
+                       System.out.printf("%n%nWhere the bytes are, for 
%s:%n%n", detail);
+                       
System.out.println(GraphLayout.parseInstance(tree(detail)).toFootprint());
+               }
+               finally
+               {
+                       WicketContext.detach();
+               }
+       }
+
+       /** A parent with {@link #CHILDREN} children, each carrying the given 
state shape. */
+       private static WebMarkupContainer tree(Shape shape)
+       {
+               WebMarkupContainer parent = new WebMarkupContainer("parent");
+               for (int i = 0; i < CHILDREN; i++)
+               {
+                       Component child = new WebMarkupContainer("c" + i);
+                       shape.populate(child);
+                       parent.add(child);
+               }
+               return parent;
+       }
+
+       private static long retained(Object root)
+       {
+               return GraphLayout.parseInstance(root).totalSize();
+       }
+
+       private static long serialized(Component root) throws IOException
+       {
+               root.detach();
+               ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+               try (ObjectOutputStream out = new ObjectOutputStream(bytes))
+               {
+                       out.writeObject(root);
+               }
+               return bytes.size();
+       }
+
+       private static String compactHeaders()
+       {
+               // a plain Object is 16 bytes with 12-byte headers, 8 with 
compact ones
+               long size = GraphLayout.parseInstance(new Object()).totalSize();
+               return size <= 8 ? "on" : "off";
+       }
+}
diff --git 
a/wicket-benchmarks/src/main/java/org/apache/wicket/benchmarks/ComponentStateBenchmark.java
 
b/wicket-benchmarks/src/main/java/org/apache/wicket/benchmarks/ComponentStateBenchmark.java
new file mode 100644
index 0000000000..063662137f
--- /dev/null
+++ 
b/wicket-benchmarks/src/main/java/org/apache/wicket/benchmarks/ComponentStateBenchmark.java
@@ -0,0 +1,467 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.wicket.benchmarks;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.wicket.AttributeModifier;
+import org.apache.wicket.Component;
+import org.apache.wicket.MetaDataKey;
+import org.apache.wicket.ajax.AjaxEventBehavior;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.behavior.Behavior;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.model.Model;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+
+/**
+ * Benchmarks the per-request accessors on {@link Component}'s flexible state 
(model, behaviors and
+ * meta data) plus the mutate-and-detach cycle.
+ * <p>
+ * Deliberately written against public Wicket API only, so that the exact same 
source can be run
+ * against different implementations of the state storage and compared.
+ * <p>
+ * Three things are measured separately, because they answer different 
questions:
+ * <ul>
+ * <li>{@code read*} - the cost of reading state, per state shape. Reads do 
not mutate, so a
+ * trial-scoped component is correct here and no per-invocation harness 
overhead is paid.
+ * <li>{@code read*MixedShapes} - the same reads, but over a component array 
holding every shape at
+ * once. This is the interesting one: with a single shape the call sites 
inside the state lookup are
+ * monomorphic and inline, which flatters any implementation that dispatches 
on the shape. Real
+ * pages interleave shapes. A large gap between the per-shape and mixed 
numbers is the signature of
+ * dispatch that stopped inlining.
+ * <li>{@code buildAndDetach} - construct a component, populate its state and 
detach it, as one
+ * operation. Detaching mutates state (temporary behaviors are removed, arrays 
are compacted), so it
+ * cannot be measured repeatedly against the same instance; folding 
construction into the operation
+ * keeps every invocation doing the real work without resorting to {@code 
Level.Invocation}.
+ * </ul>
+ * Single threaded on purpose: component state is per component and never 
contended, so extra
+ * threads measure nothing new while making the Wicket thread-local setup 
harder to get right.
+ * <p>
+ * Always run with {@code -prof gc}: {@code gc.alloc.rate.norm} (bytes per 
operation) is the number
+ * that matters for a framework that has to keep many pages in memory, and it 
is far more stable
+ * than throughput.
+ */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Fork(3)
+@Threads(1)
+@Warmup(iterations = 3, time = 2, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+public class ComponentStateBenchmark
+{
+       static final MetaDataKey<String> KEY = new MetaDataKey<>()
+       {
+               private static final long serialVersionUID = 1L;
+       };
+
+       /** One ingredient of a {@link Shape}. */
+       enum Trait
+       {
+               /** A default model. */
+               MODEL,
+               /** A behavior that never needs an id, the way an {@link 
AttributeModifier} does not. */
+               BEHAVIOR,
+               /** A single meta data entry. */
+               METADATA,
+               /**
+                * A behavior whose id has been handed out, as every link and 
ajax-enabled component has.
+                * Master keeps those ids in a {@code BehaviorIdList} held in 
the component's meta data;
+                * storing the id as the behavior's own array index removes 
that list, which WICKET-6774
+                * claimed as its biggest saving. {@link #BEHAVIOR} does not 
exercise it.
+                */
+               STABLE_ID,
+               /**
+                * Makes {@link #STABLE_ID} carry a real {@link 
AjaxEventBehavior} rather than a bare one,
+                * so the figure is comparable to the -36.2% serialized saving 
reported on WICKET-6774. A
+                * bare behavior isolates the id storage but carries almost 
nothing of its own, which
+                * flatters the percentage.
+                */
+               AJAX;
+       }
+
+       /** The shapes the flexible state of a component can take. */
+       public enum Shape
+       {
+               NONE,
+               MODEL(Trait.MODEL),
+               BEHAVIOR(Trait.BEHAVIOR),
+               METADATA(Trait.METADATA),
+               MODEL_BEHAVIOR(Trait.MODEL, Trait.BEHAVIOR),
+               MODEL_METADATA(Trait.MODEL, Trait.METADATA),
+               BEHAVIOR_METADATA(Trait.BEHAVIOR, Trait.METADATA),
+               MODEL_BEHAVIOR_METADATA(Trait.MODEL, Trait.BEHAVIOR, 
Trait.METADATA),
+               STABLE_ID_BEHAVIOR(Trait.STABLE_ID),
+               MODEL_STABLE_ID_BEHAVIOR(Trait.MODEL, Trait.STABLE_ID),
+               AJAX_BEHAVIOR(Trait.STABLE_ID, Trait.AJAX);
+
+               private final Set<Trait> traits;
+
+               Shape(Trait... traits)
+               {
+                       this.traits = EnumSet.noneOf(Trait.class);
+                       Collections.addAll(this.traits, traits);
+               }
+
+               boolean hasModel()
+               {
+                       return traits.contains(Trait.MODEL);
+               }
+
+               Component newComponent(String id)
+               {
+                       Component c = new WebMarkupContainer(id);
+                       populate(c);
+                       return c;
+               }
+
+               void populate(Component c)
+               {
+                       if (traits.contains(Trait.MODEL))
+                       {
+                               c.setDefaultModel(Model.of(c.getId()));
+                       }
+                       if (traits.contains(Trait.BEHAVIOR))
+                       {
+                               c.add(AttributeModifier.replace("class", "a"));
+                       }
+                       if (traits.contains(Trait.METADATA))
+                       {
+                               c.setMetaData(KEY, "v");
+                       }
+                       if (traits.contains(Trait.STABLE_ID))
+                       {
+                               Behavior stable = traits.contains(Trait.AJAX) ? 
new AjaxTestBehavior()
+                                       : new StableIdBehavior();
+                               c.add(stable);
+                               // rendering a callback url does this; it is 
what materialises the id storage
+                               c.getBehaviorId(stable);
+                       }
+               }
+       }
+
+       /** A real ajax behavior, with the fields and callback machinery that 
implies. */
+       private static class AjaxTestBehavior extends AjaxEventBehavior
+       {
+               private static final long serialVersionUID = 1L;
+
+               AjaxTestBehavior()
+               {
+                       super("change");
+               }
+
+               @Override
+               protected void onEvent(AjaxRequestTarget target)
+               {
+               }
+       }
+
+       /** Requires a stable behavior id, the way an ajax behavior or link 
does. */
+       private static class StableIdBehavior extends Behavior
+       {
+               private static final long serialVersionUID = 1L;
+
+               @Override
+               public boolean getStatelessHint(Component component)
+               {
+                       return false;
+               }
+       }
+
+       /** One component of the shape under test: the state lookup sees a 
single shape. */
+       @State(Scope.Benchmark)
+       public static class OneShape
+       {
+               @Param
+               public Shape shape;
+
+               Component component;
+
+               @Setup(Level.Trial)
+               public void setUp()
+               {
+                       WicketContext.attach();
+                       component = shape.newComponent("c");
+               }
+
+               @TearDown(Level.Trial)
+               public void tearDown()
+               {
+                       WicketContext.detach();
+               }
+       }
+
+       /** Every shape at once: the state lookup sees all of them, as it does 
on a real page. */
+       @State(Scope.Benchmark)
+       public static class AllShapes
+       {
+               Component[] components;
+
+               @Setup(Level.Trial)
+               public void setUp()
+               {
+                       WicketContext.attach();
+                       Shape[] shapes = Shape.values();
+                       components = new Component[shapes.length];
+                       for (int i = 0; i < shapes.length; i++)
+                       {
+                               components[i] = shapes[i].newComponent("c" + i);
+                       }
+               }
+
+               @TearDown(Level.Trial)
+               public void tearDown()
+               {
+                       WicketContext.detach();
+               }
+       }
+
+       /**
+        * Only the shapes that carry a model, so every {@code data} seen at 
the model lookup is a
+        * wrapper. Separates a type-profile effect from the cost of the lookup 
itself: if the mixed
+        * shape penalty disappears here, it was profile pollution at the type 
check.
+        */
+       @State(Scope.Benchmark)
+       public static class ModelShapes
+       {
+               Component[] components;
+
+               @Setup(Level.Trial)
+               public void setUp()
+               {
+                       WicketContext.attach();
+                       List<Component> cs = new ArrayList<>();
+                       for (Shape shape : Shape.values())
+                       {
+                               if (shape.hasModel())
+                               {
+                                       cs.add(shape.newComponent("c" + 
cs.size()));
+                               }
+                       }
+                       components = cs.toArray(new Component[0]);
+               }
+
+               @TearDown(Level.Trial)
+               public void tearDown()
+               {
+                       WicketContext.detach();
+               }
+       }
+
+       /**
+        * Only the shapes with no model, so the model lookup always comes up 
empty. Measures the
+        * absent path directly instead of inferring it by subtracting the 
all-model case, which uses
+        * a different array and a different type profile.
+        */
+       @State(Scope.Benchmark)
+       public static class NoModelShapes
+       {
+               Component[] components;
+
+               @Setup(Level.Trial)
+               public void setUp()
+               {
+                       WicketContext.attach();
+                       List<Component> cs = new ArrayList<>();
+                       for (Shape shape : Shape.values())
+                       {
+                               if (!shape.hasModel())
+                               {
+                                       cs.add(shape.newComponent("c" + 
cs.size()));
+                               }
+                       }
+                       components = cs.toArray(new Component[0]);
+               }
+
+               @TearDown(Level.Trial)
+               public void tearDown()
+               {
+                       WicketContext.detach();
+               }
+       }
+
+       /** A component that definitely carries behaviors, for the Ajax id 
lookup path. */
+       @State(Scope.Benchmark)
+       public static class WithBehaviors
+       {
+               Component component;
+
+               @Setup(Level.Trial)
+               public void setUp()
+               {
+                       WicketContext.attach();
+                       component = new WebMarkupContainer("c");
+                       component.setDefaultModel(Model.of("m"));
+                       // ids have to be handed out before they can be looked 
up: master only builds its
+                       // BehaviorIdList when getBehaviorId is called, and 
throws
+                       // InvalidBehaviorIdException otherwise. Rendering a 
callback url does this.
+                       for (int i = 0; i < 3; i++)
+                       {
+                               Behavior stable = new StableIdBehavior();
+                               component.add(stable);
+                               component.getBehaviorId(stable);
+                       }
+               }
+
+               @TearDown(Level.Trial)
+               public void tearDown()
+               {
+                       WicketContext.detach();
+               }
+       }
+
+       // ---------------------------------------------------------------- 
reads, one shape at a time
+
+       @Benchmark
+       public Object readMetaData(OneShape ctx)
+       {
+               return ctx.component.getMetaData(KEY);
+       }
+
+       @Benchmark
+       public Object readModel(OneShape ctx)
+       {
+               return ctx.component.getDefaultModel();
+       }
+
+       @Benchmark
+       public Object readBehaviors(OneShape ctx)
+       {
+               return ctx.component.getBehaviors(Behavior.class);
+       }
+
+       // ------------------------------------------------------------------- 
reads, shapes interleaved
+
+       @Benchmark
+       public void readMetaDataMixedShapes(AllShapes ctx, Blackhole bh)
+       {
+               for (Component c : ctx.components)
+               {
+                       bh.consume(c.getMetaData(KEY));
+               }
+       }
+
+       @Benchmark
+       public void readModelMixedShapes(AllShapes ctx, Blackhole bh)
+       {
+               for (Component c : ctx.components)
+               {
+                       bh.consume(c.getDefaultModel());
+               }
+       }
+
+       @Benchmark
+       public void readBehaviorsMixedShapes(AllShapes ctx, Blackhole bh)
+       {
+               for (Component c : ctx.components)
+               {
+                       bh.consume(c.getBehaviors(Behavior.class));
+               }
+       }
+
+       /**
+        * The floor for the mixed shape benchmarks: same array, same loop, 
same blackhole, reading a
+        * plain field instead of the state. Subtract this to get the cost of 
the accessor alone.
+        */
+       @Benchmark
+       public void baselineMixedShapes(AllShapes ctx, Blackhole bh)
+       {
+               for (Component c : ctx.components)
+               {
+                       bh.consume(c.getId());
+               }
+       }
+
+       @Benchmark
+       public void readModelAllHaveModel(ModelShapes ctx, Blackhole bh)
+       {
+               for (Component c : ctx.components)
+               {
+                       bh.consume(c.getDefaultModel());
+               }
+       }
+
+       @Benchmark
+       public void readModelNoneHaveModel(NoModelShapes ctx, Blackhole bh)
+       {
+               for (Component c : ctx.components)
+               {
+                       bh.consume(c.getDefaultModel());
+               }
+       }
+
+       @Benchmark
+       public void baselineNoneHaveModel(NoModelShapes ctx, Blackhole bh)
+       {
+               for (Component c : ctx.components)
+               {
+                       bh.consume(c.getId());
+               }
+       }
+
+       @Benchmark
+       public void baselineAllHaveModel(ModelShapes ctx, Blackhole bh)
+       {
+               for (Component c : ctx.components)
+               {
+                       bh.consume(c.getId());
+               }
+       }
+
+       // 
------------------------------------------------------------------------- the 
Ajax id lookup
+
+       @Benchmark
+       public Object readBehaviorById(WithBehaviors ctx)
+       {
+               return ctx.component.getBehaviorById(1);
+       }
+
+       // -------------------------------------------------------------------- 
mutate, then detach
+
+       @Benchmark
+       public Object buildAndDetach(OneShape ctx)
+       {
+               Component c = ctx.shape.newComponent("c");
+               c.detach();
+               return c;
+       }
+
+       @Benchmark
+       public Object buildOnly(OneShape ctx)
+       {
+               return ctx.shape.newComponent("c");
+       }
+}
diff --git 
a/wicket-benchmarks/src/main/java/org/apache/wicket/benchmarks/PageRenderBenchmark.java
 
b/wicket-benchmarks/src/main/java/org/apache/wicket/benchmarks/PageRenderBenchmark.java
new file mode 100644
index 0000000000..4ef5039ac3
--- /dev/null
+++ 
b/wicket-benchmarks/src/main/java/org/apache/wicket/benchmarks/PageRenderBenchmark.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.wicket.benchmarks;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.MarkupContainer;
+import org.apache.wicket.benchmarks.ComponentStateBenchmark.Shape;
+import org.apache.wicket.markup.IMarkupResourceStreamProvider;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.markup.repeater.RepeatingView;
+import org.apache.wicket.mock.MockApplication;
+import org.apache.wicket.util.resource.IResourceStream;
+import org.apache.wicket.util.resource.StringResourceStream;
+import org.apache.wicket.util.tester.WicketTester;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Warmup;
+
+/**
+ * End-to-end render of a panel with 50 stateful children, one shape at a time.
+ * <p>
+ * This is a regression guard, not a measurement of state access: markup 
parsing, hierarchy
+ * traversal and response writing dominate a render, so a change worth a few 
nanoseconds per field
+ * read disappears into the noise here. Its job is to catch a change that made 
rendering as a whole
+ * worse. Use {@link ComponentStateBenchmark} to attribute a difference to 
state handling, and
+ * {@link ComponentFootprint} for anything about memory.
+ * <p>
+ * Replaces the seven near-identical methods of the original benchmark with 
one parameterised over
+ * {@link Shape}, which also makes the missing combinations measurable.
+ */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+@Fork(3)
+@Threads(1)
+@Warmup(iterations = 3, time = 2, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@State(Scope.Thread)
+public class PageRenderBenchmark
+{
+       private static final int CHILDREN = 50;
+
+       @Param
+       public Shape shape;
+
+       private WicketTester tester;
+
+       @Setup(Level.Trial)
+       public void setUp()
+       {
+               tester = new WicketTester(new MockApplication());
+       }
+
+       @TearDown(Level.Trial)
+       public void tearDown()
+       {
+               tester.destroy();
+       }
+
+       @Benchmark
+       public Object renderPanel()
+       {
+               return tester.startComponentInPage(new StatefulPanel("panel", 
shape));
+       }
+
+       private static class StatefulPanel extends Panel implements 
IMarkupResourceStreamProvider
+       {
+               private static final long serialVersionUID = 1L;
+
+               StatefulPanel(String id, Shape shape)
+               {
+                       super(id);
+                       RepeatingView view = new RepeatingView("rv");
+                       for (int i = 0; i < CHILDREN; i++)
+                       {
+                               Component child = new 
WebMarkupContainer(view.newChildId());
+                               shape.populate(child);
+                               view.add(child);
+                       }
+                       add(view);
+               }
+
+               @Override
+               public IResourceStream getMarkupResourceStream(MarkupContainer 
container,
+                       Class< ? > containerClass)
+               {
+                       return new StringResourceStream(
+                               "<wicket:panel><div 
wicket:id=\"rv\"></div></wicket:panel>");
+               }
+       }
+}
diff --git 
a/wicket-benchmarks/src/main/java/org/apache/wicket/benchmarks/PageSerializationBenchmark.java
 
b/wicket-benchmarks/src/main/java/org/apache/wicket/benchmarks/PageSerializationBenchmark.java
new file mode 100644
index 0000000000..9914fde122
--- /dev/null
+++ 
b/wicket-benchmarks/src/main/java/org/apache/wicket/benchmarks/PageSerializationBenchmark.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.wicket.benchmarks;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.wicket.MarkupContainer;
+import org.apache.wicket.markup.IMarkupResourceStreamProvider;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.repeater.RepeatingView;
+import org.apache.wicket.mock.MockApplication;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.serialize.java.JavaSerializer;
+import org.apache.wicket.util.resource.IResourceStream;
+import org.apache.wicket.util.resource.StringResourceStream;
+import org.apache.wicket.util.tester.WicketTester;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+/**
+ * Measures writing a page to bytes, which is what {@code 
SerializingPageStore} does for every page
+ * at the end of a request.
+ * <p>
+ * Parameterised by component count because the cost tracks the number of 
objects in the graph
+ * rather than the size of the result: {@code ObjectOutputStream} keeps a 
handle table per stream
+ * and grows it as it walks, so a page of many small components costs more 
than its byte count
+ * suggests.
+ * <p>
+ * {@code -prof gc} is the point of this one. {@code gc.alloc.rate.norm} 
covers both the serialized
+ * bytes and everything the stream allocated to produce them, so read it 
against the result size:
+ * 50 components serialize to about 5kB, 500 components to about 37kB.
+ */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+@Fork(3)
+@Threads(1)
+@Warmup(iterations = 3, time = 2, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@State(Scope.Thread)
+public class PageSerializationBenchmark
+{
+       @Param({"50", "500"})
+       public int components;
+
+       private WicketTester tester;
+
+       private JavaSerializer serializer;
+
+       private WebPage page;
+
+       @Setup(Level.Trial)
+       public void setUp()
+       {
+               tester = new WicketTester(new MockApplication());
+               serializer = new JavaSerializer("benchmarks");
+               page = new BenchmarkPage(components);
+               tester.startPage(page);
+       }
+
+       @TearDown(Level.Trial)
+       public void tearDown()
+       {
+               tester.destroy();
+       }
+
+       @Benchmark
+       public byte[] serializePage()
+       {
+               return serializer.serialize(page);
+       }
+
+       private static class BenchmarkPage extends WebPage implements 
IMarkupResourceStreamProvider
+       {
+               private static final long serialVersionUID = 1L;
+
+               BenchmarkPage(int components)
+               {
+                       RepeatingView view = new RepeatingView("rv");
+                       for (int i = 0; i < components; i++)
+                       {
+                               WebMarkupContainer child = new 
WebMarkupContainer(view.newChildId());
+                               child.add(new Label("label", Model.of("value " 
+ i)));
+                               view.add(child);
+                       }
+                       add(view);
+               }
+
+               @Override
+               public IResourceStream getMarkupResourceStream(MarkupContainer 
container,
+                       Class< ? > containerClass)
+               {
+                       return new StringResourceStream(
+                               "<html><body><div wicket:id=\"rv\"><span 
wicket:id=\"label\"></span></div></body></html>");
+               }
+       }
+}
diff --git 
a/wicket-benchmarks/src/main/java/org/apache/wicket/benchmarks/ResourceNameIteratorBenchmark.java
 
b/wicket-benchmarks/src/main/java/org/apache/wicket/benchmarks/ResourceNameIteratorBenchmark.java
new file mode 100644
index 0000000000..0bec6b11c7
--- /dev/null
+++ 
b/wicket-benchmarks/src/main/java/org/apache/wicket/benchmarks/ResourceNameIteratorBenchmark.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.wicket.benchmarks;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.wicket.core.util.resource.locator.ResourceNameIterator;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+
+/**
+ * Benchmarks the candidate-filename walk that {@code ResourceStreamLocator} 
performs for every
+ * property and markup lookup, once per registered properties loader.
+ * <p>
+ * The walk dominates i18n lookup cost on a miss, and misses are the common 
case: a key is resolved
+ * by climbing the component hierarchy, so every class above the one that 
actually declares the key
+ * contributes a full traversal that finds nothing.
+ * <p>
+ * The two cases are benchmarked directly rather than derived from one 
another, because they do
+ * different amounts of work. {@link #walkAllCandidates} is the miss - every 
combination of style,
+ * variation, locale and extension is produced. {@link #firstCandidate} is the 
hit, where the
+ * locator stops at the first name and then reads back the locale, style and 
variation to stamp on
+ * the resource stream; that read-back is the only caller of {@code 
getLocale()}, so a benchmark
+ * that never performs it would miss the cost.
+ * <p>
+ * Written against public API only, so the same source can be run against two 
implementations and
+ * compared. {@code -prof gc} is the point of this one: {@code 
gc.alloc.rate.norm} is what the
+ * change being measured actually moves.
+ */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Fork(3)
+@Threads(1)
+@Warmup(iterations = 3, time = 2, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@State(Scope.Benchmark)
+public class ResourceNameIteratorBenchmark
+{
+       /**
+        * The locale shapes differ in how many candidates they produce, which 
is the main driver of
+        * both time and allocation: a language+country locale yields three 
rounds, language-only two,
+        * and no locale one.
+        */
+       public enum LocaleShape
+       {
+               LANGUAGE_COUNTRY(new Locale("nl", "NL")),
+               LANGUAGE_ONLY(new Locale("nl")),
+               WITH_VARIANT(new Locale("nl", "NL", "vlaams")),
+               NONE(null);
+
+               private final Locale locale;
+
+               LocaleShape(Locale locale)
+               {
+                       this.locale = locale;
+               }
+       }
+
+       // PropertiesFactory hands the locator a path with the extension 
already appended, and the
+       // iterator splits it back off.
+       private static final String PATH = 
"org/example/app/pages/group/GroupsPage.properties";
+
+       private static final List<String> ONE_EXTENSION = 
Arrays.asList("properties");
+
+       @Param
+       public LocaleShape localeShape;
+
+       @Param({"false", "true"})
+       public boolean styled;
+
+       @Benchmark
+       public void walkAllCandidates(Blackhole blackhole)
+       {
+               ResourceNameIterator names = newIterator();
+               while (names.hasNext())
+               {
+                       blackhole.consume(names.next());
+               }
+       }
+
+       @Benchmark
+       public void firstCandidate(Blackhole blackhole)
+       {
+               ResourceNameIterator names = newIterator();
+               if (names.hasNext())
+               {
+                       blackhole.consume(names.next());
+                       blackhole.consume(names.getLocale());
+                       blackhole.consume(names.getStyle());
+                       blackhole.consume(names.getVariation());
+               }
+       }
+
+       private ResourceNameIterator newIterator()
+       {
+               return new ResourceNameIterator(PATH, styled ? "mystyle" : null,
+                       styled ? "myvariation" : null, localeShape.locale, 
ONE_EXTENSION, false);
+       }
+}
diff --git 
a/wicket-benchmarks/src/main/java/org/apache/wicket/benchmarks/WicketContext.java
 
b/wicket-benchmarks/src/main/java/org/apache/wicket/benchmarks/WicketContext.java
new file mode 100644
index 0000000000..858f0fdfa2
--- /dev/null
+++ 
b/wicket-benchmarks/src/main/java/org/apache/wicket/benchmarks/WicketContext.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.wicket.benchmarks;
+
+import org.apache.wicket.Session;
+import org.apache.wicket.ThreadContext;
+import org.apache.wicket.mock.MockApplication;
+import org.apache.wicket.mock.MockWebRequest;
+import org.apache.wicket.protocol.http.WebSession;
+import org.apache.wicket.protocol.http.mock.MockServletContext;
+import org.apache.wicket.request.Url;
+
+/**
+ * Minimal Wicket runtime for benchmarks: enough application and session 
context for components to
+ * be constructed, read and detached, without the cost of a full {@code 
WicketTester} request cycle.
+ */
+final class WicketContext
+{
+       private WicketContext()
+       {
+       }
+
+       static void attach()
+       {
+               MockApplication application = new MockApplication();
+               application.setName("benchmarks-" + System.nanoTime());
+               application.setServletContext(new 
MockServletContext(application, null));
+               ThreadContext.setApplication(application);
+               application.initApplication();
+
+               Session session = new WebSession(new 
MockWebRequest(Url.parse("/")));
+               ThreadContext.setSession(session);
+       }
+
+       static void detach()
+       {
+               ThreadContext.detach();
+       }
+}

Reply via email to