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

commit 8c5407b2eea79f8af95bd41d85312e82ffd45bbb
Author: Emond Papegaaij <[email protected]>
AuthorDate: Sat Sep 5 14:18:02 2026 +0200

    WICKET-6774: benchmark the empty and populated model paths separately
    
    getDefaultModel() behaves differently depending on whether a model is 
actually
    there, and only the populated case was being measured. Inferring the empty 
case
    by subtracting the all-model array from the mixed array is not valid: the 
arrays
    differ in length and in the types seen at the unpacking call site, and 
doing so
    pointed an investigation at the wrong cause. Reading each case from its own
    array instead:
    
        readModelAllHaveModel   only shapes that carry a model
        readModelNoneHaveModel  only shapes that do not
    
    Each read benchmark also gets a baseline twin that walks the same array 
with the
    same blackhole and reads a plain field, so the loop and blackhole overhead 
can
    be subtracted and what is left is the accessor. That overhead is a large 
part of
    these numbers: over eleven components it is roughly 4ns of a 12ns 
measurement.
    
    The README gains the two things that cost the most time to learn: measure a 
case
    rather than deriving it from others, and set the fork heap deliberately. Too
    large is machine dependent, too small is GC noise - the render benchmark 
reports
    274 +- 437 us/op in 1GB and 102 +- 2 us/op in 4GB.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 wicket-benchmarks/README.md                        |  19 ++++
 .../wicket/benchmarks/ComponentStateBenchmark.java | 120 +++++++++++++++++++++
 2 files changed, 139 insertions(+)

diff --git a/wicket-benchmarks/README.md b/wicket-benchmarks/README.md
index 899833fce5..92125ba863 100644
--- a/wicket-benchmarks/README.md
+++ b/wicket-benchmarks/README.md
@@ -32,6 +32,17 @@ actually loaded before believing any number.
 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.
@@ -64,6 +75,14 @@ dispatches on shape. Real pages interleave shapes. A large 
gap between `readMeta
 `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
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
index b44b5a66af..79914dc282 100644
--- 
a/wicket-benchmarks/src/main/java/org/apache/wicket/benchmarks/ComponentStateBenchmark.java
+++ 
b/wicket-benchmarks/src/main/java/org/apache/wicket/benchmarks/ComponentStateBenchmark.java
@@ -16,6 +16,8 @@
  */
 package org.apache.wicket.benchmarks;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 
 import org.apache.wicket.AttributeModifier;
@@ -134,6 +136,11 @@ public class ComponentStateBenchmark
                        this.ajax = ajax;
                }
 
+               boolean hasModel()
+               {
+                       return model;
+               }
+
                Component newComponent(String id)
                {
                        Component c = new WebMarkupContainer(id);
@@ -241,6 +248,70 @@ public class ComponentStateBenchmark
                }
        }
 
+       /**
+        * 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
@@ -320,6 +391,55 @@ public class ComponentStateBenchmark
                }
        }
 
+       /**
+        * 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

Reply via email to