Skip to content

SLING-12217 : Reduce resource lookups in Sling Model resolution#86

Open
bkkothari2255 wants to merge 9 commits intoapache:masterfrom
bkkothari2255:feature/SLING-12217-reduce-resource-lookups-in-sling-model-resolution
Open

SLING-12217 : Reduce resource lookups in Sling Model resolution#86
bkkothari2255 wants to merge 9 commits intoapache:masterfrom
bkkothari2255:feature/SLING-12217-reduce-resource-lookups-in-sling-model-resolution

Conversation

@bkkothari2255
Copy link
Copy Markdown
Contributor

@bkkothari2255 bkkothari2255 commented Apr 4, 2026

Issue : SLING-12217

While SLING-12244 fixed the database side of these duplicate lookups, AdapterImplementations.lookup() was still hammering the CPU by re-evaluating search paths and querying the large ConcurrentHashMap hundreds of times per page render for the same exact resource type

changes:

  • Request scoped Cache: attach a cache directly to ResourceResolver.getPropertyMap() so it lives and dies with the HTTP request
  • Thread Safe Identity Caching: The outer cache uses Collections.synchronizedMap(new IdentityHashMap<>()) to safely bypass expensive O(N) deep equality checks without crashing multi-threaded integration tests
  • Thread Safe Inner Cache: Uses a standard ConcurrentHashMap for concurrent string lookups
  • Remembering Not Found Results: We use a placeholder (Object.class) to remember when a resource type doesn't have a model. This lets us instantly abort future checks instead of endlessly re-walking the JCR tree looking for something that isn't there.

Let me know what you think?


if (cache == null) {
// Thread-safe Identity map to prevent O(N) deep equality checks on the massive map
cache = java.util.Collections.synchronizedMap(new java.util.IdentityHashMap<>());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A ResourceResolver is not thread-safe, so this cache does not need to be thread-safe as well.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, why is an IdentityHashMap used? This would be only effective, if the same map reference is used multiple times when calling this method. Is this really the case?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And a third aspect: We probably should avoid using an unbounded map, as it could lead to massive memory usage or even a memory leak.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A ResourceResolver is not thread-safe, so this cache does not need to be thread-safe as well.

You are absolutely right!

in a production environment, the ResourceResolver is tied to a single request and single thread, so it shouldn't need thread-safety

However, during the maven build, I observed that during the integration tests (like AdapterFactoryTest), a single mocked ResourceResolver is actually shared across multiple threads. This contract violation in the test suite was corrupting the standard map buckets and causing the JVM to hang

I added synchronizedMap purely as a workaround to keep the test suite passing

If we prefer to keep the production code "pure" we can remove the synchronization and look into fixing the multi-threading in the test class instead

What do you prefer?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, why is an IdentityHashMap used? This would be only effective, if the same map reference is used multiple times when calling this method. Is this really the case?

I double-checked this, and the map reference is indeed constant

the map argument always comes from the class-level variables (e.g., resourceTypeMappingsForResources), which are declared as final, Because the reference never changes and IdentityHashMap works perfectly here to skip the deep equality check

Could you verify if my logic holds up there?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And a third aspect: We probably should avoid using an unbounded map, as it could lead to massive memory usage or even a memory leak.

For the memory leak, I actually used ResourceResolver.getPropertyMap() exactly as suggested in the Jira ticket,

Since the cache dies with the HTTP request, it can't cause a long-term leak. And based on your WKND stats (~36 distinct paths), it shouldn't use much memory during the request either

Let me know if you'd still prefer we use a bounded map just to be extra safe or if you think the request-scoped map is good to go

return null;
}

Map<Map<String, Class<?>>, Map<String, Object>> cache = getModelClassCache(resolver);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a very complex type definition and hard to reason about. Would it make sense to encapsulate some parts of it in an inner class, just to simplify this expression?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed! I'll encapsulate that structure into an inner class to make it much easier to reason about, and push the change shortly

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Apr 4, 2026

@bkkothari2255 bkkothari2255 requested a review from joerghoh April 5, 2026 21:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants