You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@wicket.apache.org by pa...@apache.org on 2026/09/06 07:10:42 UTC

(wicket) 02/02: Stop re-deriving the locale part of every candidate resource name

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 00c2ef56f1d5b79fa39e6c78cc0e1e93b4a166ec
Author: Emond Papegaaij <em...@topicus.nl>
AuthorDate: Sun Sep 6 07:09:37 2026 +0000

    Stop re-deriving the locale part of every candidate resource name
    
    ResourceNameIterator.toString() built each candidate name from four prepend()
    calls, each concatenating a char with an Object, so four throwaway strings were
    allocated on top of the result. The locale part was the expensive one: it went
    through getLocale(), which calls Locale.of() - a LocaleObjectCache lookup
    rather than a field read - even though LocaleResourceNameIterator.next() had
    just built the identical suffix and discarded it.
    
    next() now keeps that suffix and toString() reuses it; getLocale() memoises the
    derived Locale per state for the locator's own read-back; the style and
    variation parts are rebuilt only when the style iterator advances; and each
    branch of toString() is a single concatenation, so only the returned string is
    allocated. ExtensionResourceNameIterator was also building its backing iterator
    twice.
    
    ResourceNameIteratorBenchmark, nl_NL without a style, 3 forks:
    
      walkAllCandidates (miss)  923.8 -> 167.8 ns/op   1224 -> 736 B/op
      firstCandidate (hit)      775.2 -> 421.6 ns/op    600 -> 432 B/op
    
    Verified equivalent by enumerating every candidate name and its locale, style,
    variation and extension across 6 paths x 2 styles x 2 variations x 7 locales x
    5 extension lists x strict/non-strict - 9918 names - before and after the
    change: byte-identical.
    
    One behavioural note for reviewers: toString() now takes the locale segment
    from next() instead of from getLocale(). For the shipped implementation those
    are provably the same, which is what the enumeration above establishes. A
    subclass supplied through the protected newLocaleResourceNameIterator hook that
    overrode getLocale() without also overriding next() would see a difference;
    nothing in the tree does that.
    
    Co-Authored-By: Claude Opus 5 (1M context) <no...@anthropic.com>
---
 .../locator/ExtensionResourceNameIterator.java     |  5 ++--
 .../locator/LocaleResourceNameIterator.java        | 34 ++++++++++++++++++++++
 .../resource/locator/ResourceNameIterator.java     | 30 ++++++++++++-------
 3 files changed, 57 insertions(+), 12 deletions(-)

diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/ExtensionResourceNameIterator.java b/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/ExtensionResourceNameIterator.java
index b1ffe2762a..540f8efda3 100644
--- a/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/ExtensionResourceNameIterator.java
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/ExtensionResourceNameIterator.java
@@ -42,13 +42,14 @@ public class ExtensionResourceNameIterator implements Iterator<String>
 	public ExtensionResourceNameIterator(final Iterable<String> extensions)
 	{
 		// Fail safe: hasNext() needs to return at least once with true
-		if (extensions == null || !extensions.iterator().hasNext())
+		Iterator<String> extensionIterator = extensions == null ? null : extensions.iterator();
+		if (extensionIterator == null || !extensionIterator.hasNext())
 		{
 			this.iterator = NULL_ITERABLE.iterator();
 		}
 		else
 		{
-			this.iterator = extensions.iterator();
+			this.iterator = extensionIterator;
 		}
 	}
 
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/LocaleResourceNameIterator.java b/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/LocaleResourceNameIterator.java
index 3faedccda6..8edf3436df 100644
--- a/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/LocaleResourceNameIterator.java
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/LocaleResourceNameIterator.java
@@ -39,6 +39,13 @@ public class LocaleResourceNameIterator implements Iterator<String>
 
 	private final boolean strict;
 
+	/** The locale part of the resource name that {@link #next()} produced for this state */
+	private String suffix = "";
+
+	private int localeState = -1;
+
+	private Locale currentLocale;
+
 	/**
 	 * Construct.
 	 *
@@ -55,6 +62,18 @@ public class LocaleResourceNameIterator implements Iterator<String>
 	 * @return Locale
 	 */
 	public Locale getLocale()
+	{
+		// Locale.of() is a cache lookup, not a field read, and this is called once per candidate
+		// name; the state only changes in next().
+		if (localeState != state)
+		{
+			localeState = state;
+			currentLocale = localeForState();
+		}
+		return currentLocale;
+	}
+
+	private Locale localeForState()
 	{
 		if (state == 1)
 		{
@@ -72,6 +91,15 @@ public class LocaleResourceNameIterator implements Iterator<String>
 		return null;
 	}
 
+	/**
+	 * @return the locale part of the resource name for the current state, already prefixed with
+	 *         {@code '_'}, or the empty string if this state carries no locale
+	 */
+	public String getSuffix()
+	{
+		return suffix;
+	}
+
 	/**
 	 *
 	 * @see java.util.Iterator#hasNext()
@@ -93,6 +121,12 @@ public class LocaleResourceNameIterator implements Iterator<String>
 	 */
 	@Override
 	public String next()
+	{
+		suffix = nextSuffix();
+		return suffix;
+	}
+
+	private String nextSuffix()
 	{
 		if (locale == null)
 		{
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/ResourceNameIterator.java b/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/ResourceNameIterator.java
index f89c959a3a..93265c880d 100644
--- a/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/ResourceNameIterator.java
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/ResourceNameIterator.java
@@ -69,6 +69,11 @@ public class ResourceNameIterator implements IResourceNameIterator
 	// The various iterators used to locate the resource file
 	private final StyleAndVariationResourceNameIterator styleIterator;
 	private LocaleResourceNameIterator localeIterator;
+
+	// The variation and style parts of the name, rebuilt only when the style iterator advances.
+	private String variationPart = "";
+
+	private String stylePart = "";
 	private ExtensionResourceNameIterator extensionsIterator;
 
 	/**
@@ -199,6 +204,8 @@ public class ResourceNameIterator implements IResourceNameIterator
 		while (styleIterator.hasNext())
 		{
 			styleIterator.next();
+			variationPart = part(styleIterator.getVariation(), '_');
+			stylePart = part(styleIterator.getStyle(), '_');
 
 			localeIterator = newLocaleResourceNameIterator(locale, strict);
 			while (localeIterator.hasNext())
@@ -244,19 +251,22 @@ public class ResourceNameIterator implements IResourceNameIterator
 	@Override
 	public String toString()
 	{
-		return path + prepend(getVariation(), '_') + prepend(getStyle(), '_') +
-			prepend(getLocale(), '_') + prepend(getExtension(), '.');
+		// The locale part was already built by LocaleResourceNameIterator.next(); deriving it
+		// again through getLocale() would cost a Locale.of() lookup and its string form for every
+		// candidate name. Each branch is a single concatenation, so only the result is allocated.
+		String localePart = localeIterator != null ? localeIterator.getSuffix() : "";
+		String extension = getExtension();
+
+		if (extension == null)
+		{
+			return path + variationPart + stylePart + localePart;
+		}
+		return path + variationPart + stylePart + localePart + '.' + extension;
 	}
 
-	/**
-	 * 
-	 * @param string
-	 * @param prepend
-	 * @return The string prepended with the char
-	 */
-	private String prepend(Object string, char prepend)
+	private static String part(String value, char prepend)
 	{
-		return (string != null) ? prepend + string.toString() : "";
+		return value == null ? "" : prepend + value;
 	}
 
 	/**