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

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

commit fd1df62c15efbd24b88313cc7c042e752f572771
Author: Emond Papegaaij <[email protected]>
AuthorDate: Sun Sep 6 07:09:18 2026 +0000

    Benchmark the resource name iterator walk
    
    ResourceStreamLocator walks a list of candidate filenames for every property
    and markup lookup, once per registered properties loader. That 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 it contributes a full traversal that finds nothing.
    
    The hit and the miss are benchmarked directly rather than derived from one
    another, because they do different amounts of work, and the read-back of
    locale, style and variation is included in the hit - it is the only caller 
of
    getLocale(), so leaving it out would hide that cost. Parameterised over the
    locale shapes, which drive how many candidates a traversal produces.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 .../benchmarks/ResourceNameIteratorBenchmark.java  | 126 +++++++++++++++++++++
 1 file changed, 126 insertions(+)

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..b9579446ce
--- /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(Locale.of("nl", "NL")),
+               LANGUAGE_ONLY(Locale.of("nl")),
+               WITH_VARIANT(Locale.of("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);
+       }
+}

Reply via email to