Skip to content

8378895: Reduce object allocations in Renderer.getPeerInstance()#2091

Open
mstr2 wants to merge 5 commits intoopenjdk:masterfrom
mstr2:fixes/renderer-alloc
Open

8378895: Reduce object allocations in Renderer.getPeerInstance()#2091
mstr2 wants to merge 5 commits intoopenjdk:masterfrom
mstr2:fixes/renderer-alloc

Conversation

@mstr2
Copy link
Collaborator

@mstr2 mstr2 commented Mar 1, 2026

The current implementation of Renderer.getPeerInstance() looks up mappings by concatenating strings to form a combined key:

peer = peerCache.get(name + "_" + unrollCount);

This can be improved by not using strings to store the combined key, but using a simple PeerCacheKey class instead.
The Renderer class then uses a single reusable PeerCacheKey object to look up mappings, making the lookup allocation-free.

/reviewers 2


Progress

  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue
  • Change must be properly reviewed (2 reviews required, with at least 1 Reviewer, 1 Author)

Issue

  • JDK-8378895: Reduce object allocations in Renderer.getPeerInstance() (Enhancement - P4)

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jfx.git pull/2091/head:pull/2091
$ git checkout pull/2091

Update a local copy of the PR:
$ git checkout pull/2091
$ git pull https://git.openjdk.org/jfx.git pull/2091/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 2091

View PR using the GUI difftool:
$ git pr show -t 2091

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jfx/pull/2091.diff

Using Webrev

Link to Webrev Comment

@bridgekeeper
Copy link

bridgekeeper bot commented Mar 1, 2026

👋 Welcome back mstrauss! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk
Copy link

openjdk bot commented Mar 1, 2026

❗ This change is not yet ready to be integrated.
See the Progress checklist in the description for automated requirements.

@openjdk openjdk bot added the rfr Ready for review label Mar 1, 2026
@openjdk
Copy link

openjdk bot commented Mar 1, 2026

@mstr2
The total number of required reviews for this PR (including the jcheck configuration and the last /reviewers command) is now set to 2 (with at least 1 Reviewer, 1 Author).

@mlbridge
Copy link

mlbridge bot commented Mar 1, 2026

Webrevs

new HashMap<>(1);
private Map<String, EffectPeer> peerCache =
Collections.synchronizedMap(new HashMap<String, EffectPeer>(5));
private static final Map<FilterContext, Renderer> rendererMap = new HashMap<>(1);
Copy link
Collaborator

Choose a reason for hiding this comment

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

If this is meant to initialize the map to a size of 1 then HashMap.newHashMap should be used.

}
}

private final Map<PeerCacheKey, EffectPeer> peerCache = Collections.synchronizedMap(new HashMap<>(5));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Isn't a ConcurrentHashMap a better alternative?

Copy link
Collaborator Author

@mstr2 mstr2 Mar 1, 2026

Choose a reason for hiding this comment

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

No, I don't think so, and I think Collections.synchronizedMap is also pointless here. If this is supposed to allow concurrent access to the peerCache map, the original implementation is defective: Collections.synchronizedMap clearly states that iteration must be synchronized on the map instance, which this code fails to do.

I've changed the implementation to use a normal HashMap, and synchronize manually where it is accessed or modified. For this purpose, I've removed the Renderer.getPeers() method, which is only called in PPSRenderer.dispose() to iterate over the peers and dispose each one of them. Instead, the operation is now encapsulated in Renderer.clearPeers().

Copy link
Contributor

@andy-goryachev-oracle andy-goryachev-oracle left a comment

Choose a reason for hiding this comment

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

I see several structural changes here (synchronization, changing the order of events around DISPOSED) that go beyond a simple optimization.

I am all for replacing string concatenation with a compound key like object, my main question though is how big of the problem is this? Do we really incur significant overhead creating those keys?


@Override
public boolean equals(Object o) {
return o instanceof PeerCacheKey other
Copy link
Contributor

Choose a reason for hiding this comment

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

typically

if(o == this) {
  return true;
}

Copy link
Collaborator Author

@mstr2 mstr2 Mar 2, 2026

Choose a reason for hiding this comment

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

Yes, but since the lookup key is never inserted into the map, o is never this. That would be an optimization that'll never be hit.

Copy link
Contributor

Choose a reason for hiding this comment

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

makes sense. maybe add a comment then?

public final EffectPeer getPeerInstance(FilterContext fctx, String name, int unrollCount) {
synchronized (peerCache) {
// The lookup key is only used to look up mappings, it is never inserted into the map.
peerCacheKey.name = name;
Copy link
Contributor

Choose a reason for hiding this comment

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

this code is not re-entrant. can you guarantee it's safe?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

To be fair, I don't know why the code here is synchronized at all. I only ever see this method be called on the renderer thread (as it should be). However, I don't think that I've changed anything substantial here: the method was synchronized before, and is synchronized now.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hm, I meant something else: you are changing a peerCacheKey instance, are you sure it will never re-enter the same method (and clobber the same instance?)

You've also added synchronization on a different object here, which might create a deadlock. And you do bring another good point - if this code is always called from the same thread, why synchronize at all?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The reusable peerCacheKey instance is only used in these three lines:

peerCacheKey.name = name;
peerCacheKey.unrollCount = unrollCount;
EffectPeer<?> peer = peerCache.get(peerCacheKey);

Since the Map.get() method won't call out to other code, I don't think there is any chance that something can go wrong in this particular segment of code.

You've also added synchronization on a different object here, which might create a deadlock.

I don't see any potential problem that wasn't there before. Previously, this method synchronized on this, now it synchronizes on peerCache. That's not a material difference in itself, considering that there's only one other place where we synchronize on peerCache, and I've made sure that it won't call out to other methods.

protected final void clearPeers() {
EffectPeer<?>[] peers;

synchronized (peerCache) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Having two monitors involved (peerCache here and peer's one elsewhere) might lead to a deadlock. Just by looking at the code, I see one potential with createPeer() in Renderer:273 . Could you take a look please?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm not calling any dangerous method while holding a lock here. The code first copies the values of the map into a local array, then releases the lock, and only then calls EffectPeer.dispose().

Copy link
Contributor

@andy-goryachev-oracle andy-goryachev-oracle Mar 2, 2026

Choose a reason for hiding this comment

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

Right, my original comment should have been placed at L237, where inside a block synchronized on peerCache, you are calling createPeer(). It looks like the latter is not synchronized on anything (can't see past reflection though), but if inside of createPeer() there is another code synchronized around a different object, there might be deadlock.

It looks like there isn't, but my question remains: why do you want to introduce these non-trivial changes in the first place, how big of a problem is this allocation is?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I want to make these changes to improve the code. I don't really see the problem here...?

@mstr2
Copy link
Collaborator Author

mstr2 commented Mar 2, 2026

I am all for replacing string concatenation with a compound key like object, my main question though is how big of the problem is this? Do we really incur significant overhead creating those keys?

As always with these kinds of optimizations: it depends on the application. Generally, you shouldn't expect this particular issue to be noticeable. But when you have 1000 of these "not generally noticeable" small issues, quantity can become a quality of its own.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rfr Ready for review

Development

Successfully merging this pull request may close these issues.

3 participants