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();
+ }
+}