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

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


The following commit(s) were added to refs/heads/wicket-6774 by this push:
     new 36626baf20 WICKET-6774: add wicket-benchmarks module
36626baf20 is described below

commit 36626baf20bb26069e4f52395d38e9c04a62d60a
Author: Emond Papegaaij <[email protected]>
AuthorDate: Fri Sep 4 21:52:45 2026 +0200

    WICKET-6774: add wicket-benchmarks module
    
    The component benchmarks used to investigate this issue only ever existed as
    attachments on WICKET-6774, 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.
    
    Three tools, because the constraints they check are not the same question:
    
    * ComponentStateBenchmark - JMH, ns/op and bytes/op for the per-request 
state
      accessors. Reads are measured per state shape and again over a component
      array holding every shape at once. With a single shape the call sites that
      unpack Component.data are monomorphic and inline, which flatters any
      implementation dispatching on the shape, while real pages interleave 
shapes.
      Mutation is measured as construct-and-detach in one operation, because
      detach() is not idempotent and so cannot be measured repeatedly against 
the
      same instance.
    
    * ComponentFootprint - retained heap via JOL and serialized size via Java
      serialization, per shape, against an identical stateless tree so that the
      difference isolates the state itself. Neither is a throughput question, 
and
      a footprint claim also depends on -XX:+UseCompactObjectHeaders, which can
      change which layout wins.
    
    * PageRenderBenchmark - a full render as an end-to-end regression guard. The
      seven near-identical methods of the original are replaced by one
      parameterised over the state shape.
    
    The shapes include a behavior that requires a stable id, which is what every
    link and ajax-enabled component has, and which the original benchmarks never
    covered even though the largest saving claimed on this issue is there.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 pom.xml                                            |  19 ++
 wicket-benchmarks/README.md                        |  92 ++++++
 wicket-benchmarks/pom.xml                          | 100 +++++++
 .../wicket/benchmarks/ComponentFootprint.java      | 122 ++++++++
 .../wicket/benchmarks/ComponentStateBenchmark.java | 309 +++++++++++++++++++++
 .../wicket/benchmarks/PageRenderBenchmark.java     | 118 ++++++++
 .../apache/wicket/benchmarks/WicketContext.java    |  53 ++++
 7 files changed, 813 insertions(+)

diff --git a/pom.xml b/pom.xml
index af4192f148..1cf6df679b 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>
                <!-- must stay last: aggregates the JaCoCo data of every module 
above -->
                <module>wicket-coverage</module>
        </modules>
@@ -182,6 +183,8 @@
                <jakarta.servlet-api.version>6.1.0</jakarta.servlet-api.version>
                
<jdk-serializable-functional.version>1.9.0</jdk-serializable-functional.version>
                <jetty.version>12.1.12</jetty.version>
+               <jmh.version>1.37</jmh.version>
+               <jol.version>0.17</jol.version>
                <junit.version>6.1.3</junit.version>
                <jsr305.version>3.0.2</jsr305.version>
                <logback.version>1.2.7</logback.version>
@@ -290,6 +293,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.github.openjson</groupId>
                                <artifactId>openjson</artifactId>
@@ -525,6 +534,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..899833fce5
--- /dev/null
+++ b/wicket-benchmarks/README.md
@@ -0,0 +1,92 @@
+# 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
+```
+
+`-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.
+
+**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..55ca1ba365
--- /dev/null
+++ b/wicket-benchmarks/pom.xml
@@ -0,0 +1,100 @@
+<?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>11.0.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>
+               <!-- order enforced by pedantic-pom-enforcers: scope, then 
groupId, then artifactId -->
+               <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>
+                       <plugin>
+                               <groupId>com.github.siom79.japicmp</groupId>
+                               <artifactId>japicmp-maven-plugin</artifactId>
+                               <executions>
+                                       <execution>
+                                               <!-- a new module has no 
previous release to compare against -->
+                                               <id>default</id>
+                                               <configuration>
+                                                       <skip>true</skip>
+                                               </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..c43fd50976
--- /dev/null
+++ 
b/wicket-benchmarks/src/main/java/org/apache/wicket/benchmarks/ComponentStateBenchmark.java
@@ -0,0 +1,309 @@
+/*
+ * 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.AttributeModifier;
+import org.apache.wicket.Component;
+import org.apache.wicket.MetaDataKey;
+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;
+       };
+
+       /** The eight shapes the flexible state of a component can take. */
+       public enum Shape
+       {
+               NONE(false, false, false),
+               MODEL(true, false, false),
+               BEHAVIOR(false, true, false),
+               METADATA(false, false, true),
+               MODEL_BEHAVIOR(true, true, false),
+               MODEL_METADATA(true, false, true),
+               BEHAVIOR_METADATA(false, true, true),
+               MODEL_BEHAVIOR_METADATA(true, true, true),
+               /**
+                * A behavior with a stable id, 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. None of the other shapes exercise it.
+                */
+               STABLE_ID_BEHAVIOR(false, false, false, true),
+               MODEL_STABLE_ID_BEHAVIOR(true, false, false, true);
+
+               private final boolean model;
+               private final boolean behavior;
+               private final boolean metaData;
+               private final boolean stableId;
+
+               Shape(boolean model, boolean behavior, boolean metaData)
+               {
+                       this(model, behavior, metaData, false);
+               }
+
+               Shape(boolean model, boolean behavior, boolean metaData, 
boolean stableId)
+               {
+                       this.model = model;
+                       this.behavior = behavior;
+                       this.metaData = metaData;
+                       this.stableId = stableId;
+               }
+
+               Component newComponent(String id)
+               {
+                       Component c = new WebMarkupContainer(id);
+                       populate(c);
+                       return c;
+               }
+
+               void populate(Component c)
+               {
+                       if (model)
+                       {
+                               c.setDefaultModel(Model.of(c.getId()));
+                       }
+                       if (behavior)
+                       {
+                               c.add(AttributeModifier.replace("class", "a"));
+                       }
+                       if (metaData)
+                       {
+                               c.setMetaData(KEY, "v");
+                       }
+                       if (stableId)
+                       {
+                               Behavior stable = new StableIdBehavior();
+                               c.add(stable);
+                               // rendering a callback url does this; it is 
what materialises the id storage
+                               c.getBehaviorId(stable);
+                       }
+               }
+       }
+
+       /** 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();
+               }
+       }
+
+       /** 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"));
+                       component.setMetaData(KEY, "v");
+                       component.add(AttributeModifier.replace("class", "a"),
+                               AttributeModifier.replace("style", "b"), 
AttributeModifier.replace("title", "c"));
+               }
+
+               @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 
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/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